Web Audio API Cheatsheet
The Web Audio API models audio as a directed graph: sources → processing nodes → destination. Every operation (gain, filter, reverb, synthesis, FFT analysis) is a node. This cheatsheet covers the essential nodes, parameter automation, custom DSP via AudioWorklet, and real-time spectrum analysis.
AudioContext Graph Architecture
Every sound pipeline starts with creating a context and connecting nodes like a signal chain:
// Always create on user gesture (click/keypress) — browsers require it
const ctx = new AudioContext();
// Source → [effect nodes] → destination (speakers)
source.connect(gainNode);
gainNode.connect(filterNode);
filterNode.connect(ctx.destination);
ctx.state === 'suspended' and
call ctx.resume() inside a click handler. A common
pattern is to create the context on first click into the canvas.
| Node Type | Class | Role |
|---|---|---|
| Source | OscillatorNode, AudioBufferSourceNode, MediaStreamSourceNode | Generates audio signal |
| Effect | GainNode, BiquadFilterNode, ConvolverNode, DynamicsCompressorNode | Processes signal |
| Analysis | AnalyserNode | Tap signal without altering it |
| Splitter/Merger | ChannelSplitterNode, ChannelMergerNode | Multi-channel routing |
| Destination | AudioDestinationNode | Final output (speakers/headphones) |
OscillatorNode — Synthesis
const osc = ctx.createOscillator();
osc.type = 'sine'; // sine | square | sawtooth | triangle | custom
osc.frequency.setValueAtTime(440, ctx.currentTime); // 440 Hz = A4
const gain = ctx.createGain();
gain.gain.value = 0.3; // -1..1 safe range; >1 clips
osc.connect(gain);
gain.connect(ctx.destination);
osc.start();
osc.stop(ctx.currentTime + 2.0); // plays for 2 seconds
// Custom waveform via PeriodicWave:
const real = new Float32Array([0, 1, 0.5, 0.25]); // cosine terms (DC, f, 2f, 3f)
const imag = new Float32Array([0, 0, 0, 0]);
const wave = ctx.createPeriodicWave(real, imag);
osc.type = 'custom';
osc.setPeriodicWave(wave);
GainNode & BiquadFilterNode
// Gain: simple volume control
const gain = ctx.createGain();
gain.gain.value = 0.5; // linear amplitude; decibels: 10^(dB/20)
// BiquadFilter: EQ / tone control
const filter = ctx.createBiquadFilter();
filter.type = 'lowpass'; // lowpass|highpass|bandpass|notch|peaking|lowshelf|highshelf|allpass
filter.frequency.value = 1000; // cutoff freq (Hz)
filter.Q.value = 1.0; // resonance; >10 → ringing; Q=0.707 = Butterworth
filter.gain.value = 6; // only for peaking/shelf types (dB)
// Telephone effect (band-limited to 300–3400 Hz)
const lowCut = ctx.createBiquadFilter();
lowCut.type = 'highpass';
lowCut.frequency.value = 300;
const highCut = ctx.createBiquadFilter();
highCut.type = 'lowpass';
highCut.frequency.value = 3400;
source.connect(lowCut).connect(highCut).connect(ctx.destination);
ConvolverNode — Impulse Response Reverb
Convolution reverb multiplies every sample with a recorded room impulse response (IR) to add realistic acoustics. IR files (.wav) are measured by firing a starter pistol or sine sweep in the target space.
async function loadReverb(url) {
const resp = await fetch(url);
const arrBuf = await resp.arrayBuffer();
const audioBuf = await ctx.decodeAudioData(arrBuf);
const conv = ctx.createConvolver();
conv.buffer = audioBuf;
conv.normalize = true; // auto-scale IR to prevent clipping
return conv;
}
// Wet/dry mix with GainNodes:
const dry = ctx.createGain(); dry.gain.value = 0.7;
const wet = ctx.createGain(); wet.gain.value = 0.3;
source.connect(dry).connect(ctx.destination);
source.connect(conv).connect(wet).connect(ctx.destination);
exp(-4 * t / duration) — gives a natural diffuse hall
sound good enough for games.
AnalyserNode — Real-Time FFT
const analyser = ctx.createAnalyser();
analyser.fftSize = 2048; // must be power of 2; 32–32768
analyser.smoothingTimeConstant = 0.8; // 0=no smoothing, 1=max (sluggish)
analyser.minDecibels = -90;
analyser.maxDecibels = -10;
source.connect(analyser);
analyser.connect(ctx.destination); // analyser passes audio through unmodified
const bufLen = analyser.frequencyBinCount; // = fftSize / 2
const freqBuf = new Uint8Array(bufLen); // 0–255 per bin (magnitude)
const timeBuf = new Uint8Array(bufLen); // waveform (time domain)
function drawSpectrum(canvas) {
analyser.getByteFrequencyData(freqBuf); // fill freq data
const ctx2 = canvas.getContext('2d');
ctx2.clearRect(0, 0, canvas.width, canvas.height);
const barW = canvas.width / bufLen;
freqBuf.forEach((val, i) => {
const pct = val / 255;
ctx2.fillStyle = `hsl(${200 + pct * 160}, 80%, 50%)`;
ctx2.fillRect(i * barW, canvas.height * (1 - pct), barW - 1, canvas.height * pct);
});
requestAnimationFrame(() => drawSpectrum(canvas));
}
Frequency of bin i:
f = i × sampleRate / fftSize. Default sample rate is
44100 Hz, so with fftSize=2048: bin 0 = 0 Hz, bin 1 = 21.5 Hz, bin
1024 = 22050 Hz (Nyquist).
AudioWorkletProcessor — Custom DSP
ScriptProcessorNode is deprecated.
AudioWorkletProcessor runs in the dedicated audio thread
(no garbage collection pauses) and processes audio in 128-sample
blocks:
// worklet-processor.js (separate file, served as script)
class WhiteNoiseProcessor extends AudioWorkletProcessor {
process(inputs, outputs, parameters) {
const output = outputs[0];
for (const channel of output) {
for (let i = 0; i < channel.length; i++) {
channel[i] = (Math.random() * 2 - 1) * 0.3;
}
}
return true; // return false to deactivate node
}
}
registerProcessor('white-noise', WhiteNoiseProcessor);
// In main script:
await ctx.audioWorklet.addModule('worklet-processor.js');
const noise = new AudioWorkletNode(ctx, 'white-noise');
noise.connect(ctx.destination);
addModule() requires the worklet file to be same-origin
or have CORS headers. During local development, serve
files with a local HTTP server — do not use
file:// protocol.
AudioParam Automation
All AudioParams support sample-accurate scheduling — far more precise
than setTimeout:
const g = ctx.createGain();
const t = ctx.currentTime;
g.gain.setValueAtTime(0, t); // instant jump
g.gain.linearRampToValueAtTime(1, t + 0.01); // attack
g.gain.exponentialRampToValueAtTime(0.7, t + 0.3); // decay (must be >0)
g.gain.setTargetAtTime(0.5, t + 0.3, 0.1); // sustain (time constant)
g.gain.cancelScheduledValues(t + 2.0);
g.gain.linearRampToValueAtTime(0, t + 2.5); // release
// LFO modulating pitch:
const lfo = ctx.createOscillator();
lfo.frequency.value = 5; // 5 Hz vibrato
const lfoGain = ctx.createGain();
lfoGain.gain.value = 15; // ±15 Hz pitch deviation
lfo.connect(lfoGain).connect(osc.frequency);
Common Gotchas
-
Autoplay Policy: AudioContext starts suspended
until a user gesture. Always call
ctx.resume()inside a click/keydown handler or theAudioContextconstructor should be placed inside one. -
Source nodes are one-shot: You cannot restart a
stopped
OscillatorNodeorAudioBufferSourceNode. Create a new instance each time. -
Gain > 1 clips: Digital clipping produces harsh
distortion. Use a
DynamicsCompressorNodeat the end of the chain to protect the output. -
Float vs Uint8 frequency data:
getByteFrequencyData()returns 0–255.getFloatFrequencyData()returns exact dB values (negative, e.g., -90 to -10). Use float for precise measurements. -
sampleRate: Default 44100 Hz. Some systems use
48000 Hz. Always use
ctx.sampleRaterather than hardcoding for frequency-bin calculations.