Web Audio APIwithout the browser

A JavaScript DSP engine for Node.js.
Render existing Web Audio graphs offline.

  • Render sound on the server
  • Test audio in CI without speakers
  • Run Tone.js in Node
  • Script audio from the terminal
Node 18+. Full conformance suite in CI; speakers, microphone, files, streams.Deno 2. Renders offline in CI; speaker output and decoding work, microphone untested.Renders offline in CI; speaker output and decoding work, microphone untested.Renders offline in CI as a bundle; no audio device, codecs stay external.Compiles and runs bundled offline graphs on current development builds. Full conformance and device adapters are not verified.Compiles and runs bundled offline graphs on current development builds. Support depends on the JZ revision; full conformance is not verified.
AudioBufferSourceNode1 sampleDelayNode4.5 msBiquadFilterNodelowpass 4 kHzGainNodegain 0.995AudioDestinationNode2 ch, 44.1 kHz

npm install web-audio-api

167kB167 kB gzip / 611 kB unpacked Excludes dependencies. WPT100%4,322 / 4,322 passing in Node.
Not W3C certification.
import { AudioContext } from 'web-audio-api'

const ctx = new AudioContext()
await ctx.resume()
await ctx.resume()

const pluck = ctx.createBufferSource()
pluck.buffer = ctx.createBuffer(1, 1, ctx.sampleRate)
pluck.buffer.getChannelData(0)[0] = 1
const string = ctx.createDelay()
string.delayTime.value = 1 / 220
const damping = ctx.createBiquadFilter()
damping.frequency.value = 4000
damping.Q.value = -3 // dB: no resonance, the loop only decays
const feedback = ctx.createGain()
feedback.gain.value = 0.995
pluck.connect(string)
string.connect(damping).connect(feedback).connect(string)
string.connect(ctx.destination)
pluck.start()
setTimeout(() => ctx.close(), 6000)
2 ch, 44.1 kHz
0 s3 s
40 Hz16 kHz

Examples

All 46 examples

FAQ

Will my existing code work?

Web Audio graph code can usually be reused. Tone.js integration is tested through the polyfill. Code that depends on HTML media elements or other browser APIs needs adaptation. Load the globals before importing Tone.js:

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')

Or start Node with the polyfill already loaded, node --import web-audio-api/polyfill app.js, and a static import * as Tone from 'tone' works.

Also checked through the polyfill: tunajs effects, smplr with its soundfonts fetched over the network, and Pizzicato once its context is resumed. soundfont-player and Howler reach for XMLHttpRequest, navigator, and HTML5 Audio, which Node does not have.

How does it differ from browser Web Audio?

100% of the Web Platform Tests in this project’s Node runner pass: 4,322 of 4,322. This is not W3C certification or a guarantee of complete browser equivalence. See the CI results and test scope and harness adaptations.

Verified for v1.6.0 on .

The graph API follows Web Audio, including automation and 128-frame rendering. Device access, media elements, and worklet execution differ:

AspectBrowserNode
Outputdestination plays through the device@audio/speaker by default, or any writable stream via sinkId
MicrophonegetUserMedia()@audio/mic feeding a MediaStreamTrack; the polyfill installs getUserMedia()
Filesfetch(), then the browser’s codecsfs, then @audio/decode
Media elementsMediaElementAudioSourceNode plays a tagA silent stub; decode to a buffer instead
AudioWorkletIts own realtime threadSynchronous, on the same thread
Lifecycleresume() inside a user gestureresume() at will; close() when the process is done

Keep graph construction in one function that takes a context; the browser adapter supplies the user gesture and the native context, the Node adapter imports the package and closes the context when the process is done.

How do I render and test in CI?

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))

@audio/assert provides checks for frequency, peak level, clipping, and DC offset. They throw ordinary errors and work with any test runner.

Where does it run?

Node has the broadest test coverage. Deno and Bun run offline rendering checks in CI. Other targets have narrower verification, listed below; audio devices and codecs are separate from the DSP engine.

RuntimeOffline renderSpeakerMicrophoneDecoding
Node 18+YesYesYesYes
Deno 2YesYesUntestedYes
BunYesYesUntestedYes
Browser tab, Web WorkerYesThrough the browser’s Web AudioUntestedYes
LLRTYesNo deviceNoUntested
Cloudflare workerdYesNo deviceNoUntested
QuickJSYes, with a shimNo deviceNoUntested
JavaScriptCoreYes, with a shimNo deviceNoUntested
PorfforCompiles and runsDevelopment builds; conformance and devices unverified
JZCompiles and runsCompiler-revision dependent; conformance and devices unverified

