Skip to content

Latest commit

 

History

465 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

web-audio-api W3C WPT platforms npm

Web Audio API pure JavaScript implementation. Useful for:

  • Agents and bots – any writable stream, no audio device needed.
  • Audio analysisdecodeAudioData for 20+ formats, AnalyserNode as in browser.
  • Server-side renderingOfflineAudioContext renders 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 libsimport 'web-audio-api/polyfill' installs the globals.
  • CLI audio scripting – PCM in and out through stdio, 46 runnable examples.
npm install web-audio-api

Use

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 samples

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

Then 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/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.

How do I 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.

Examples

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.

Test signals

Illusions

Synthesis

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

API

Performance

All benchmark scenarios render faster than real time. Pure JS matches Rust napi on simple graphs. Convolution and compression are 2–4× slower.

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

952 stars

Watchers

33 watching

Forks

Releases

Packages

Used by

Contributors

Languages