Web Audio API Explorer
AudioContext
The root of every Web Audio graph. Creates nodes, owns the sample clock, and connects to the speakers. Must be resumed after a user gesture (browser autoplay policy).
Create & Resume
// State: "suspended" | "running" | "closed"
await ctx.resume(); // required after user gesture
ctx.suspend(); // pause processing
// Sample rate is usually 44100 or 48000 Hz
console.log(ctx.sampleRate, ctx.currentTime);
initAudio() creates the context on first pad click and wires a master GainNode + AnalyserNode.OscillatorNode
Periodic waveform generator. Four built-in types + custom PeriodicWave. One-shot: create → connect → start → stop. Cannot be restarted after stop.
Play a tone
osc.type = 'sawtooth'; // sine | square | sawtooth | triangle
osc.frequency.setValueAtTime(440, ctx.currentTime);
// or: osc.frequency.value = 440
osc.connect(gain); gain.connect(ctx.destination);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 1);
GainNode & Envelopes
Volume control. The real power is scheduling gain over time to create ADSR-style envelopes. Always use exponentialRamp for natural decays (never ramp to exactly 0).
ADSR-style envelope
const t = ctx.currentTime;
g.gain.setValueAtTime(0.001, t); // never start at 0 for exp ramps
g.gain.exponentialRampToValueAtTime(1, t + attack);
g.gain.exponentialRampToValueAtTime(sustain, t + attack + decay);
g.gain.setValueAtTime(sustain, t + attack + decay + hold);
g.gain.exponentialRampToValueAtTime(0.001, t + total + release);
Noise via AudioBuffer
There is no built-in noise oscillator. Generate a buffer filled with random samples (−1…1) and play it with AudioBufferSourceNode. Filter it to shape the color (white → pink → brown).
Filtered noise burst
const buf = ctx.createBuffer(1, len, ctx.sampleRate);
const data = buf.getChannelData(0);
for (let i = 0; i < len; i++) data[i] = Math.random() * 2 - 1;
const src = ctx.createBufferSource();
src.buffer = buf;
src.connect(filter); filter.connect(gain);
BiquadFilterNode
Second-order filter. Types: lowpass, highpass, bandpass, lowshelf, highshelf, peaking, notch, allpass. Frequency, Q (resonance), and gain are AudioParams — you can automate them.
Sweeping filter
f.type = 'lowpass';
f.frequency.setValueAtTime(200, t);
f.frequency.exponentialRampToValueAtTime(3000, t + 1);
f.Q.value = 5; // resonance / bandwidth
DelayNode & Feedback
Delay line (max delay set at creation). Combine with a feedback GainNode to create echoes, slapback, or infinite ambient trails. Keep feedback < 1.0 to avoid runaway.
Echo / Feedback loop
delay.delayTime.value = 0.35;
const fb = ctx.createGain(); fb.gain.value = 0.55;
src.connect(delay);
delay.connect(fb); fb.connect(delay); // feedback loop
delay.connect(ctx.destination);
WaveShaperNode (Distortion)
Applies a non-linear transfer curve sample-by-sample. Classic soft/hard clipping, tube-ish saturation, or extreme digital destruction. Curve is a Float32Array mapping −1…1 → −1…1.
Drive amount
const n = 44100, curve = new Float32Array(n);
for (let i = 0; i < n; i++) {
const x = i * 2 / n - 1;
curve[i] = (Math.PI + amount) * x / (Math.PI + amount * Math.abs(x));
}
return curve;
}
const ws = ctx.createWaveShaper();
ws.curve = makeDistortionCurve(40);
ws.oversample = '4x';
DynamicsCompressorNode
Reduces dynamic range. Keeps loud peaks under control so multiple simultaneous pads don’t clip the master. Standard mastering tool.
Hear compression
comp.threshold.value = -24; // dB
comp.knee.value = 10;
comp.ratio.value = 4;
comp.attack.value = 0.003;
comp.release.value = 0.25;
// Place near the end of the master chain
Convolution Reverb (ConvolverNode)
Applies an impulse response via convolution. Real IRs come from recordings; synthetic IRs can be generated procedurally (noise + exponential decay).
Synthetic space
const rate = ctx.sampleRate;
const len = rate * decaySeconds;
const ir = ctx.createBuffer(2, len, rate);
for (let c = 0; c < 2; c++) {
const ch = ir.getChannelData(c);
for (let i = 0; i < len; i++) {
ch[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / len, 2);
}
}
const conv = ctx.createConvolver();
conv.buffer = ir;
LFO Modulation
An OscillatorNode running at sub-audio rates (0.1–20 Hz) can modulate another AudioParam (frequency, gain, filter cutoff) for vibrato, tremolo, or filter wobble.
Vibrato / Tremolo
lfo.frequency.value = 5; // Hz
const lfoGain = ctx.createGain();
lfoGain.gain.value = 20; // depth in Hz for vibrato
lfo.connect(lfoGain);
lfoGain.connect(osc.frequency); // or gain.gain for tremolo
lfo.start();
AnalyserNode
Provides time-domain and frequency-domain data for visualization (or adaptive logic). Does not alter the audio signal — pure observer.
Live spectrum
analyser.fftSize = 256; // power of 2
const data = new Uint8Array(analyser.frequencyBinCount);
function draw() {
analyser.getByteFrequencyData(data);
// paint bars from data[]
requestAnimationFrame(draw);
}
Scheduling & Timing
All timing is relative to ctx.currentTime (seconds, high-precision). Never use setTimeout for musical timing — schedule AudioParam changes and start/stop times on the audio clock.
Sequence of notes
const step = 60 / bpm;
notes.forEach((freq, i) => {
const osc = ctx.createOscillator();
osc.frequency.value = freq;
osc.start(t0 + i * step);
osc.stop(t0 + i * step + step * 0.9);
});
Common Node Graph Patterns
How nodes are typically chained in the Fretwalkers soundboard and in general synthesis.
Typical one-shot voice
With delay send
Noise layer
AudioWorklet (concept)
Custom audio processing on the dedicated audio thread. Replaces the deprecated ScriptProcessorNode. Required for low-latency custom synthesis, granular, or DSP that pure nodes can’t express.
Minimal structure
class MyProcessor extends AudioWorkletProcessor {
process(inputs, outputs, parameters) {
const out = outputs[0][0];
for (let i = 0; i < out.length; i++) {
out[i] = Math.random() * 2 - 1; // example
}
return true; // keep alive
}
}
registerProcessor('my-processor', MyProcessor);
// main thread
await ctx.audioWorklet.addModule('processor.js');
const node = new AudioWorkletNode(ctx, 'my-processor');
node.connect(ctx.destination);
© Copyright Don Beckett