Web Audio APIwithout browser
Your web-audio code, cross-platform. No native bindings, no compilation. Process audio in batch, unit-test it in CI, stream from servers, render at the edge, script it in CLI.
npm install web-audio-api
import { AudioContext } from 'web-audio-api'
const ctx = new AudioContext()
await ctx.resume()
const osc = ctx.createOscillator()
osc.connect(ctx.destination)
osc.start()
Examples
Every example is one runnable file: play it here, run it in Node, drop it into CI.
Utilities
Test signals
Illusions
Synthesis
Generative
API
Questions
Is it fast enough for realtime?
For simple graphs, yes: every measured scenario renders hundreds of times faster than realtime. The native Rust binding stays 1.5–3× quicker; AudioWorklet execution is currently synchronous, so production low-latency work remains the boundary.
| Scenario | web-audio-api | node-web-audio-api (Rust) |
|---|---|---|
| OscillatorNode | 0.4 ms (2500× realtime) | 0.3 ms (3333×) |
| Oscillator through BiquadFilter | 1.6 ms (625×) | 0.5 ms (2000×) |
| DynamicsCompressor | 1.2 ms (833×) | 0.6 ms (1667×) |
| ConvolverNode, 128-tap IR | 6.5 ms (154×) | 2.1 ms (476×) |
| Chain: osc, filter, gain | 0.9 ms (1111×) | 0.4 ms (2500×) |
| 8-voice polyphony | 4.1 ms (244×) | 1.5 ms (667×) |
How does audio I/O work?
@audio/speaker provides the default output device. Microphone examples use the optional @audio/mic adapter, then feed PCM into a MediaStreamTrack. You can also set sinkId to any Node writable stream.
Which formats can it decode?
decodeAudioData() uses @audio/decode. Its codec and container coverage is broader and more predictable than the native decodeAudioData() set in a typical browser, which varies by browser and operating system. Codecs are detected and loaded on demand for MP3, WAV, Ogg Vorbis, Opus, FLAC, AAC, ALAC, AIFF, CAF, QOA, WebM, WMA, AC-3, DTS, tracker modules, DSD, and more. Supported tracks can also be extracted from MP4, MOV, MKV, WebM, and AVI without FFmpeg or native bindings. Codec packages carry their own licences.
How big is the install?
The core is 208 KB unpacked. Codecs load on demand through @audio/decode, TypeScript types ship in the package, and the project has been maintained since 2013.
Does Tone.js work?
Yes. Install the globals before dynamically importing Tone.js so its browser checks see the Web Audio constructors.
import 'web-audio-api/polyfill'
const Tone = await import('tone')
const ctx = new AudioContext()
await ctx.resume()
Tone.setContext(ctx)
new Tone.Synth().toDestination()
.triggerAttackRelease('C4', '8n')Can I test audio in CI?
Yes. Render in memory, then assert samples, peaks, RMS, spectra, duration, or channel count. No audio device is opened.
import assert from 'node:assert/strict'
import { OfflineAudioContext } from 'web-audio-api'
const ctx = new OfflineAudioContext(1, 44100, 44100)
const osc = ctx.createOscillator()
osc.connect(ctx.destination)
osc.start()
const audio = await ctx.startRendering()
assert(audio.getChannelData(0).some(Boolean))Can it run without speakers?
Yes. OfflineAudioContext renders without opening @audio/speaker. A realtime context can instead send PCM to a writable stream through sinkId.
import { OfflineAudioContext } from 'web-audio-api'
const ctx = new OfflineAudioContext(2, 44100, 44100)
// build the same graph here
const audio = await ctx.startRendering()Does it support AudioWorklets?
Yes. Modules can be registered by URL, data URL, Blob URL, or an inline callback in Node. Worklets currently execute synchronously rather than on an isolated realtime thread.
import { AudioContext, AudioWorkletNode } from 'web-audio-api'
const ctx = new AudioContext()
await ctx.resume()
await ctx.audioWorklet.addModule(scope => {
class PassThrough extends scope.AudioWorkletProcessor {
process(inputs, outputs) {
outputs[0][0].set(inputs[0][0] || [])
return true
}
}
scope.registerProcessor('pass-through', PassThrough)
})
const node = new AudioWorkletNode(ctx, 'pass-through')What differs from a browser?
The I/O boundary. Node has no built-in speakers, microphones, or media elements: realtime output goes through @audio/speaker or any writable stream via sinkId, microphones come from @audio/mic, and files decode through @audio/decode. AudioWorklets run synchronously instead of on an isolated realtime thread. Everything else follows the same specification: the graph API, the 128-frame render quantum, automation, and node behavior.
How does it compare to alternatives?
| Aspect | web-audio-api | node-web-audio-api | standardized-audio-context | web-audio-engine |
|---|---|---|---|---|
| Engine | Pure JavaScript | Rust via Node-API | Browser ponyfill over native Web Audio | Pure JavaScript |
| Coverage | 4,317 / 4,317 WPT; offline, worklets, media streams | WPT harness; minimal MediaStream support | Almost-complete browser subset; no ScriptProcessor | Older partial API; streaming and offline contexts |
| Environment | Node 18+, Deno, Bun | Node 22+, supported native binaries | Supported browsers | Node and browser build |
| Realtime | Simple measured graphs run faster than realtime | Native engine with low-latency backends | Uses the browser’s native engine | PCM streaming context |
| Decode support | @audio/decode, 20+ format families | Symphonia common formats | Browser codec support | WAV by default; custom decoders |
| Package size | 208 KB core, codecs on demand | 42.2 MB package | 3.0 MB package | Registry metadata unavailable |
| Best fit | Portable graphs, CI, scripts, servers | Native performance and low latency | Consistent behavior across browsers | Legacy PCM streaming and rendering |