From 23ca815cb25f613ec6f0f6d62f2c2236fa751aae Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 25 Apr 2026 11:58:27 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20handlePcmChunk=20serialisiert=20?= =?UTF-8?q?=E2=80=94=20fixes=20Race=20bei=20kurzen=20Streams?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bei kurzen Saetzen (nur ein paar Chunks + sofort final) konnten die async handlePcmChunk-Calls parallel laufen. Der final-Chunk konnte native end() aufrufen BEVOR der vorherige Chunk seinen native start() abgeschlossen hatte. Der Writer-Thread startete dann mit endRequested bereits true, verarbeitete keine Chunks sauber → Audio ging verloren. Fix: Wrapper chaint alle Chunk-Calls an eine Promise-Queue: _pcmChunkQueue = Promise.resolve() handlePcmChunk → _pcmChunkQueue.then(() => _handlePcmChunkImpl(p)) So werden start/writeChunk/end garantiert in der richtigen Reihenfolge verarbeitet. Der API-Contract bleibt (gleiches return-Promise). Co-Authored-By: Claude Opus 4.7 (1M context) --- android/src/services/audio.ts | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/android/src/services/audio.ts b/android/src/services/audio.ts index a82670c..a0cd062 100644 --- a/android/src/services/audio.ts +++ b/android/src/services/audio.ts @@ -459,7 +459,13 @@ class AudioService { /** Einen PCM-Chunk aus einer audio_pcm Nachricht empfangen. * silent=true → nur cachen, nicht abspielen (z.B. wenn TTS geraetelokal gemutet). - * Gibt bei final=true den Cache-Pfad zurueck (file://) oder '' wenn nicht gecached. */ + * Gibt bei final=true den Cache-Pfad zurueck (file://) oder '' wenn nicht gecached. + * + * Wrapper serialisiert aufeinanderfolgende Chunk-Calls via Promise-Queue — + * sonst gabs bei kurzen Streams einen Race: final-Chunk konnte `end()` rufen + * BEVOR der vorherige `start()` im Native-Modul fertig war. Der Writer- + * Thread sah dann endRequested=true ohne jemals Chunks zu verarbeiten. */ + private _pcmChunkQueue: Promise = Promise.resolve(); async handlePcmChunk(payload: { base64: string; sampleRate?: number; @@ -468,6 +474,24 @@ class AudioService { chunk?: number; final?: boolean; silent?: boolean; + }): Promise { + const p = this._pcmChunkQueue.then(() => this._handlePcmChunkImpl(payload)).catch(err => { + console.warn('[Audio] handlePcmChunk queued err:', err); + return ''; + }); + // Chain only on the side effect — callers still get the per-call result + this._pcmChunkQueue = p; + return p; + } + + private async _handlePcmChunkImpl(payload: { + base64: string; + sampleRate?: number; + channels?: number; + messageId?: string; + chunk?: number; + final?: boolean; + silent?: boolean; }): Promise { const silent = !!payload.silent; if (!silent && !PcmStreamPlayer) {