Web Audio API pure JavaScript implementation. Useful for:
- Agents and bots – any writable stream, no audio device needed.
- Audio analysis –
decodeAudioDatafor 20+ formats,AnalyserNodeas in browser. - Server-side rendering –
OfflineAudioContextrenders faster than realtime. - Audio in CI – any JS engine: node, deno, bun, llrt, quickjs, jz.
- Unit-testing – real samples, no mocks, 100% WPT.
- Tone.js and web audio libs –
import 'web-audio-api/polyfill'installs the globals. - CLI audio scripting – PCM in and out through stdio, 46 runnable examples.
npm install web-audio-api
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.
How do I render offline, without speakers?
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 samplesRendering runs faster than realtime and opens no audio device, so it also works under any test runner for asserting on samples. See render-to-buffer.js.
How do I close an AudioContext?
await ctx.close()Or with explicit resource management: using ctx = new AudioContext()
Why does it start suspended?
AudioContext starts suspended to match the Web Audio lifecycle. In Node, call await ctx.resume(). Browsers may require that call inside a user gesture. OfflineAudioContext doesn't need it.
Does it work with Tone.js?
Yes. Tone.js uses standardized-audio-context, which needs globals such as window.AudioParam for instanceof checks. 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 in app.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/mic with CustomMediaStreamTrack:
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() accepts Float32Array, Float32Array[], or interleaved 8/16/32-bit integer PCM buffers. Integer PCM conversion uses pcm-convert. CustomMediaStreamTrack extends MediaStreamTrack. 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, use ctx.createMediaStreamDestination().
If the default microphone backend cannot open the device, pass backend: 'process' to use sox/ffmpeg instead: mic({ ..., backend: 'process' }). All bundled examples accept backend=process on 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/mic peer 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/mic installed, getUserMedia rejects with a NotFoundError containing an install hint.
node examples/<name>.js runs each example with its defaults.
node examples/<name>.js --help for every accepted argument, option, keyboard control, and alternate invocation.
- 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) - latency-tester.js: Round-trip latency: speakers → mic, in ms (requires
@audio/mic) - level-meter.js: Mic RMS and peak in dBFS, fast or slow ballistics (requires
@audio/mic)
- 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 - octave-illusion.js: One tone jumps register and side (headphones!) –
400 800 2 12s - scale-illusion.js: Two scales split between ears, regrouped by pitch (headphones!) –
200 261.63 8s - tritone-paradox.js: Up or down? Tritone pairs that refuse to decide –
0 8 1.2 - continuity.js: A tone sounds unbroken through noise bursts –
440 0.6 on 15s - streaming.js: Two tones fuse into one stream or split into two –
240 4 15s - huggins-pitch.js: A pitch that exists in neither ear (headphones!) –
600 20s - zwicker-tone.js: An after-tone lingers where the notch was –
2000 3 2 20s
- 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 - wavetable.js: Fourier wavetables, crossfaded –
organ 220 0.3 6s - granular.js: Grain cloud from a seeded buffer –
0.08 15 4 10s
- 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: Jazz in seven styles, modal first, lead on guitar, flute, harp, or piano –
style=ambient lead=harp - euclidean.js: Bjorklund rhythms, 2–3 voices –
120 16 3,5,7 20s
- 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) - reverb.js: Convolver with a seeded impulse response –
2 0.35 3s
All benchmark scenarios render faster than real time. Pure JS matches Rust napi on simple graphs. Convolution and compression are 2–4× slower.
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
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.
- 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.
MIT