Web Audio API Explorer

Web Audio API Explorer

Nodes & techniques used in the Fretwalkers soundboard
suspended

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

const ctx = new AudioContext(); // or webkitAudioContext
// 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);
In the Fretwalkers soundboard, 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

440
OscillatorNode GainNode destination
const osc = ctx.createOscillator();
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);
Fretwalkers uses detuned pairs of oscillators (e.g. Hollow Tone) for thickness, and exponential frequency ramps for sweeps (Gate Collapse, Feedback Collapse).

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 g = ctx.createGain();
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);
Almost every Fretwalkers pad uses this pattern. exponentialRampToValueAtTime gives organic pluck/decay character that linear ramps lack.

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 len = ctx.sampleRate * duration;
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);
Used for: Gate crackle, Loop Dust, Static Choir, Jam-Core, Kilauean Dust, Noise Ward, etc.

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

const f = ctx.createBiquadFilter();
f.type = 'lowpass';
f.frequency.setValueAtTime(200, t);
f.frequency.exponentialRampToValueAtTime(3000, t + 1);
f.Q.value = 5; // resonance / bandwidth
Feedback Collapse and Gate Collapse both sweep a lowpass/bandpass downward while the oscillator dives — classic “implode” gesture.

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

Osc Delay Feedback Gain destination
const delay = ctx.createDelay(2.0); // max delay seconds
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);
Echo Burn, Sync Lag, Echo Rift and Feedback Communion all use this exact topology.

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

function makeDistortionCurve(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';
Useful for Gain Blood, Hollow Tone grit, and any “corrupted signal” aesthetic. Oversample reduces aliasing.

DynamicsCompressorNode

Reduces dynamic range. Keeps loud peaks under control so multiple simultaneous pads don’t clip the master. Standard mastering tool.

Hear compression

const comp = ctx.createDynamicsCompressor();
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
A good master bus addition for the soundboard — prevents multiple overlapping pads from hard-clipping.

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

// Build a simple stereo impulse response
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;
Would give Chord Vault, Auralith Hum, and Silent Score a sense of space. Real cathedral/cave IRs are even richer.

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

const lfo = ctx.createOscillator();
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();
The Old Signal pad uses a 4 Hz LFO on gain for a broadcast “flutter”. Frequency gyros and Temporal Drift also lean on modulation.

AnalyserNode

Provides time-domain and frequency-domain data for visualization (or adaptive logic). Does not alter the audio signal — pure observer.

Live spectrum

const analyser = ctx.createAnalyser();
analyser.fftSize = 256; // power of 2
const data = new Uint8Array(analyser.frequencyBinCount);
function draw() {
  analyser.getByteFrequencyData(data);
  // paint bars from data[]
  requestAnimationFrame(draw);
}
The soundboard header bars are driven by exactly this. Also usable for reactive gameplay or automatic gate detection.

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 t0 = ctx.currentTime + 0.05; // small lookahead
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);
});
Pulse Sigil, Memory Vault arpeggios, By The Ninth, and Solar Marking all schedule multiple start times against currentTime.

Common Node Graph Patterns

How nodes are typically chained in the Fretwalkers soundboard and in general synthesis.

Typical one-shot voice

Oscillator(s) BiquadFilter Gain (envelope) master Gain Analyser destination

With delay send

voice Gain Delay FB Gain master

Noise layer

BufferSource (noise) BiquadFilter Gain master
Keep long-lived nodes (master, analyser, compressor, convolver) alive. Create short-lived oscillators/buffers per note and let them stop/garbage-collect.

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

// processor.js (loaded via audioWorklet.addModule)
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);
Overkill for the current soundboard (native nodes are enough), but the path to custom granular engines, physical models, or advanced Fretwalker “signal entities”.

© Copyright Don Beckett