98 lines
5.3 KiB
Markdown
98 lines
5.3 KiB
Markdown
# `engines`
|
||
|
||
<!-- begin:auto-docs -->
|
||
_(no submodules)_
|
||
<!-- end:auto-docs -->
|
||
|
||
## Purpose
|
||
|
||
The `reverb.engines` module owns the **per-sample DSP kernels** that `ReverbPlugin` calls every block: the Schroeder-Moorer algorithmic engine (comb-bank + all-pass diffusion), the direct-convolution engine (used when an IR is loaded), the pre-delay tap, and the per-channel state shape they all consume. Splitting these out of the plugin file keeps `reverb.ts` focused on the plugin lifecycle (parameters, setup, presets, loadImpulseResponse) and concentrates the inner-loop hot paths in one allocation-free helper that other reverb-shaped plugins (a future plate-only reverb, a Hadamard-matrix FDN) can reuse without dragging the full `ReverbPlugin` in.
|
||
|
||
The engines are pure functions over a mutable `ChannelState`: every per-sample call updates the state's circular buffer and indices in place, so the plugin's `processBlock` allocates nothing.
|
||
|
||
## Features
|
||
|
||
- `ChannelState`, `CombState`, `AllPassState` interfaces — the full per-channel state shape, sized once at `setup()`.
|
||
- `COMB_DELAYS_MS`, `ALLPASS_DELAYS_MS` — fixed Freeverb-style delay tables, scaled per session sample rate.
|
||
- `MAX_FEEDBACK`, `ALLPASS_FEEDBACK` — clamping and diffusion constants used by both engines.
|
||
- `createChannelState(sampleRate, preDelayMaxMs, irLength)` — fresh per-channel state allocator.
|
||
- `readPreDelay(state, sample, delaySamples)` — pre-delay tap; returns `sample` directly when `delaySamples === 0`.
|
||
- `algorithmicStep(state, input)` — Schroeder-Moorer per-sample step.
|
||
- `convolveStep(state, ir, input)` — direct time-domain convolution per-sample step.
|
||
- `updateCombFeedbacks(state, sampleRate, decaySec)` — recompute comb feedbacks from the current decay time (called once per block by the plugin).
|
||
- `resetChannelState(state)` — clear every buffer + index in place; no re-allocation.
|
||
|
||
## Usage: Basic Example
|
||
|
||
```ts
|
||
import {
|
||
createChannelState,
|
||
algorithmicStep,
|
||
updateCombFeedbacks,
|
||
} from "@suaveplan/audio-fx-plugin-reverb";
|
||
|
||
const state = createChannelState(48_000, 200, 0);
|
||
updateCombFeedbacks(state, 48_000, 2.5);
|
||
|
||
for (let i = 0; i < input.length; i++) {
|
||
output[i] = algorithmicStep(state, input[i]);
|
||
}
|
||
```
|
||
|
||
## Usage: Advanced Example
|
||
|
||
```ts
|
||
import {
|
||
createChannelState,
|
||
algorithmicStep,
|
||
convolveStep,
|
||
readPreDelay,
|
||
updateCombFeedbacks,
|
||
resetChannelState,
|
||
} from "@suaveplan/audio-fx-plugin-reverb";
|
||
|
||
// Build a custom dual-path reverb: pre-delay → convolver (if loaded)
|
||
// or algorithmic fallback, mixed wet/dry.
|
||
const states = Array.from({ length: channels }, () =>
|
||
createChannelState(sampleRate, 200, impulseResponse?.length ?? 0)
|
||
);
|
||
|
||
for (let ch = 0; ch < channels; ch++) {
|
||
const state = states[ch]!;
|
||
updateCombFeedbacks(state, sampleRate, decaySec);
|
||
for (let i = 0; i < blockSize; i++) {
|
||
const sample = input[ch]![i]!;
|
||
const wetIn = readPreDelay(state, sample, preDelaySamples);
|
||
const wetOut = impulseResponse
|
||
? convolveStep(state, impulseResponse, wetIn)
|
||
: algorithmicStep(state, wetIn);
|
||
output[ch]![i] = dry * sample + wet * wetOut;
|
||
}
|
||
}
|
||
|
||
// Transport stop: clear all state without reallocating.
|
||
for (const state of states) resetChannelState(state);
|
||
```
|
||
|
||
## API Reference
|
||
|
||
- `CombState`, `AllPassState`, `ChannelState` — state interfaces consumed by every engine function.
|
||
- `COMB_DELAYS_MS: readonly [29.7, 37.1, 41.1, 43.7]`, `ALLPASS_DELAYS_MS: readonly [5, 1.7]` — delay tables.
|
||
- `MAX_FEEDBACK = 0.997`, `ALLPASS_FEEDBACK = 0.5`.
|
||
- `createChannelState(sampleRate, preDelayMaxMs, irLength): ChannelState`.
|
||
- `readPreDelay(state, sample, delaySamples): number`.
|
||
- `algorithmicStep(state, input): number`.
|
||
- `convolveStep(state, ir, input): number`.
|
||
- `updateCombFeedbacks(state, sampleRate, decaySec): void`.
|
||
- `resetChannelState(state): void`.
|
||
|
||
## Implementation Notes
|
||
|
||
The algorithmic step parallels Freeverb's topology: four parallel comb filters with sample-rate-scaled delays, their outputs averaged, then fed through two series all-pass diffusers. The comb feedback formula `exp(-3 · ln10 · τ / T)` (where τ is the delay in seconds and T is the RT60) is mathematically equivalent to `10^(-3·τ/T)` and gives an analytically-correct RT60 in the linear-decay limit; the explicit `MAX_FEEDBACK = 0.997` clamp guarantees the engine cannot diverge regardless of the configured `decay` value.
|
||
|
||
`convolveStep` is direct time-domain convolution; it costs `O(blockSize · irLength)` per channel, which is appropriate for short room and plate IRs (up to a few thousand samples). Long cathedral IRs benefit from a partitioned FFT convolver — that is deliberately deferred until a real bottleneck demonstrates the need, since the algorithmic path covers long tails efficiently and the convolution path stays simple and obviously correct.
|
||
|
||
`readPreDelay` short-circuits when `delaySamples === 0` so the bit-transparent dry path stays tight at zero pre-delay. The buffer is sized to the maximum allowed pre-delay (200 ms × sample rate) at setup and never reallocated; the current pre-delay length is read per-block from the live parameter so automation lanes stay continuous.
|
||
|
||
`resetChannelState` mutates buffers in place (`fill(0)`) rather than reallocating so the plugin's `reset()` honours the spec's "buffers are the same instances as before reset" guarantee.
|