Skip to content
Kamau Wanyee

A love letter to react-native-audio-api

By ·
·7 min read
#posts#react-native#audio#recording#open-source

A recorder API often begins with start() and ends with stop(). That interface works until the device changes underneath the application.

A phone call can take audio focus. A Bluetooth device can disconnect. The operating system can change the input route. Another app can duck or interrupt audio. The microphone can remain technically active while delivering unusable samples.

A recorder that must respond to those conditions needs more than a file URI and an isRecording flag. It needs access to the audio system and, sometimes, to the samples moving through it.

I learned this while building a mobile recorder around react-native-audio-api. The library made native audio facts visible. That changed the design from guessing what happened to observing the condition, then applying product policy.

Polling turns causes into guesses

A high-level recorder may expose a status object:

const status = recorder.getStatus();

if (previous.isRecording && !status.isRecording) {
  onInterruptionBegin();
}

Polling can detect that a state changed. It cannot explain the change.

If the application samples status every 500 milliseconds, it sees a transition after the fact. The recorder may have lost audio focus, lost its input route, failed internally, or stopped because the application requested it. The boolean carries none of that context.

Metering has the same limitation. A low level can mean a quiet room, a pause in speech, a disconnected route, a broken capture path, or a stream of zeroes. A threshold turns those conditions into one category.

Polling remains useful for displaying status. It becomes fragile when it serves as the only interruption detector or capture-health monitor. The application starts tuning thresholds against symptoms instead of responding to causes.

Subscribe to native events

react-native-audio-api exposes system events through AudioManager. The current implementation subscribes to:

  • interruption, with began and ended events
  • routeChange, with a native reason
  • duck, when another audio source changes the session conditions

The interruption event also carries shouldResume. The route-change event can identify reasons such as OldDeviceUnavailable or NoSuitableRouteForCategory.

A useful boundary looks like this:

AudioManager.addSystemEventListener("interruption", (event) => {
  session.send({
    type: event.type,
    shouldResume: event.shouldResume,
  });
});

The event is an observation. The next action is policy.

The application can preserve the current segment, wait for focus to return, start a new segment, show a recovery message, or require the user to continue manually. The library does not choose among those actions. That separation matters because two products can receive the same interruption and need different behavior.

The same principle applies to audio-session configuration. The application chooses the session category, activates or deactivates the session, and decides how to handle route changes. The library exposes the controls. The application owns the consequence.

Read the samples

A meter reduces a buffer to a number. onAudioReady provides the buffer itself.

In the current implementation, the recorder receives PCM data and passes one channel to a capture-health monitor:

recorder.onAudioReady(
  { sampleRate: 16_000, bufferLength: 1_600, channelCount: 1 },
  ({ buffer }) => {
    const samples = buffer.getChannelData(0);
    const health = monitor.accept(samples);
    emitCaptureHealth(health);
  }
);

The monitor inspects features such as RMS level, peak level, and zero-crossing behavior. It distinguishes useful signal from several failure modes:

  • quiet silence, which remains valid capture
  • flatline input
  • repeated buffers
  • clipping
  • static-like audio
  • recorder errors

That distinction prevents a common mistake. Silence does not prove that the microphone failed. A quiet room can produce valid PCM. A flatline can indicate that the capture path stopped producing meaningful data.

The monitor also uses time windows. One suspicious buffer should not immediately invalidate a session. Sustained flatline, repeated data, clipping, or static-like input can move the health state from healthy to suspect and then invalid.

Interruption handling needs its own reset behavior. Audio hardware can emit empty or repeated buffers while focus returns. The current implementation pauses health checks briefly after interruption and resets the monitor before evaluating new samples. That policy prevents the recovery path from treating expected restart behavior as a new microphone failure.

Facts belong to the library. Decisions belong to the application.

The useful division is simple. It is the boundary between the native observations in this article and the durable recording lifecycle described in Recording is a distributed system:

LayerResponsibility
Audio libraryReport interruptions, routes, ducking, PCM buffers, recorder results
Session stateDecide whether to pause, resume, restart, preserve, or stop
Local storageKeep segments and recovery metadata
Product UIExplain the state and request user action when needed

This division also clarifies testing. Tests can feed the state machine a native interruption, a route-loss reason, or an invalid capture snapshot. They can verify the policy without pretending that a timer is the operating system.

Capture state must survive the file lifecycle

Audio capture, session state, local files, and recovery form one system.

The current recorder writes mono M4A output to the app’s document directory. It configures an Android flush interval and rotates output by size. Rotation limits the amount of media exposed to one interrupted file and gives recovery several smaller artifacts to inspect.

When an interruption begins, the implementation attempts to finalize the active segment while the native recorder is still available. A resumed session can then create another segment under the same recording identity. The final recording can join those segments later.

Stopping also has several stages. The recorder returns paths, duration, and size. The application validates the resulting file and checks its duration. getAudioDuration supplies a metadata-based duration check. concatAudioFiles joins rotated segments when necessary.

A path confirms that a file exists. Validation determines whether the media is usable.

When validation fails, the current policy preserves non-empty output with metadata describing the native duration and failure reason. Missing or empty files cannot be recovered from storage. That distinction keeps cleanup from destroying the only surviving artifact.

The local file becomes a checkpoint. Upload and later processing can continue from that checkpoint. Deletion belongs after the product’s actual finish line, which may include validation, upload acknowledgement, and any required processing.

What the lower-level API makes easier

react-native-audio-api removes much of the platform plumbing required to reach these facts from React Native:

  • native interruption and route events
  • audio-session activation and configuration
  • PCM callbacks
  • recorder errors
  • file output with duration and size results
  • concatenation of rotated files
  • duration inspection

That is a meaningful reduction in native integration work. The application can focus on session identity, state transitions, capture policy, local persistence, and recovery.

The lower level also creates obligations. PCM callbacks require CPU time, memory, and careful scheduling. Audio-session behavior varies across platforms and devices. Thresholds need tests and calibration. Route reasons require platform-specific interpretation. Local retention creates storage and privacy decisions. More visibility means more states to model.

The library cannot guarantee that every sample reaches durable storage. It cannot decide whether an interruption should resume automatically. It cannot define when a product may safely delete a local artifact.

Those decisions remain application work.

A mobile recorder should use a high-level abstraction when start(), stop(), and a completed file are enough. When device conditions can change the meaning of “recording,” the abstraction needs to expose more of the system.

The practical question is simple: can the application observe the failures that matter before the user discovers them?

If it can see native interruptions, route changes, ducking, PCM buffers, and file-validation results, it can build a deliberate response. If it sees only a sampled status object, it will eventually infer the hard states from timing and hope.