From 0c03b4f161132dae6de77830563ccf8ae151a56d Mon Sep 17 00:00:00 2001 From: duffyduck Date: Wed, 22 Apr 2026 18:31:12 +0200 Subject: [PATCH] fix: Stream-Ende wartet auf playbackHeadPosition vor release() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../com/ariacockpit/PcmStreamPlayerModule.kt | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/android/android/app/src/main/java/com/ariacockpit/PcmStreamPlayerModule.kt b/android/android/app/src/main/java/com/ariacockpit/PcmStreamPlayerModule.kt index bcc71ed..6f5ec5f 100644 --- a/android/android/app/src/main/java/com/ariacockpit/PcmStreamPlayerModule.kt +++ b/android/android/app/src/main/java/com/ariacockpit/PcmStreamPlayerModule.kt @@ -45,6 +45,7 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex @Volatile private var prerollBytes: Int = 0 @Volatile private var playbackStarted = false @Volatile private var bytesBuffered: Long = 0 + @Volatile private var streamBytesPerFrame: Int = 2 // mono s16le default // ── Lifecycle ── @@ -64,6 +65,7 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex prerollBytes = prerollTarget bytesBuffered = 0 playbackStarted = false + streamBytesPerFrame = channels * 2 // s16 = 2 bytes per sample val newTrack = AudioTrack.Builder() .setAudioAttributes( @@ -127,6 +129,30 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex } catch (e: Exception) { Log.w(TAG, "Writer-Thread Fehler: ${e.message}") } 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.release() } catch (_: Exception) {} }