Components
Time and sequencing
Blocks
An equalizer as a device you can plug in: a low shelf, two bells and a high shelf as a state, a panel with a handle on each band, a Web Audio engine and the tools an agent drives it with.
Audio UI
"use client";
import { useCallback, useEffect, useRef, useState } from "react";The equalizer is a device like the compressor: a state, a panel that draws it, an engine that plays it and tools an agent calls, each its own file. What is specific to it is the curve: the panel draws the filters the engine runs, so what you see is what you hear.
Four bands from the bottom up, in series, then an output trim. Each band has a type, a frequency, a gain and a width (Q), and the type decides which of them it uses:
The bands start as a low shelf, two bells and a high shelf. A band's parameters are lowType, lowHz, lowDb, lowQ, then the same for lowMid, highMid and high, and outputDb for the trim. The cuts are 12 dB per octave, what a browser filter node does.
import { EqualizerPanel } from "@/components/audio-ui/devices/equalizer/panel";
import { equalizerDevice } from "@/components/audio-ui/devices/equalizer/device";
const [state, setState] = useState(() => equalizerDevice.defaults()); // flat
<EqualizerPanel onValueChange={setState} value={state} />;The curve is the sum of the four filters and the trim. Each band has a handle on it: drag it to set the band's frequency and gain, and it follows the pointer one to one (a cut or a notch has no gain, so its handle only slides); double-click to flatten the band. Pick a band by its number or its handle, and its type, frequency, gain and Q appear under the curve, the unused ones drawn off. Pass a spectrum (from useSpectrum) to draw the level of the sound behind the curve.
import { createEqualizerEngine } from "@/components/audio-ui/devices/equalizer/engine";
const engine = createEqualizerEngine(ctx, state);
source.connect(engine.input);
engine.output.connect(ctx.destination);
engine.update(nextState); // parameters ramp: no click
engine.dispose();Four BiquadFilterNodes in series and a trim. The curve is drawn from the same formulas the node runs, and a band's width goes through toWebAudioQ on the way to the node, so the two agree. Off, every band goes flat and the trim to 0 dB.
A host that brings its own engine can check it against what the panel draws. device-measure renders an engine in an offline audio context and compares it with the device's curve. It works for any device whose curve is a frequency response, the filter and the channel EQ too:
import { equalizerCurve, equalizerDevice } from "@/components/audio-ui/devices/equalizer/device";
import { measureLatency, measureResponse } from "@/lib/device-measure";
const state = equalizerDevice.defaults();
// Your factory: (ctx, state) => { input, output, dispose }.
const { worstDb, at } = await measureResponse(createMyEngine, state, equalizerCurve(state));
// worstDb: how far your engine is from the panel, in dB, and where (at, in Hz).
const latencyMs = await measureLatency(createMyEngine, state);
// What a host reports to compensate it.It is a measurement, not a certification: the curve is the device's own and the tolerance is yours.
import { deviceTools } from "@audio-ui/core/device";
const [set] = deviceTools(equalizerDevice);
set.run(state, { lowMidHz: 1000, lowMidDb: -6 });As for any device: the ranges a model is told are the ones the device enforces, a value out of range is brought in and reported, and a wrong one is refused by name.
The handles and the knobs are sliders named after their parameter, with the value and unit as the text value ("Low mid freq, 400 Hz"), the switch is a toggle button, and everything takes the keyboard.