# `biquad` _(no submodules)_ ## Purpose The `eq.biquad` module owns the **RBJ Audio EQ Cookbook coefficient maths** and the per-band biquad state shapes consumed by `EQPlugin`. Splitting the coefficient computation out of the plugin file keeps `eq.ts` focused on the plugin lifecycle (parameters, setup, presets, processBlock) and concentrates the dense numeric formulae in one well-tested helper that other plugins (a future shelf-only EQ, a tilt EQ) can reuse without dragging the full plugin in. The formulae match Robert Bristow-Johnson's canonical reference verbatim: the `a0` term is folded into the numerator so the per-sample inner loop in the plugin's `processBlock` costs only five multiplies and four adds per band per channel. ## Features - `BiquadCoeffs` interface — five-field record `{ b0, b1, b2, a1, a2 }` representing one normalised biquad section. - `BiquadState` interface — `{ x1, x2, y1, y2 }` direct-form-I state. The EQ creates one per channel per band and clears them in-place from `resetState`. - `identityCoeffs()` — returns pass-through coefficients (`b0 = 1`, others zero). Used as a placeholder before `setup()` runs the real RBJ formulae. - `computeBiquad(band, sampleRate)` — returns coefficients for any of the five shipped band types (`peaking`, `low-shelf`, `high-shelf`, `low-pass`, `high-pass`). ## Usage: Basic Example ```ts import { computeBiquad } from "@suaveplan/audio-fx-plugin-eq"; const coeffs = computeBiquad( { frequency: 1_000, gain: 6, q: 1, type: "peaking" }, 48_000 ); // coeffs.b0, coeffs.b1, ... ``` ## Usage: Advanced Example ```ts import { computeBiquad, type BiquadState } from "@suaveplan/audio-fx-plugin-eq"; // Roll your own cascade using just the coefficient helper. const states: BiquadState[] = bands.map(() => ({ x1: 0, x2: 0, y1: 0, y2: 0 })); const coeffs = bands.map((b) => computeBiquad(b, sampleRate)); for (let i = 0; i < blockSize; i++) { let sample = input[i]!; for (let k = 0; k < bands.length; k++) { const c = coeffs[k]!; const s = states[k]!; const y = c.b0 * sample + c.b1 * s.x1 + c.b2 * s.x2 - c.a1 * s.y1 - c.a2 * s.y2; s.x2 = s.x1; s.x1 = sample; s.y2 = s.y1; s.y1 = y; sample = y; } output[i] = sample; } ``` ## API Reference - `BiquadCoeffs` (type) — `{ b0: number; b1: number; b2: number; a1: number; a2: number }`. - `BiquadState` (type) — `{ x1: number; x2: number; y1: number; y2: number }`. - `identityCoeffs(): BiquadCoeffs` — returns `{ b0: 1, b1: 0, b2: 0, a1: 0, a2: 0 }`. - `computeBiquad(band: EQBand, sampleRate: number): BiquadCoeffs` — RBJ Cookbook formulae for peaking / low-shelf / high-shelf / low-pass / high-pass. ## Implementation Notes A flat band — `peaking` with `gain === 0` — collapses to identity: `A = 10^(0/40) = 1`, so `b0 = a0 = 1 + alpha`, `b1 = a1 = -2·cosW`, `b2 = a2 = 1 - alpha`. After normalising by `a0` the transfer function `H(z) = 1`, which makes the EQ's `flat` preset bit-transparent at the sample-by-sample level (the plugin's test suite asserts equality within `1e-6` against mixed-frequency input). The chain of `if / else if` branches over `band.type` is intentional: a single `switch` would trip biome's `lexical-let-in-switch` rule for the `beta = 2·sqrt(A)·alpha` shelf intermediate, and the alternative — declaring `beta` once at the top and reusing it across cases — would compute the shelf coefficient unnecessarily for the three non-shelf branches. The current structure keeps each branch self-contained while staying lint-clean. `computeBiquad` does not validate `band.frequency` against the Nyquist limit because the plugin instance does not yet know its target `sampleRate` at construction time and the engine's `setup()` call carries the rate; out-of-Nyquist frequencies still produce numerically stable (if musically meaningless) coefficients via `cos(omega) / sin(omega)`, and the cost of a per-band check at every recompute is not worth the protection.