Coverage Summary for Class: PrimitiveAudioPlayer (com.vsevolodganin.clicktrack.primitiveaudio)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| PrimitiveAudioPlayer |
0%
(0/4)
|
0%
(0/10)
|
0%
(0/54)
|
0%
(0/363)
|
| PrimitiveAudioPlayer$Companion |
|
| PrimitiveAudioPlayer$MetroFactory |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/25)
|
| PrimitiveAudioPlayer$MetroFactory$Companion |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/16)
|
| PrimitiveAudioPlayer$play$1 |
|
| PrimitiveAudioPlayer$play$2 |
0%
(0/3)
|
|
0%
(0/4)
|
0%
(0/21)
|
| PrimitiveAudioPlayer$play$3$1 |
0%
(0/3)
|
|
0%
(0/3)
|
0%
(0/10)
|
| Total |
0%
(0/12)
|
0%
(0/10)
|
0%
(0/63)
|
0%
(0/435)
|
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 dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import kotlinx.coroutines.delay
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.yield
import kotlin.coroutines.resume
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
@Inject
@SingleIn(PlayerServiceScope::class)
actual class PrimitiveAudioPlayer(
primitiveAudioMonoRendererFactory: PrimitiveAudioMonoRenderer.Factory,
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.create(SAMPLE_RATE)
actual suspend fun play(
startingAt: Duration,
singleIterationDuration: Duration,
playerEvents: Sequence<PlayerEvent>,
reportProgress: (Duration) -> Unit,
soundSourceProvider: SoundSourceProvider,
) {
try {
audioTrack.play()
reportProgress(startingAt)
val samplesNumber = convertDurationToFramesNumber(singleIterationDuration)
val samplesChunks = primitiveAudioMonoRenderer.renderToMonoSamples(playerEvents, soundSourceProvider)
.chunked(BUFFER_LENGTH_IN_FRAMES, List<Float>::toFloatArray)
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
},
)
for (samplesChunk in samplesChunks) {
var samplesWritten = 0
while (samplesWritten < samplesChunk.size) {
yield()
val result = audioTrack.write(
samplesChunk,
samplesWritten,
samplesChunk.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()
}
}
actual 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
}
}