Initial Commit
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
# `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.
|
||||
Reference in New Issue
Block a user