Filters and effects: biquads, delay, and convolution reverb
Between a source and the speakers sits the part of the graph that actually shapes character — a BiquadFilterNode carving frequency content, a DelayNode with feedback turning a single hit into a decaying series of echoes, and a ConvolverNode stamping a signal with the acoustic fingerprint of a real room.
Filters and effects: biquads, delay, and convolution reverb
Gain, mixing, and envelopes covered the one processing node that touches amplitude. This lesson covers the nodes that touch everything else a sound can be shaped by: its frequency content, its sense of space and repetition, and its raw waveshape. Three nodes, three different mechanisms — a filter that reweights frequencies, a delay that repeats a signal in time, and a convolver that folds a signal through a recording of a real acoustic space. Put them between a source and context.destination and you've built an effects chain.
BiquadFilterNode: a second-order filter
BiquadFilterNode is named after the math underneath it: a biquadratic transfer function, built from two poles and two zeros. You don't need the algebra to use it well, but the name is worth keeping, because it tells you what kind of filter this is — a general-purpose second-order filter efficient enough to run natively on the audio thread, and flexible enough to become a dozen different effects just by changing its type.
const context = new AudioContext();
const filter = context.createBiquadFilter();
filter.type = "lowpass";
filter.frequency.value = 800; // cutoff, in Hz
filter.Q.value = 1; // resonance at the cutoffFour properties do essentially all the work:
typepicks which shape of filter this node currently is:"lowpass","highpass","bandpass","notch","peaking","lowshelf","highshelf","allpass". One node, reconfigurable into any of these — the biquad math is general enough to express all of them by adjusting its coefficients internally.frequencysets the cutoff (for lowpass/highpass) or center (for bandpass/notch/peaking) frequency the filter acts around. It's anAudioParam, which means everything lesson 4 covered applies directly — you canlinearRampToValueAtTimea filter's cutoff exactly like a gain, producing the classic sweeping-filter effect.Qcontrols resonance and bandwidth: how sharply the filter cuts around its frequency, versus how gradually. A lowQis a gentle, wide transition; a highQnarrows the affected band and adds emphasis right at the cutoff, which can sound like a resonant "peak" riding the frequency you named.gainonly matters forpeaking,lowshelf, andhighshelf, where it sets how many dB that band is boosted or cut, rather than removed outright.
The intuition: which frequencies survive
The names describe exactly what passes through:
- Lowpass lets frequencies below the cutoff through mostly unchanged and attenuates everything above it. The effect is a sound getting muffled, warmer, and darker — think of a sound heard through a closed door, which is a real-world lowpass filter (the door itself).
- Highpass is the mirror image: frequencies above the cutoff pass, everything below is attenuated. The effect is thinner and brighter, with low-end weight stripped out — a telephone or an old radio speaker sounds highpassed because their hardware simply can't reproduce low frequencies.
- Bandpass keeps only a band around the center frequency and attenuates both above and below it, useful for isolating a narrow slice of a sound (or, swept over time, that classic wah/telephone-filter sweep).
- Notch is bandpass's photographic negative: it removes a narrow band around the center and passes everything else, handy for surgically cutting a single problem frequency (a 60 Hz hum) without touching the rest of the spectrum.
All of this is really a statement about the sound's frequency domain — its energy at each frequency — rather than its waveform. The next lesson shows you how to actually see that frequency domain with an AnalyserNode and the FFT; for now it's enough to know a filter's type and frequency are reshaping exactly that spectrum, even though you can't look at it yet.
// A lowpass sweep: starts bright, closes down to muffled over 3 seconds.
const osc = context.createOscillator();
osc.type = "sawtooth";
osc.connect(filter);
filter.connect(context.destination);
const t = context.currentTime;
filter.frequency.setValueAtTime(8000, t);
filter.frequency.exponentialRampToValueAtTime(200, t + 3);
osc.start(t);DelayNode and feedback: one hit becomes an echo
DelayNode does one simple thing: whatever comes in at time t comes out at time t + delayTime. On its own that's just a repeat, useful for effects like a slap-back delay or offsetting one channel from another to widen a stereo image. The interesting behavior shows up when you feed the delay's output back into its own input through a GainNode:
const delay = context.createDelay(5.0); // max delay time, in seconds
delay.delayTime.value = 0.3;
const feedback = context.createGain();
feedback.gain.value = 0.4; // must be below 1 — see the callout
source.connect(delay);
delay.connect(feedback);
feedback.connect(delay); // the loop: delay's output feeds back into itself
delay.connect(context.destination);delay.connect(feedback) then feedback.connect(delay) wires the delay's output back around into its own input — a deliberate cycle in a graph that is otherwise a strict source-to-destination flow. Each pass through the loop, the signal is delayed by delayTime again and scaled down by feedback.gain again, so a single input produces a series of repeats, each one delayTime later and quieter than the last. That decaying series of repeats is both the mechanism behind a discrete echo effect and the rough basis of what a reverb's tail sounds like — many delayed, decaying copies arriving close enough together to blur into a wash rather than distinct echoes.
ConvolverNode: reverb by convolution
A real room isn't silent between the sound and your ear — it's a maze of reflections off walls, ceiling, and furniture, all arriving at slightly different times and volumes, which is what makes a cathedral sound different from a closet even playing the same note. You could try to approximate that with a long chain of individual delays, but there's a more direct trick: record the room's own response to a single, instantaneous click (an impulse response, often abbreviated IR), and then mathematically combine your sound with that recording so your sound inherits the room's echoes.
That mathematical combination is convolution, and ConvolverNode performs it in real time. The intuition: convolution takes every sample of your input and "stamps" a scaled, time-shifted copy of the entire impulse response at that point, then sums all those overlapping stamps together. The impulse response encodes the room — every reflection at every delay and every volume the room actually produces — so convolving your dry signal with it effectively smears your sound through the space the recording captured, and what comes out sounds like your sound played in that room.
const convolver = context.createConvolver();
const response = await fetch("hall-impulse-response.wav");
const arrayBuffer = await response.arrayBuffer();
convolver.buffer = await context.decodeAudioData(arrayBuffer);
source.connect(convolver);
convolver.connect(context.destination);convolver.buffer is just an AudioBuffer — lesson 2's raw array of samples — except here those samples aren't a song to play back, they're the room's own impulse response, and the node's whole job is folding your live signal through it. Because convolution over a long impulse response is computationally heavy, the browser typically does this work efficiently in the frequency domain under the hood, but that's an implementation detail; what you control is simply which impulse response you load, and different recordings (a small room, a hall, a plate reverb unit, a cave) give you correspondingly different reverb characters for free, with no synthesis or parameter tuning required.
WaveShaperNode: distortion as a per-sample lookup
One more effect worth naming: WaveShaperNode applies distortion by treating a curve array as a lookup table — for every input sample, it maps the sample's value to a new output value according to that curve, the same way a photo filter remaps each pixel's brightness through a tone curve. A gentle curve barely changes the waveform; a curve that flattens out toward the top and bottom (a "soft clip") rounds off peaks the way an overdriven guitar amp does; a curve with hard steps produces the harsher, buzzier distortion of a fuzz pedal. The mechanism is intentionally the simplest one in this lesson — no history, no feedback, no frequency analysis, just a remap applied independently to each sample as it passes through.
Chaining it together
Nothing stops you from wiring several of these in series, and that's exactly how a real effects chain is built — a signal path where each node hands a progressively more processed signal to the next:
source.connect(filter);
filter.connect(delay);
delay.connect(feedback);
feedback.connect(delay);
delay.connect(convolver);
convolver.connect(context.destination);Every node in that chain is still just a node in the graph the first lesson described — connected with the same .connect(), evaluated on the same pull-based render thread, on the same 128-sample cadence. Filters, delay, and convolution don't add new rules to the model; they're proof of how much character a handful of node types, chained together, can produce.
Where this goes next
Everything in this lesson talked about "frequency content" and "the frequency domain" as an intuition you'll take on faith — a lowpass filter removes highs, a notch removes a narrow band, an impulse response encodes a room's reflections. Analysis and the FFT makes that concrete: it shows you how to actually read a signal's spectrum out of the graph and draw it, using the same AnalyserNode that powers every music visualizer you've ever seen.
Go deeper
- MDN — BiquadFilterNode — The full list of filter types with their frequency-response behavior and how frequency, Q, and gain interact for each one.
- MDN — DelayNode — DelayNode's maxDelayTime constructor argument and delayTime AudioParam, including the feedback-loop pattern this lesson builds.
- MDN — ConvolverNode — How the buffer property and the normalize flag control convolution reverb, straight from the node's reference page.
Check yourself
Answer out loud, as if an interviewer asked. If you hand-wave, reread that section.
- What does the 'biquad' in BiquadFilterNode refer to, and why does one node type suffice for lowpass, highpass, bandpass, notch, peaking, and shelf filters?
- Explain, in terms of which frequencies survive, the difference between a lowpass and a highpass filter — and between a bandpass and a notch.
- Why is filter.frequency an AudioParam rather than a plain number, and what does that let you do that a plain property couldn't?
- Walk through the delay feedback loop: what does the signal look like after one trip around versus three trips, and why must the feedback gain be below 1?
- What is an impulse response, and what does convolving a dry signal with one intuitively do to that signal?
- How does WaveShaperNode's curve mechanism differ from a filter's or a delay's — what state does it need to keep across samples?
- In the chained example (filter -> delay/feedback -> convolver -> destination), which parts of the model from lesson 1 still apply unchanged, and what's actually new here?