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) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 02:21:19 +02:00
parent a7eb3cf433
commit 0428c06612
2 changed files with 64 additions and 26 deletions
+54 -19
View File
@@ -58,6 +58,8 @@ class AudioService {
// Audio-Queue fuer sequentielle TTS-Wiedergabe // Audio-Queue fuer sequentielle TTS-Wiedergabe
private audioQueue: string[] = []; private audioQueue: string[] = [];
private isPlaying: boolean = false; private isPlaying: boolean = false;
private preloadedSound: Sound | null = null;
private preloadedPath: string = '';
// VAD State // VAD State
private vadEnabled: boolean = false; private vadEnabled: boolean = false;
@@ -220,35 +222,62 @@ class AudioService {
} }
this.isPlaying = true; this.isPlaying = true;
// Preloaded Sound verwenden wenn verfuegbar, sonst neu laden
let sound: Sound;
let soundPath: string;
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()!; const base64Data = this.audioQueue.shift()!;
try { try {
const tmpPath = `${RNFS.CachesDirectoryPath}/aria_tts_${Date.now()}.wav`; soundPath = `${RNFS.CachesDirectoryPath}/aria_tts_${Date.now()}.wav`;
await RNFS.writeFile(tmpPath, base64Data, 'base64'); await RNFS.writeFile(soundPath, base64Data, 'base64');
sound = await new Promise<Sound>((resolve, reject) => {
this.currentSound = new Sound(tmpPath, '', (error) => { const s = new Sound(soundPath, '', (err) => err ? reject(err) : resolve(s));
if (error) { });
console.error('[Audio] Fehler beim Laden:', error); } catch (err) {
RNFS.unlink(tmpPath).catch(() => {}); console.error('[Audio] Laden fehlgeschlagen:', err);
this._playNext(); this._playNext();
return; return;
} }
this.currentSound?.play((success) => {
if (success) {
console.log('[Audio] Wiedergabe abgeschlossen');
} else {
console.warn('[Audio] Wiedergabe fehlgeschlagen');
} }
this.currentSound?.release();
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; this.currentSound = null;
RNFS.unlink(tmpPath).catch(() => {}); RNFS.unlink(soundPath).catch(() => {});
// Naechstes Audio abspielen
this._playNext(); this._playNext();
}); });
}
/** Naechstes Audio im Hintergrund vorladen (verhindert Stottern) */
private async _preloadNext(): Promise<void> {
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<Sound>((resolve, reject) => {
const s = new Sound(tmpPath, '', (err) => err ? reject(err) : resolve(s));
}); });
} catch (err) { this.preloadedPath = tmpPath;
console.error('[Audio] Wiedergabefehler:', err); } catch {
this._playNext(); this.preloadedSound = null;
this.preloadedPath = '';
} }
} }
@@ -261,6 +290,12 @@ class AudioService {
this.currentSound.release(); this.currentSound.release();
this.currentSound = null; this.currentSound = null;
} }
if (this.preloadedSound) {
this.preloadedSound.release();
this.preloadedSound = null;
if (this.preloadedPath) RNFS.unlink(this.preloadedPath).catch(() => {});
this.preloadedPath = '';
}
} }
// --- Status & Callbacks --- // --- Status & Callbacks ---
+4 -1
View File
@@ -101,7 +101,10 @@ async function handleTTSRequest(payload) {
const cleanText = text.replace(/\*\*([^*]+)\*\*/g, "$1").trim(); const cleanText = text.replace(/\*\*([^*]+)\*\*/g, "$1").trim();
// Text in Saetze aufteilen (sequentiell rendern fuer korrekte Reihenfolge) // 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; if (sentences.length === 0) return;
log(`TTS-Request: "${cleanText.slice(0, 60)}..." (${sentences.length} Saetze, voice: ${voice || "default"}, lang: ${language || "de"})`); log(`TTS-Request: "${cleanText.slice(0, 60)}..." (${sentences.length} Saetze, voice: ${voice || "default"}, lang: ${language || "de"})`);