npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-native-waveform-recorder

v1.0.1

Published

High-performance React Native audio recorder with a native live waveform, multi-segment record/pause/resume, in-place preview + scrub playback, slide-to-cancel / slide-to-lock gestures, silence detection, and a WhatsApp-style 64-bucket export. Built on th

Downloads

552

Readme

react-native-waveform-recorder

High-performance React Native audio recorder with a native live waveform, multi-segment record/pause/resume, in-place preview + scrub playback, and a 64-bucket export ready for chat-bubble waveforms. Built on the New Architecture (Fabric), zero JS in the metering hot path.

Best paired with react-native-waveform-player — this library handles the recording side; the player handles playback. Use them together for a complete voice-message stack: record with a live waveform, then drop the resulting URI + 64-bucket peaks straight into the player for the chat bubble — no decode round-trip needed. Both libraries are intentionally standalone (zero shared peer deps) and share the same visual language.

Works with Expo — fully supported via a development build (EAS Build or expo prebuild). Doesn't work in Expo Go because Expo Go can't load third-party native modules. See Expo install below.

Demo

| iOS | Android | | :---: | :---: | | | |

Why another recorder?

| | this library | @simform_solutions/react-native-audio-waveform | react-native-nitro-sound | @lodev09/expo-recorder | @bhojaniasgar/react-native-audio-waveform | | --- | --- | --- | --- | --- | --- | | Renders waveform | Native, on-thread (Swift / Kotlin) | Native (mode="live" for recording, mode="static" for playback) | No built-in renderer — exposes currentMetering, you draw it yourself | JS-driven via Reanimated | Native | | Live waveform during recording | Yes | Yes | Manual (from metering callback) | Yes | Yes | | Record + pause/resume → same file | Yes | Yes (pauseRecord / resumeRecord) | Yes (pauseRecorder / resumeRecorder) | Via underlying expo-audio | Yes (pauseRecording / resumeRecording, Android 7.0+) | | Preview state (in-place playback + scrub) | Yes | Separate static mode pointing at the file | No integrated preview UI | Manual (composed externally) | Separate Waveform component for playback | | WhatsApp-style 64-bucket export | Built in (onComplete.samples) | No | No | No | No | | Slide-to-cancel / slide-to-lock | Native gestures | No | No | No | No | | Silence detection w/ auto-stop | Yes | No | No | No | No | | Output formats | m4a, aac, wav, opus | Configurable encoder (AAC/AAC-LD/HE-AAC/…) | Configurable via AudioSet (AAC/AAC-LD/…) | Whatever expo-audio supports | m4a (default) | | Raw-PCM streaming hook | Opt-in subpath (/pcm-stream) | No | No (metering only) | No | No | | Dependencies | None | react-native-gesture-handler | react-native-nitro-modules | expo-audio + react-native-reanimated + react-native-gesture-handler | None | | Ecosystem | Bare RN + Expo (dev client) | Bare React Native | Bare React Native | Expo-only | Bare React Native |

Install

npm install react-native-waveform-recorder
# or
yarn add react-native-waveform-recorder

Requires React Native 0.85+ with the New Architecture enabled (Fabric + TurboModules). Bare minimums: iOS 13, Android API 24 (Android API 29 for opus output).

iOS

pod install

Add a microphone usage description to Info.plist:

<key>NSMicrophoneUsageDescription</key>
<string>Record voice messages.</string>

If you opt into backgroundRecording, also add:

<key>UIBackgroundModes</key>
<array>
  <string>audio</string>
</array>

Android

The library declares RECORD_AUDIO for you. You still need to request the runtime permission from JS — the wrapper does this automatically on start() via PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.RECORD_AUDIO).

For backgroundRecording, declare the foreground service in your android/app/src/main/AndroidManifest.xml:

<service
    android:name="com.waveformrecorder.WaveformRecorderBackgroundService"
    android:foregroundServiceType="microphone"
    android:exported="false" />

FOREGROUND_SERVICE and FOREGROUND_SERVICE_MICROPHONE are already declared by the library and merged in for you.

