Compare commits

..

2 Commits

Author SHA1 Message Date
duffyduck f5b4285d15 release: bump version to 0.0.6.0 2026-04-25 01:13:42 +02:00
duffyduck 248e7c9ae4 fix: preroll=0 wirklich sofort + Trailing-Silence gegen Wort-Cutoff
Zwei Bugs die zusammen dafuer sorgen dass Worte "verschluckt" werden:

1) play() wurde bei preroll=0 erst beim ersten echten Chunk aufgerufen
   — nicht schon nach der Leading-Silence. Dadurch musste AudioTrack
   gleichzeitig Startup UND Audio abspielen, die Hardware-Anfahr-Latenz
   schluckt die ersten Samples.

   Fix: Bei prerollBytes==0 direkt nach dem silence-write play() rufen.
   AudioTrack haelt den Play-State und wartet auf mehr Samples — die
   naechsten Chunks kommen in den bereits laufenden Stream rein.

2) Nach letztem Chunk ging der Writer via return@Thread in den finally-
   Block. Der wartete zwar auf playbackHeadPosition >= totalFrames, aber
   Android's Hardware-Pipeline puffert oft noch ein paar Samples nach —
   stop() kam, Samples futsch.

   Fix: 300ms TRAILING_SILENCE am Ende schreiben. playbackHeadPosition
   erreicht echt bis zum Ende der echten Samples bevor die Stille abspielt.
   Loop umgeschrieben auf mainLoop-Label (break statt return@Thread) damit
   Trailing-Silence garantiert laeuft.

LEADING_SILENCE auf 300ms erhoeht fuer bessere AudioTrack-Warmup-Toleranz.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 01:11:23 +02:00
3 changed files with 43 additions and 12 deletions
+2 -2
View File
@@ -79,8 +79,8 @@ android {
applicationId "com.ariacockpit"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 509
versionName "0.0.5.9"
versionCode 600
versionName "0.0.6.0"
// Fallback fuer Libraries mit Product Flavors
missingDimensionStrategy 'react-native-camera', 'general'
}
@@ -39,7 +39,10 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex
private const val MAX_PREROLL_SECONDS = 10.0
// Stille am Stream-Anfang, damit AudioTrack sauber anfaehrt und die
// ersten Samples nicht abgeschnitten werden (XTTS-Warmup + play()-Latenz).
private const val LEADING_SILENCE_SECONDS = 0.2
private const val LEADING_SILENCE_SECONDS = 0.3
// Stille am Ende — puffert das Hardware-Flushen damit die letzten
// echten Samples garantiert ausgespielt werden bevor stop() kommt.
private const val TRAILING_SILENCE_SECONDS = 0.3
}
override fun getName() = "PcmStreamPlayer"
@@ -109,9 +112,9 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex
val t = track ?: return@Thread
try {
// Leading-Silence in den Buffer — gibt AudioTrack Zeit anzufahren.
val silenceBytes = ((sampleRate * channels * 2) * LEADING_SILENCE_SECONDS).toInt() and 0x7FFFFFFE
if (silenceBytes > 0) {
val silence = ByteArray(silenceBytes)
val leadingBytes = ((sampleRate * channels * 2) * LEADING_SILENCE_SECONDS).toInt() and 0x7FFFFFFE
if (leadingBytes > 0) {
val silence = ByteArray(leadingBytes)
var silOff = 0
while (silOff < silence.size && !writerShouldStop) {
val w = t.write(silence, silOff, silence.size - silOff)
@@ -120,8 +123,23 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex
}
bytesBuffered += silence.size
}
while (!writerShouldStop) {
val data = queue.poll(50, java.util.concurrent.TimeUnit.MILLISECONDS) ?: run {
// Bei preroll=0: play() SOFORT nach Leading-Silence aufrufen,
// nicht erst bei Ankunft des ersten echten Chunks. Android's
// AudioTrack haelt den Play-State und wartet auf neue Samples.
// So verschluckt es keine Worte wenn der erste Chunk erst
// nach play()-Startup-Latenz eintrifft.
if (prerollBytes == 0 && !playbackStarted) {
try {
t.play()
playbackStarted = true
Log.i(TAG, "Playback sofort gestartet (preroll=0, ${bytesBuffered}B silence)")
} catch (e: Exception) {
Log.w(TAG, "play() sofort failed: ${e.message}")
}
}
mainLoop@ while (!writerShouldStop) {
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) {
@@ -133,10 +151,10 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex
Log.w(TAG, "play() fallback failed: ${e.message}")
}
}
return@Thread
break@mainLoop
}
null
} ?: continue
continue@mainLoop
}
// Pre-Roll Check: play() erst wenn genug gepuffert
if (!playbackStarted && bytesBuffered + data.size >= prerollBytes) {
@@ -157,6 +175,19 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex
}
bytesBuffered += data.size
}
// Trailing-Silence damit die letzten echten Samples garantiert
// durch das Hardware-Buffering kommen bevor stop() sie abschneidet
val trailingBytes = ((sampleRate * channels * 2) * TRAILING_SILENCE_SECONDS).toInt() and 0x7FFFFFFE
if (trailingBytes > 0 && !writerShouldStop) {
val silence = ByteArray(trailingBytes)
var silOff = 0
while (silOff < silence.size && !writerShouldStop) {
val w = t.write(silence, silOff, silence.size - silOff)
if (w <= 0) break
silOff += w
}
bytesBuffered += silence.size
}
} catch (e: Exception) {
Log.w(TAG, "Writer-Thread Fehler: ${e.message}")
} finally {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "aria-cockpit",
"version": "0.0.5.9",
"version": "0.0.6.0",
"private": true,
"scripts": {
"android": "react-native run-android",