hyochan
react-native-nitro-sound
TypeScript

react-native native module for audio recorder and player with nitromodule!

Last updated Aug 7, 2026
960
Stars
252
Forks
10
Issues
0
Stars/day
Attention Score
90
Language breakdown
TypeScript 46.7%
Swift 22.2%
Kotlin 17.2%
JavaScript 7.6%
Shell 3.2%
Ruby 2.0%
โ–ธ Files click to expand
README

react-native-nitro-sound

yarn Version Monthly Downloads Weekly Downloads CI publish-package TypeScript MIT License code style: prettier

Platform - iOS Platform - Android Platform - Web

GreatFrontEnd

๐Ÿค– This repository is now AI-native maintained. Hyo is juggling a lot of open-source repos with very limited time (currently focused on hyodotdev/openiap), so day-to-day triage, bug fixes, and dependency bumps here are handled by Claude on his behalf. Commits and pull requests labeled ๐Ÿค– Autogenerated by Claude are produced through the .claude/commands/ skills in this repo and the .github/workflows/ai-dispatch.yml workflow. If you see something off with an AI-generated change, please comment on the issue or PR โ€” Hyo still reviews direction, and humans are always welcome to send PRs.
>
โžก๏ธ To nudge the AI maintainer on an issue from the GitHub mobile app, just comment /ai resolve-issue on the issue. Supported commands: /ai resolve-issue, /ai upgrade-deps, /ai compile-knowledge, /ai triage-all.
โ„น๏ธ Swift 6 build requirement: Use Xcode 26.0 or newer. Earlier Apple toolchains can crash or emit function type mismatch โ€ฆ hasvalue errors while compiling generated Nitrogen bindings (see #718). Upgrade and cleanup steps are documented in the FAQ.

Logo

Legacy Package (react-native-audio-recorder-player)

yarn Version Downloads Weekly Downloads

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.86.2 is the repository's verified baseline)
  • iOS: Deployment Target >= 15.1
- Xcode >= 26.0 is required to compile the generated Swift/Nitrogen bindings reliably.
  • Android: minSdk >= 24 (JDK 17 recommended; compileSdk 36 recommended)
- Note: RN 0.82+ requires Gradle 9.0+
  • New Architecture: optional (Nitro works on both old and new arch; RN 0.82+ is New Architecture only)
  • 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!

Buy Me A Coffee Paypal

Preview

Web Demo

Screenshot

Screenshot2

Documentation & Resources

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-modules

Or using npm:

npm install react-native-nitro-sound react-native-nitro-modules
  • Align React Native dependencies (recommended):
npx @rnx-kit/align-deps --requirements react-native@0.86 --write

Post Installation

After installing the packages, follow these steps:

  • iOS Setup:
npx pod-install

- If resolution fails, try npx pod-install --repo-update. - Use Xcode >= 26.0; earlier toolchains are not supported for this package's generated Swift/Nitrogen bindings.

  • Android Setup:
No additional steps required. The module uses autolinking.
  • Web Setup:
For React Native Web, install the additional dependency:
yarn add react-native-web

Then configure your webpack to include the web-specific implementation:

// webpack.config.js
   module.exports = {
     resolve: {
       alias: {
         'react-native': 'react-native-web',
       },
     },
   };
Note: The nitrogen command 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 15.1 or higher in your Podfile:
platform :ios, '15.1'

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" />

Android 6.0 and newer also require the microphone permission at runtime:

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; } }

The default recording path is app-private storage, so legacy external-storage permissions are not required. If your app supplies a custom shared-storage URI, request only the media access that URI needs and follow current Android scoped-storage guidance.

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 (idempotent โ€” extra calls resolve with "recorder already stopped") | | 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() and useSound() 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, and AudioChannelsAndroid are no longer
supported. Use the common keys AudioSamplingRate, AudioEncodingBitRate,
and AudioChannels.

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, mono
  • medium: 44100 Hz, 128 kbps, mono
  • high: 48000 Hz, 192 kbps, stereo (default when AudioQuality is omitted)
Notes:
  • If AudioQuality is not provided, the recorder defaults to high.
  • You can still override any of AudioSamplingRate, AudioEncodingBitRate, or AudioChannels explicitly; explicit values take precedence over AudioQuality defaults.

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.

Supported Playback Sources

startPlayer(uri) accepts the following URI forms:

| Source | iOS | Android | Notes | | :------------------------------------------- | :-: | :-----: | :------------------------------------------------------------- | | Absolute file path (/data/.../sound.mp4) | โœ… | โœ… | e.g. the path returned by startRecorder() | | file:// URI | โœ… | โœ… | | | http(s):// remote URL | โœ… | โœ… | Optional HTTP headers are supported on Android | | content:// URI | โ€” | โœ… | Android content providers (document picker, media store) | | android.resource://<appId>/raw/<name> | โ€” | โœ… | Bundled res/raw resources (v0.2.16+) โ€” see note below |

Bundled sounds in Android release builds: in debug builds Metro serves require('./sound.mp3') assets over HTTP, but in release builds Image.resolveAssetSource() returns a resource name, not a playable path. For sounds shipped with your app, place the file in android/app/src/main/res/raw/ and play it via android.resource://<applicationId>/raw/<filename-without-extension> (or copy the asset to a real file first). On iOS, bundle the file with the app and resolve its path via the main bundle.

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 prepare
  • Start the development server:
yarn start
  • Run 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

Recording fails only in iOS release/TestFlight builds

Symptoms: Recording setup failed: Failed to prepare recorder or Unknown std::runtime_error in production while debug builds work fine.

Root cause: in optimized release builds, optional numeric settings can arrive corrupted through the native interop and poison the recorder configuration (AudioQueueNew rejects it as kAudioFormatUnsupportedDataFormatError). Mitigated in v0.2.17 โ€” v0.2.16 added range validation, and v0.2.17 also caps the requested channel count to the active input route so rejected values fall back to safe presets. The upstream optional-interop issue remains open; this library applies defensive validation rather than claiming the bridge bug is fixed. If you see this on an older version, upgrade first.

Android: sound plays in debug but not in release builds

Release builds don't serve Metro assets over HTTP, so paths that worked in debug (from Image.resolveAssetSource) are not real files anymore. Use a bundled resource URI instead โ€” see Supported Playback Sources. android.resource:// URIs are supported from v0.2.16.

Google Play warning: library doesn't support 16 KB page sizes

Fixed in v0.2.16 โ€” libNitroSound.so is linked with 16 KB page alignment on every NDK version. On older library versions, building your app with React Native 0.80+ (NDK r27) also resolves it.

iOS Recording Error: "Unknown std::runtime_error"

If you encounter this error when trying to record on iOS (after upgrading to v0.2.16+, which fixes the most common production cause โ€” see above):

  • 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 ios
  • Make 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 during pod 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 install after installing the package.
  • Android build issues: Ensure your minSdkVersion is 24 or higher in android/build.gradle.
- If you see :react-native:generateCodegenSchemaFromJavaScript failing, 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 react-native@0.86 --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-android

Both Platforms

You can also try resetting Metro cache:

npx react-native start --reset-cache

Contributing

See the contributing guide to learn how to contribute to the repository and the development workflow.

License

MIT

ยฉ 2026 GitRepoTrend ยท hyochan/react-native-nitro-sound ยท Updated daily from GitHub