Expo

This library works with Expo via a development build (EAS Build or npx expo prebuild). It does not work in Expo Go — Expo Go can't load third-party native modules.

1. Install the package.

npx expo install react-native-waveform-recorder

2. Add the config plugin to app.json / app.config.js.

The library ships an Expo config plugin that wires up the native permissions and the Android foreground service for you — no manual Info.plist / AndroidManifest.xml edits.

{
  "expo": {
    "plugins": [
      [
        "react-native-waveform-recorder",
        {
          "microphonePermission": "Allow $(PRODUCT_NAME) to record voice messages.",
          "backgroundRecording": false
        }
      ]
    ]
  }
}

Both options are optional:

| Option | Type | Default | What it does | | --- | --- | --- | --- | | microphonePermission | string \| false | generic message | Sets iOS NSMicrophoneUsageDescription. Pass false to leave it untouched (e.g. if you set it elsewhere). | | backgroundRecording | boolean | false | When true, adds the iOS audio UIBackgroundMode and the Android foreground <service> declaration so the backgroundRecording prop works. |

If you don't need background recording you can add the plugin with no options at all:

{
  "expo": {
    "plugins": ["react-native-waveform-recorder"]
  }
}

The library auto-merges RECORD_AUDIO + foreground-service permissions into the Android manifest via manifest-merger, so you don't need to list them in android.permissions.

3. Generate the native projects (or rebuild your dev client).

npx expo prebuild --clean
# then re-build your dev client / EAS Build
eas build --profile development --platform all

The library's WaveformRecorder.podspec uses standard new-architecture autolinking (install_modules_dependencies(s)), so expo prebuild picks it up automatically — no manual Podfile or build.gradle edits required.

SDK compatibility. The library requires React Native 0.85+ with the New Architecture enabled. Pick an Expo SDK that ships RN 0.85 or newer (check the Expo SDK ↔ RN compatibility table). New Architecture is the default on recent Expo SDKs; if you're on an older one, enable it with "newArchEnabled": true in app.json.

Quick start

import { useRef } from 'react';
import { Button, View } from 'react-native';
import {
  WaveformRecorderView,
  type WaveformRecorderViewRef,
} from 'react-native-waveform-recorder';

export function VoiceNote() {
  const ref = useRef<WaveformRecorderViewRef>(null);
  return (
    <View>
      <WaveformRecorderView
        ref={ref}
        style={{ height: 56 }}
        playedBarColor="#25D366"
        onComplete={(e) => {
          console.log('saved:', e.uri, e.durationMs, e.samples.length);
        }}
      />
      <Button title="Record" onPress={() => ref.current?.start()} />
      <Button title="Stop" onPress={() => ref.current?.stop()} />
    </View>
  );
}

onComplete.samples is a 64-bucket number[] in [0, 1] computed natively post-recording — drop it straight into a chat-bubble waveform without re-decoding the file.

State machine

              start()                                   stop()
   idle ─────────────────► recording ─────────────────► stopped ──► (onComplete)
                              │   ▲                       ▲
                       pause()│   │resume()               │
                              ▼   │                       │
                            paused ────► enterPreview ────┤
                                            │             │
                                            │ resume()    │
                                            ▼             │
                                       betweenSegments ───┘
                                       (continue-record)

| state | what's happening | | --- | --- | | idle | view rendered, nothing recorded yet | | recording | meter + bars live, writing to disk | | paused | meter paused, file flushed but not finalized | | preview | playback-style view with scrub gesture + play button | | betweenSegments | post-preview, ready to resume() and append a new segment | | stopped | terminal state; segments concatenated, onComplete fired |

controlledState flips the component into a controlled mode where you advance state via prop updates and commands become inert.

Props

Recording config

