feat(audio): "Bereit"-Sound (Ding-Dong) wenn Mikro nach Wake-Word offen ist
Kurzer akustischer Hinweis (Airplane Ding-Dong, 20KB MP3) bei audioService.startRecording-Erfolg im Wake-Word-Pfad — User weiss exakt ab wann er reden darf, statt das Toast nur zu sehen. Quelldatei: android/sounds/Airplane-ding-dong.mp2 → ffmpeg-konvertiert zu MP3 64kbps, abgelegt in android/app/src/main/res/raw/ damit Android sie als Resource laden kann. Toggle in App-Settings → Wake-Word, default aktiv. Bei Aktivierung spielt direkt eine Vorschau ab damit man weiss wie's klingt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -26,6 +26,7 @@ import rvs, { RVSMessage, ConnectionState } from '../services/rvs';
|
|||||||
import audioService from '../services/audio';
|
import audioService from '../services/audio';
|
||||||
import wakeWordService from '../services/wakeword';
|
import wakeWordService from '../services/wakeword';
|
||||||
import phoneCallService from '../services/phoneCall';
|
import phoneCallService from '../services/phoneCall';
|
||||||
|
import { playWakeReadySound } from '../services/wakeReadySound';
|
||||||
import updateService from '../services/updater';
|
import updateService from '../services/updater';
|
||||||
import VoiceButton from '../components/VoiceButton';
|
import VoiceButton from '../components/VoiceButton';
|
||||||
import FileUpload, { FileData } from '../components/FileUpload';
|
import FileUpload, { FileData } from '../components/FileUpload';
|
||||||
@@ -496,8 +497,10 @@ const ChatScreen: React.FC = () => {
|
|||||||
if (started) {
|
if (started) {
|
||||||
// Erst JETZT signalisieren dass das Mikro wirklich offen ist —
|
// Erst JETZT signalisieren dass das Mikro wirklich offen ist —
|
||||||
// vorher war's noch in der Init-Phase. So weiss der User exakt
|
// 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);
|
ToastAndroid.show('🎤 Mikro offen — sprich jetzt', ToastAndroid.SHORT);
|
||||||
|
playWakeReadySound().catch(() => {});
|
||||||
} else {
|
} else {
|
||||||
// Mikrofon nicht verfuegbar, naechsten Versuch
|
// Mikrofon nicht verfuegbar, naechsten Versuch
|
||||||
wakeWordService.resume();
|
wakeWordService.resume();
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ import {
|
|||||||
TTS_SPEED_MAX,
|
TTS_SPEED_MAX,
|
||||||
TTS_SPEED_STORAGE_KEY,
|
TTS_SPEED_STORAGE_KEY,
|
||||||
} from '../services/audio';
|
} from '../services/audio';
|
||||||
|
import {
|
||||||
|
isWakeReadySoundEnabled,
|
||||||
|
setWakeReadySoundEnabled,
|
||||||
|
playWakeReadySound,
|
||||||
|
} from '../services/wakeReadySound';
|
||||||
import wakeWordService, {
|
import wakeWordService, {
|
||||||
WAKE_KEYWORDS,
|
WAKE_KEYWORDS,
|
||||||
KEYWORD_LABELS,
|
KEYWORD_LABELS,
|
||||||
@@ -122,6 +127,7 @@ const SettingsScreen: React.FC = () => {
|
|||||||
const [ttsSpeed, setTtsSpeed] = useState<number>(TTS_SPEED_DEFAULT);
|
const [ttsSpeed, setTtsSpeed] = useState<number>(TTS_SPEED_DEFAULT);
|
||||||
const [wakeKeyword, setWakeKeyword] = useState<string>(DEFAULT_KEYWORD);
|
const [wakeKeyword, setWakeKeyword] = useState<string>(DEFAULT_KEYWORD);
|
||||||
const [wakeStatus, setWakeStatus] = useState<string>('');
|
const [wakeStatus, setWakeStatus] = useState<string>('');
|
||||||
|
const [wakeReadySound, setWakeReadySound] = useState<boolean>(true);
|
||||||
const [editingPath, setEditingPath] = useState(false);
|
const [editingPath, setEditingPath] = useState(false);
|
||||||
const [xttsVoice, setXttsVoice] = useState('');
|
const [xttsVoice, setXttsVoice] = useState('');
|
||||||
const [loadingVoice, setLoadingVoice] = useState<string | null>(null);
|
const [loadingVoice, setLoadingVoice] = useState<string | null>(null);
|
||||||
@@ -194,6 +200,7 @@ const SettingsScreen: React.FC = () => {
|
|||||||
AsyncStorage.getItem(WAKE_KEYWORD_STORAGE).then(saved => {
|
AsyncStorage.getItem(WAKE_KEYWORD_STORAGE).then(saved => {
|
||||||
if (saved && (WAKE_KEYWORDS as readonly string[]).includes(saved)) setWakeKeyword(saved);
|
if (saved && (WAKE_KEYWORDS as readonly string[]).includes(saved)) setWakeKeyword(saved);
|
||||||
});
|
});
|
||||||
|
isWakeReadySoundEnabled().then(setWakeReadySound);
|
||||||
AsyncStorage.getItem('aria_xtts_voice').then(saved => {
|
AsyncStorage.getItem('aria_xtts_voice').then(saved => {
|
||||||
if (saved) setXttsVoice(saved);
|
if (saved) setXttsVoice(saved);
|
||||||
});
|
});
|
||||||
@@ -828,6 +835,31 @@ const SettingsScreen: React.FC = () => {
|
|||||||
{!!wakeStatus && (
|
{!!wakeStatus && (
|
||||||
<Text style={{marginTop: 8, fontSize: 12, color: '#8888AA'}}>{wakeStatus}</Text>
|
<Text style={{marginTop: 8, fontSize: 12, color: '#8888AA'}}>{wakeStatus}</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<View style={[styles.toggleRow, {marginTop: 20, borderTopWidth: 1, borderTopColor: '#1E1E2E', paddingTop: 16}]}>
|
||||||
|
<View style={styles.toggleInfo}>
|
||||||
|
<Text style={styles.toggleLabel}>Bereit-Sound abspielen</Text>
|
||||||
|
<Text style={styles.toggleHint}>
|
||||||
|
Kurzer Ding-Dong wenn das Mikro nach Wake-Word offen ist —
|
||||||
|
akustische Bestaetigung dass du jetzt sprechen darfst.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Switch
|
||||||
|
value={wakeReadySound}
|
||||||
|
onValueChange={async (val) => {
|
||||||
|
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'}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</>)}
|
</>)}
|
||||||
|
|
||||||
|
|||||||
@@ -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<Sound | null> {
|
||||||
|
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<boolean> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user