10 KiB
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:
useAudioRecorderanduseAudioPlayer - 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
FileandPathsclasses - Required for converting between audio URIs and Blobs
Permissions Configuration
app.json Updates
iOS:
{
"ios": {
"infoPlist": {
"NSMicrophoneUsageDescription": "This app needs access to the microphone for voice commands."
}
}
}
Android:
{
"android": {
"permissions": [
"RECORD_AUDIO"
]
}
}
Plugins:
{
"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:
android: {
extension: '.m4a',
outputFormat: 'mpeg4',
audioEncoder: 'aac',
sampleRate: 16000,
audioSource: 'voice_communication', // Key for voice optimization
}
iOS:
ios: {
extension: '.m4a',
audioQuality: 127, // High quality
sampleRate: 16000,
linearPCMBitDepth: 16,
linearPCMIsBigEndian: false,
linearPCMIsFloat: false,
}
Web:
web: {
mimeType: 'audio/webm;codecs=opus',
bitsPerSecond: 128000,
}
Usage:
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:
- Recording creates temporary file (managed by expo-audio)
- On stop, reads file URI from
audioRecorder.uri - Converts to Blob using expo-file-system's
Fileclass - 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:
interface QueuedAudio {
audioData: Blob;
resolve: (duration: number) => void;
reject: (error: Error) => void;
}
Usage:
const audioPlayer = useAudioPlayer();
// Play audio (queued automatically)
const duration = await audioPlayer.play(audioBlob);
// Controls
audioPlayer.pause();
audioPlayer.resume();
audioPlayer.stop();
audioPlayer.clearQueue();
File Handling:
- Converts Blob to ArrayBuffer
- Creates temporary file in cache directory
- Writes ArrayBuffer to file
- Plays from file URI
- 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_communicationaudio 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
-
Start Expo Development Server:
cd packages/voice-mobile npm start -
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
-
Test on iOS Simulator:
- Press 'i' in terminal
- Note: iOS Simulator doesn't have microphone access
- Test on physical device for full functionality
-
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:
// 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
-
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
-
Audio Visualization:
- Add waveform display during recording
- Use expo-audio's metering feature
- Display audio levels in real-time
-
Error Handling:
- Add retry logic for failed recordings
- Better error messages for users
- Fallback for unsupported formats
-
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:
- Check app.json has correct permission entries
- Rebuild app after changing permissions
- 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:
// Hook return types
interface AudioRecorder {
start(): Promise<void>;
stop(): Promise<Blob>;
isRecording(): boolean;
getSupportedMimeType(): string | null;
}
interface AudioPlayer {
play(audioData: Blob): Promise<number>;
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.