| prop | type | default | notes | | --- | --- | --- | --- | | outputUri | string | cache dir | Where to write the final file. The host's cache dir is used if omitted. | | output | { format, sampleRate, channels, bitrate, quality } | m4a / 44.1k / 1ch / 128kbps / high | Convenience nested form. Equivalent to individual outputFormat, outputSampleRate, outputChannels, outputBitrate, outputQuality props. | | outputFormat | 'm4a' \| 'aac' \| 'wav' \| 'opus' | 'm4a' | Opus requires iOS 11+ / Android API 29+; older OSes silently fall back to AAC. Container extension follows the format. | | outputSampleRate | number | 44100 | | | outputChannels | 1 \| 2 | 1 | | | outputBitrate | number | 128000 | Ignored for wav (lossless). | | outputQuality | 'low' \| 'medium' \| 'high' | 'high' | | | maxDurationMs | number | 0 | 0 = no cap. When hit, onMaxDurationReached fires + recording auto-stops. | | minDurationMs | number | 0 | Soft hint surfaced via onComplete.durationMs (useful for "press-and-hold" UIs). | | meterUpdatesPerSecond | number | 30 | Native polling rate for the meter (iOS CADisplayLink, Android Handler). | | samplesPerSecond | number | 12 | Visual sample rate — how many bars/sec we paint on screen. |

Visual

| prop | type | default | | --- | --- | --- | | playedBarColor / unplayedBarColor / futureBarColor | ColorValue | system blue / #3a3a3a / unplayedBarColor | | barWidth / barGap / barRadius | number | 3 / 2 / barWidth/2 | | containerBackgroundColor / containerBorderRadius / showBackground | ColorValue / number / boolean | transparent / 16 / true | | showTime / timeColor / timeMode | boolean / ColorValue / 'count-up' \| 'count-down' | true / #fff / 'count-up' | | recordingMode | 'scroll' \| 'morph' \| 'centered' | 'scroll' | How new bars enter — feed from the right (scroll), grow in place (morph), centered around a playhead. | | futureBarStyle | 'dot' \| 'line' \| 'hidden' | 'hidden' | What to render in the un-recorded portion. Default matches WhatsApp / Slack / Messenger (nothing until the first sample arrives). Set to 'dot' or 'line' for an Instagram / Zalo-style placeholder. | | newSampleEntry | 'grow' \| 'fade' \| 'none' | 'grow' | Per-bar entry animation. |

Preview & continue-recording

| prop | type | default | notes | | --- | --- | --- | --- | | enablePreview | boolean | true | Gates enterPreview command. | | enableContinueRecording | boolean | true | Gates resume() from preview (WhatsApp-style multi-segment). | | showPlayButton / playButtonColor | boolean / ColorValue | true / playedBarColor | Built-in play/pause button shown during preview state. |

Gestures

| prop | type | default | | --- | --- | --- | | enableSlideToCancel / slideToCancelThresholdDp | boolean / number | false / 80 | | enableSlideToLock / slideToLockThresholdDp | boolean / number | false / 80 |

Silence detection

| prop | type | default | notes | | --- | --- | --- | --- | | silenceThresholdDb | number | -160 | Negative dBFS. -50 is a typical "room is quiet" threshold. | | silenceTimeoutMs | number | 0 | 0 disables. | | autoStopOnSilence | boolean | false | When true, the engine calls stop() itself after a silence window. |

Background recording

| prop | type | default | notes | | --- | --- | --- | --- | | backgroundRecording | boolean | false | Requires plist + manifest setup (see Install). On iOS we emit onError('background-capability', …) once if audio is missing from UIBackgroundModes. | | backgroundNotificationTitle / backgroundNotificationBody | string | generic | Android-only: shown in the foreground-service notification. |

Raw PCM streaming

| prop | type | default | notes | | --- | --- | --- | --- | | enablePcmStream | boolean | false | WAV-only. Stream raw PCM via onPcmChunk — see the Raw PCM streaming section below for the full guide. | | pcmChunkMs | number | 200 | Target chunk cadence in ms. |

Controlled state

| prop | type | default | notes | | --- | --- | --- | --- | | controlledState | 'auto' \| 'idle' \| 'recording' \| 'paused' \| 'preview' \| 'stopped' | 'auto' | Switch to controlled mode where the host advances state via prop updates and commands become inert (they still emit onStateChange with the requested new state). |

