Portable Web Audio API / polyfill.
OfflineAudioContext renders without speakers.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
Built-in speaker output via audio-speaker — no extra setup.
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
node examples/<name>.js — all parametric. Positional args or key=value with prefix matching (f=440, freq=440 both work). Note names (A4, C#3, Eb5), k for kHz (20k), s/m/h for duration (10m).
| 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 click — 120..240 10m X-x- |
| 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) |
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 }) then node synth.js | aplay -f cdnumberOfChannels, bitDepth — control output format in the constructornavigator.mediaDevices.getUserMedia({ audio: true }) — browser-parity microphone capture in Node. Load web-audio-api/polyfill and install audio-mic; browser mic code then runs verbatim. See the mic FAQ.await ctx.close()
Or with [explicit resource management](https://github.com/tc39/proposal-explicit-resource-management): `using ctx = new AudioContext()`
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 be a dynamic `import()` — static imports get hoisted before the polyfill runs. Alternatively, use `--import`:
node --import web-audio-api/polyfill app.js
Then static `import * as Tone from 'tone'` works in `app.js`.
const buffer = await ctx.decodeAudioData(readFileSync('track.mp3'))
WAV, MP3, FLAC, OGG, AAC via [audio-decode](https://github.com/audiojs/audio-decode).
npm install audio-mic
import { AudioContext, MediaStreamAudioSourceNode } from 'web-audio-api'
import mic from 'audio-mic'
const ctx = new AudioContext()
await ctx.resume()
const src = new MediaStreamAudioSourceNode(ctx, { numberOfChannels: 1, bitDepth: 16 })
src.connect(ctx.destination) // live monitor
const read = mic({ sampleRate: ctx.sampleRate, channels: 1, bitDepth: 16 })
read((err, buf) => {
if (err || !buf) return
src.pushData(buf, { channels: 1, bitDepth: 16 })
})
`pushData()` accepts `Float32Array`, `Float32Array[]`, or interleaved 8/16/32-bit integer PCM buffers. Integer PCM conversion uses `pcm-convert`.
With `web-audio-api/polyfill`, `navigator.mediaDevices.getUserMedia()` is also available and maps constraints to `audio-mic` options: `{ audio: { sampleRate, channelCount, sampleSize } }`.
See [examples/mic.js](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()`.
import 'web-audio-api/polyfill'
// AudioContext, GainNode, etc. are now global
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
MIT
$ claude mcp add web-audio-api \
-- python -m otcore.mcp_server <graph>