diff --git a/android/android/app/src/main/res/raw/wake_ready_sound.mp3 b/android/android/app/src/main/res/raw/wake_ready_sound.mp3 new file mode 100644 index 0000000..916d73c Binary files /dev/null and b/android/android/app/src/main/res/raw/wake_ready_sound.mp3 differ diff --git a/android/sounds/Airplane-ding-dong.mp2 b/android/sounds/Airplane-ding-dong.mp2 new file mode 100644 index 0000000..6fc5437 Binary files /dev/null and b/android/sounds/Airplane-ding-dong.mp2 differ diff --git a/android/src/screens/ChatScreen.tsx b/android/src/screens/ChatScreen.tsx index 51badf0..34bfc36 100644 --- a/android/src/screens/ChatScreen.tsx +++ b/android/src/screens/ChatScreen.tsx @@ -26,6 +26,7 @@ import rvs, { RVSMessage, ConnectionState } from '../services/rvs'; import audioService from '../services/audio'; import wakeWordService from '../services/wakeword'; import phoneCallService from '../services/phoneCall'; +import { playWakeReadySound } from '../services/wakeReadySound'; import updateService from '../services/updater'; import VoiceButton from '../components/VoiceButton'; import FileUpload, { FileData } from '../components/FileUpload'; @@ -496,8 +497,10 @@ const ChatScreen: React.FC = () => { if (started) { // Erst JETZT signalisieren dass das Mikro wirklich offen ist — // vorher war's noch in der Init-Phase. So weiss der User exakt - // ab wann er reden kann. + // ab wann er reden kann. "Bereit"-Sound (Ding-Dong) ist optional + // ueber Settings → Wake-Word abschaltbar. ToastAndroid.show('🎤 Mikro offen — sprich jetzt', ToastAndroid.SHORT); + playWakeReadySound().catch(() => {}); } else { // Mikrofon nicht verfuegbar, naechsten Versuch wakeWordService.resume(); diff --git a/android/src/screens/SettingsScreen.tsx b/android/src/screens/SettingsScreen.tsx index f151757..068c138 100644 --- a/android/src/screens/SettingsScreen.tsx +++ b/android/src/screens/SettingsScreen.tsx @@ -44,6 +44,11 @@ import { TTS_SPEED_MAX, TTS_SPEED_STORAGE_KEY, } from '../services/audio'; +import { + isWakeReadySoundEnabled, + setWakeReadySoundEnabled, + playWakeReadySound, +} from '../services/wakeReadySound'; import wakeWordService, { WAKE_KEYWORDS, KEYWORD_LABELS, @@ -122,6 +127,7 @@ const SettingsScreen: React.FC = () => { const [ttsSpeed, setTtsSpeed] = useState(TTS_SPEED_DEFAULT); const [wakeKeyword, setWakeKeyword] = useState(DEFAULT_KEYWORD); const [wakeStatus, setWakeStatus] = useState(''); + const [wakeReadySound, setWakeReadySound] = useState(true); const [editingPath, setEditingPath] = useState(false); const [xttsVoice, setXttsVoice] = useState(''); const [loadingVoice, setLoadingVoice] = useState(null); @@ -194,6 +200,7 @@ const SettingsScreen: React.FC = () => { AsyncStorage.getItem(WAKE_KEYWORD_STORAGE).then(saved => { if (saved && (WAKE_KEYWORDS as readonly string[]).includes(saved)) setWakeKeyword(saved); }); + isWakeReadySoundEnabled().then(setWakeReadySound); AsyncStorage.getItem('aria_xtts_voice').then(saved => { if (saved) setXttsVoice(saved); }); @@ -828,6 +835,31 @@ const SettingsScreen: React.FC = () => { {!!wakeStatus && ( {wakeStatus} )} + + + + Bereit-Sound abspielen + + Kurzer Ding-Dong wenn das Mikro nach Wake-Word offen ist — + akustische Bestaetigung dass du jetzt sprechen darfst. + + + { + setWakeReadySound(val); + await setWakeReadySoundEnabled(val); + if (val) { + // Direkt eine Vorschau abspielen damit der User weiss wie's klingt. + // playWakeReadySound checked das gerade gesetzte Flag — wenn val=true, + // wird abgespielt; bei false bleibt es still. + setTimeout(() => playWakeReadySound().catch(() => {}), 150); + } + }} + trackColor={{ false: '#2A2A3E', true: '#0096FF' }} + thumbColor={wakeReadySound ? '#FFFFFF' : '#666680'} + /> + )} diff --git a/android/src/services/wakeReadySound.ts b/android/src/services/wakeReadySound.ts new file mode 100644 index 0000000..df632cf --- /dev/null +++ b/android/src/services/wakeReadySound.ts @@ -0,0 +1,71 @@ +/** + * Spielt einen kurzen "Bereit"-Sound (Airplane Ding-Dong) wenn das Mikrofon + * nach Wake-Word-Erkennung wirklich offen ist. Datei liegt in + * android/app/src/main/res/raw/wake_ready_sound.mp3 — wird ueber Android's + * Resource-System per react-native-sound abgespielt. + * + * Toggle: AsyncStorage-Key 'aria_wake_ready_sound_enabled' (default true). + */ + +import Sound from 'react-native-sound'; +import AsyncStorage from '@react-native-async-storage/async-storage'; + +export const WAKE_READY_SOUND_STORAGE_KEY = 'aria_wake_ready_sound_enabled'; + +Sound.setCategory('Playback', false); + +let cachedSound: Sound | null = null; +let cachedFailed = false; + +function getSound(): Promise { + if (cachedFailed) return Promise.resolve(null); + if (cachedSound) return Promise.resolve(cachedSound); + return new Promise(resolve => { + const s = new Sound('wake_ready_sound', Sound.MAIN_BUNDLE, (err) => { + if (err) { + console.warn('[WakeReadySound] Konnte nicht geladen werden:', err); + cachedFailed = true; + resolve(null); + return; + } + cachedSound = s; + resolve(s); + }); + }); +} + +/** True wenn der User den "Bereit"-Sound aktiviert hat. Default: true. */ +export async function isWakeReadySoundEnabled(): Promise { + try { + const raw = await AsyncStorage.getItem(WAKE_READY_SOUND_STORAGE_KEY); + if (raw === null) return true; // Default an + return raw === 'true'; + } catch { + return true; + } +} + +export async function setWakeReadySoundEnabled(enabled: boolean): Promise { + try { + await AsyncStorage.setItem(WAKE_READY_SOUND_STORAGE_KEY, String(enabled)); + } catch {} +} + +/** Spielt den Bereit-Sound einmal ab — non-blocking. Wenn der User ihn + * in den Settings deaktiviert hat oder die Datei nicht ladbar ist, + * passiert einfach nichts. */ +export async function playWakeReadySound(): Promise { + if (!(await isWakeReadySoundEnabled())) return; + const s = await getSound(); + if (!s) return; + try { + s.stop(() => { + s.setCurrentTime(0); + s.play((success) => { + if (!success) console.warn('[WakeReadySound] Wiedergabe fehlgeschlagen'); + }); + }); + } catch (e) { + console.warn('[WakeReadySound] play() Exception:', e); + } +}