Events

| event | payload | when | | --- | --- | --- | | onStateChange | { state, durationMs } | every state transition | | onMeter | { amplitude, peak, db } | meterUpdatesPerSecond Hz while recording | | onMaxDurationReached | {} | maxDurationMs reached | | onComplete | { uri, durationMs, format, mimeType, sizeBytes, sampleRate, channels, samples (64-bucket number[]), peakAmplitude } | after stop() finalises | | onError | { message, code } | non-fatal warnings + fatal failures (code namespaced per origin: session, start, pcm-stream, …) | | onPermissionDenied | {} | mic permission denied at start() | | onSeek | { positionMs } | scrub gesture or seekPreview() | | onPlaybackTimeUpdate | { positionMs, durationMs } | preview playback tick | | onSlideProgress | { cancelProgress, lockProgress } (each 0-1) | live during slide gesture | | onSlideCancel / onSlideLock | {} | once when the gesture threshold is crossed | | onSilenceDetected | { durationMs } | once per silence window | | onPcmChunk | { chunk (base64), sampleRate, channels, bytesPerSample, timestampMs } | every pcmChunkMs while enablePcmStream + outputFormat='wav' |

Imperative commands

Hold a ref to the view and call methods directly.

| command | notes | | --- | --- | | start() | Begins (or restarts) recording. Auto-requests mic permission. | | pause() | Pauses the active segment without finalising it. | | resume() | Resumes from paused (same segment) or betweenSegments (new segment). | | stop() | Finalises, concatenates segments, fires onComplete. | | cancel() | Discards all segments + returns to idle. Does not fire onComplete. | | enterPreview() / exitPreview() | Switch into / out of the preview state. | | togglePreviewPlayback() | Play / pause preview audio. | | seekPreview(positionMs) | Seek the preview playhead. |

Raw PCM streaming

The default bundle stays small — raw PCM lives behind an opt-in subpath so apps that don't need it never pay the cost.

import { WaveformRecorderView } from 'react-native-waveform-recorder';
import {
  decodePcmChunk,
  pcmToMonoFloat32,
} from 'react-native-waveform-recorder/pcm-stream';

<WaveformRecorderView
  output={{ format: 'wav', sampleRate: 16000, channels: 1 }}
  enablePcmStream
  pcmChunkMs={100}
  onPcmChunk={(e) => {
    const int16 = decodePcmChunk(e.chunk);
    const f32 = pcmToMonoFloat32(int16, e.channels);
    // hand `f32` to your STT / VAD pipeline of choice
  }}
/>;

Caveats:

  • Only works with outputFormat = 'wav' — m4a / opus paths don't expose pre-encoded samples.
  • Payloads cross the bridge as base64, not zero-copy buffers. Fine up to ~1 MB/s; beyond that prefer a true JSI module like react-native-audio-api.

Recipes

The example/ workspace ships themed, copy-pastable recipes that mirror real-world voice-message UIs. All six chat-style recipes pair this library with react-native-waveform-playeronComplete hands the player both the recorded file URI and the 64-bucket peaks, so the playback bubble paints instantly with no decode round-trip. See example/src/components/SentVoiceNote.tsx for the shared bubble.

