Validate Android voice streams and keep playback cancellation off UI waits

This commit is contained in:
evgeny committed 2026-10-01 19:09:07 +03:00
1 parent dd8041b30f
commit c863b80917
2 files changed
+50 -30

No files matched your search

@@ -196,24 +196,27 @@ class AudioRecorderManager {
fun playFromPosition(filePath: String, startMs: Int, onProgress: ((Float, Int) -> Unit)? = null, onComplete: () -> Unit = {}) {
stopPlayback()
playing = true
val myGen = playGeneration
playThread = Thread {
val myGen = playGeneration
var handle = 0L
var track: AudioTrack? = null
try {
val info = IntArray(3)
val handle = NativeLib.voiceDecodeOpen(filePath, info)
handle = NativeLib.voiceDecodeOpen(filePath, info)
if (handle == 0L) {
LogManager.addLog("ERROR", "AudioPlayer", "decode open failed: $filePath")
playing = false; onComplete(); return@Thread
return@Thread
}
val sampleRate = info[0]; val channels = info[1]; val totalSamples = info[2]
/* skip to startMs by decoding and discarding frames */
val frameSamples = FRAME_SAMPLES * channels
val frameSamples = sampleRate / 50 * channels
val skipFrames = startMs / FRAME_MS
val skipBuf = ShortArray(frameSamples)
var skippedSamples = 0L
for (f in 0 until skipFrames) {
if (myGen != playGeneration) return@Thread
val dec = NativeLib.voiceDecodeRead(handle, skipBuf)
if (dec <= 0) break
skippedSamples += dec
@@ -223,43 +226,49 @@ class AudioRecorderManager {
if (channels == 2) AudioFormat.CHANNEL_OUT_STEREO else AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT)
val track = AudioTrack(AudioManager.STREAM_MUSIC, sampleRate,
val output = AudioTrack(AudioManager.STREAM_MUSIC, sampleRate,
if (channels == 2) AudioFormat.CHANNEL_OUT_STEREO else AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT, bufSize, AudioTrack.MODE_STREAM)
track.play()
track = output
output.play()
val pcm = ShortArray(frameSamples)
var readTotal = 0L
var read: Int
while (playing) {
while (playing && myGen == playGeneration) {
read = NativeLib.voiceDecodeRead(handle, pcm)
if (read <= 0) break
track.write(pcm, 0, read)
if (myGen != playGeneration) break
output.write(pcm, 0, read)
readTotal += read
val total = (skippedSamples + readTotal).toFloat() / (totalSamples * channels).coerceAtLeast(1).toFloat()
val total = (skippedSamples + readTotal).toFloat() / totalSamples.coerceAtLeast(1).toFloat()
val eMs = startMs + (readTotal * FRAME_MS / frameSamples).toInt()
onProgress?.invoke(total, eMs)
}
val drainMs = (bufSize * 1000L) / (sampleRate.toLong() * channels * 2) + 100
try { Thread.sleep(drainMs) } catch (_: InterruptedException) {}
track.stop(); track.release()
NativeLib.voiceDecodeClose(handle)
if (myGen == playGeneration) Thread.sleep(drainMs)
} catch (_: InterruptedException) {
LogManager.addLog("DEBUG", "AudioPlayer", "playback cancelled")
} catch (e: Exception) {
LogManager.addLog("ERROR", "AudioPlayer", "play error: ${e.message}")
}
if (playGeneration == myGen) {
playing = false; onComplete()
} finally {
track?.apply {
try { stop() } catch (e: Exception) { LogManager.addLog("WARN", "AudioPlayer", "stop failed: ${e.message}") }
release()
}
if (handle != 0L) NativeLib.voiceDecodeClose(handle)
if (playGeneration == myGen) { playing = false; onComplete() }
}
}.apply { start() }
}
fun pausePlayback() {
playGeneration++; playing = false
playThread?.join(500); playThread = null
playThread?.interrupt(); playThread = null
}
fun stopPlayback() {
playGeneration++; playing = false
playThread?.join(500); playThread = null
playThread?.interrupt(); playThread = null
}
}