fix: Stream-Ende wartet auf playbackHeadPosition vor release()

AudioTrack.stop() + release() direkt nach dem letzten write() killt die
letzten Sekunden Audio — die Samples sind zwar im Buffer, aber noch
nicht durch die Hardware rausgespielt. Deshalb brach die Sprachausgabe
mitten im Satz ab (z.B. bei "diesmal").

Fix: Writer-Thread wartet im finally-Block bis playbackHeadPosition die
Anzahl geschriebener Frames erreicht, dann erst stop()/release().
Safety: 2s Stall-Detection, falls AudioTrack haengen bleibt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
duffyduck 2026-04-22 18:31:12 +02:00
parent 31fe70bab5
commit 0c03b4f161
1 changed files with 26 additions and 0 deletions

View File

@ -45,6 +45,7 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex
@Volatile private var prerollBytes: Int = 0 @Volatile private var prerollBytes: Int = 0
@Volatile private var playbackStarted = false @Volatile private var playbackStarted = false
@Volatile private var bytesBuffered: Long = 0 @Volatile private var bytesBuffered: Long = 0
@Volatile private var streamBytesPerFrame: Int = 2 // mono s16le default
// ── Lifecycle ── // ── Lifecycle ──
@ -64,6 +65,7 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex
prerollBytes = prerollTarget prerollBytes = prerollTarget
bytesBuffered = 0 bytesBuffered = 0
playbackStarted = false playbackStarted = false
streamBytesPerFrame = channels * 2 // s16 = 2 bytes per sample
val newTrack = AudioTrack.Builder() val newTrack = AudioTrack.Builder()
.setAudioAttributes( .setAudioAttributes(
@ -127,6 +129,30 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "Writer-Thread Fehler: ${e.message}") Log.w(TAG, "Writer-Thread Fehler: ${e.message}")
} finally { } finally {
// Warten bis alle geschriebenen Samples tatsaechlich abgespielt sind,
// sonst cuttet t.release() die letzten Sekunden ab.
try {
val totalFrames = (bytesBuffered / streamBytesPerFrame).toInt()
var lastPos = -1
var stalledCount = 0
while (!writerShouldStop) {
val pos = t.playbackHeadPosition
if (pos >= totalFrames) break
// Safety: wenn Position 2s nicht mehr vorwaerts → AudioTrack hing
if (pos == lastPos) {
stalledCount++
if (stalledCount > 40) {
Log.w(TAG, "playback stalled at $pos/$totalFrames — give up")
break
}
} else {
stalledCount = 0
lastPos = pos
}
Thread.sleep(50)
}
Log.i(TAG, "Playback fertig: frames=$totalFrames pos=${t.playbackHeadPosition}")
} catch (_: Exception) {}
try { t.stop() } catch (_: Exception) {} try { t.stop() } catch (_: Exception) {}
try { t.release() } catch (_: Exception) {} try { t.release() } catch (_: Exception) {}
} }