first commit
This commit is contained in:
46
docs/media/audio-fx/audio-fx-plugin-eq/README.md
Normal file
46
docs/media/audio-fx/audio-fx-plugin-eq/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# @suaveplan/audio-fx-plugin-eq - Parametric / shelving / high-pass / low-pass equaliser plugin for the @suaveplan/audio-fx-plugin-* family. Multi-band biquad implementation, sample-accurate parameter automation, preset round-tripping, universal.
|
||||
|
||||
Parametric / shelving / high-pass / low-pass equaliser for the `@suaveplan/audio-fx-plugin-*` family. Multi-band biquad implementation, sample-accurate parameter automation, Zod-validated preset round-tripping, universal (browser / server / offline / worklet identical).
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [eq](./src/eq/eq.md) — `EQPlugin` is the 8-band parametric equaliser that anchors every mix bus, vocal chain, and master strip in the project.…
|
||||
- [eq](./src/eq/eq.md) — `EQPlugin` is the 8-band parametric equaliser that anchors every mix bus, vocal chain, and master strip in the project.…
|
||||
- [schemas](./src/schemas/schemas.md) — The `eq.schemas` module owns every Zod schema that guards `EQPlugin`'s boundaries: construction options, single-band re…
|
||||
- [schemas](./src/schemas/schemas.md) — The `eq.schemas` module owns every Zod schema that guards `EQPlugin`'s boundaries: construction options, single-band re…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { createStorageUriResolver } from "@suaveplan/storage-uri";
|
||||
import { PluginChain, DefaultBusRegistry } from "@suaveplan/audio-fx-plugin-base/chain";
|
||||
import { EQPlugin, EQ_PRESETS } from "@suaveplan/audio-fx-plugin-eq";
|
||||
|
||||
const eq = new EQPlugin();
|
||||
const chain = new PluginChain({
|
||||
buses: new DefaultBusRegistry(),
|
||||
plugins: [eq],
|
||||
});
|
||||
|
||||
eq.applyPreset(EQ_PRESETS.vocalAir);
|
||||
```
|
||||
|
||||
## Surface
|
||||
|
||||
| Symbol | Kind | Notes |
|
||||
|---|---|---|
|
||||
| `EQPlugin` | class | Concrete `AudioFxPlugin` implementation; extends `AbstractAudioFxPlugin` |
|
||||
| `EQ_PRESETS` | const | Built-in factory presets |
|
||||
| `EQBand`, `EQBandType`, `EQPluginOptions`, `EQPresetData`, `EQPresetName` | type | Public type surface |
|
||||
| `EQBandSchema`, `EQBandTypeSchema`, `EQPluginOptionsSchema`, `EQPresetDataSchema`, `EQ_BAND_COUNT` | const / schema | Zod boundaries |
|
||||
| `computeBiquad`, `BiquadCoeffs`, `BiquadState`, `identityCoeffs` | utility | Biquad math helpers (used internally; re-exported for DSP-curious consumers) |
|
||||
|
||||
## Composition
|
||||
|
||||
This package depends on `@suaveplan/audio-fx-plugin-base` for the `AudioFxPlugin` contract and the chain primitives. It declares no transitive `@suaveplan/audio-fx-plugin-*` deps — the dependency arrow points only at the foundation.
|
||||
|
||||
## Status
|
||||
|
||||
Replaces the `./eq` subpath of the bundled `@suaveplan/audio-plugins-fx`.
|
||||
82
docs/media/audio-fx/audio-fx-plugin-eq/src/eq/eq.md
Normal file
82
docs/media/audio-fx/audio-fx-plugin-eq/src/eq/eq.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# `eq`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`EQPlugin` is the 8-band parametric equaliser that anchors every mix bus, vocal chain, and master strip in the project. It implements the universal `AudioFxPlugin` contract by extending `AbstractAudioFxPlugin`, so the same plugin runs identically inside the Web Audio engine, the server renderer, the offline render, and the worklet wrapper. Every band is a self-contained RBJ Audio EQ Cookbook biquad computed in pure JavaScript — no `BiquadFilterNode`, no Web Audio context, no platform-specific code on the processing path.
|
||||
|
||||
The plugin declares no automatable parameters because EQ tunables (`frequency`, `gain`, `q`, `type`) are band records, not scalar parameter automation lanes. Band state lives in the universal `PresetSnapshot.data` payload validated by `EQPresetDataSchema`, so `getPreset()` / `applyPreset()` round-trip the full plugin state without losing precision.
|
||||
|
||||
## Features
|
||||
|
||||
- Eight independent bands, each selectable between `peaking`, `low-shelf`, `high-shelf`, `low-pass`, and `high-pass` shapes.
|
||||
- RBJ Audio EQ Cookbook biquad coefficients computed at `setup()` time and recomputed lazily when `setBand` / `setBandType` / `applyPreset` mutate a band; no per-block recompute.
|
||||
- Per-channel direct-form biquad state cleared in-place by `reset()`; processing is allocation-free.
|
||||
- Numerically stable across the audible range at any session sample rate — a flat (all-peaking-at-0dB) configuration is bit-transparent at the transfer function level and reproduces the input within float tolerance.
|
||||
- Four factory presets (`flat`, `vocal`, `bass`, `podcast`) exported as `EQ_PRESETS` and applied by name via `applyPresetByName`.
|
||||
- Zod boundary validation on construction, `setBand`, `setBandType`, and `applyPreset`; every rejection throws an `AudioFxError` with the offending field surfaced in `context`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { EQPlugin } from "@suaveplan/audio-fx-plugin-eq";
|
||||
|
||||
const eq = new EQPlugin();
|
||||
eq.applyPresetByName("vocal");
|
||||
eq.setBand(4, 2_500, 4, 1); // band 4 → 2.5 kHz, +4 dB, Q=1
|
||||
// Hand off to the chain / engine:
|
||||
chain.add(eq);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { EQPlugin, EQ_PRESETS } from "@suaveplan/audio-fx-plugin-eq";
|
||||
|
||||
// Custom 8-band layout for a kick-drum bus
|
||||
const eq = new EQPlugin({
|
||||
initialBands: [
|
||||
{ frequency: 40, gain: 3, q: 0.8, type: "low-shelf" },
|
||||
{ frequency: 60, gain: 5, q: 1.5, type: "peaking" },
|
||||
{ frequency: 120, gain: -3, q: 1.5, type: "peaking" },
|
||||
{ frequency: 400, gain: -6, q: 2, type: "peaking" },
|
||||
{ frequency: 1_000, gain: 2, q: 1, type: "peaking" },
|
||||
{ frequency: 3_500, gain: 4, q: 1.2, type: "peaking" },
|
||||
{ frequency: 6_000, gain: 2, q: 1, type: "peaking" },
|
||||
{ frequency: 12_000, gain: 0, q: 0.7, type: "high-shelf" },
|
||||
],
|
||||
});
|
||||
|
||||
// Snapshot for session save and rehydrate later
|
||||
const snap = eq.getPreset();
|
||||
localStorage.setItem("kick-eq", JSON.stringify(snap));
|
||||
|
||||
// Later, in a fresh process:
|
||||
const restored = new EQPlugin();
|
||||
restored.applyPreset(JSON.parse(localStorage.getItem("kick-eq")!));
|
||||
expect(restored.getBands()).toEqual(EQ_PRESETS.flat.bands); // false — restored matches kick layout
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `EQPlugin` (class extends `AbstractAudioFxPlugin`)
|
||||
- `new EQPlugin(options?: EQPluginOptions)` — throws `AUDIO_FX.INVALID_OPTIONS` on malformed `initialBands`.
|
||||
- `getBands(): ReadonlyArray<EQBand>` — read-only snapshot of the live band configuration.
|
||||
- `setBand(index, frequency, gain, q): void` — throws `AUDIO_FX.INVALID_OPTIONS` on out-of-range `index` or invalid field values.
|
||||
- `setBandType(index, type): void` — switches one band to a different response shape.
|
||||
- `applyPresetByName(name: EQPresetName): void` — applies one of the four shipped factory presets.
|
||||
- Inherited: `setup`, `process`, `reset`, `dispose`, `getPreset`, `applyPreset`, `onLatencyChanged`.
|
||||
- `EQ_PRESETS: Readonly<Record<EQPresetName, EQPresetData>>` — exported factory presets.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The biquad coefficient formulas are the canonical RBJ Audio EQ Cookbook variants, normalised by `a0` so the per-sample inner loop costs five multiplies and four adds plus four state updates per band. A flat configuration — every band `peaking` with `gain === 0` — collapses to identity (`b0/a0 = 1`, `b1/a0 = a1/a0`, `b2/a0 = a2/a0`); the test suite verifies this by feeding a mixed-frequency input and asserting sample-for-sample equality within `1e-6`.
|
||||
|
||||
Band state is stored as `BiquadState` objects (`x1, x2, y1, y2`) per channel per band; `reset()` mutates them in place rather than reallocating so the spec's "buffers are the same instances as before reset" guarantee holds without bookkeeping. `setup()` reallocates state only when channel count changes (it always rebuilds in this implementation, which is acceptable because plugin lifecycle setups are rare relative to block processing).
|
||||
|
||||
The plugin deliberately exports no `createAudioNode` — the universal path is the canonical path. Engine adapters that want a `BiquadFilterNode`-backed graph in the browser can wrap eight `BiquadFilterNode`s in series; this plugin's role is the universal source of truth that runs identically on every backend.
|
||||
|
||||
`applyPresetData` validates the incoming `data` against `EQPresetDataSchema` before mutating any band so a malformed snapshot leaves the plugin in its previous state.
|
||||
@@ -0,0 +1,71 @@
|
||||
# `schemas`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The `eq.schemas` module owns every Zod schema that guards `EQPlugin`'s boundaries: construction options, single-band records, and preset data payloads. Per the spec's "Zod boundary validation" requirement, every public options object on every plugin SHALL be validated against a sibling `*.schemas.ts` before any state is mutated, and `EQPlugin` is no exception — `setBand`, `setBandType`, constructor options, and `applyPreset` all funnel their input through these schemas and throw `AUDIO_FX.INVALID_OPTIONS` or `AUDIO_FX.INVALID_PRESET` with the Zod issue list in `context.issues` on failure.
|
||||
|
||||
Keeping the schemas in a sibling file (rather than inline in `eq.ts`) is the project-wide convention: external consumers can import the schema, run their own `safeParse` against UI input, and surface field-level errors before the plugin sees the data.
|
||||
|
||||
## Features
|
||||
|
||||
- `EQ_BAND_COUNT` — the fixed 8-band count enforced by both `EQPluginOptionsSchema` and `EQPresetDataSchema`; preset payloads with anything other than exactly eight bands are rejected at the boundary so the plugin's per-block inner loop can hoist the count as a literal.
|
||||
- `EQBandTypeSchema` — discriminated enum over the five shipped response shapes.
|
||||
- `EQBandSchema` — full per-band record with `frequency > 0`, `gain` finite, `q > 0`, and `type` from the band-type enum.
|
||||
- `EQPluginOptionsSchema` — constructor options; the `initialBands` array is optional but, when present, must have exactly `EQ_BAND_COUNT` entries.
|
||||
- `EQPresetDataSchema` — plugin-specific preset payload; same length constraint on `bands`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { EQBandSchema, EQPluginOptionsSchema } from "@suaveplan/audio-fx-plugin-eq";
|
||||
|
||||
const result = EQBandSchema.safeParse(userInput);
|
||||
if (!result.success) {
|
||||
return showFieldErrors(result.error.issues);
|
||||
}
|
||||
const band = result.data;
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
EQ_BAND_COUNT,
|
||||
EQBandSchema,
|
||||
EQPluginOptionsSchema,
|
||||
EQPresetDataSchema,
|
||||
} from "@suaveplan/audio-fx-plugin-eq";
|
||||
|
||||
// 1. Bulk-validate a UI form's eight rows before constructing the plugin.
|
||||
const rows = readUIForm();
|
||||
if (rows.length !== EQ_BAND_COUNT) {
|
||||
return showError(`EQ requires exactly ${EQ_BAND_COUNT} bands; got ${rows.length}.`);
|
||||
}
|
||||
const validated = EQPluginOptionsSchema.parse({ initialBands: rows });
|
||||
|
||||
// 2. Re-validate a deserialised preset against EQPresetDataSchema before
|
||||
// handing it to the plugin's applyPreset. This duplicates the plugin's
|
||||
// own check, but lets the host surface user-friendly errors *before*
|
||||
// a plugin instance exists.
|
||||
const preset = EQPresetDataSchema.parse(JSON.parse(savedJson));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `EQ_BAND_COUNT: number` — exported constant; always `8`.
|
||||
- `EQBandTypeSchema` — `z.ZodEnum<['peaking', 'low-shelf', 'high-shelf', 'low-pass', 'high-pass']>`.
|
||||
- `EQBandSchema` — `z.ZodObject` matching `EQBand`. Rejects non-positive `frequency` / `q`, non-finite values, and unknown `type` strings.
|
||||
- `EQPluginOptionsSchema` — `z.ZodObject` matching `EQPluginOptions`. `initialBands` is optional; when present, length must equal `EQ_BAND_COUNT`.
|
||||
- `EQPresetDataSchema` — `z.ZodObject` matching `EQPresetData`. `bands` length must equal `EQ_BAND_COUNT`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The schemas use `.finite()` on numeric fields so `NaN` and `±Infinity` are rejected at the boundary; the biquad coefficient formulas would propagate non-finite values into IIR state otherwise and contaminate every subsequent block. Q is `.positive()` rather than `.nonnegative()` because the cookbook formulas divide by `Q` in the `alpha` calculation; allowing `Q === 0` would produce `Infinity` coefficients.
|
||||
|
||||
`EQBandSchema` does not validate `frequency` against the Nyquist limit because the plugin instance does not yet know its target `sampleRate` at construction time — the engine's `setup()` call carries the rate. Out-of-Nyquist frequencies still produce numerically stable (if musically meaningless) coefficients, and the cost of a per-band check at every coefficient recompute is not worth the protection.
|
||||
|
||||
The schemas are exported from the package's `./eq` subpath so consumers can reuse them in form validation, server-side payload guards, and integration tests without pulling in the full plugin runtime.
|
||||
Reference in New Issue
Block a user