Avoid capture-lock acquisition while rolling back a playback worker launch
This commit is contained in:
1 parent
3f2cc2abb3
commit
5ee87c535b
1 file changed
+103
-101
+103
-101
@@ -276,116 +276,118 @@ class AudioRecorderManager {
|
||||
fun playFromPosition(filePath: String, startMs: Int, onProgress: ((Float, Int) -> Unit)? = null, onComplete: () -> Unit = {}) {
|
||||
if (closing.get()?.isDone == false) { LogManager.addLog("WARN", "AudioPlayer", "play rejected: cleanup pending"); return }
|
||||
synchronized(playbackLock) {
|
||||
stopPlayback()
|
||||
playing = true
|
||||
val myGen = playGeneration
|
||||
stopPlayback()
|
||||
playing = true
|
||||
val myGen = playGeneration
|
||||
|
||||
val worker = Thread {
|
||||
var handle = 0L
|
||||
var track: AudioTrack? = null
|
||||
try {
|
||||
val info = IntArray(3)
|
||||
handle = NativeLib.voiceDecodeOpen(filePath, info)
|
||||
if (handle == 0L) {
|
||||
LogManager.addLog("ERROR", "AudioPlayer", "decode open failed: $filePath")
|
||||
return@Thread
|
||||
}
|
||||
val sampleRate = info[0]; val channels = info[1]; val totalSamples = info[2]
|
||||
check(sampleRate in listOf(8000, 12000, 16000, 24000, 48000) && channels in 1..2 && totalSamples > 0) {
|
||||
"invalid decode info rate=$sampleRate channels=$channels samples=$totalSamples"
|
||||
}
|
||||
|
||||
// Seek выполняется по полным 20мс кадрам; позиция берётся из фактически пропущенных сэмплов.
|
||||
val frameSamples = sampleRate / 50 * channels
|
||||
val skipFrames = startMs.coerceAtLeast(0) / 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)
|
||||
check(dec in 0..skipBuf.size) { "decode seek failed result=$dec" }
|
||||
if (dec == 0) break
|
||||
skippedSamples += dec
|
||||
}
|
||||
|
||||
val bufSize = AudioTrack.getMinBufferSize(sampleRate,
|
||||
if (channels == 2) AudioFormat.CHANNEL_OUT_STEREO else AudioFormat.CHANNEL_OUT_MONO,
|
||||
AudioFormat.ENCODING_PCM_16BIT)
|
||||
check(bufSize > 0) { "AudioTrack minimum=$bufSize" }
|
||||
|
||||
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 = output
|
||||
check(output.state == AudioTrack.STATE_INITIALIZED) { "AudioTrack not initialized" }
|
||||
output.play()
|
||||
LogManager.addLog("INFO", "AudioPlayer", "playback started generation=$myGen rate=$sampleRate channels=$channels " +
|
||||
"skippedSamples=$skippedSamples totalSamples=$totalSamples")
|
||||
|
||||
val pcm = ShortArray(frameSamples)
|
||||
var readTotal = 0L
|
||||
var read: Int
|
||||
while (playing && myGen == playGeneration) {
|
||||
read = NativeLib.voiceDecodeRead(handle, pcm)
|
||||
check(read in 0..pcm.size) { "decode read failed result=$read" }
|
||||
if (read == 0) break
|
||||
if (myGen != playGeneration) break
|
||||
var offset = 0
|
||||
while (playing && myGen == playGeneration && offset < read) {
|
||||
val written = output.write(pcm, offset, read - offset)
|
||||
check(written in 1..(read - offset)) { "AudioTrack.write=$written remaining=${read - offset}" }
|
||||
offset += written; readTotal += written
|
||||
val worker = Thread {
|
||||
var handle = 0L
|
||||
var track: AudioTrack? = null
|
||||
try {
|
||||
val info = IntArray(3)
|
||||
handle = NativeLib.voiceDecodeOpen(filePath, info)
|
||||
if (handle == 0L) {
|
||||
LogManager.addLog("ERROR", "AudioPlayer", "decode open failed: $filePath")
|
||||
return@Thread
|
||||
}
|
||||
val total = (skippedSamples + readTotal).toFloat() / totalSamples.coerceAtLeast(1).toFloat()
|
||||
val eMs = ((skippedSamples + readTotal) * 1000 / (sampleRate * channels)).toInt()
|
||||
synchronized(playbackLock) {
|
||||
if (myGen == playGeneration) onProgress?.invoke(total, eMs)
|
||||
val sampleRate = info[0]; val channels = info[1]; val totalSamples = info[2]
|
||||
check(sampleRate in listOf(8000, 12000, 16000, 24000, 48000) && channels in 1..2 && totalSamples > 0) {
|
||||
"invalid decode info rate=$sampleRate channels=$channels samples=$totalSamples"
|
||||
}
|
||||
}
|
||||
val drainMs = (bufSize * 1000L) / (sampleRate.toLong() * channels * 2) + 100
|
||||
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}")
|
||||
} finally {
|
||||
var retryMs = 250L
|
||||
var notified = false
|
||||
var interrupted = Thread.interrupted()
|
||||
while (true) {
|
||||
track?.let { output ->
|
||||
try { output.stop() } catch (e: Exception) { LogManager.addLog("WARN", "AudioPlayer", "stop failed: $e") }
|
||||
try { output.release(); track = null }
|
||||
catch (e: Exception) { LogManager.addLog("ERROR", "AudioPlayer", "release failed; worker retains output: $e") }
|
||||
|
||||
// Seek выполняется по полным 20мс кадрам; позиция берётся из фактически пропущенных сэмплов.
|
||||
val frameSamples = sampleRate / 50 * channels
|
||||
val skipFrames = startMs.coerceAtLeast(0) / 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)
|
||||
check(dec in 0..skipBuf.size) { "decode seek failed result=$dec" }
|
||||
if (dec == 0) break
|
||||
skippedSamples += dec
|
||||
}
|
||||
try { if (handle != 0L) { NativeLib.voiceDecodeClose(handle); handle = 0 } }
|
||||
catch (e: Exception) { LogManager.addLog("ERROR", "AudioPlayer", "decode close failed generation=$myGen; retained: $e") }
|
||||
if (!notified) {
|
||||
|
||||
val bufSize = AudioTrack.getMinBufferSize(sampleRate,
|
||||
if (channels == 2) AudioFormat.CHANNEL_OUT_STEREO else AudioFormat.CHANNEL_OUT_MONO,
|
||||
AudioFormat.ENCODING_PCM_16BIT)
|
||||
check(bufSize > 0) { "AudioTrack minimum=$bufSize" }
|
||||
|
||||
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 = output
|
||||
check(output.state == AudioTrack.STATE_INITIALIZED) { "AudioTrack not initialized" }
|
||||
output.play()
|
||||
LogManager.addLog("INFO", "AudioPlayer", "playback started generation=$myGen rate=$sampleRate channels=$channels " +
|
||||
"skippedSamples=$skippedSamples totalSamples=$totalSamples")
|
||||
|
||||
val pcm = ShortArray(frameSamples)
|
||||
var readTotal = 0L
|
||||
var read: Int
|
||||
while (playing && myGen == playGeneration) {
|
||||
read = NativeLib.voiceDecodeRead(handle, pcm)
|
||||
check(read in 0..pcm.size) { "decode read failed result=$read" }
|
||||
if (read == 0) break
|
||||
if (myGen != playGeneration) break
|
||||
var offset = 0
|
||||
while (playing && myGen == playGeneration && offset < read) {
|
||||
val written = output.write(pcm, offset, read - offset)
|
||||
check(written in 1..(read - offset)) { "AudioTrack.write=$written remaining=${read - offset}" }
|
||||
offset += written; readTotal += written
|
||||
}
|
||||
val total = (skippedSamples + readTotal).toFloat() / totalSamples.coerceAtLeast(1).toFloat()
|
||||
val eMs = ((skippedSamples + readTotal) * 1000 / (sampleRate * channels)).toInt()
|
||||
synchronized(playbackLock) {
|
||||
if (playGeneration == myGen) {
|
||||
playing = false; playThread = null
|
||||
try { onComplete() }
|
||||
catch (e: Exception) { LogManager.addLog("ERROR", "AudioPlayer", "completion callback failed generation=$myGen: $e") }
|
||||
}
|
||||
if (myGen == playGeneration) onProgress?.invoke(total, eMs)
|
||||
}
|
||||
notified = true
|
||||
}
|
||||
if (track == null && handle == 0L) break
|
||||
try { Thread.sleep(retryMs) } catch (_: InterruptedException) { interrupted = true }
|
||||
retryMs = (retryMs * 2).coerceAtMost(30_000)
|
||||
val drainMs = (bufSize * 1000L) / (sampleRate.toLong() * channels * 2) + 100
|
||||
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}")
|
||||
} finally {
|
||||
var retryMs = 250L
|
||||
var notified = false
|
||||
var interrupted = Thread.interrupted()
|
||||
while (true) {
|
||||
track?.let { output ->
|
||||
try { output.stop() } catch (e: Exception) { LogManager.addLog("WARN", "AudioPlayer", "stop failed: $e") }
|
||||
try { output.release(); track = null }
|
||||
catch (e: Exception) { LogManager.addLog("ERROR", "AudioPlayer", "release failed; worker retains output: $e") }
|
||||
}
|
||||
try { if (handle != 0L) { NativeLib.voiceDecodeClose(handle); handle = 0 } }
|
||||
catch (e: Exception) { LogManager.addLog("ERROR", "AudioPlayer", "decode close failed generation=$myGen; retained: $e") }
|
||||
if (!notified) {
|
||||
synchronized(playbackLock) {
|
||||
if (playGeneration == myGen) {
|
||||
playing = false; playThread = null
|
||||
try { onComplete() }
|
||||
catch (e: Exception) { LogManager.addLog("ERROR", "AudioPlayer", "completion callback failed generation=$myGen: $e") }
|
||||
}
|
||||
}
|
||||
notified = true
|
||||
}
|
||||
if (track == null && handle == 0L) break
|
||||
try { Thread.sleep(retryMs) } catch (_: InterruptedException) { interrupted = true }
|
||||
retryMs = (retryMs * 2).coerceAtMost(30_000)
|
||||
}
|
||||
playbackWorkers.remove(Thread.currentThread()); forgetIfIdle()
|
||||
if (interrupted) Thread.currentThread().interrupt()
|
||||
LogManager.addLog("DEBUG", "AudioPlayer", "playback worker exited generation=$myGen current=$playGeneration")
|
||||
}
|
||||
playbackWorkers.remove(Thread.currentThread()); forgetIfIdle()
|
||||
if (interrupted) Thread.currentThread().interrupt()
|
||||
LogManager.addLog("DEBUG", "AudioPlayer", "playback worker exited generation=$myGen current=$playGeneration")
|
||||
}
|
||||
}
|
||||
playThread = worker; playbackWorkers.add(worker); owners.add(this)
|
||||
try { worker.start() }
|
||||
catch (e: Exception) {
|
||||
playbackWorkers.remove(worker); playThread = null; playing = false; forgetIfIdle()
|
||||
LogManager.addLog("ERROR", "AudioPlayer", "Playback worker start failed: $e")
|
||||
onComplete()
|
||||
}
|
||||
playThread = worker; playbackWorkers.add(worker); owners.add(this)
|
||||
try { worker.start() }
|
||||
catch (e: Exception) {
|
||||
playbackWorkers.remove(worker); playThread = null; playing = false
|
||||
LogManager.addLog("ERROR", "AudioPlayer", "Playback worker start failed: $e")
|
||||
try { onComplete() }
|
||||
catch (callback: Exception) { LogManager.addLog("ERROR", "AudioPlayer", "Startup failure callback failed: $callback") }
|
||||
requestClose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user