Compare commits

..

4 Commits

Author SHA1 Message Date
duffyduck 9276a92c83 release: bump version to 0.0.8.9 2026-05-10 14:20:43 +02:00
duffyduck d16896c4b4 fix(audio): kurze TTS-Texte — play() erst NACH Buffer-Fuellung mit Padding
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) <noreply@anthropic.com>
2026-05-10 14:19:45 +02:00
duffyduck 20050d4077 release: bump version to 0.0.8.8 2026-05-10 14:12:59 +02:00
duffyduck 79760d1b2e fix(audio): kurze TTS-Texte spielen wieder ab — AudioTrack-Buffer entkoppelt von Preroll
OnePlus A12 stallte bei kurzem Text mit pos=0/34112: 336KB Buffer fuer
3.5s Preroll, aber nur 68KB Daten drin → AudioTrack faehrt nicht an.

Fix: Buffer fest auf ~2s, plus play()-Retry bei pos=0 nach 500ms.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:11:53 +02:00
3 changed files with 49 additions and 16 deletions
+2 -2
View File
@@ -79,8 +79,8 @@ android {
applicationId "com.ariacockpit"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 807
versionName "0.0.8.7"
versionCode 809
versionName "0.0.8.9"
// Fallback fuer Libraries mit Product Flavors
missingDimensionStrategy 'react-native-camera', 'general'
}
@@ -78,9 +78,14 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex
val encoding = AudioFormat.ENCODING_PCM_16BIT
val minBuf = AudioTrack.getMinBufferSize(sampleRate, channelConfig, encoding)
val bytesPerSecond = sampleRate * channels * 2 // 16-bit = 2 bytes
// Buffer muss mindestens PREROLL + etwas Spielraum fassen.
val prerollTarget = (bytesPerSecond * prerollSec).toInt()
val bufferSize = (minBuf * 32).coerceAtLeast(prerollTarget * 2)
// 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).
// 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
@@ -156,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-
@@ -228,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 {
@@ -237,12 +261,21 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex
val totalFrames = (bytesBuffered / streamBytesPerFrame).toInt()
var lastPos = -1
var stalledCount = 0
var retried = false
while (!writerShouldStop) {
val pos = t.playbackHeadPosition
if (pos >= totalFrames) break
// Safety: wenn Position 2s nicht mehr vorwaerts → AudioTrack hing
if (pos == lastPos) {
stalledCount++
// Nach 500ms Stillstand: AudioTrack-Quirk auf manchen
// Geraeten (OnePlus A12) — play() nochmal anstossen.
if (stalledCount == 10 && pos == 0 && !retried) {
retried = true
Log.w(TAG, "playback nicht angefahren — retry play()")
try { t.play() } catch (e: Exception) {
Log.w(TAG, "retry play() failed: ${e.message}")
}
}
if (stalledCount > 40) {
Log.w(TAG, "playback stalled at $pos/$totalFrames — give up")
break
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "aria-cockpit",
"version": "0.0.8.7",
"version": "0.0.8.9",
"private": true,
"scripts": {
"android": "react-native run-android",