react-native-nitro-sound
v0.2.10
Published
React Native Sound Module ๐ฅ by Nitro
Readme
react-native-nitro-sound
โน๏ธ Swift 6 build warning: If Xcode shows
function type mismatch โฆ has_valueerrors (see #718), upgrade to Xcode 16.4 or newer. The workaround and cleanup steps are documented in the FAQ.
Legacy Package (react-native-audio-recorder-player)
React Native Nitro Sound ๐
React Native Nitro Sound is a high-performance audio recording and playback library built with NitroModules, offering:
- Zero Bridge Overhead: Direct native module access for maximum performance
- Full Type Safety: TypeScript definitions generated from native specs
- Synchronous Methods: Where appropriate, for better developer experience
- Event Listeners: Native callbacks with type-safe event payloads
- Cross-Platform Code Generation: Automatic code generation for iOS (Swift) and Android (Kotlin)
- Background Processing: Recording operations now run in background threads to prevent UI blocking, requiring loading state management
- Web Platform Support: Full support for web browsers using Web Audio API and MediaRecorder API
Requirements
- React Native: >= 0.79 (0.81 recommended)
- iOS: Deployment Target >= 13.0
- Note: With RN 0.81+, build using Xcode >= 16.1 (toolchain requirement; iOS runtime minimum remains 13.0)
- Android: minSdk >= 24 (JDK 17 recommended; compileSdk 36 recommended)
- New Architecture: optional (Nitro works on both old and new arch)
- Expo SDK >= 50 (for Expo users)
๐ React Native Nitro Sound - Reborn from React Native Audio Recorder Player
React Native Nitro Sound is the reborn version of React Native Audio Recorder Player!
For those unfamiliar, this was a beloved library with 40k+ weekly downloads and 180k+ monthly downloads. Now, this library embarks on a new journey with NitroModules, starting fresh as react-native-nitro-sound.
As the creator of Flutter Sound, the name 'Sound' feels much more familiar and close to my heart. This rebranding is not just a name change, but a commitment to actively maintain and evolve this library with even greater dedication.
Starting fresh with version 1.0.0, we're delivering a more powerful and stable audio solution. Special thanks to @mrousavy for creating this amazing technology! ๐
Your continued support and interest mean the world to us!
This is a high-performance React Native module for audio recording and playback, now powered by NitroModules for direct native module access without bridge overhead. The library provides simple recorder and player functionalities for iOS, Android, and Web platforms with full TypeScript support and type safety.
๐ด Critical: Recording operations now run in background threads. You MUST implement loading states to handle the async delays, or your UI may appear unresponsive. See Component Examples for proper implementation.
Help Maintenance
This is one of those projects that brings me joy to work on. If you find it useful, consider buying me a coffee โ๏ธ โ your support keeps me motivated!
Preview
Documentation & Resources
- ๐ง NitroModules Documentation - Learn about the underlying technology
- ๐ Version 3 Release Note
- ๐ฐ Original Blog Post
Migration from react-native-audio-recorder-player
If you're migrating from react-native-audio-recorder-player (version 3.x or earlier), the API remains largely the same. Simply update your package name:
- import AudioRecorderPlayer from 'react-native-audio-recorder-player';
+ import Sound from 'react-native-nitro-sound';Getting started
Install packages:
yarn add react-native-nitro-sound react-native-nitro-modulesOr using npm:
npm install react-native-nitro-sound react-native-nitro-modulesAlign React Native dependencies (recommended):
npx @rnx-kit/align-deps --requirements [email protected] --write
Post Installation
After installing the packages, follow these steps:
iOS Setup:
npx pod-install- If resolution fails, try
npx pod-install --repo-update. - RN 0.81+ requires Xcode >= 16.1 to build.
- If resolution fails, try
Android Setup: No additional steps required. The module uses autolinking.
Web Setup: For React Native Web, install the additional dependency:
yarn add react-native-webThen configure your webpack to include the web-specific implementation:
// webpack.config.js module.exports = { resolve: { alias: { 'react-native': 'react-native-web', }, }, };
Note: The
nitrogencommand is already run during the library's build process. You don't need to run it in your application.
Platform-specific Configuration
iOS Configuration
Microphone Permission: Add to your
Info.plist:<key>NSMicrophoneUsageDescription</key> <string>Give $(PRODUCT_NAME) permission to use your microphone. Your record wont be shared without your permission.</string>Minimum iOS Version: Ensure your minimum deployment target is iOS 13.0 or higher in your
Podfile:platform :ios, '13.0'
Android Configuration
On Android you need to add permissions to AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />Also, android above Marshmallow needs runtime permission to record audio. Below are two approaches:
Minimal Approach (Recommended for Android 13+):
if (Platform.OS === 'android') {
try {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
{
title: 'Audio Recording Permission',
message: 'This app needs access to your microphone to record audio.',
buttonNeutral: 'Ask Me Later',
buttonNegative: 'Cancel',
buttonPositive: 'OK',
}
);
if (granted === PermissionsAndroid.RESULTS.GRANTED) {
console.log('Recording permission granted');
} else {
console.log('Recording permission denied');
return;
}
} catch (err) {
console.warn(err);
return;
}
}Full Permissions Approach (For older Android versions):
if (Platform.OS === 'android') {
try {
const grants = await PermissionsAndroid.requestMultiple([
PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
]);
if (
grants['android.permission.WRITE_EXTERNAL_STORAGE'] ===
PermissionsAndroid.RESULTS.GRANTED &&
grants['android.permission.READ_EXTERNAL_STORAGE'] ===
PermissionsAndroid.RESULTS.GRANTED &&
grants['android.permission.RECORD_AUDIO'] ===
PermissionsAndroid.RESULTS.GRANTED
) {
console.log('All permissions granted');
} else {
console.log('All required permissions not granted');
return;
}
} catch (err) {
console.warn(err);
return;
}
}Methods
| Method | Param | Return | Description |
| :------------------------ | :----------------------------------------------: | :---------------: | :---------------------------------------------------- |
| mmss | number seconds | string | Convert seconds to minute:second string |
| mmssss | number seconds | string | Convert seconds to minute:second:millisecond string |
| setSubscriptionDuration | number duration | void | Set callback interval in ms (default 500ms) |
| startRecorder | string? uri, AudioSet? audioSet, | Promise<string> | Start recording with optional path and audio settings |
| | boolean? meteringEnabled | | |
| pauseRecorder | | Promise<string> | Pause recording |
| resumeRecorder | | Promise<string> | Resume recording |
| stopRecorder | | Promise<string> | Stop recording and return file path |
| startPlayer | string? uri, Record<string, string>? headers | Promise<string> | Start playback with optional URI and HTTP headers |
| stopPlayer | | Promise<string> | Stop playback |
| pausePlayer | | Promise<string> | Pause playback |
| resumePlayer | | Promise<string> | Resume playback |
| seekToPlayer | number milliseconds | Promise<string> | Seek to position in milliseconds |
| setVolume | number value | Promise<string> | Set volume (0.0 - 1.0) |
| setPlaybackSpeed | number speed | Promise<string> | Set playback speed (0.5 - 2.0) |
| addRecordBackListener | Function callback | void | Add recording progress listener |
| removeRecordBackListener | | void | Remove recording progress listener |
| addPlayBackListener | Function callback | void | Add playback progress listener |
| removePlayBackListener | | void | Remove playback progress listener |
| addPlaybackEndListener | Function callback | void | Add playback completion listener |
| removePlaybackEndListener | | void | Remove playback completion listener |
Usage
Basic Usage
import Sound, {
AudioEncoderAndroidType,
AudioSourceAndroidType,
AVEncoderAudioQualityIOSType,
AVEncodingOption,
RecordBackType,
PlayBackType,
} from 'react-native-nitro-sound';
// Sound is a singleton instance, use directly
// Recording
const onStartRecord = async () => {
// Set up recording progress listener
Sound.addRecordBackListener((e: RecordBackType) => {
console.log('Recording progress:', e.currentPosition, e.currentMetering);
setRecordSecs(e.currentPosition);
setRecordTime(Sound.mmssss(Math.floor(e.currentPosition)));
});
const result = await Sound.startRecorder();
console.log('Recording started:', result);
};
const onStopRecord = async () => {
const result = await Sound.stopRecorder();
Sound.removeRecordBackListener();
console.log('Recording stopped:', result);
};
// Pause/Resume Recording
const onPauseRecord = async () => {
await Sound.pauseRecorder();
console.log('Recording paused');
};
const onResumeRecord = async () => {
await Sound.resumeRecorder();
console.log('Recording resumed');
};
// Playback
const onStartPlay = async () => {
// Set up playback progress listener
Sound.addPlayBackListener((e: PlayBackType) => {
console.log('Playback progress:', e.currentPosition, e.duration);
setCurrentPosition(e.currentPosition);
setTotalDuration(e.duration);
setPlayTime(Sound.mmssss(Math.floor(e.currentPosition)));
setDuration(Sound.mmssss(Math.floor(e.duration)));
});
// Set up playback end listener
Sound.addPlaybackEndListener((e: PlaybackEndType) => {
console.log('Playback completed:', e);
// Handle playback completion
setIsPlaying(false);
setCurrentPosition(0);
});
const result = await Sound.startPlayer();
console.log('Playback started:', result);
};
const onPausePlay = async () => {
await Sound.pausePlayer();
};
const onStopPlay = async () => {
Sound.stopPlayer();
Sound.removePlayBackListener();
Sound.removePlaybackEndListener();
};
// Seeking
const seekTo = async (milliseconds: number) => {
await Sound.seekToPlayer(milliseconds);
};
// Volume control
const setVolume = async (volume: number) => {
await Sound.setVolume(volume); // 0.0 - 1.0
};
// Speed control
const setSpeed = async (speed: number) => {
await Sound.setPlaybackSpeed(speed); // 0.5 - 2.0
};Modern API: Multiple Instances
import { createSound } from 'react-native-nitro-sound';
// Create independent instances (recorder/player per instance)
const soundA = createSound();
const soundB = createSound();
await soundA.startPlayer('https://example.com/a.mp3');
await soundB.startPlayer('https://example.com/b.mp3');
// Control them independently
await soundA.pausePlayer();
await soundB.setVolume(0.5);
// Clean up when done
soundA.dispose();
soundB.dispose();React Hook API
import { useSound } from 'react-native-nitro-sound';
export function Player() {
const {
sound,
state,
startPlayer,
pausePlayer,
resumePlayer,
stopPlayer,
seekToPlayer,
mmssss,
} = useSound({
subscriptionDuration: 0.05, // 50ms updates
});
return (
<View>
<Text>
{mmssss(Math.floor(state.currentPosition))} /{' '}
{mmssss(Math.floor(state.duration))}
</Text>
<Button
title="Play"
onPress={() => startPlayer('https://example.com/audio.mp3')}
/>
<Button title="Pause" onPress={pausePlayer} />
<Button title="Resume" onPress={resumePlayer} />
<Button title="Stop" onPress={stopPlayer} />
<Button title="Seek 10s" onPress={() => seekToPlayer(10_000)} />
</View>
);
}Note: The default export remains a singleton for backward compatibility. Prefer
createSound()anduseSound()for new code and multiple instances.
Audio Configuration
The library automatically detects the platform and applies the appropriate settings. Use platform-specific properties (with IOS or Android suffixes) for fine-grained control, or use common properties for cross-platform consistency.
iOS Configuration
const audioSet: AudioSet = {
// iOS-specific settings
AVSampleRateKeyIOS: 44100,
AVFormatIDKeyIOS: AVEncodingOption.aac,
AVEncoderAudioQualityKeyIOS: AVEncoderAudioQualityIOSType.high,
AVNumberOfChannelsKeyIOS: 2,
AVModeIOS: 'measurement', // Available options: 'gameChatAudio', 'measurement', 'moviePlayback', 'spokenAudio', 'videoChat', 'videoRecording', 'voiceChat', 'voicePrompt'
};Android Configuration
const audioSet: AudioSet = {
// Android-specific settings
AudioEncoderAndroid: AudioEncoderAndroidType.AAC,
AudioSourceAndroid: AudioSourceAndroidType.MIC,
// Common audio settings (apply on Android as well)
// Tip: prefer these for consistent quality
AudioSamplingRate: 44100,
AudioEncodingBitRate: 128000,
AudioChannels: 1,
};Cross-Platform Configuration
For consistent settings across platforms, use common properties that work on both iOS and Android:
const audioSet: AudioSet = {
// Common settings automatically applied to the appropriate platform
AudioSamplingRate: 44100,
AudioEncodingBitRate: 128000,
AudioChannels: 1,
};const meteringEnabled = true; // Enable audio metering
const uri = await Sound.startRecorder(
undefined, // Use default path
audioSet,
meteringEnabled
);Note: Legacy Android-specific keys like
AudioSamplingRateAndroid,AudioEncodingBitRateAndroid, andAudioChannelsAndroidare no longer supported. Use the common keysAudioSamplingRate,AudioEncodingBitRate, andAudioChannels.
Android Defaults via AudioQuality
On Android, when specific numeric values are not provided, the library applies sensible defaults based on AudioQuality.
low:22050 Hz,64 kbps,monomedium:44100 Hz,128 kbps,monohigh:48000 Hz,192 kbps,stereo(default whenAudioQualityis omitted)
Notes:
- If
AudioQualityis not provided, the recorder defaults tohigh. - You can still override any of
AudioSamplingRate,AudioEncodingBitRate, orAudioChannelsexplicitly; explicit values take precedence overAudioQualitydefaults.
Default Path
- Default path for android uri is
{cacheDir}/sound.mp4. - Default path for ios uri is
{cacheDir}/sound.m4a. - Default path for web: Files are stored as Blob URLs in memory.
Tip: Store the file path returned by
startRecorder()immediately for later use in playback or file management.
Web Platform Support
Features
- Audio recording using MediaRecorder API
- Audio playback using Web Audio API
- Support for common audio formats (depends on browser)
- Real-time playback progress updates
- Volume and speed control
Limitations
- Recording format is browser-dependent (typically webm/opus)
- Some audio configuration options are not supported
- File paths are Blob URLs instead of file system paths
- Metering during recording is not currently supported
Browser Compatibility
- Chrome/Edge: Full support
- Firefox: Full support
- Safari: Limited recording format support (may require polyfills)
Component-Based Implementation
For better code organization, consider separating recording and playback into separate components:
Important: Loading States
Note: Starting from version 4.x, recording operations (start/stop) are processed in the background to prevent UI blocking. This means there's a slight delay between calling the method and the actual operation completing. We strongly recommend implementing loading states to provide better user experience.
AudioRecorder Component with Loading States
import React, { useState } from 'react';
import { View, Button, Text, ActivityIndicator } from 'react-native';
import Sound from 'react-native-nitro-sound';
export const AudioRecorder = ({ onRecordingComplete }) => {
const [isRecording, setIsRecording] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [recordTime, setRecordTime] = useState('00:00:00');
const onStartRecord = async () => {
setIsLoading(true);
try {
const result = await Sound.startRecorder();
Sound.addRecordBackListener((e) => {
setRecordTime(Sound.mmssss(Math.floor(e.currentPosition)));
});
setIsRecording(true);
} catch (error) {
console.error('Failed to start recording:', error);
} finally {
setIsLoading(false);
}
};
const onStopRecord = async () => {
setIsLoading(true);
try {
const result = await Sound.stopRecorder();
Sound.removeRecordBackListener();
setIsRecording(false);
onRecordingComplete?.(result);
} catch (error) {
console.error('Failed to stop recording:', error);
} finally {
setIsLoading(false);
}
};
return (
<View>
<Text>{recordTime}</Text>
<Button
title={isRecording ? 'Stop Recording' : 'Start Recording'}
onPress={isRecording ? onStopRecord : onStartRecord}
disabled={isLoading}
/>
{isLoading && <ActivityIndicator />}
</View>
);
};AudioPlayer Component with Loading States
import React, { useState } from 'react';
import { View, Button, Text, ActivityIndicator } from 'react-native';
import Sound from 'react-native-nitro-sound';
export const AudioPlayer = ({ audioPath }) => {
const [isPlaying, setIsPlaying] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [playTime, setPlayTime] = useState('00:00:00');
const [duration, setDuration] = useState('00:00:00');
const onStartPlay = async () => {
setIsLoading(true);
try {
const msg = await Sound.startPlayer(audioPath);
Sound.addPlayBackListener((e) => {
setPlayTime(Sound.mmssss(Math.floor(e.currentPosition)));
setDuration(Sound.mmssss(Math.floor(e.duration)));
});
// Use the proper playback end listener
Sound.addPlaybackEndListener((e) => {
console.log('Playback completed', e);
setIsPlaying(false);
setPlayTime('00:00:00');
});
setIsPlaying(true);
} catch (error) {
console.error('Failed to start playback:', error);
} finally {
setIsLoading(false);
}
};
const onStopPlay = async () => {
setIsLoading(true);
try {
await Sound.stopPlayer();
Sound.removePlayBackListener();
Sound.removePlaybackEndListener();
setIsPlaying(false);
setPlayTime('00:00:00');
setDuration('00:00:00');
} catch (error) {
console.error('Failed to stop playback:', error);
} finally {
setIsLoading(false);
}
};
return (
<View>
<Text>{playTime} / {duration}</Text>
<Button
title={isPlaying ? 'Stop' : 'Play'}
onPress={isPlaying ? onStopPlay : onStartPlay}
disabled={!audioPath || isLoading}
/>
{isLoading && <ActivityIndicator />}
</View>
);
};Example App
Running the Example
Because this repo uses a Yarn workspace, run everything from the repository root.
Install dependencies and build the library:
yarn yarn prepareStart the development server:
yarn startRun on your platform:
# iOS # First time on a new machine, you may need to install pods: (cd example/ios && pod install) yarn example ios # Android yarn example android
Troubleshooting
iOS Recording Error: "Unknown std::runtime_error"
If you encounter this error when trying to record on iOS:
Ensure microphone permissions are properly configured in your
Info.plist:<key>NSMicrophoneUsageDescription</key> <string>Your app needs microphone access to record audio</string>Clean and rebuild your iOS project:
cd ios rm -rf build Pods pod install cd .. yarn iosMake sure you're testing on a real device if using the simulator doesn't work. Some audio features require real hardware.
Verify the Nitro modules are properly linked by checking that the
[NitroModules] ๐ฅ Sound is boosted by nitro!message appears duringpod install.
Common Issues
- "nitrogen" command not found: This command is only needed when developing the library itself, not when using it in your app.
- Module not found errors: Make sure to run
pod installafter installing the package. - Android build issues: Ensure your
minSdkVersionis 24 or higher inandroid/build.gradle.If you see
:react-native:generateCodegenSchemaFromJavaScriptfailing, this comes from RN's Gradle plugin (not Nitro). Ensure RN >= 0.79 (0.81 recommended) and JDK 17, then align and clean:npx @rnx-kit/align-deps --requirements [email protected] --write rm -rf node_modules android/.gradle yarn cd android && ./gradlew clean assembleDebug
Clean Build Instructions
If you're experiencing build issues or runtime errors after updating the library:
iOS Clean Build
cd ios
rm -rf ~/Library/Caches/CocoaPods
rm -rf Pods
rm -rf ~/Library/Developer/Xcode/DerivedData/*
pod cache clean --all
pod install
cd ..Then in Xcode:
- Product โ Clean Build Folder (โงโK)
- Product โ Build (โB)
Android Clean Build
cd android
./gradlew clean
rm -rf ~/.gradle/caches/
cd ..Then rebuild:
yarn android
# or
npx react-native run-androidBoth Platforms
You can also try resetting Metro cache:
npx react-native start --reset-cacheContributing
See the contributing guide to learn how to contribute to the repository and the development workflow.
License
MIT

