web-audio-api [](https://github.com/audiojs/web-audio-api/actions/workflows/test.yml) [](https://npmjs.org/package/web-audio-api)
August 31, 2026 · View on GitHub
Web Audio, without the browser. Run the same Web Audio graph in browsers, Node, CI, servers, and scripts.
- 100% WPT conformance, with a pure-JS audio graph and DSP core.
- Audio in CI –
OfflineAudioContextrenders without speakers. - CLI audio scripting – pipe, process, synthesize from terminal.
- Server-side audio – generate from APIs, bots, pipelines.
- Tone.js and web audio libs work in Node as-is.
npm install web-audio-api
Use
import { AudioContext } from 'web-audio-api'
const ctx = new AudioContext()
await ctx.resume()
const osc = ctx.createOscillator()
osc.frequency.value = 440
osc.connect(ctx.destination)
osc.start()
// → A440 through your speakers
@audio/speaker provides speaker output without extra setup.
Offline rendering
import { OfflineAudioContext } from 'web-audio-api'
const ctx = new OfflineAudioContext(2, 44100, 44100) // 1 second, stereo
const osc = ctx.createOscillator()
osc.frequency.value = 440
osc.connect(ctx.destination)
osc.start()
const buffer = await ctx.startRendering()
// buffer.getChannelData(0) → Float32Array of 44100 samples
Examples
node examples/<name>.js runs each example with its defaults. Each CLI is a thin adapter over a standalone examples/graphs/<name>.js module; the website imports that same graph into the browser’s native Web Audio context. Use node examples/<name>.js --help for every accepted argument, option, keyboard control, and alternate invocation. Parametric examples accept positional args or key=value with prefix matching (f=440, freq=440 both work). Note names (A4, C#3, Eb5), k for kHz (20k), and s/m/h for duration (10m) are supported.
| Example | |
|---|---|
| Test Signals | |
| tone.js | Reference pitch – sine A4 2s |
| sweep.js | Hear the audible range – 20..20k exp 3s |
| noise.js | White, pink, brown, blue, violet – pink 2s |
| impulse.js | Dirac click – 5 0.5s |
| dtmf.js | Dial a phone number – 5551234 |
| stereo-test.js | Left, right, center – 1k 1s |
| metronome.js | Programmable stick click – 80..240 10m X-x-x-x- |
| tuner.js | Guitar tuner – mic pitch in cents – 440 (requires @audio/mic) |
| Illusions | |
| shepard.js | Pitch that rises forever – up 15s |
| risset-rhythm.js | Beat that accelerates forever – up 120 20s |
| binaural-beats.js | Third tone from two (headphones!) – 200 10 10s |
| missing-fundamental.js | Your brain fills in the note – 100 3s |
| beating.js | Two close frequencies dance – 440 3 5s |
| Synthesis | |
| subtractive-synth.js | Sawtooth → filter sweep → ADSR |
| additive.js | Waveforms from harmonics – square 220 16 3s |
| fm-synthesis.js | DX7 frequency modulation – 440 2 5 3s |
| karplus-strong.js | A string plucked from noise – A4 4s |
| Generative | |
| sequencer.js | Step sequencer – precise timing |
| serial.js | Twelve-tone rows (Webern) – 72 30s |
| gamelan.js | Balinese kotekan – two parts, one melody – 120 20s |
| drone.js | Tanpura shimmer – C3 30s |
| jazz.js | Modal jazz – new every time |
| API | |
| speaker.js | Hello world |
| lfo.js | Tremolo via LFO |
| spatial.js | Sound moving through space |
| worklet.js | Custom AudioWorkletProcessor |
| linked-params.js | One source controlling many gains |
| fft.js | Frequency spectrum |
| render-to-buffer.js | Offline render → buffer |
| process-file.js | Audio file → EQ + compress → render |
| pipe-stdout.js | PCM to stdout – pipe to aplay, sox, etc. |
| mic.js | Live microphone → speakers with RMS meter (requires @audio/mic) |
| recorder.js | Record the mic to a WAV file, with a level meter (requires @audio/mic) |
FAQ
- How do I close an AudioContext?
-
await ctx.close()Or with explicit resource management:
using ctx = new AudioContext() - Why does it start suspended?
-
AudioContextstarts suspended to match the Web Audio lifecycle. In Node, callawait ctx.resume(). Browsers may require that call inside a user gesture.OfflineAudioContextdoesn't need it. - Does it work with Tone.js?
-
Yes. Tone.js uses
standardized-audio-context, which needs globals such aswindow.AudioParamforinstanceofchecks. Load the polyfill before Tone.js:import 'web-audio-api/polyfill' const Tone = await import('tone') Tone.setContext(new AudioContext()) const synth = new Tone.Synth().toDestination() synth.triggerAttackRelease('C4', '8n')Tone.js must use a dynamic
import()because static imports run before the polyfill. Alternatively, use--import:node --import web-audio-api/polyfill app.jsThen static
import * as Tone from 'tone'works inapp.js. - How do I decode audio files?
-
const buffer = await ctx.decodeAudioData(readFileSync('track.mp3'))decodeAudioData()uses @audio/decode for MP3, WAV, Ogg Vorbis, Opus, FLAC, AAC, ALAC, AIFF, CAF, WebM, and other supported audio or video containers without FFmpeg or native bindings. - How do I capture audio from the microphone?
-
In Node, pair
@audio/micwithCustomMediaStreamTrack:npm install @audio/micimport { AudioContext, MediaStreamAudioSourceNode, CustomMediaStreamTrack, MediaStream } from 'web-audio-api' import mic from '@audio/mic' const ctx = new AudioContext() await ctx.resume() const track = new CustomMediaStreamTrack({ kind: 'audio', label: 'mic', settings: { channelCount: 1, sampleSize: 16, sampleRate: ctx.sampleRate } }) const stream = new MediaStream([track]) const src = new MediaStreamAudioSourceNode(ctx, { mediaStream: stream }) src.connect(ctx.destination) // live monitor // @audio/mic's read(cb) is single-shot; re-arm it inside the callback. const read = mic({ sampleRate: ctx.sampleRate, channels: 1, bitDepth: 16 }) const pump = () => read((err, buf) => { if (err || !buf) return track.pushData(buf, { channels: 1, bitDepth: 16 }) pump() }) pump()track.pushData()acceptsFloat32Array,Float32Array[], or interleaved 8/16/32-bit integer PCM buffers. Integer PCM conversion usespcm-convert.CustomMediaStreamTrackextendsMediaStreamTrack. Prior art:CanvasCaptureMediaStreamTrack.See examples/mic.js for a runnable demo with gain and VU meter. To record the graph to a buffer, use
OfflineAudioContext.startRendering(). To capture live graph output as a stream, usectx.createMediaStreamDestination().If the default microphone backend cannot open the device, pass
backend: 'process'to usesox/ffmpeginstead:mic({ ..., backend: 'process' }). All bundled examples acceptbackend=processon the command line. - How do I use it as a polyfill?
-
import 'web-audio-api/polyfill' // AudioContext, GainNode, etc. are now globalThe polyfill also installs
navigator.mediaDevices.getUserMedia({ audio: true }), backed by the optional@audio/micpeer dependency. This lets browser mic-capture code run verbatim in Node:import 'web-audio-api/polyfill' // npm install @audio/mic const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) const ctx = new AudioContext() const src = ctx.createMediaStreamSource(stream) src.connect(ctx.destination) // stop capture stream.getAudioTracks()[0].stop()Without
@audio/micinstalled,getUserMediarejects with aNotFoundErrorcontaining an install hint. - Can I unit-test audio code?
-
Use
OfflineAudioContextwith any test runner; it renders without speakers. See render-to-buffer.js. - How fast is it?
-
All benchmark scenarios render faster than real time. Pure JS matches Rust napi on simple graphs. Convolution and compression are 2–4× slower. The JZ/WASM path is experimental. Run
npm run bench:allto measure.
Architecture
Pull-based audio graph. AudioDestinationNode pulls upstream via _tick(), 128-sample render quanta per spec. AudioWorklet runs synchronously (no thread isolation). DSP kernels separated from graph plumbing for future WASM swap.
EventTarget ← Emitter ← DspObject ← AudioNode ← concrete nodes
← AudioParam
EventTarget ← Emitter ← AudioPort ← AudioInput / AudioOutput
Node extensions
Beyond the spec, for Node.js. Not portable to browsers.
addModule(fn)– register a processor via callback instead of URL, no file neededsinkId: stream– pipe PCM to any writable:new AudioContext({ sinkId: process.stdout })thennode synth.js | aplay -f cdnumberOfChannels,bitDepth– control output format in the constructor.CustomMediaStreamTrack– extendsMediaStreamTrackwith a public constructor andpushData(chunk, options)to feed audio data (e.g. from a microphone). Prior art:CanvasCaptureMediaStreamTrack. See the mic FAQ.
Alternatives
- node-web-audio-api – Rust napi bindings. Faster heavy DSP, but node-only with compilation step and partial spec.
- standardized-audio-context – Browser-only. Normalizes cross-browser quirks.
- web-audio-api-rs – Pure Rust / WASM.
- web-audio-engine – Archived. Partial spec coverage.
- react-native-audio-api – React native partial implementation.
License
MIT