| recipe | location | what it shows | | --- | --- | --- | | WhatsApp | example/src/screens/WhatsAppScreen.tsx | green pill, mic-as-send swap, swipe-up-to-lock + slide-left-to-cancel chevron, preview with delete + send. Sent voice notes become playable "out" bubbles in the chat thread. | | Messenger | example/src/screens/MessengerScreen.tsx | continue-recording flow, gradient pill, locked vs unlocked variants. Sent notes appear as playable bubbles in the conversation feed. | | Instagram | example/src/screens/InstagramScreen.tsx | gradient ring meter + bottom-sheet preview. Sent notes appear as right-aligned DM player bubbles. | | Slack | example/src/screens/SlackScreen.tsx | single-press send, no preview. Sent notes appear as #general voice-memo messages. | | TikTok | example/src/screens/TikTokScreen.tsx | hold-to-record, no preview, dark theme. Sent notes appear in a "Recent sends" reel. | | Zalo | example/src/screens/ZaloScreen.tsx | light-mode pill with explicit delete / preview / send action row. Sent notes appear as light-mode player bubbles. | | Gesture playground | example/src/screens/GestureScreen.tsx | live cancel/lock progress bars driven by onSlideProgress. | | Silence demo | example/src/screens/SilenceScreen.tsx | adjustable threshold + auto-stop on silence. | | 64-sample export | example/src/screens/SamplesExportScreen.tsx | renders onComplete.samples three ways for chat-bubble parity. | | Stress test | example/src/screens/StressTestScreen.tsx | 30-minute run with tick / peak / RMS counters. | | Raw PCM stream | example/src/screens/PcmStreamScreen.tsx | end-to-end VAD-style consumer of onPcmChunk. |

Run the example app:

yarn install
cd example
yarn ios   # or yarn android

How it works

See ARCHITECTURE.md for the full breakdown — file layout, state machine, threading model, memory bounds, codegen quirks, drawing pipeline, Fabric lifecycle, and segment concat strategies per format.

In short: the JS layer is intentionally thin (permission flow + ref shape + DirectEvent re-emission). Everything else — recording, drawing, metering, scrub gestures, preview playback, multi-segment concat — lives in Swift / Kotlin behind a Fabric composite view.

Lessons learned

If you're building something similar (a native-rendering Fabric component, an audio recorder, a stateful UI with async lifecycle) — or you're picking this codebase up to extend it — start with LESSONS_LEARNED.md. It's the field report of every non-obvious bug we hit (Fabric quirks like display: 'none' unmounting iOS views, async race conditions in preview snapshots, the WAV multi-segment memory bomb, state-machine UI desync patterns, …) along with the meta-lessons each one taught us. Cheaper to read than to re-discover.

Roadmap & known limitations

The library is feature-complete and production-ready, but a few items are deferred for future releases. PRs welcome.

Planned improvements

  • recordingMode modes morph and centered — currently only 'scroll' is fully implemented. 'morph' (grow new bars in place, no scrolling) and 'centered' (playhead-anchored layout with bars expanding outward) are accepted as prop values but fall back to 'scroll'. Implementing them needs new sub-pixel slot math in WaveformBarsView on both platforms — the scroll math doesn't translate directly because it assumes new samples always enter from the right edge.
  • Streaming WAV multi-segment concat — the only known long-recording memory hazard. When the user pauses + resumes a wav-format recording multiple times then calls stop() or enterPreview(), the current concat path buffers every segment's PCM in RAM before writing the output. Peak memory ~1× total PCM on Android, ~3× on iOS. For a 1-hour multi-segment WAV at 44.1 kHz mono 16-bit (~317 MB raw), iOS can peak ~950 MB and crash. The fix is straightforward — replace the buffer-then-write path with a chunked-stream-then-write path (~256 KB peak instead of 3× PCM) on both platforms. Continuous (no-pause) WAV recordings and any-length recordings in m4a/aac/opus are unaffected.
  • CI workflowlint / typecheck / iOS build / Android build pipeline. Currently runs locally only.
  • npm publish — first publish to npm is still pending; install from the GitHub repo until then.
  • True zero-copy PCM streaming — current onPcmChunk payloads cross the bridge as base64. Fine up to ~1 MB/s. For higher throughput a JSI-backed buffer module (or delegating to react-native-audio-api) would avoid the base64 round-trip.

Out of scope

These were intentionally rejected during planning to keep the library focused:

  • Streaming uploads from the recorder — handle in your own code on onComplete.
  • Multi-track mixing.
  • Recording effects / filters / VST-style processing — use react-native-audio-api if you need this.
  • JS-side custom waveform renderers — the native renderer is the whole point.
  • Old React Native architecture (paper-only) support.

Contributing

License

MIT