From d16896c4b448fca8fe5da9f1358163f8074f3aad Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sun, 10 May 2026 14:19:45 +0200 Subject: [PATCH] =?UTF-8?q?fix(audio):=20kurze=20TTS-Texte=20=E2=80=94=20p?= =?UTF-8?q?lay()=20erst=20NACH=20Buffer-Fuellung=20mit=20Padding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auf OnePlus A12 startet AudioTrack nicht zuverlaessig wenn play() bei duennem Buffer gerufen wird (pos blieb 0/34112 trotz 71KB Daten + Retry). Neue Reihenfolge bei kurzem Stream: 1. Daten in Buffer schreiben (mainLoop) 2. Trailing-Silence (0.3s) 3. Padding bis min. 2s gepuffert 4. DANN erst play() Buffer auf 3s erhoeht damit blockingem write() noch Headroom bleibt. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../com/ariacockpit/PcmStreamPlayerModule.kt | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) 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 2c44c18..8a42744 100644 --- a/android/android/app/src/main/java/com/ariacockpit/PcmStreamPlayerModule.kt +++ b/android/android/app/src/main/java/com/ariacockpit/PcmStreamPlayerModule.kt @@ -79,11 +79,13 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex val minBuf = AudioTrack.getMinBufferSize(sampleRate, channelConfig, encoding) val bytesPerSecond = sampleRate * channels * 2 // 16-bit = 2 bytes val prerollTarget = (bytesPerSecond * prerollSec).toInt() - // Buffer entkoppelt von Preroll — fester ~2s-Buffer reicht. Wenn er + // Buffer entkoppelt von Preroll — fester ~3s-Buffer reicht. Wenn er // an Preroll gekoppelt ist (z.B. 7s bei preroll=3.5s) und nur kurz // gefuettert wird, stallt AudioTrack auf manchen Geraeten (OnePlus // Android 12: pos bleibt 0 obwohl play() lief). - val bufferSize = (bytesPerSecond * 2).coerceAtLeast(minBuf * 8) + // 3s damit Padding bis 2s vor play() noch Headroom hat (write() ist + // blocking — wenn Buffer voll ist, deadlockt es vor play()). + val bufferSize = (bytesPerSecond * 3).coerceAtLeast(minBuf * 8) prerollBytes = prerollTarget bytesBuffered = 0 playbackStarted = false @@ -159,16 +161,11 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex val data = queue.poll(50, java.util.concurrent.TimeUnit.MILLISECONDS) if (data == null) { if (endRequested) { - // Falls wir vor Pre-Roll enden (kurzer Text): trotzdem abspielen - if (!playbackStarted) { - try { - t.play() - playbackStarted = true - Log.i(TAG, "Playback gestartet VOR Pre-Roll (kurzer Text, ${bytesBuffered}B gepuffert)") - } catch (e: Exception) { - Log.w(TAG, "play() fallback failed: ${e.message}") - } - } + // Bei kurzem Text NICHT hier play() callen — erst nach + // Trailing-Silence + Padding (siehe Block nach mainLoop), + // damit AudioTrack mit komplett gefuelltem Buffer startet. + // OnePlus A12: AudioTrack startet nicht zuverlaessig wenn + // play() bei dünnem Buffer gerufen wird. break@mainLoop } // Underrun-Schutz: Stille reinfuettern wenn der AudioTrack- @@ -231,6 +228,30 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex } bytesBuffered += silence.size } + // Bei kurzem Text (play() noch nicht gestartet): Buffer auf min. + // 2s padden + DANN play(). Auf OnePlus A12 startet AudioTrack + // bei einem zu duennen Buffer nicht — pos bleibt auf 0 stehen. + if (!playbackStarted && !writerShouldStop) { + val minStartBytes = bytesPerSecond * 2 + if (bytesBuffered < minStartBytes) { + val padBytes = (minStartBytes - bytesBuffered.toInt()) and 0x7FFFFFFE + val pad = ByteArray(padBytes) + var padOff = 0 + while (padOff < pad.size && !writerShouldStop) { + val w = t.write(pad, padOff, pad.size - padOff) + if (w <= 0) break + padOff += w + } + bytesBuffered += pad.size + } + try { + t.play() + playbackStarted = true + Log.i(TAG, "Playback gestartet (kurzer Text, ${bytesBuffered}B komplett gepuffert)") + } catch (e: Exception) { + Log.w(TAG, "play() short-text failed: ${e.message}") + } + } } catch (e: Exception) { Log.w(TAG, "Writer-Thread Fehler: ${e.message}") } finally {