Coverage Summary for Class: PrimitiveAudioPlayer (com.vsevolodganin.clicktrack.primitiveaudio)
Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
PrimitiveAudioPlayer |
0%
(0/4)
|
0%
(0/8)
|
0%
(0/55)
|
0%
(0/389)
|
PrimitiveAudioPlayer$Companion |
|
PrimitiveAudioPlayer$play$1 |
|
PrimitiveAudioPlayer$play$2 |
0%
(0/3)
|
|
0%
(0/4)
|
0%
(0/21)
|
PrimitiveAudioPlayer$play$5$1 |
0%
(0/3)
|
|
0%
(0/3)
|
0%
(0/10)
|
Total |
0%
(0/10)
|
0%
(0/8)
|
0%
(0/62)
|
0%
(0/420)
|
package com.vsevolodganin.clicktrack.primitiveaudio
import android.media.AudioAttributes
import android.media.AudioFormat
import android.media.AudioManager
import android.media.AudioTrack
import com.vsevolodganin.clicktrack.di.component.PlayerServiceScope
import com.vsevolodganin.clicktrack.player.PlayerEvent
import com.vsevolodganin.clicktrack.soundlibrary.SoundSourceProvider
import com.vsevolodganin.clicktrack.utils.log.Logger
import kotlinx.coroutines.delay
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.yield
import me.tatarka.inject.annotations.Inject
import kotlin.coroutines.resume
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
@Inject
@PlayerServiceScope
class PrimitiveAudioPlayer(
primitiveAudioMonoRendererFactory: (targetSampleRate: Int) -> PrimitiveAudioMonoRenderer,
audioManager: AudioManager,
private val logger: Logger,
) {
private val audioSessionId: Int = audioManager.generateAudioSessionId()
private val audioTrack: AudioTrack = AudioTrack(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build(),
AudioFormat.Builder()
.setSampleRate(SAMPLE_RATE)
.setEncoding(AudioFormat.ENCODING_PCM_FLOAT)
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
.build(),
BUFFER_LENGTH_IN_FRAMES * Float.SIZE_BYTES,
AudioTrack.MODE_STREAM,
audioSessionId,
)
private val primitiveAudioMonoRenderer = primitiveAudioMonoRendererFactory(SAMPLE_RATE)
suspend fun play(
startingAt: Duration,
singleIterationDuration: Duration,
playerEvents: Sequence<PlayerEvent>,
reportProgress: (Duration) -> Unit,
soundSourceProvider: SoundSourceProvider,
) {
try {
audioTrack.play()
reportProgress(startingAt)
val samplesNumber = convertDurationToFramesNumber(singleIterationDuration)
audioTrack.notificationMarkerPosition = samplesNumber - convertDurationToFramesNumber(startingAt)
audioTrack.setPlaybackPositionUpdateListener(
object : AudioTrack.OnPlaybackPositionUpdateListener {
override fun onMarkerReached(track: AudioTrack) {
reportProgress(Duration.ZERO)
audioTrack.notificationMarkerPosition += samplesNumber
}
override fun onPeriodicNotification(track: AudioTrack) = Unit
},
)
primitiveAudioMonoRenderer.renderToMonoSamples(playerEvents, soundSourceProvider)
.chunked(BUFFER_LENGTH_IN_FRAMES, List<Float>::toFloatArray)
.forEach { samples ->
var samplesWritten = 0
while (samplesWritten < samples.size) {
yield()
val result = audioTrack.write(
samples,
samplesWritten,
samples.size - samplesWritten,
AudioTrack.WRITE_NON_BLOCKING,
)
if (result == 0) {
delay(BUFFER_LENGTH_IN_SECONDS.seconds / 2)
} else if (result > 0) {
yield()
samplesWritten += result
} else {
logger.logError(TAG, "Got unexpected result: $result")
return
}
}
}
suspendCancellableCoroutine { continuation ->
audioTrack.setPlaybackPositionUpdateListener(
object : AudioTrack.OnPlaybackPositionUpdateListener {
override fun onMarkerReached(track: AudioTrack) = continuation.resume(Unit)
override fun onPeriodicNotification(track: AudioTrack) = Unit
},
)
if (audioTrack.playbackHeadPosition >= audioTrack.notificationMarkerPosition) {
continuation.resume(Unit)
}
}
} finally {
audioTrack.setPlaybackPositionUpdateListener(null)
audioTrack.pause()
audioTrack.flush()
}
}
fun getLatencyMs(): Int {
try {
val getLatencyMethod = AudioTrack::class.java.getMethod("getLatency")
return getLatencyMethod.invoke(audioTrack) as Int - BUFFER_LENGTH_IN_SECONDS.seconds.inWholeMilliseconds.toInt()
} catch (throwable: Throwable) {
logger.logError(TAG, "Failed to get latency using getLatency method", throwable)
}
return 0
}
private fun convertDurationToFramesNumber(duration: Duration): Int {
return convertDurationToFramesNumber(duration, SAMPLE_RATE, 1)
}
private companion object {
const val TAG = "PrimitiveAudioPlayer"
const val SAMPLE_RATE = 44100
const val BUFFER_LENGTH_IN_SECONDS = 2
const val BUFFER_LENGTH_IN_FRAMES = SAMPLE_RATE * BUFFER_LENGTH_IN_SECONDS
}
}