Skip to content

Latest commit

 

History

473 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

web-audio-api W3C WPT platforms npm

A pure JavaScript implementation of the Web Audio API for Node.js. Render existing audio graphs on the server, test audio processing in CI, or play sound through your speakers.

npm install web-audio-api

Use

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

This renders one second of a 440 Hz tone into memory without opening an audio device. Use the samples in a test or save them to a file. See render-to-buffer.js.

Where does it run?

Node.js has the broadest test coverage, including the fully passing checked-in WPT corpus under our Node runner. Deno and Bun run offline rendering checks in CI. See the runtime support details for device access and other targets.

How to use it as a polyfill?
import 'web-audio-api/polyfill'
// AudioContext, GainNode, etc. are now global

The 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.

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.js

Then static import * as Tone from 'tone' works in app.js.

How to 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 to play sound through speakers?
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.onended = () => ctx.close()
osc.start()
osc.stop(ctx.currentTime + 1) // play for one second, then close the device

@audio/speaker provides device output through platform-specific backends, including native dependencies. The DSP engine itself is JavaScript.

How to capture audio from the microphone?

In Node, pair @audio/mic with CustomMediaStreamTrack:

npm install @audio/mic
import { 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.

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.

How to close an AudioContext?
await ctx.close()

Or with explicit resource management: using ctx = new AudioContext()

Examples

46 runnable examples cover rendering, analysis, synthesis, and PCM streaming.

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.

API
Test signals
Synthesis
Generative

Musical models, styles, and offline listening fixtures.

  • 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, pads, or cinematic strings – voice=strings melody=pentatonic freq=D3 -d 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
Illusions

Performance

Run npm run bench:compare for an offline-render comparison with node-web-audio-api. The runner alternates engines after warmup and reports median and p95 times over 50 renders. Saved results include raw samples, hardware, and versions.

These are short offline graphs, not audio-device latency measurements or realtime guarantees. Measure your own graph and deployment host.

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 needed
  • sinkId: stream – pipe PCM to any writable: new AudioContext({ sinkId: process.stdout }) then node synth.js | aplay -f cd
  • numberOfChannels, bitDepth – control output format in the constructor.
  • CustomMediaStreamTrack – extends MediaStreamTrack with a public constructor and pushData(chunk, options) to feed audio data (e.g. from a microphone). Prior art: CanvasCaptureMediaStreamTrack. See the mic FAQ.

Alternatives

License

MIT

About

Headless Web Audio API

Resources

Stars

949 stars

Watchers

33 watching

Forks

Releases

Packages

Used by

Contributors

Languages