# Phase 2: Audio Capture & Playback Implementation ## Overview This document describes the implementation of audio recording and playback functionality for the voice-mobile React Native app using expo-audio. ## Dependencies Installed ### expo-audio (~1.0.13) - Cross-platform audio recording and playback library - Provides hooks: `useAudioRecorder` and `useAudioPlayer` - Supports iOS, Android, and Web platforms - Auto-configured via config plugin in app.json ### expo-file-system (~19.0.17) - File system operations for reading/writing audio files - New API using `File` and `Paths` classes - Required for converting between audio URIs and Blobs ## Permissions Configuration ### app.json Updates **iOS**: ```json { "ios": { "infoPlist": { "NSMicrophoneUsageDescription": "This app needs access to the microphone for voice commands." } } } ``` **Android**: ```json { "android": { "permissions": [ "RECORD_AUDIO" ] } } ``` **Plugins**: ```json { "plugins": [ "expo-audio" ] } ``` ## File Structure ``` packages/voice-mobile/ ├── hooks/ │ ├── use-audio-recorder.ts # Audio recording hook (NEW) │ └── use-audio-player.ts # Audio playback hook (NEW) ├── lib/ │ ├── audio-capture.ts # Legacy/factory version (NOT USED) │ └── audio-playback.ts # Legacy/factory version (NOT USED) ├── app/ │ ├── index.tsx # Updated with link to audio test │ └── audio-test.tsx # Audio test screen (NEW) └── app.json # Updated with permissions ``` ## Implementation Details ### Audio Recording (`use-audio-recorder.ts`) **Key Features**: - Matches web app audio constraints: - Sample rate: 16000 Hz (optimal for Whisper STT) - Channels: 1 (mono) - Format: M4A/AAC on native, WebM/Opus on web - Audio source: `voice_communication` (enables echo cancellation, noise suppression, auto gain control on Android) **Platform-Specific Configuration**: Android: ```typescript android: { extension: '.m4a', outputFormat: 'mpeg4', audioEncoder: 'aac', sampleRate: 16000, audioSource: 'voice_communication', // Key for voice optimization } ``` iOS: ```typescript ios: { extension: '.m4a', audioQuality: 127, // High quality sampleRate: 16000, linearPCMBitDepth: 16, linearPCMIsBigEndian: false, linearPCMIsFloat: false, } ``` Web: ```typescript web: { mimeType: 'audio/webm;codecs=opus', bitsPerSecond: 128000, } ``` **Usage**: ```typescript const audioRecorder = useAudioRecorder({ sampleRate: 16000, numberOfChannels: 1, }); // Start recording await audioRecorder.start(); // Stop and get Blob for WebSocket transmission const audioBlob = await audioRecorder.stop(); ``` **File Handling**: 1. Recording creates temporary file (managed by expo-audio) 2. On stop, reads file URI from `audioRecorder.uri` 3. Converts to Blob using expo-file-system's `File` class 4. Cleans up temporary file after conversion ### Audio Playback (`use-audio-player.ts`) **Key Features**: - Queue system matching web version - Supports playing Blobs from server (base64-encoded MP3) - Automatic cleanup of temporary files - Play/pause/resume/stop controls **Queue System**: ```typescript interface QueuedAudio { audioData: Blob; resolve: (duration: number) => void; reject: (error: Error) => void; } ``` **Usage**: ```typescript const audioPlayer = useAudioPlayer(); // Play audio (queued automatically) const duration = await audioPlayer.play(audioBlob); // Controls audioPlayer.pause(); audioPlayer.resume(); audioPlayer.stop(); audioPlayer.clearQueue(); ``` **File Handling**: 1. Converts Blob to ArrayBuffer 2. Creates temporary file in cache directory 3. Writes ArrayBuffer to file 4. Plays from file URI 5. Cleans up after playback ### Audio Test Screen (`audio-test.tsx`) **Features**: - Permission request/check on mount - Visual permission status indicator - Recording controls (Start/Stop) - Recording status display - Last recording size/type display - Playback controls (Play Last Recording, Stop) - Audio configuration info display - Styled with NativeWind (Tailwind CSS) **Navigation**: - Added link button on main screen (index.tsx) - Route: `/audio-test` ## Key Differences from Web Implementation ### 1. API Differences **Web (MediaRecorder)**: - `new MediaRecorder(stream, options)` - Events: `ondataavailable`, `onstop` - Returns Blob directly **React Native (expo-audio)**: - Hook-based: `useAudioRecorder(options)` - Returns Promise from `stop()` - URI property for file location - Requires manual Blob conversion ### 2. File System **Web**: - Direct Blob creation from chunks - No file system access needed **React Native**: - Must use file system (expo-file-system) - Temporary files in cache directory - Manual cleanup required ### 3. Permissions **Web**: - Runtime prompt via `getUserMedia()` - Browser-managed **React Native**: - Platform-specific: Info.plist (iOS), AndroidManifest.xml (Android) - Requires explicit `requestRecordingPermissionsAsync()` - Must be configured in app.json ### 4. Audio Processing **Web**: - WebRTC constraints: `echoCancellation`, `noiseSuppression`, `autoGainControl` **React Native**: - Android: `audioSource: 'voice_communication'` enables similar features - iOS: Built-in audio quality settings - Platform-specific implementations ## Platform-Specific Considerations ### iOS - M4A/AAC format (better compatibility) - Microphone permission required (Info.plist) - Audio session management handled by expo-audio - No manual echo cancellation needed (handled by system) ### Android - M4A/AAC format - RECORD_AUDIO permission required - `voice_communication` audio source crucial for voice quality - Enables: echo cancellation, noise suppression, auto gain control - Works well with VoIP and voice commands ### Web - WebM/Opus format (better compression) - MediaRecorder API used under the hood - Permissions via browser prompt - WebRTC audio processing ## Testing ### Manual Testing Steps 1. **Start Expo Development Server**: ```bash cd packages/voice-mobile npm start ``` 2. **Test on Web**: - Press 'w' in terminal or visit http://localhost:8081 - Click "Test Audio Capture & Playback" button - Grant microphone permission - Test recording and playback 3. **Test on iOS Simulator**: - Press 'i' in terminal - Note: iOS Simulator doesn't have microphone access - Test on physical device for full functionality 4. **Test on Android Emulator/Device**: - Press 'a' in terminal - Grant microphone permission - Test recording and playback ### Expected Behavior **Recording**: - Status shows "Recording..." with red indicator - Stop button becomes enabled - After stop, shows recording size and type **Playback**: - "Play Last Recording" enabled after recording - Plays back the recorded audio - Shows duration after playback completes **Permissions**: - Green indicator when granted - Red indicator when denied - Automatic request on mount ## Integration with WebSocket The audio hooks are designed to work seamlessly with the WebSocket client: ```typescript // In your component with WebSocket const audioRecorder = useAudioRecorder(); const { send } = useWebSocket(WS_URL); // Record and send to server async function handleRecord() { await audioRecorder.start(); // ... wait for user to stop const audioBlob = await audioRecorder.stop(); // Convert to base64 for WebSocket const reader = new FileReader(); reader.onload = () => { const base64 = reader.result.split(',')[1]; send({ type: 'audio_chunk', payload: { audio: base64, format: 'audio/m4a', isLast: true, }, }); }; reader.readAsDataURL(audioBlob); } // Play audio from server const audioPlayer = useAudioPlayer(); // When receiving audio from server function handleAudioOutput(message) { const audioBlob = base64ToBlob(message.payload.audio, 'audio/mp3'); audioPlayer.play(audioBlob); } ``` ## Future Enhancements 1. **Streaming Audio**: - Implement chunk-based recording for real-time streaming - Send audio chunks as they're recorded (not just on stop) - Requires updating expo-audio usage 2. **Audio Visualization**: - Add waveform display during recording - Use expo-audio's metering feature - Display audio levels in real-time 3. **Error Handling**: - Add retry logic for failed recordings - Better error messages for users - Fallback for unsupported formats 4. **Performance**: - Optimize Blob conversion - Reduce memory usage for long recordings - Implement audio compression if needed ## Common Issues & Solutions ### Issue: "No recording URI returned" **Cause**: Recording stopped before it started or expo-audio initialization failed **Solution**: Ensure `audioRecorder.record()` completes before stopping ### Issue: Playback fails with "Failed to load audio" **Cause**: File was deleted before playback or invalid Blob **Solution**: Ensure Blob is valid and file exists before playing ### Issue: Permission denied **Cause**: User denied microphone permission or app.json not configured **Solution**: 1. Check app.json has correct permission entries 2. Rebuild app after changing permissions 3. Check device settings ### Issue: Poor audio quality **Cause**: Incorrect audio source or low sample rate **Solution**: Ensure `audioSource: 'voice_communication'` on Android ## TypeScript Support All audio hooks are fully typed: ```typescript // Hook return types interface AudioRecorder { start(): Promise; stop(): Promise; isRecording(): boolean; getSupportedMimeType(): string | null; } interface AudioPlayer { play(audioData: Blob): Promise; stop(): void; pause(): void; resume(): void; isPlaying(): boolean; isPaused(): boolean; clearQueue(): void; } // Configuration interface AudioCaptureConfig { sampleRate?: number; numberOfChannels?: number; bitRate?: number; } ``` ## Summary Phase 2 successfully implements audio capture and playback for the voice-mobile app using expo-audio. The implementation: ✅ Matches web app audio constraints (16kHz, mono, optimized for speech) ✅ Supports all platforms (iOS, Android, Web) ✅ Provides Blob output compatible with WebSocket transmission ✅ Includes test screen for verification ✅ Fully typed with TypeScript ✅ Follows React Native best practices ✅ Properly manages permissions and temporary files The implementation is ready for integration with Phase 1 (WebSocket) to create the complete voice assistant mobile experience.