expo-audio-studio
v1.3.1
Published
Professional audio recording & playback with advanced Voice Activity Detection for React Native
Downloads
743
Maintainers
Readme
Demo
What's included
- Record high-quality audio (WAV/PCM16 format, 16kHz, 16-bit mono)
- Capture real-time audio chunks during recording (PCM base64 encoded)
- Play audio with speed control and seeking
- Detect when someone is speaking (voice activity detection)
- Get real-time amplitude data and waveform visualizations
- Join multiple audio files together
- Works the same on iOS and Android
- Full TypeScript support
Installation
npm install expo-audio-studioSetup permissions
Add the plugin to your app.config.ts to automatically configure microphone
permissions:
export default {
plugins: [
[
'expo-audio-studio',
{
microphonePermission:
'Allow $(PRODUCT_NAME) to access your microphone for audio recording',
},
],
],
};Or use it without options for default permissions:
export default {
plugins: ['expo-audio-studio'],
};This adds:
- iOS: Microphone usage description
- Android: Audio recording permissions
Build your app
Since this uses native code, you'll need a development build:
# Prebuild to apply the plugin
npx expo prebuild
# Create development build
npx expo run:ios
npx expo run:androidGetting started
Record audio
import ExpoAudioStudio from 'expo-audio-studio';
// Request permission first
const permission = await ExpoAudioStudio.requestMicrophonePermission();
if (!permission.granted) {
console.log('Microphone permission denied');
return;
}
// Listen to recording events
const subscription = ExpoAudioStudio.addListener(
'onRecorderStatusChange',
(event: AudioRecordingStateChangeEvent) => {
console.log('Recording status:', event.status);
}
);
// Start recording
const filePath = ExpoAudioStudio.startRecording();
console.log('Recording to:', filePath);
// Stop recording
const finalPath = ExpoAudioStudio.stopRecording();
console.log('Recording saved to:', finalPath);
// Cleanup
subscription.remove();Detect speech
import ExpoAudioStudio from 'expo-audio-studio';
// Choose how often you want events
ExpoAudioStudio.setVADEventMode('onEveryFrame'); // Real-time (default)
// setVADEventMode('onChange'); // Only state changes
// setVADEventMode('throttled', 100); // Every 100ms
// Enable VAD
ExpoAudioStudio.setVADEnabled(true);
// Listen to voice activity
const vadSubscription = ExpoAudioStudio.addListener(
'onVoiceActivityDetected',
(event: VoiceActivityEvent) => {
if (event.isStateChange) {
// State just changed - someone started or stopped talking
console.log(
event.isVoiceDetected ? 'Started talking!' : 'Stopped talking'
);
}
console.log('Confidence:', event.confidence);
console.log('Event type:', event.eventType);
}
);
// Start recording - VAD will automatically start
ExpoAudioStudio.startRecording();Capture audio chunks
import ExpoAudioStudio from 'expo-audio-studio';
import { encode as base64Encode, decode as base64Decode } from 'base-64';
// Enable chunk listening (disabled by default)
ExpoAudioStudio.setListenToChunks(true);
// Store chunks as they arrive
const audioChunks: string[] = [];
// Listen to audio chunks during recording
const chunkSubscription = ExpoAudioStudio.addListener(
'onAudioChunk',
(event: AudioChunkEvent) => {
// event.base64 contains raw PCM audio data (Int16, 16kHz, mono)
audioChunks.push(event.base64);
console.log('Received chunk, total:', audioChunks.length);
}
);
// Start recording - chunks will be sent in real-time
ExpoAudioStudio.startRecording();
// Later, when stopping...
ExpoAudioStudio.stopRecording();
// Process the chunks - decode, concatenate, add WAV header, etc.
// See example/components/ChunkRecorder.tsx for full implementation
// Cleanup
chunkSubscription.remove();
ExpoAudioStudio.setListenToChunks(false);Play audio
import ExpoAudioStudio from 'expo-audio-studio';
// Listen to playback events
const playerSubscription = ExpoAudioStudio.addListener(
'onPlayerStatusChange',
(event: PlayerStatusChangeEvent) => {
console.log('Playing:', event.isPlaying);
}
);
// Start playback
ExpoAudioStudio.startPlaying('/path/to/audio/file.wav');
// Control playback speed
ExpoAudioStudio.setPlaybackSpeed('1.5'); // 1.5x speed
// Cleanup
playerSubscription.remove();API Reference
Recording Functions
| Function | Description | Returns |
| -------------------------------- | ---------------------------------- | -------------------------- |
| startRecording(directoryPath?) | Start audio recording | string - File path |
| stopRecording() | Stop recording | string - Final file path |
| pauseRecording() | Pause recording | string - Status message |
| resumeRecording() | Resume recording | string - Status message |
| setListenToChunks(enabled) | Enable/disable real-time chunk capture | boolean - Enabled state |
| lastRecording() | Get last recording path | string or null |
Playback Functions
| Function | Description | Returns |
| ------------------------- | ---------------------------- | ----------------- |
| startPlaying(path) | Start audio playback | string - Status |
| stopPlayer() | Stop playback | string - Status |
| pausePlayer() | Pause playback | string - Status |
| resumePlayer() | Resume playback | string - Status |
| setPlaybackSpeed(speed) | Set playback speed (0.5-2.0) | string - Status |
| seekTo(position) | Seek to position in seconds | string - Status |
Voice Activity Detection
| Function | Description | Returns |
| -------------------------------------- | --------------------------------- | ----------------- |
| setVADEnabled(enabled) | Enable/disable VAD | string - Status |
| setVoiceActivityThreshold(threshold) | Set detection threshold (0.0-1.0) | string - Status |
| setVADEventMode(mode, throttleMs?) | Control event frequency | string - Status |
Audio Analysis
| Function | Description | Returns |
| ---------------------------------------- | ----------------------------- | ------------------------------ |
| getDuration(uri) | Get audio file duration | number - Duration in seconds |
| getAudioAmplitudes(fileUrl, barsCount) | Get waveform data (dB values) | object - Amplitude data |
| setAmplitudeUpdateFrequency(hz) | Set amplitude update rate | string - Status |
Constants
| Constant | Description | Returns |
| ----------------- | -------------------------------------------- | --------- |
| currentPosition | Current playback position in seconds | number |
| meterLevel | Current audio level during recording (in dB) | number |
| playerStatus | Detailed player status information | object |
| isVADActive | Whether VAD is currently active | boolean |
| isVADEnabled | Whether VAD is enabled by user preference | boolean |
File Management
| Function | Description | Returns |
| --------------------------------------- | ----------------------- | ---------------------- |
| listRecordings(directoryPath?) | List audio files | array - File list |
| joinAudioFiles(filePaths, outputPath) | Concatenate audio files | string - Output path |
Event Listeners
Recording Events
import ExpoAudioStudio from 'expo-audio-studio';
ExpoAudioStudio.addListener(
'onRecorderStatusChange',
(event: AudioRecordingStateChangeEvent) => {
// event.status: 'recording' | 'stopped' | 'paused' | 'resumed' | 'error'
}
);
const subscription = ExpoAudioStudio.addListener(
'onRecorderAmplitude',
(event: AudioMeteringEvent) => {
// event.amplitude: number (dB level)
}
);
// Listen to audio chunks (requires setListenToChunks(true))
const chunkSubscription = ExpoAudioStudio.addListener(
'onAudioChunk',
(event: AudioChunkEvent) => {
// event.base64: string - Base64 encoded PCM audio data
// Format: Int16 samples, 16kHz sample rate, mono channel
}
);Voice Activity Events
import ExpoAudioStudio from 'expo-audio-studio';
const subscription = ExpoAudioStudio.addListener(
'onVoiceActivityDetected',
(event: VoiceActivityEvent) => {
// event.isVoiceDetected: boolean - Is someone speaking?
// event.confidence: number - How confident is the detection (0.0-1.0)
// event.timestamp: number - When this happened
// event.isStateChange: boolean - Did the state just change?
// event.previousState: boolean - What was the previous state
// event.eventType: 'speech_start' | 'speech_continue' | 'silence_start' | 'silence_continue'
}
);Playback Events
import ExpoAudioStudio from 'expo-audio-studio';
// Listen to player status changes
const subscription = ExpoAudioStudio.addListener(
'onPlayerStatusChange',
(event: PlayerStatusChangeEvent) => {
// event.isPlaying: boolean
// event.didJustFinish: boolean
}
);More examples
Save to a custom folder
const filePath = startRecording('/path/to/custom/directory');iOS audio session setup
Note: Don't reconfigure the audio session while recording or playing - this can freeze your app.
import ExpoAudioStudio from 'expo-audio-studio';
await ExpoAudioStudio.configureAudioSession({
category: 'playAndRecord',
mode: 'default',
options: {
defaultToSpeaker: true,
allowBluetooth: true,
},
});
await ExpoAudioStudio.activateAudioSession();Adjust voice detection sensitivity
// More sensitive (for quiet rooms)
ExpoAudioStudio.setVoiceActivityThreshold(0.3);
// Less sensitive (for noisy places)
ExpoAudioStudio.setVoiceActivityThreshold(0.7);
ExpoAudioStudio.setVADEnabled(true);Real-world example: Voice-activated recording
Example 1
import ExpoAudioStudio from 'expo-audio-studio';
// Only notify on state changes for battery efficiency
ExpoAudioStudio.setVADEventMode('onChange');
ExpoAudioStudio.setVADEnabled(true);
const subscription = ExpoAudioStudio.addListener(
'onVoiceActivityDetected',
(event: VoiceActivityEvent) => {
if (event.eventType === 'speech_start') {
console.log('🎤 Voice detected');
} else if (event.eventType === 'silence_start') {
console.log('🔇 Silence detected');
}
}
);Example 2
import ExpoAudioStudio from 'expo-audio-studio';
// Only notify in every 250ms
ExpoAudioStudio.setVADEventMode('throttled', 250);
ExpoAudioStudio.setVADEnabled(true);
const subscription = ExpoAudioStudio.addListener(
'onVoiceActivityDetected',
(event: VoiceActivityEvent) => {
if (event.isVoiceDetected) {
console.log('Voice detected');
} else {
console.log('Silence detected');
}
}
);Get waveform data
const waveformData = ExpoAudioStudio.getAudioAmplitudes(
'/path/to/file.wav',
100
);
console.log('Amplitude values:', waveformData.amplitudes);
// Normalize for UI visualization (dB to 0-1 range)
const normalized = waveformData.amplitudes.map(dB =>
Math.max(0, (dB + 60) / 60)
);
const duration = ExpoAudioStudio.getDuration('/path/to/file.wav');Merge audio files
const inputFiles = [
'/path/to/file1.wav',
'/path/to/file2.wav',
'/path/to/file3.wav',
];
const outputPath = '/path/to/joined_audio.wav';
const result = ExpoAudioStudio.joinAudioFiles(inputFiles, outputPath);
console.log('Joined file created:', result);Audio format
Recordings use WAV format (PCM16, 16kHz, 16-bit mono) on both platforms. This provides good quality while keeping file sizes reasonable.
- iOS: Linear PCM using AVFoundation
- Android: PCM using AndroidWaveRecorder
Additional formats are on the roadmap.
Voice detection details
Both iOS and Android now fire the same events, so you can expect identical behavior.
Controlling event frequency
You can choose how often you want to receive voice detection events:
// Get events for every audio frame processed (~32ms, about 30 per second)
// Perfect for real-time visualizations or instant response
ExpoAudioStudio.setVADEventMode('onEveryFrame');
// Only get notified when voice state changes (speech starts/stops)
// Battery-friendly and great for simple on/off detection
ExpoAudioStudio.setVADEventMode('onChange');
// Get updates every X milliseconds, plus immediate state changes
// Nice balance between real-time and performance
ExpoAudioStudio.setVADEventMode('throttled', 250); // every 250msWhich one should you use?
- Building a live voice visualizer? Use
onEveryFrame - Just need to know when someone starts/stops talking? Use
onChange - Want periodic updates without overwhelming your app? Use
throttledwith 100-250ms
You can change the mode anytime, even while recording is happening. The setting sticks around until you change it again.
Under the hood
iOS uses Apple's Core ML Sound Classification:
- Real confidence scores from machine learning (0.0-1.0)
- Analyzes audio in 1.5 second windows with smart overlap
- Works on iOS 14.0 and up
Android uses Silero VAD:
- Compact neural network implementation
- Fixed confidence values (0.85 for voice, 0.15 for silence)
- Processes 32 ms chunks at 16 kHz
Platform requirements
- iOS: 14.0 or higher (for voice detection)
- Android: API 21 (Android 5.0) or higher
- Web: Coming soon
What's under the hood
Android libraries:
- AndroidWaveRecorder (v2.1.0) - for WAV recording
- Silero VAD (v2.0.10) - for voice detection
iOS frameworks:
- AVFoundation - for recording/playback
- Core ML - for voice detection
Everything is MIT licensed.
Running the Example
cd example
npm install
npx expo run:ios
# or
npx expo run:android
# Web support coming soon!Building from Source
# Install dependencies
npm install
# Build TypeScript
npm run build
# Run tests
npm testWhat's next
Streaming & real-time processing
- Stream audio chunks in real time with configurable chunk sizes
- Access raw PCM16 data during recording
- Reduce latency for live transcription and other low-latency pipelines
- Use the stream for speech-to-text or custom analysis
More formats
- M4A, MP3, and FLAC recording options
- Configurable quality settings
Web support
- WebRTC-based recording
- Browser-side voice detection
- Same API across native and web targets
Other features
- Stereo and multi-channel recording
- Real-time audio effects
- Additional analytics hooks
Got ideas? Open a discussion!
Contributing
Pull requests are welcome! Check out the Contributing Guide for details.
- Fork the repo
- Create a branch:
git checkout -b feature/my-feature - Make your changes
- Push and open a pull request
License
This project is licensed under the MIT License - see the LICENSE file for details.
Acknowledgments
- Built with Expo Modules API
- Android WAV recording powered by AndroidWaveRecorder by @squti
- Android Voice Activity Detection powered by Silero VAD by @gkonovalov
- iOS Voice Activity Detection using Apple's Core ML Sound Classification
- Built for production use in audio applications
Support
- Email: [email protected]
- Issues: GitHub Issues
- Discussions: GitHub Discussions
