Skip to main content
Version: Next

Best practices

When working with audio in a web or mobile application, following best practices ensures optimal performance, user experience, and maintainability. Here are some key best practices to consider when using the React Native Audio API:

AudioContext Management

  • Single Audio Context: Create one instance of AudioContext in order to easily and efficiently manage the audio layer's state in your application. Creating many instances could lead to undefined behavior. Some of them could still be in running state while others could be suspended or closed, if you do not manage them by yourself.

  • Resume before play: On mobile, AudioContext may start in the suspended state. Call resume() before starting playback or recording that depends on the context.

  • Clean up: Always close the AudioContext using the close() method when it is no longer needed. This releases system audio resources and prevents memory leaks. For long-lived apps that keep audio available in the background, prefer suspend() / resume() over repeatedly creating and closing contexts.

  • Suspend when not in use: Suspend the AudioContext when audio is not needed to save system resources and battery life, especially on mobile devices. Running AudioContext is still playing silence even if there is no playing source node connected to the destination. Additionally, on iOS devices, the state of the AudioContext is directly related with state of the lock screen. If a running AudioContext exists, it is impossible to set lock screen state to state_paused.

  • Configure the audio session early: Set AudioManager.setAudioSessionOptions() once at startup — for example iosCategory: 'playback' for media apps or iosCategory: 'playAndRecord' when recording and playback coexist.

AudioRecorder Management

  • Single AudioRecorder: It is preferred to create only a single instance of the AudioRecorder class for the best performance, memory, and battery consumption. While an idle recorder has minimal impact, switching between separate recorder instances might have a noticeable impact on the device.

  • Activate the session before recording: Call AudioManager.setAudioSessionActivity(true) and enable observeAudioInterruptions() before start() or resume().

  • Rotate files for crash resilience: When recording to disk, set rotateIntervalBytes (for example every 1 MB) so a crash only loses the current segment. Merge segments afterward with concatAudioFiles().

  • Subscribe and unsubscribe to callbacks: If you use onAudioReady() for metering or visualization, always clear the callback when recording stops or the component unmounts. Use a lower sample rate for analysis callbacks than for playback to reduce CPU usage.

Architecture

  • Create a singleton class to manage the audio layer: Instead of storing AudioContext or nodes directly in your React components using useState or useRef, consider creating a singleton class that encapsulates the audio layer logic using React Native Audio API. This class can manage the lifecycle of the AudioContext, handle audio nodes, and provide methods for playing, pausing, and stopping audio. This approach promotes separation of concerns and makes it easier to manage audio state across your application.

  • Separate native logic from React state: Keep AudioContext, AudioRecorder, and node graph management in a service singleton. Use a React context or custom event emitter for UI state and to bridge playback/recording events into components.

  • Serialize async audio operations: Route mutations such as play, pause, seek, and cleanup through a single promise queue. Concurrent calls from UI gestures, lock-screen controls, and interruption handlers can otherwise race and leave the graph in an inconsistent state.

  • Guard concurrent sessions: Use a state machine (for example idlerecordingpaused) and reject overlapping start() / stop() calls. This prevents duplicate recorder sessions and simplifies error recovery.

  • Clean up at the app root: On provider unmount, remove AudioManager event listeners, hide PlaybackNotificationManager / RecordingNotificationManager notifications, and reset recorder state.

Source nodes and audio graph

AudioParam changes

System integration

  • Handle audio interruptions: Listen for interruption events via AudioManager.addSystemEventListener(). Pause playback or recording when event.type === 'began'. On ended, resume only if event.shouldResume is true — a short delay (around 1 second) before resuming gives the OS time to settle.

  • Configure background audio in Expo: For background playback or recording, enable the required options in the react-native-audio-api Expo plugin — iOS background mode, Android foreground service, and the appropriate permissions.

  • Wire lock-screen and notification controls once per session: Register PlaybackNotificationManager or RecordingNotificationManager listeners when a session starts and remove them on cleanup. On iOS, lock-screen play/pause state follows the AudioContext — you must suspend() the context to show paused. For recording indicators on iOS, use a Live Activity with expo-widgets instead of RecordingNotificationManager.

Performance

  • Throttle position callbacks: Set onPositionChangedInterval on queue source nodes to limit how often progress events fire. Avoid driving UI updates on every audio buffer.

  • Avoid per-frame allocations in visualizations: When passing analyser or recorder data into Reanimated shared values, mutate the existing Float32Array in place rather than allocating a new one each frame. See the audio visualization guide for patterns.

  • Decouple analysis rate from playback rate: Metering and waveform previews do not need the same sample rate as playback. Use a lower rate for onAudioReady() or AnalyserNode when full fidelity is not required.

Debugging

Avoid logging audio nodes

Do not pass AudioNode instances or raw JSI objects (e.g. the value returned by context.context.createGain()) directly to console.log.

Logging a node enumerates its properties and may warm internal caches. In development, repeatedly logging nodes — especially in a tight loop — can cause the console to retain references and make objects appear to leak.

Prefer logging plain values instead:

console.log({ channelCount: node.channelCount, numberOfInputs: node.numberOfInputs });