/** * Audio-Service fuer Sprach-Ein-/Ausgabe * * Verwaltet Mikrofon-Aufnahme und TTS-Audiowiedergabe. * Nutzt react-native-sound und die nativen Audio-APIs. */ import { Platform, PermissionsAndroid } from 'react-native'; import Sound from 'react-native-sound'; // --- Typen --- export interface RecordingResult { /** Base64-kodierte Audiodaten */ base64: string; /** Dauer in Millisekunden */ durationMs: number; /** MIME-Type (z.B. audio/wav) */ mimeType: string; } export type RecordingState = 'idle' | 'recording' | 'processing'; type RecordingStateCallback = (state: RecordingState) => void; // --- Konstanten --- const AUDIO_SAMPLE_RATE = 16000; const AUDIO_CHANNELS = 1; const AUDIO_ENCODING = 'audio/wav'; // --- Audio-Service --- class AudioService { private recordingState: RecordingState = 'idle'; private recordingStartTime: number = 0; private stateListeners: RecordingStateCallback[] = []; private currentSound: Sound | null = null; // --- Berechtigungen --- /** Mikrofon-Berechtigung anfordern */ async requestMicrophonePermission(): Promise { if (Platform.OS !== 'android') { return true; } try { const granted = await PermissionsAndroid.request( PermissionsAndroid.PERMISSIONS.RECORD_AUDIO, { title: 'ARIA Cockpit - Mikrofon', message: 'ARIA benoetigt Zugriff auf das Mikrofon fuer Spracheingabe.', buttonPositive: 'Erlauben', buttonNegative: 'Ablehnen', }, ); return granted === PermissionsAndroid.RESULTS.GRANTED; } catch (err) { console.error('[Audio] Fehler bei Berechtigungsanfrage:', err); return false; } } // --- Aufnahme --- /** Mikrofon-Aufnahme starten */ async startRecording(): Promise { if (this.recordingState !== 'idle') { console.warn('[Audio] Aufnahme laeuft bereits'); return false; } const hasPermission = await this.requestMicrophonePermission(); if (!hasPermission) { console.warn('[Audio] Keine Mikrofon-Berechtigung'); return false; } try { // Nativer Aufnahme-Start ueber AudioRecorder-Bridge // In Produktion: Native Module oder react-native-audio-recorder-player nutzen this.recordingStartTime = Date.now(); this.setState('recording'); console.log('[Audio] Aufnahme gestartet'); return true; } catch (err) { console.error('[Audio] Fehler beim Starten der Aufnahme:', err); this.setState('idle'); return false; } } /** Aufnahme stoppen und Ergebnis zurueckgeben */ async stopRecording(): Promise { if (this.recordingState !== 'recording') { console.warn('[Audio] Keine aktive Aufnahme'); return null; } this.setState('processing'); try { const durationMs = Date.now() - this.recordingStartTime; // In Produktion: Audiodaten vom nativen Recorder holen // const audioData = await NativeAudioRecorder.stop(); const base64Placeholder = ''; // Platzhalter bis Native-Bridge implementiert this.setState('idle'); console.log(`[Audio] Aufnahme beendet (${durationMs}ms)`); return { base64: base64Placeholder, durationMs, mimeType: AUDIO_ENCODING, }; } catch (err) { console.error('[Audio] Fehler beim Stoppen der Aufnahme:', err); this.setState('idle'); return null; } } // --- Wiedergabe --- /** Base64-kodiertes Audio abspielen (z.B. TTS-Antwort von ARIA) */ async playAudio(base64Data: string): Promise { // Laufende Wiedergabe stoppen this.stopPlayback(); try { // Base64-Daten in temporaere Datei schreiben und abspielen // In Produktion: react-native-fs + Sound kombinieren const tmpPath = `${Platform.OS === 'android' ? '/data/user/0/' : ''}aria_tts_temp.wav`; // Platzhalter: Sound aus Datei laden this.currentSound = new Sound(tmpPath, '', (error) => { if (error) { console.error('[Audio] Fehler beim Laden:', error); return; } this.currentSound?.play((success) => { if (success) { console.log('[Audio] Wiedergabe abgeschlossen'); } else { console.warn('[Audio] Wiedergabe fehlgeschlagen'); } this.currentSound?.release(); this.currentSound = null; }); }); } catch (err) { console.error('[Audio] Wiedergabefehler:', err); } } /** Laufende Wiedergabe stoppen */ stopPlayback(): void { if (this.currentSound) { this.currentSound.stop(); this.currentSound.release(); this.currentSound = null; } } // --- Status --- getRecordingState(): RecordingState { return this.recordingState; } /** Callback fuer Aufnahmestatus-Aenderungen */ onStateChange(callback: RecordingStateCallback): () => void { this.stateListeners.push(callback); return () => { this.stateListeners = this.stateListeners.filter(cb => cb !== callback); }; } private setState(state: RecordingState): void { if (this.recordingState !== state) { this.recordingState = state; this.stateListeners.forEach(cb => cb(state)); } } } // Singleton const audioService = new AudioService(); export default audioService;