expo-audio-waveform
v0.1.0
Published
Offline audio waveform (peak amplitude) extraction for Expo & React Native. Decodes local or remote audio files natively — no playback — into a normalized 0..1 peak array for drawing waveforms.
Maintainers
Readme
expo-audio-waveform
Offline audio waveform (peak amplitude) extraction for Expo & React Native.
Decodes an audio file natively and offline — no playback, no audio session,
no interference with anything else that's playing — and returns a normalized
0..1 array of peak amplitudes you can render as a waveform.
- 🎧 iOS —
AVAssetReader(AVFoundation) - 🤖 Android —
MediaExtractor+MediaCodec - 🌐 Web — Web Audio
decodeAudioData - 🔗 Works with local and remote URIs (
file://, bare paths,content://on Android, andhttp(s)://) - 🧱 New Architecture (Fabric/bridgeless) compatible — built on the Expo Modules API
- 📦 Zero third-party native dependencies (platform decoders only)
Built for the common "waveform behind a scrubber" UI where the old trick of
playing audio at 16× to sample it no longer works (expo-audio caps playback at
2×). This decodes the file directly instead — faster than realtime and silent.
Installation
npx expo install expo-audio-waveformThen rebuild the native app (this is a native module — it does not run in Expo Go):
npx expo prebuild
npx expo run:ios # or run:androidNo config plugin is required. On Android, decoding a remote URL needs the
INTERNET permission, which the module declares and most apps already have.
Usage
import { extractWaveform } from "expo-audio-waveform";
// Returns number[] of length `samples`, each value 0..1 (peak of that segment
// relative to the loudest segment). Accepts local or remote URIs.
const bars = await extractWaveform(audioUrl, { samples: 64 });
// Analyze only a sub-range (milliseconds):
const chorus = await extractWaveform(audioUrl, {
samples: 64,
startMs: 30_000,
endMs: 45_000,
});API
function extractWaveform(
uri: string,
options?: {
samples?: number; // number of bars / array length (default 64)
startMs?: number; // window start in ms (default 0)
endMs?: number; // window end in ms (default: end of file)
metric?: "rms" | "peak"; // amplitude metric (default "rms")
},
): Promise<number[]>;Returns a promise resolving to an array of exactly samples numbers in the
range 0..1. Pure silence yields all zeros.
Throws if the URI can't be read, has no audio track, or has an unknown / open-ended duration (e.g. a live stream). Wrap the call and fall back to a placeholder waveform if you need to render something regardless:
let bars: number[];
try {
bars = await extractWaveform(uri, { samples: 64 });
} catch {
bars = myPlaceholderBars; // deterministic fallback
}Notes & limitations
- Supported formats follow the OS decoders — MP3, AAC/M4A, WAV, FLAC, and more on both iOS and Android. Anything the platform can't decode will throw.
metric:rms(default) vspeak.rmsis root-mean-square energy per slice — it tracks perceived loudness, so even loud/compressed tracks keep a varied, natural waveform shape.peaktakes the maximum absolute sample per slice; it's punchier but saturates toward1.0on mastered music, flattening the shape. Usermsfor visualizations;peakwhen you specifically want transient peaks.- Amplitudes are normalized to the loudest bar in the requested window, so a quiet track still fills the display. Compare bars within one result, not across different files.
- Duration required. Bucketing is done by timestamp, so the source must report a finite duration. Regular files (local or CDN) always do.
- Use
https://for remote audio. On Android 9+, cleartexthttp://is blocked by default; the consuming app must opt in viausesCleartextTraffic/ a network-security config for plain HTTP to work. On iOS, remote sources are downloaded to a temp file before decoding.
How it works
Each platform decoder is asked for linear PCM. As decoded frames stream in, the
module walks them once, reducing each time-bucket (samples buckets spread
evenly across the requested window) to a single value — RMS energy or peak, per
metric — then normalizes the buckets to 0..1. Nothing is ever played back,
and the whole file is never held in memory at once — only the running
per-bucket accumulators.
License
MIT © Yusuf Emirhan Şahin
