first commit
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
# `reverb`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`ReverbPlugin` is the universal reverb effect: a Schroeder-style algorithmic engine (four parallel comb filters plus two series all-pass filters) that doubles as a direct-convolution renderer when an impulse response is loaded. The same plugin handles short room ambience, large hall colour, plate-style metallic shimmer, and full cathedral tails — preset by name or sculpted by adjusting `preDelay` and `decay`. Pre-delay is applied to the wet path only so the dry signal stays sample-aligned with the source, and `wet === 0` collapses to bit-transparency.
|
||||
|
||||
The plugin satisfies the spec's "impulse-response convolver with algorithmic fallback" requirement: once `loadImpulseResponse(url)` succeeds, every block runs direct convolution; before any IR is loaded (or after `applyPreset` clears one), the algorithmic engine renders the tail.
|
||||
|
||||
## Features
|
||||
|
||||
- Four automatable parameters — `preDelay` (ms), `decay` (s, mapped to per-comb feedback via `0.001^(τ/T)`), `wet` (0..1), `dry` (0..1).
|
||||
- Allocation-free per-block processing once `setup()` has allocated comb / all-pass / pre-delay / convolution delay lines.
|
||||
- Bounded feedback (clamped to `0.997`) so the algorithmic path cannot diverge regardless of `decay` value.
|
||||
- `wet === 0` early-return path: output equals `dry * input` exactly, bit-transparent when `dry === 1`.
|
||||
- `loadImpulseResponse(url, { signal })` — fetches the IR, switches the reverb into convolution mode. A pre-aborted `AbortSignal` short-circuits with `AUDIO_FX.ABORTED` before any fetch is issued.
|
||||
- Four shipped factory presets — `room`, `hall`, `plate`, `cathedral` — applied via `applyPresetByName`.
|
||||
- Round-trips the loaded IR through the universal preset envelope as a plain `number[]` so a saved session captures the cathedral.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { ReverbPlugin } from "@suaveplan/audio-fx-plugin-reverb";
|
||||
|
||||
const verb = new ReverbPlugin({
|
||||
initialWet: 0.25,
|
||||
initialDry: 0.75,
|
||||
});
|
||||
verb.applyPresetByName("hall");
|
||||
chain.add(verb);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { ReverbPlugin } from "@suaveplan/audio-fx-plugin-reverb";
|
||||
|
||||
const verb = new ReverbPlugin({ initialWet: 0.6, initialDry: 0.4 });
|
||||
await verb.setup({ sampleRate: 48_000, channels: 2, blockSize: 512 }, ctx);
|
||||
|
||||
// Load a cathedral IR from a CDN, but let the user cancel the load
|
||||
// if they switch presets before it finishes.
|
||||
const ac = new AbortController();
|
||||
cancelButton.addEventListener("click", () => ac.abort());
|
||||
|
||||
try {
|
||||
await verb.loadImpulseResponse("https://cdn.example/impulses/cathedral.f32", {
|
||||
signal: ac.signal,
|
||||
});
|
||||
console.log("Cathedral loaded:", verb.getImpulseResponse()?.length, "samples");
|
||||
} catch (error) {
|
||||
if ((error as AudioFxError).code === "AUDIO_FX.ABORTED") {
|
||||
// User cancelled — algorithmic fallback continues to render.
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Persist with the session: the IR rides along inside the preset envelope.
|
||||
const snapshot = verb.getPreset();
|
||||
saveSession(JSON.stringify(snapshot));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `ReverbPlugin` (class extends `AbstractAudioFxPlugin`)
|
||||
- `new ReverbPlugin(options?: ReverbPluginOptions)` — throws `AUDIO_FX.INVALID_OPTIONS` on malformed options.
|
||||
- `loadImpulseResponse(url: string, opts?: { signal?: AbortSignal }): Promise<void>` — throws `AUDIO_FX.ABORTED` on pre-aborted signal.
|
||||
- `applyPresetByName(name: ReverbPresetName): void` — applies `room` / `hall` / `plate` / `cathedral`.
|
||||
- `getImpulseResponse(): Float32Array | undefined` — current IR or undefined when algorithmic.
|
||||
- Inherited parameter accessors for `preDelay`, `decay`, `wet`, `dry`.
|
||||
- `REVERB_PRESETS: Readonly<Record<ReverbPresetName, { preDelayMs, decaySec }>>` — exported preset table.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The Schroeder-Moorer comb/all-pass topology was chosen because every state buffer is short (≈30–45 ms × `sampleRate`), feedback is closed-form derivable from RT60 (`feedback = 10^(-3 · τ / T)`, clamped to `0.997`), and the implementation has zero per-block allocation after `setup`. Comb buffers and all-pass buffers are per-channel: stereo material gets four combs and two all-pass filters per channel, with the same delay times — engine-level decorrelation (different delays per channel) is a future enhancement, not a current blocker.
|
||||
|
||||
Pre-delay is implemented as a single circular buffer per channel sized to the maximum allowed (`200 ms` at the configured sample rate). The current pre-delay length is read per-block from the `preDelay` parameter so automation lanes stay live; the buffer itself is never reallocated. When `preDelayMs === 0` the read short-circuits (input passes straight through to the wet engine) so the bit-transparent path remains tight.
|
||||
|
||||
The convolution path is direct (time-domain) — appropriate for short room and plate IRs (≲ a few thousand samples). Long cathedral IRs would benefit from a partitioned FFT convolver; that is deferred until a real bottleneck demonstrates the need, since the algorithmic path covers long tails efficiently. The convolution delay line is reallocated whenever an IR is loaded or applied via preset, so per-block cost stays linear in the IR length without per-block allocation.
|
||||
Reference in New Issue
Block a user