From 0428c06612e4b7e0b0d7395b1711e4e998c5d15d Mon Sep 17 00:00:00 2001 From: duffyduck Date: Fri, 10 Apr 2026 02:21:19 +0200 Subject: [PATCH] fix: Audio preloading to prevent stuttering, remove trailing dots for XTTS - Preload next audio while current plays (eliminates gap between sentences) - Remove trailing dots from sentences (XTTS reads them aloud) - stopPlayback cleans up preloaded audio Co-Authored-By: Claude Opus 4.6 (1M context) --- android/src/services/audio.ts | 85 ++++++++++++++++++++++++----------- xtts/bridge.js | 5 ++- 2 files changed, 64 insertions(+), 26 deletions(-) diff --git a/android/src/services/audio.ts b/android/src/services/audio.ts index 6588c6c..c484f74 100644 --- a/android/src/services/audio.ts +++ b/android/src/services/audio.ts @@ -58,6 +58,8 @@ class AudioService { // Audio-Queue fuer sequentielle TTS-Wiedergabe private audioQueue: string[] = []; private isPlaying: boolean = false; + private preloadedSound: Sound | null = null; + private preloadedPath: string = ''; // VAD State private vadEnabled: boolean = false; @@ -220,35 +222,62 @@ class AudioService { } this.isPlaying = true; - const base64Data = this.audioQueue.shift()!; - try { - const tmpPath = `${RNFS.CachesDirectoryPath}/aria_tts_${Date.now()}.wav`; - await RNFS.writeFile(tmpPath, base64Data, 'base64'); + // Preloaded Sound verwenden wenn verfuegbar, sonst neu laden + let sound: Sound; + let soundPath: string; - this.currentSound = new Sound(tmpPath, '', (error) => { - if (error) { - console.error('[Audio] Fehler beim Laden:', error); - RNFS.unlink(tmpPath).catch(() => {}); - this._playNext(); - return; - } - this.currentSound?.play((success) => { - if (success) { - console.log('[Audio] Wiedergabe abgeschlossen'); - } else { - console.warn('[Audio] Wiedergabe fehlgeschlagen'); - } - this.currentSound?.release(); - this.currentSound = null; - RNFS.unlink(tmpPath).catch(() => {}); - // Naechstes Audio abspielen - this._playNext(); + if (this.preloadedSound) { + sound = this.preloadedSound; + soundPath = this.preloadedPath; + this.preloadedSound = null; + this.preloadedPath = ''; + // Daten aus Queue entfernen (wurde schon preloaded) + this.audioQueue.shift(); + } else { + const base64Data = this.audioQueue.shift()!; + try { + soundPath = `${RNFS.CachesDirectoryPath}/aria_tts_${Date.now()}.wav`; + await RNFS.writeFile(soundPath, base64Data, 'base64'); + sound = await new Promise((resolve, reject) => { + const s = new Sound(soundPath, '', (err) => err ? reject(err) : resolve(s)); }); - }); - } catch (err) { - console.error('[Audio] Wiedergabefehler:', err); + } catch (err) { + console.error('[Audio] Laden fehlgeschlagen:', err); + this._playNext(); + return; + } + } + + this.currentSound = sound; + + // Naechstes Audio schon vorbereiten waehrend dieses abspielt + this._preloadNext(); + + sound.play((success) => { + if (!success) console.warn('[Audio] Wiedergabe fehlgeschlagen'); + sound.release(); + this.currentSound = null; + RNFS.unlink(soundPath).catch(() => {}); this._playNext(); + }); + } + + /** Naechstes Audio im Hintergrund vorladen (verhindert Stottern) */ + private async _preloadNext(): Promise { + if (this.audioQueue.length === 0 || this.preloadedSound) return; + + const base64Data = this.audioQueue[0]; // Nicht shift — bleibt in Queue + try { + const tmpPath = `${RNFS.CachesDirectoryPath}/aria_tts_pre_${Date.now()}.wav`; + await RNFS.writeFile(tmpPath, base64Data, 'base64'); + this.preloadedSound = await new Promise((resolve, reject) => { + const s = new Sound(tmpPath, '', (err) => err ? reject(err) : resolve(s)); + }); + this.preloadedPath = tmpPath; + } catch { + this.preloadedSound = null; + this.preloadedPath = ''; } } @@ -261,6 +290,12 @@ class AudioService { this.currentSound.release(); this.currentSound = null; } + if (this.preloadedSound) { + this.preloadedSound.release(); + this.preloadedSound = null; + if (this.preloadedPath) RNFS.unlink(this.preloadedPath).catch(() => {}); + this.preloadedPath = ''; + } } // --- Status & Callbacks --- diff --git a/xtts/bridge.js b/xtts/bridge.js index 7a1e04d..0300474 100644 --- a/xtts/bridge.js +++ b/xtts/bridge.js @@ -101,7 +101,10 @@ async function handleTTSRequest(payload) { const cleanText = text.replace(/\*\*([^*]+)\*\*/g, "$1").trim(); // Text in Saetze aufteilen (sequentiell rendern fuer korrekte Reihenfolge) - const sentences = cleanText.split(/(?<=[.!?])\s+/).map(s => s.trim()).filter(s => s.length > 0); + const sentences = cleanText.split(/(?<=[.!?])\s+/) + .map(s => s.trim()) + .filter(s => s.length > 0) + .map(s => s.replace(/[.]+$/, '')); // Punkt am Ende entfernen (XTTS liest ihn sonst vor) if (sentences.length === 0) return; log(`TTS-Request: "${cleanText.slice(0, 60)}..." (${sentences.length} Saetze, voice: ${voice || "default"}, lang: ${language || "de"})`);