Node runs the full suite and 4,322 WPT in CI. Deno, Bun, and LLRT render offline in CI; Deno and Bun devices and decoding were checked on macOS, the browser row by loading esm.sh in Chromium, workerd and the bare engine shells with the engine bundled and its adapters stubbed. The shim a bare shell needs is EventTarget, Event, queueMicrotask, and timers. Not yet tried: Hermes, GraalJS. See compiler results and reproductions for JZ and Porffor.

The engine also runs as ESM in a browser tab or Web Worker. Workers have no native Web Audio API; use the package’s OfflineAudioContext for rendering there.

import { OfflineAudioContext } from 'https://esm.sh/web-audio-api'
Is it fast enough for realtime?

Realtime playback is supported. The simple graphs below render faster than realtime, but offline throughput does not measure playback latency. AudioWorklets run on the main thread, without realtime thread isolation; benchmark your workload if low latency is essential.

1 s of audio at 44100 Hz. Apple M4 Max, darwin arm64, Node v25.9.0. 10 warm-ups, 50 measurements; graph construction excluded. 2026-09-05. Raw results; reproduce with npm run bench:compare.
Scenarioweb-audio-api (JS) v1.5.5
median / p95
node-web-audio-api (Rust) v1.0.8
median / p95
silence (baseline)0.06 / 0.09 ms0.08 / 0.14 ms
OscillatorNode0.23 / 0.27 ms0.27 / 0.33 ms
Osc → Gain0.34 / 0.50 ms0.28 / 0.36 ms
Osc → BiquadFilter0.39 / 0.45 ms0.35 / 0.45 ms
Osc → StereoPanner0.34 / 0.38 ms0.23 / 0.33 ms
DynamicsCompressor0.85 / 0.99 ms0.50 / 0.63 ms
ConvolverNode (128-tap IR)2.10 / 2.22 ms1.80 / 1.99 ms
Osc → Filter → Gain (chain)0.47 / 0.60 ms0.38 / 0.45 ms
8-voice polyphony2.26 / 2.34 ms1.18 / 1.95 ms
Do AudioWorklets work?

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')
How do I use speakers and microphones?

@audio/speaker provides speaker output through a native device backend. The DSP engine itself needs no native bindings. Microphone examples use the optional @audio/mic adapter, then feed PCM into a MediaStreamTrack. You can also set sinkId to any Node writable stream. Speaker output works on Node, Deno, Bun, and through the browser's own Web Audio; the microphone adapter is verified on Node.

Which audio formats can I decode?

decodeAudioData() uses @audio/decode: the format is detected and its codec loaded on demand, without FFmpeg or native bindings, independently of the browser’s built-in codecs.

Compressed
MP3, AAC, Ogg Vorbis, Opus, WMA, AMR, Musepack, QOA
Lossless
FLAC, ALAC, WavPack, APE, TTA
Uncompressed
WAV, AIFF, CAF, DSF and DFF (DSD)
Surround
AC-3, E-AC-3, DTS
Video containers
MP4, M4A, MOV, M4V, 3GP, WebM, MKV, AVI: the audio track is extracted
Tracker modules
MOD, XM, S3M, IT

Codec packages have separate licences, including GPL and LGPL. Check the decoder licence table before redistributing them. Decoding loads the complete audio into memory.

The npm archive (167 KB gzip) contains the engine, TypeScript definitions, and examples. Dependencies are additional, including @audio/decode and its codecs. Codecs are installed together but loaded only when needed; disk usage varies by platform and dependency version.

When should I use another engine?

Choose this package for portable JavaScript DSP, server rendering, and tests. Evaluate IRCAM’s node-web-audio-api for native execution and low-latency device backends; it ships prebuilt binaries for supported platforms. Keep a browser when your application needs DOM or HTML media behavior.

Aspectweb-audio-apinode-web-audio-apistandardized-audio-contextweb-audio-engine
EnginePure JavaScriptRust via Node-APIBrowser ponyfill over native Web AudioPure JavaScript
Coverage4,322 WPT passing in the Node runnerWPT harness; minimal MediaStream supportAlmost-complete browser subset; no ScriptProcessorOlder partial API; streaming and offline contexts
EnvironmentNode 18+, Deno, BunNode 22+, supported native binariesSupported browsersNode and browser build
RealtimeSimple measured graphs run faster than realtimeNative engine with low-latency backendsUses the browser’s native enginePCM streaming context
Decode support@audio/decode, 20+ format familiesSymphonia common formatsBrowser codec supportWAV by default; custom decoders
Best fitPortable graphs, CI, scripts, serversNative performance and low latencyConsistent behavior across browsersLegacy PCM streaming and rendering

Comparison reviewed 29 August 2026; package names link to their documentation. The WPT count refers to this project’s Node runner, not a shared comparison run across engines.

Example