# react-native-nitro-sound
[](https://npmjs.org/package/react-native-nitro-sound)
[](https://npmjs.org/package/react-native-nitro-sound)
[](https://npmjs.org/package/react-native-nitro-sound)
[](https://github.com/hyochan/react-native-nitro-sound/actions/workflows/ci.yml)
[](https://github.com/hyochan/react-native-nitro-sound/actions/workflows/publish-package.yml)
[](https://www.typescriptlang.org/)
[](https://npmjs.org/package/react-native-nitro-sound)
[](https://github.com/prettier/prettier)
[](https://developer.apple.com/ios/)
[](https://developer.android.com/)
[](https://reactnative.dev/docs/react-native-web)
[](https://www.greatfrontend.com?fpr=hyo73&fp_sid=rnsound)
> 🤖 **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](https://github.com/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 … has_value` errors while compiling generated Nitrogen bindings (see [#718](https://github.com/hyochan/react-native-nitro-sound/issues/718)). Upgrade and cleanup steps are documented in the [FAQ](docs/FAQ.md#swift-6-compile-error-function-type-mismatch--has_value-718).
<img src="https://github.com/user-attachments/assets/81ce7b7b-0b7d-413b-8a26-505372349ecb" width="70%" alt="Logo" />
## Legacy Package (react-native-audio-recorder-player)
[](https://npmjs.org/package/react-native-audio-recorder-player)
[](https://npmjs.org/package/react-native-audio-recorder-player)
[](https://npmjs.org/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](https://github.com/mrousavy/nitro), 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](https://www.npmjs.com/package/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](https://github.com/mrousavy/nitro), starting fresh as **react-native-nitro-sound**.
As the creator of [Flutter Sound](https://pub.dev/packages/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](https://github.com/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](https://github.com/mrousavy/nitro) 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](#component-based-implementation) 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!
<a href="https://www.buymeacoffee.com/hyochan" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/purple_img.png" alt="Buy Me A Coffee" style="height: auto !important;width: auto !important;" ></a>
[](https://paypal.me/dooboolab)
## Preview
[Web Demo](https://hyochan.github.io/react-native-nitro-sound/)
<img src="https://github.com/user-attachments/assets/02545c15-f41d-4186-be79-2a13fd67ccb2" width=800 alt="Screenshot"/>
<img src="https://github.com/user-attachments/assets/6d8a5a80-cd31-450f-b410-9b8f42ae8d03" width=800 alt="Screenshot2"/>
## Documentation & Resources
- 🔧 [NitroModules Documentation](https://github.com/mrousavy/nitro) - Learn about the underlying technology
- 📝 [Version 3 Release Note](https://medium.com/dooboolab/react-native-audio-player-and-recorder-v3-7697e25cd07)
- 📰 [Original Blog Post](https://medium.com/@dooboolab/react-native-audio-recorder-and-player-4aa5f26a666)
## 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:
```diff
- import AudioRecorderPlayer from 'react-native-audio-recorder-player';
+ import Sound from 'react-native-nitro-sound';
```
## Getting started
1. **Install packages**:
```sh
yarn add react-native-nitro-sound react-native-nitro-modules
```
Or using npm:
```sh
npm install react-native-nitro-sound react-native-nitro-modules
```
2. **Align React Native dependencies (recommended)**:
```sh
npx @rnx-kit/align-deps --requirements react-native@0.86 --write
```
## Post Installation
After installing the packages, follow these steps:
1. **iOS Setup**:
```sh
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.
2. **Android Setup**:
No additional steps required. The module uses autolinking.
3. **Web Setup**:
For React Native Web, install the additional dependency:
```sh
yarn add react-native-web
```
Then configure your webpack to include the web-specific implementation:
```js
// 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
1. **Microphone Permission**: Add to your `Info.plist`:
```xml
<key>NSMicrophoneUsageDescription</key>
<string>Give $(PRODUCT_NAME) permission to use your microphone. Your record wont be shared without your permission.</string>
```
2. **Minimum iOS Version**: Ensure your minimum deployment target is iOS 15.1 or higher in your `Podfile`:
```ruby
platform :ios, '15.1'
```
### Android Configuration
On _Android_ you need to add permissions to `AndroidManifest.xml`:
```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:
```ts
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
```typescript
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
```tsx
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
```tsx
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
```typescript
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
```typescript
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:
```typescript
const audioSet: AudioSet = {
// Common settings automatically applied to the appropriate platform
AudioSamplingRate: 44100,
AudioEncodingBitRate: 128000,
AudioChannels: 1,
};
```
```typescript
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
```typescript
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
```typescript
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.
1. Install dependencies and build the library:
```sh
yarn
yarn prepare
```
2. Start the development server:
```sh
yarn start
```
3. Run on your platform:
```sh
# 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](https://github.com/mrousavy/nitro/issues/1319) 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](#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):
1. **Ensure microphone permissions are properly configured** in your `Info.plist`:
```xml
<key>NSMicrophoneUsageDescription</key>
<string>Your app needs microphone access to record audio</string>
```
2. **Clean and rebuild your iOS project**:
```sh
cd ios
rm -rf build Pods
pod install
cd ..
yarn ios
```
3. **Make sure you're testing on a real device** if using the simulator doesn't work. Some audio features require real hardware.
4. **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:
```sh
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
```sh
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:
1. Product → Clean Build Folder (⇧⌘K)
2. Product → Build (⌘B)
#### Android Clean Build
```sh
cd android
./gradlew clean
rm -rf ~/.gradle/caches/
cd ..
```
Then rebuild:
```sh
yarn android
# or
npx react-native run-android
```
#### Both Platforms
You can also try resetting Metro cache:
```sh
npx react-native start --reset-cache
```
## Contributing
See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.
## License
MIT