From e9615d987e8fab14d182b7f49e464e6a3d698264 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 30 May 2026 23:18:53 +0200 Subject: [PATCH] fix(audio): playbackFinished-Listener feuern erst wenn AudioTrack wirklich durch ist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Race-Condition entdeckt im Log: nach jeder ARIA-Antwort lief endConversation 5s nach TTS-Start (= "letzter Chunk eingetroffen"), nicht wenn der AudioTrack-Hardware-Buffer wirklich am Ende war. ARIA sprach also noch hoerbar, waehrend OpenWakeWord schon re-armte. Folge: ARIAs eigene Stimme ging direkt nach AudioRecord.startRecording ins Mikro. Die OpenWakeWord-Sessions von AudioRecord und AudioTrack sind verschieden → AcousticEchoCanceler kann den Output nicht subtrahieren (kein gemeinsamer Reference-Stream). Threshold + Patience-State der Wake-Word-Inferenz wird durch ARIAs konstante Audio-Eingabe verwirrt, der naechste echte "Computer"-Trigger geht unter. Fix: Listener-Fire aus handlePcmChunk(isFinal=true) raus, dafuer in den schon existierenden PcmPlaybackFinished-Native-Event-Handler rein. Die Kotlin-Seite emittiert das Event aus dem Writer-Thread- finally-Block — also genau dann wenn AudioTrack alle Samples durchgeschrieben hat. Side-Effect: UI-Konsumenten von onPlaybackFinished sehen den "finished"-State jetzt 1-2s spaeter (= ehrlicher zur Realitaet, ist eigentlich eine UX-Verbesserung). --- android/src/services/audio.ts | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/android/src/services/audio.ts b/android/src/services/audio.ts index 793db02..7cd5903 100644 --- a/android/src/services/audio.ts +++ b/android/src/services/audio.ts @@ -341,8 +341,21 @@ class AudioService { try { const emitter = new NativeEventEmitter(NativeModules.PcmStreamPlayer as any); emitter.addListener('PcmPlaybackFinished', () => { - console.log('[Audio] PcmPlaybackFinished — Focus jetzt freigeben'); + console.log('[Audio] PcmPlaybackFinished — AudioTrack drained'); this._releaseFocusDeferred(); + // Erst HIER playbackFinished-Listener feuern — nicht schon beim + // Empfang des letzten PCM-Chunks (siehe handlePcmChunk). AudioTrack + // braucht nach end() noch 1-2s zum Drainen seines Hardware-Buffers. + // Wenn wir die Listener zu frueh feuern, re-armt OpenWakeWord + // waehrend ARIA noch hoerbar spricht → ARIAs Stimme verwirrt die + // Wake-Word-Detection (kein gemeinsames AEC zwischen AudioTrack- + // und AudioRecord-Session). Stefan-Reproduktion: nach jeder ARIA- + // Antwort schluckte das Wake-Word den naechsten Trigger. + import('./logger').then(m => m.reportAppDebug('audio.playback', + 'PcmPlaybackFinished native event → fire listeners')).catch(()=>{}); + this.playbackFinishedListeners.forEach(cb => { + try { cb(); } catch (e) { console.warn('[Audio] playbackFinished cb err:', e); } + }); }); } catch (err) { console.warn('[Audio] PcmPlaybackFinished-Subscription fehlgeschlagen:', err); @@ -1368,12 +1381,13 @@ class AudioService { // releasen den AudioFocus NICHT hier — der writer braucht u.U. noch // 30+ Sekunden bis der Buffer wirklich abgespielt ist. Den release // triggert das native Event "PcmPlaybackFinished" wenn AudioTrack - // wirklich am Ende ist (siehe ensurePlaybackFinishedListener). + // wirklich am Ende ist (siehe Constructor-PcmPlaybackFinished-Handler). + // + // playbackFinishedListeners feuern AUCH erst dort — frueher feuerten + // sie hier (beim Eintreffen des letzten Chunks), das fuehrte zu + // einem Race: OpenWakeWord re-armte waehrend AudioTrack noch hoerbar + // ARIAs Stimme abspielte → naechstes Wake-Word ging unter. try { await PcmStreamPlayer!.end(); } catch {} - // playbackFinished-Listener informieren (UI-Logik) - this.playbackFinishedListeners.forEach(cb => { - try { cb(); } catch (e) { console.warn('[Audio] playbackFinished cb err:', e); } - }); } this.pcmStreamActive = false;