first release 0.0.0.2
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* 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<boolean> {
|
||||
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<boolean> {
|
||||
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<RecordingResult | null> {
|
||||
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<void> {
|
||||
// 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;
|
||||
Reference in New Issue
Block a user