Files
skills/docs/media/audio-fx/audio-fx-plugin-reverb/src/reverb/reverb.md
2026-05-29 08:20:56 +01:00

5.2 KiB
Raw Blame History

reverb

(no submodules)

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

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

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.