feat: App TTS-Einstellungen vereinfacht + Mund-Button fuer lokales Muten

SettingsScreen:
- Piper-Reste entfernt (defaultVoice, highlightVoice, Speed-Slider,
  Highlight-Trigger-Info)
- Nur noch EIN Toggle 'Sprachausgabe auf diesem Geraet' — geraetelokal,
  persistent in aria_tts_enabled (AsyncStorage)
- Keine Config-Propagation mehr via RVS (das waere ja global gewesen)
- Hinweis dass Stimme + Voice-Cloning zentral in der Diagnose sind

ChatScreen: Mund-Button (👄 / 🤐)
- Neben Ohr-Button im Eingabebereich, NUR sichtbar wenn TTS im Setting
  grundsaetzlich aktiv ist
- Tap toggelt Mute: 👄 an / 🤐 rot gemutet
- Persistent in aria_tts_muted (AsyncStorage)
- Stoppt bei Muten sofort laufende Wiedergabe (stopPlayback)
- Settings-Toggle wird alle 2s gepollt damit Aenderungen greifen
  (einfache Loesung ohne globalen State-Context)

Audio-Handling respektiert lokalen Zustand
- Incoming audio/audio_pcm: nur abspielen wenn ttsDeviceEnabled && !ttsMuted
- Cache wird TROTZDEM immer geschrieben — Play-Button funktioniert
  spaeter aus Cache, auch waehrend Mute
- audioService.handlePcmChunk akzeptiert silent-Flag: skipt AudioTrack
  aber baut weiterhin den WAV-Cache pro messageId

Jedes Android-Geraet mit der App hat seinen eigenen Mute-Zustand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-19 22:33:36 +02:00
parent f801d99748
commit 40e48b046b
3 changed files with 78 additions and 154 deletions
+24 -22
View File
@@ -335,7 +335,8 @@ class AudioService {
}
}
/** Einen PCM-Chunk aus einer audio_pcm Nachricht empfangen und spielen/cachen.
/** 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. */
async handlePcmChunk(payload: {
base64: string;
@@ -344,8 +345,10 @@ class AudioService {
messageId?: string;
chunk?: number;
final?: boolean;
silent?: boolean;
}): Promise<string> {
if (!PcmStreamPlayer) {
const silent = !!payload.silent;
if (!silent && !PcmStreamPlayer) {
console.warn('[Audio] PcmStreamPlayer Native Module nicht verfuegbar');
return '';
}
@@ -358,10 +361,8 @@ class AudioService {
// Neuer Stream? (messageId Wechsel oder nicht aktiv)
if (!this.pcmStreamActive || this.pcmMessageId !== messageId) {
// Vorherigen Stream clean beenden (falls da)
if (this.pcmStreamActive) {
try { await PcmStreamPlayer.stop(); } catch {}
// Altes Buffer verwerfen (wurde nicht final — neue Message kam dazwischen)
if (this.pcmStreamActive && !silent) {
try { await PcmStreamPlayer!.stop(); } catch {}
this.pcmBuffer = [];
this.pcmBytesCollected = 0;
}
@@ -371,35 +372,36 @@ class AudioService {
this.pcmChannels = channels;
this.pcmBuffer = [];
this.pcmBytesCollected = 0;
try {
await PcmStreamPlayer.start(sampleRate, channels);
} catch (err) {
console.error('[Audio] PcmStreamPlayer.start fehlgeschlagen:', err);
this.pcmStreamActive = false;
return '';
if (!silent) {
try {
await PcmStreamPlayer!.start(sampleRate, channels);
} catch (err) {
console.error('[Audio] PcmStreamPlayer.start fehlgeschlagen:', err);
this.pcmStreamActive = false;
return '';
}
AudioFocus?.requestDuck().catch(() => {});
}
// Audio-Focus: andere Apps ducken
AudioFocus?.requestDuck().catch(() => {});
}
// Chunk abspielen + cachen
// Chunk — immer cachen, nur bei !silent auch abspielen
if (base64) {
try { await PcmStreamPlayer.writeChunk(base64); } catch (err) { console.warn('[Audio] writeChunk', err); }
// Buffer fuer Cache sammeln (wenn noch nicht zu gross)
if (!silent) {
try { await PcmStreamPlayer!.writeChunk(base64); } catch (err) { console.warn('[Audio] writeChunk', err); }
}
if (messageId && this.pcmBytesCollected < this.PCM_MAX_CACHE_BYTES) {
this.pcmBuffer.push(base64);
// 4 base64-chars ≈ 3 bytes — grobe Schaetzung
this.pcmBytesCollected += Math.floor(base64.length * 0.75);
}
}
if (isFinal) {
// Stream sauber beenden (spielt noch bis Puffer leer ist)
try { await PcmStreamPlayer.end(); } catch {}
if (!silent) {
try { await PcmStreamPlayer!.end(); } catch {}
AudioFocus?.release().catch(() => {});
}
this.pcmStreamActive = false;
AudioFocus?.release().catch(() => {});
// Aus gesammelten PCM-Chunks eine WAV-Datei fuer Replay bauen
if (messageId && this.pcmBuffer.length > 0) {
const audioPath = await this._savePcmBufferAsWav(messageId);
this.pcmBuffer = [];