From 5bdcc3c65b7ae81c9d292408cf5188c039a8ddae Mon Sep 17 00:00:00 2001 From: duffyduck Date: Thu, 7 May 2026 08:24:26 +0200 Subject: [PATCH] feat(vad): Stille-Pegel manuell in Settings + Info-Modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wenn die adaptive Baseline-Logik in einer Umgebung nicht zuverlaessig greift (Stefan: "manchmal funktioniert die Stille-Erkennung nicht"), kann der User die Schwelle jetzt manuell setzen. Settings → Spracheingabe: - "Stille-Pegel (dB)" mit −1/+1 Buttons + "Auf automatisch zuruecksetzen" - Range −55 bis −15 dB, default "auto" (= adaptive Baseline) - Info-Icon (i) oeffnet Modal mit Erklaerung: • dB-Skala (negativ, naeher 0 = lauter) • Faustregel-Pegel mit Farb-Code (−45 sensibel, −38 ausgewogen, −25 robust) • Klarstellung "niedrigere Zahl = sensibler" audio.ts: - VAD_SILENCE_DB_OVERRIDE_KEY in AsyncStorage - loadVadSilenceDbOverride() liefert null oder Zahl - startRecording: wenn Override gesetzt, Adaptive-Baseline uebersteuert. Speech-Schwelle wird auf Override + 10 dB gesetzt. Toast zeigt "VAD: manuell stille>-XX dB" Co-Authored-By: Claude Opus 4.7 (1M context) --- android/src/screens/SettingsScreen.tsx | 144 +++++++++++++++++++++++++ android/src/services/audio.ts | 38 ++++++- issue.md | 1 + 3 files changed, 181 insertions(+), 2 deletions(-) diff --git a/android/src/screens/SettingsScreen.tsx b/android/src/screens/SettingsScreen.tsx index 5121632..8dc4de2 100644 --- a/android/src/screens/SettingsScreen.tsx +++ b/android/src/screens/SettingsScreen.tsx @@ -17,6 +17,7 @@ import { Platform, ToastAndroid, ActivityIndicator, + Modal, } from 'react-native'; import AsyncStorage from '@react-native-async-storage/async-storage'; import RNFS from 'react-native-fs'; @@ -39,6 +40,10 @@ import { MAX_RECORDING_MIN_SEC, MAX_RECORDING_MAX_SEC, MAX_RECORDING_STORAGE_KEY, + VAD_SILENCE_DB_DEFAULT, + VAD_SILENCE_DB_MIN, + VAD_SILENCE_DB_MAX, + VAD_SILENCE_DB_OVERRIDE_KEY, TTS_SPEED_DEFAULT, TTS_SPEED_MIN, TTS_SPEED_MAX, @@ -124,6 +129,9 @@ const SettingsScreen: React.FC = () => { const [vadSilenceSec, setVadSilenceSec] = useState(VAD_SILENCE_DEFAULT_SEC); const [convWindowSec, setConvWindowSec] = useState(CONV_WINDOW_DEFAULT_SEC); const [maxRecordingSec, setMaxRecordingSec] = useState(MAX_RECORDING_DEFAULT_SEC); + // null = automatisch (adaptive Baseline), sonst manueller dB-Override + const [vadSilenceDb, setVadSilenceDb] = useState(null); + const [showVadInfo, setShowVadInfo] = useState(false); const [ttsSpeed, setTtsSpeed] = useState(TTS_SPEED_DEFAULT); const [wakeKeyword, setWakeKeyword] = useState(DEFAULT_KEYWORD); const [wakeStatus, setWakeStatus] = useState(''); @@ -194,6 +202,14 @@ const SettingsScreen: React.FC = () => { } } }); + AsyncStorage.getItem(VAD_SILENCE_DB_OVERRIDE_KEY).then(saved => { + if (saved != null && saved !== '') { + const n = parseFloat(saved); + if (isFinite(n) && n >= VAD_SILENCE_DB_MIN && n <= VAD_SILENCE_DB_MAX) { + setVadSilenceDb(n); + } + } + }); AsyncStorage.getItem(TTS_SPEED_STORAGE_KEY).then(saved => { if (saved != null) { const n = parseFloat(saved); @@ -782,8 +798,94 @@ const SettingsScreen: React.FC = () => { +1m + + + Stille-Pegel (dB) + setShowVadInfo(true)} style={styles.infoBtn}> + i + + + + Welcher Mikro-Pegel als "Stille" gilt. Standard: automatisch (Baseline aus + den ersten 500ms). Manuell setzen wenn Auto nicht zuverlaessig greift. + + + { + const next = vadSilenceDb == null + ? VAD_SILENCE_DB_DEFAULT - 1 + : Math.max(VAD_SILENCE_DB_MIN, vadSilenceDb - 1); + setVadSilenceDb(next); + AsyncStorage.setItem(VAD_SILENCE_DB_OVERRIDE_KEY, String(next)); + }} + > + −1 + + + {vadSilenceDb == null ? 'auto' : `${vadSilenceDb} dB`} + + { + const next = vadSilenceDb == null + ? VAD_SILENCE_DB_DEFAULT + 1 + : Math.min(VAD_SILENCE_DB_MAX, vadSilenceDb + 1); + setVadSilenceDb(next); + AsyncStorage.setItem(VAD_SILENCE_DB_OVERRIDE_KEY, String(next)); + }} + > + +1 + + + {vadSilenceDb != null && ( + { + setVadSilenceDb(null); + AsyncStorage.removeItem(VAD_SILENCE_DB_OVERRIDE_KEY); + }} + style={{alignSelf: 'center', marginTop: 8, paddingVertical: 6, paddingHorizontal: 12}} + > + ↻ Auf automatisch zuruecksetzen + + )} + setShowVadInfo(false)} + > + + + Stille-Pegel (dB) + + Lautstaerken werden in Dezibel (dB) gemessen — negative Werte, je + hoeher (naeher an 0), desto lauter.{'\n\n'} + Standard: automatisch. + Die App misst die ersten 500ms Hintergrundpegel und setzt die + Stille-Schwelle auf Baseline + 6 dB. Funktioniert in den meisten + Umgebungen.{'\n\n'} + Manuell: Pegel unter dem + eingestellten Wert gilt als "Stille" → Aufnahme stoppt.{'\n\n'} + Faustregel:{'\n'} + • −45 dB sehr empfindlich (stoppt schnell, auch bei Atmen){'\n'} + • −38 dB ausgewogen (typische Bueroumgebung){'\n'} + • −25 dB unempfindlich (laute Umgebung, nur klare Sprache zaehlt){'\n\n'} + Niedrigere Zahl (z.B. −50) = sensibler.{'\n'} + Hoehere Zahl (z.B. −20) = robuster gegen Hintergrundlaerm, + braucht aber lautere Sprache. + + setShowVadInfo(false)} + > + OK + + + + )} {/* === Wake-Word (komplett on-device, openWakeWord) === */} @@ -1635,6 +1737,48 @@ const styles = StyleSheet.create({ textAlign: 'center', }, + infoBtn: { + width: 22, + height: 22, + borderRadius: 11, + borderWidth: 1.5, + borderColor: '#0096FF', + alignItems: 'center', + justifyContent: 'center', + }, + infoBtnText: { + color: '#0096FF', + fontSize: 13, + fontWeight: '700', + fontStyle: 'italic', + lineHeight: 16, + }, + modalOverlay: { + flex: 1, + backgroundColor: 'rgba(0,0,0,0.7)', + justifyContent: 'center', + alignItems: 'center', + padding: 20, + }, + modalCard: { + backgroundColor: '#1E1E2E', + borderRadius: 14, + padding: 20, + maxWidth: 460, + width: '100%', + }, + modalTitle: { + color: '#FFFFFF', + fontSize: 18, + fontWeight: '700', + marginBottom: 12, + }, + modalText: { + color: '#E0E0F0', + fontSize: 14, + lineHeight: 20, + }, + keywordChip: { backgroundColor: '#1E1E2E', borderWidth: 1, diff --git a/android/src/services/audio.ts b/android/src/services/audio.ts index 578f639..bfc331b 100644 --- a/android/src/services/audio.ts +++ b/android/src/services/audio.ts @@ -85,6 +85,29 @@ const VAD_SPEECH_OFFSET_DB = 12; // sicheres Speech = Baseline + 12dB const VAD_BASELINE_SAMPLES = 5; // 5 × 100ms = 500ms Baseline const VAD_SPEECH_MIN_MS = 500; // ms Sprache bevor Aufnahme zaehlt — laenger = keine Huestler/Klopfer mehr +// Override fuer die Stille-Schwelle — wenn gesetzt, wird die adaptive Baseline +// ignoriert. Nuetzlich wenn die adaptive Logik in spezifischen Umgebungen +// nicht zuverlaessig greift. Range -55..-15 dB. Speech-Schwelle wird auf +// override+10 dB gesetzt (Speech muss klar lauter als Stille sein). +export const VAD_SILENCE_DB_DEFAULT = -38; // wenn User Manuell-Modus waehlt +export const VAD_SILENCE_DB_MIN = -55; // sehr empfindlich, fast jeder Pegel ist "Sprache" +export const VAD_SILENCE_DB_MAX = -15; // sehr unempfindlich, nur lautes Reden gilt +export const VAD_SILENCE_DB_OVERRIDE_KEY = 'aria_vad_silence_db_override'; + +/** Liefert den manuellen Override-Wert oder null wenn "automatisch". */ +export async function loadVadSilenceDbOverride(): Promise { + try { + const raw = await AsyncStorage.getItem(VAD_SILENCE_DB_OVERRIDE_KEY); + if (raw == null || raw === '') return null; + const n = parseFloat(raw); + if (!isFinite(n)) return null; + if (n < VAD_SILENCE_DB_MIN || n > VAD_SILENCE_DB_MAX) return null; + return n; + } catch { + return null; + } +} + // VAD-Stille (in Sekunden) — wie lange Sprechpause toleriert wird, bevor // die Aufnahme automatisch beendet wird. Einstellbar in den App-Settings. export const VAD_SILENCE_DEFAULT_SEC = 2.8; @@ -443,11 +466,22 @@ class AudioService { this.speechDetected = false; this.speechStartTime = 0; // VAD-Adaptive zurueckgesetzt: Baseline wird in den ersten 500ms neu - // gemessen. Bis dahin gelten die Fallback-Schwellen — die sind etwas - // empfindlicher als die alten Werte (-38 statt -45 fuer Stille). + // gemessen. Bis dahin gelten die Fallback-Schwellen. this.vadBaselineSamples = []; this.vadAdaptiveSilenceDb = VAD_SILENCE_FALLBACK_DB; this.vadAdaptiveSpeechDb = VAD_SPEECH_FALLBACK_DB; + + // Manueller Override aus Settings — wenn gesetzt, wird die adaptive + // Baseline-Messung uebersteuert. User-Wahl gewinnt vor Auto-Magic. + const dbOverride = await loadVadSilenceDbOverride(); + if (dbOverride != null) { + this.vadAdaptiveSilenceDb = dbOverride; + this.vadAdaptiveSpeechDb = dbOverride + 10; // Speech klar ueber Stille + this.vadBaselineSamples = new Array(VAD_BASELINE_SAMPLES).fill(0); // Baseline-Sammeln deaktivieren + const msg = `VAD: manuell stille>${dbOverride}dB`; + console.log('[Audio] %s', msg); + try { ToastAndroid.show(msg, ToastAndroid.SHORT); } catch {} + } this.setState('recording'); // Andere Apps waehrend der Aufnahme pausieren (Musik, Videos etc.) diff --git a/issue.md b/issue.md index 84f57b6..1bdd074 100644 --- a/issue.md +++ b/issue.md @@ -33,6 +33,7 @@ - [x] **Wake-Word pausiert bei Anruf**: phoneCall ruft pauseForCall (openWakeWord.stop) bei RINGING/OFFHOOK, resumeFromCall bei IDLE. Pre-Call-State wird gemerkt — armed bleibt armed, conversing degraded zu armed (User soll nicht in halbem Dialog landen) - [x] **App-Resume-Cooldown**: Wechsel von Background → Foreground triggert keinen falschen Wake-Word-Trigger mehr. AppState-Listener setzt 1.5s Cooldown in dem onWakeDetected-Events ignoriert werden (Audio-Pegel-Spike beim AudioFocus-Switch sonst als Wake-Word interpretiert) - [x] Background-Mikro robust: acquireBackgroundAudio('rec'/'wake') wird jetzt VOR AudioRecord.startRecording gerufen — Foreground-Service mit foregroundServiceType=microphone muss aktiv sein bevor das Mikro greift, sonst blockiert Android ab 11+ den Background-Zugriff +- [x] **Stille-Pegel manuell setzbar** (Settings → Spracheingabe): Override-Wert in dB von -55 bis -15, default "automatisch". Info-Button mit Modal erklaert die Skala (niedriger = sensibler, hoeher = robuster gegen Hintergrundlaerm). Bei manuell gesetztem Wert wird die adaptive Baseline ignoriert ### App Features