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

4.2 KiB
Raw Blame History

limiter

(no submodules)

Purpose

LimiterPlugin is the universal brick-wall limiter. Its core contract is unconditional: for any bounded input, no output sample's absolute value exceeds the linear conversion of the ceiling parameter. The plugin pairs an instantaneous (zero-lookahead) attack with a one-pole exponential release, so transients are clamped on the sample they occur — no lookahead buffer, no group delay, no latency to report.

The plugin extends AbstractAudioFxPlugin, so it runs identically across browser / server / worklet renderers. State is a single linked gain value plus a frame-peak reduction metric for telemetry. reset() snaps the gain back to unity in place.

Features

  • Three parameters: ceiling (dB, -60..0), release (ms, 1..1000), and gainReductionDb (telemetry).
  • Linked-stereo brick-wall: gain is shared across all input channels per sample, so a transient on one channel attenuates every channel identically — the stereo image stays phase-coherent.
  • Zero-lookahead attack: when |peak| > ceilingLin at sample i, the gain at sample i is set to ceilingLin / |peak| before writing the output, so the ceiling is a hard contract verifiable by audit.
  • One-pole release: gain += releaseCoef · (1 − gain) per sample with releaseCoef = 1 − exp(-1/(τ · sr)).
  • gainReductionDb is updated once per block with the worst-case in-block reduction so meters get a useful peak reading without per-sample state.
  • getGainReductionDb() accessor mirrors the parameter for low-friction metering.

Usage: Basic Example

import { LimiterPlugin } from "@suaveplan/audio-fx-plugin-limiter";

const limiter = new LimiterPlugin({
    initialCeilingDb: -1,
    initialReleaseMs: 50,
});
chain.add(limiter);

Usage: Advanced Example

import { LimiterPlugin } from "@suaveplan/audio-fx-plugin-limiter";

// Master-bus limiter feeding a UI meter from the live gain reduction.
const limiter = new LimiterPlugin({
    initialCeilingDb: -0.3,
    initialReleaseMs: 80,
});
await limiter.setup(
    { sampleRate: 48_000, channels: 2, blockSize: 512 },
    ctx
);

setInterval(() => {
    const gr = limiter.getGainReductionDb();
    if (gr > 6) {
        console.warn(`Master limiter pulling ${gr.toFixed(2)} dB`);
    }
}, 100);

API Reference

  • LimiterPlugin (class extends AbstractAudioFxPlugin)
    • new LimiterPlugin(options?: LimiterPluginOptions) — throws AUDIO_FX.INVALID_OPTIONS on malformed options.
    • getGainReductionDb(): number — last block's peak gain reduction (positive number, in dB).
    • Inherited parameter accessors for ceiling, release, gainReductionDb.

Implementation Notes

The limiter is intentionally a true brick-wall, not a "soft" or "transparent" limiter. We do not interpolate around the threshold or apply a knee. The reason: many use sites layer this plugin downstream of an output-stage compressor that already provides knee shaping, and rely on the limiter for guaranteed bound semantics. Adding a knee would weaken the contract that the test in output peak never exceeds the configured ceiling enforces.

There is no lookahead buffer. That keeps the plugin latency-free (latencySamples: 0, inherited default) and makes the bound testable by static inspection of the inner loop: at sample i, if peak > ceilingLin then gain := ceilingLin / peak before output[ch][i] = input[ch][i] * gain. The multiplication therefore can produce at most |input[ch][i]| * (ceilingLin / |peak|) ≤ ceilingLin. The downside is that an unusually fast transient (single-sample spike at a very low source sample rate) can produce visible distortion on its leading edge — for those cases a future LookaheadLimiterPlugin can sit on a separate subpath without breaking this contract.

gainReductionDb is reported as a per-block peak rather than a running average. Meters typically expect peak; if a future surface wants RMS, it can compute that from the gain-reduction stream by subscribing to a parameter automation channel. The release coefficient is recomputed every block from the current release parameter so automation works without introducing zipper noise — the one-pole step is small per sample even when the parameter changes mid-block.