first commit
This commit is contained in:
31
docs/media/audio-fx/audio-fx-plugin-limiter/README.md
Normal file
31
docs/media/audio-fx/audio-fx-plugin-limiter/README.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# @suaveplan/audio-fx-plugin-limiter - limiter plugin for the @suaveplan/audio-fx-plugin-* family.
|
||||
|
||||
limiter plugin for the @suaveplan/audio-fx-plugin-* family.
|
||||
|
||||
Member of the `@suaveplan/audio-fx-plugin-*` family. Universal (browser / server / offline / worklet identical). Depends on `@suaveplan/audio-fx-plugin-base` for the `AudioFxPlugin` contract; declares no other `@suaveplan/audio-fx-plugin-*` deps.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [limiter](./src/limiter/limiter.md) — `LimiterPlugin` is the universal brick-wall limiter. Its core contract is unconditional: for any bounded input, no outp…
|
||||
- [limiter](./src/limiter/limiter.md) — `LimiterPlugin` is the universal brick-wall limiter. Its core contract is unconditional: for any bounded input, no outp…
|
||||
- [schemas](./src/schemas/schemas.md) — Zod schemas validating the construction-time options of `LimiterPlugin` and the (currently empty) preset payload contra…
|
||||
- [schemas](./src/schemas/schemas.md) — Zod schemas validating the construction-time options of `LimiterPlugin` and the (currently empty) preset payload contra…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Composition
|
||||
|
||||
```ts
|
||||
import { PluginChain, DefaultBusRegistry } from "@suaveplan/audio-fx-plugin-base/chain";
|
||||
import { /* Plugin */ } from "@suaveplan/audio-fx-plugin-limiter";
|
||||
|
||||
const plugin = new /* Plugin */();
|
||||
const chain = new PluginChain({
|
||||
buses: new DefaultBusRegistry(),
|
||||
plugins: [plugin],
|
||||
});
|
||||
```
|
||||
|
||||
## Status
|
||||
|
||||
Replaces the `./limiter` subpath of the bundled `@suaveplan/audio-plugins-fx`.
|
||||
@@ -0,0 +1,51 @@
|
||||
# `limiter.types`
|
||||
|
||||
## Purpose
|
||||
|
||||
TypeScript-only type definitions for `LimiterPlugin`'s construction options. Co-located with the implementation so the public type surface (the `LimiterPluginOptions` interface) and its Zod validator stay in lockstep. Consumers can import the type for typed form state, dependency-injection wiring, or storybook controls without pulling in the plugin's runtime code.
|
||||
|
||||
This module declares no runtime values — it is the canonical "shape" contract the constructor accepts, mirrored by `LimiterPluginOptionsSchema` in `schemas/schemas.ts`.
|
||||
|
||||
## Features
|
||||
|
||||
- `LimiterPluginOptions` — all fields optional so the default constructor (`new LimiterPlugin()`) is valid.
|
||||
- `readonly` properties — emphasises that the options object is consumed once at construction and never mutated.
|
||||
- Numeric ranges are documented here via the Zod schema (not encoded in TS types) so a single source of truth governs validation.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type { LimiterPluginOptions } from "@suaveplan/audio-fx-plugin-limiter";
|
||||
|
||||
const opts: LimiterPluginOptions = { initialCeilingDb: -1 };
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type { LimiterPluginOptions } from "@suaveplan/audio-fx-plugin-limiter";
|
||||
|
||||
// Use the options type as a state shape for a UI form.
|
||||
interface LimiterFormState extends Required<LimiterPluginOptions> {
|
||||
readonly dirty: boolean;
|
||||
}
|
||||
|
||||
function saveLimiterOptions(state: LimiterFormState): LimiterPluginOptions {
|
||||
return {
|
||||
initialCeilingDb: state.initialCeilingDb,
|
||||
initialReleaseMs: state.initialReleaseMs,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `LimiterPluginOptions`
|
||||
- `readonly initialCeilingDb?: number` — initial brick-wall ceiling in dB.
|
||||
- `readonly initialReleaseMs?: number` — initial release time constant in ms.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The options interface deliberately omits a `gainReductionDb` field. That parameter is a telemetry output, not a configurable input — it is always derived from the per-block worst-case attenuation and would not be meaningful as a construction-time value. Constraining the options surface to inputs prevents callers from creating plugins in an impossible "pre-set" telemetry state.
|
||||
|
||||
All fields are `readonly` so structural-equality checks on saved options (e.g., undo/redo stacks) do not need defensive cloning. The constructor never mutates the passed object — it copies relevant values into the parameter list once and forgets the input — but typing the surface as `readonly` lets the type system enforce that intent at every call site, including in long-lived editor stores.
|
||||
@@ -0,0 +1,70 @@
|
||||
# `limiter`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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
|
||||
|
||||
```ts
|
||||
import { LimiterPlugin } from "@suaveplan/audio-fx-plugin-limiter";
|
||||
|
||||
const limiter = new LimiterPlugin({
|
||||
initialCeilingDb: -1,
|
||||
initialReleaseMs: 50,
|
||||
});
|
||||
chain.add(limiter);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
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.
|
||||
@@ -0,0 +1,48 @@
|
||||
# `limiter/schemas`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Zod schemas validating the construction-time options of `LimiterPlugin` and the (currently empty) preset payload contract. These run on the public boundary between user code and the plugin: any malformed object hitting the constructor or `applyPreset` is rejected with `AUDIO_FX.INVALID_OPTIONS` / `AUDIO_FX.INVALID_PRESET` before it can corrupt runtime state.
|
||||
|
||||
Separating schemas from the plugin module lets `index.ts` re-export them so callers can introspect the option surface (for forms, editors, or RPC validation) without importing the audio implementation.
|
||||
|
||||
## Features
|
||||
|
||||
- `LimiterPluginOptionsSchema` — validates `initialCeilingDb` ∈ [-60, 0] and `initialReleaseMs` ∈ [1, 1000].
|
||||
- `LimiterPresetDataSchema` — empty strict object: the limiter has no plugin-specific preset payload, so any extra keys are rejected by `.strict()`.
|
||||
- Both schemas are `z.object` / `z.number().finite()` based so `NaN` and `Infinity` cannot sneak through.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { LimiterPluginOptionsSchema } from "@suaveplan/audio-fx-plugin-limiter";
|
||||
|
||||
const parsed = LimiterPluginOptionsSchema.parse({ initialCeilingDb: -3 });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { LimiterPluginOptionsSchema } from "@suaveplan/audio-fx-plugin-limiter";
|
||||
|
||||
// Drive an editor form from the schema's introspected shape.
|
||||
const fields = LimiterPluginOptionsSchema.keyof().options;
|
||||
for (const field of fields) {
|
||||
form.addRow(field);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `LimiterPluginOptionsSchema: z.ZodObject<{ initialCeilingDb?, initialReleaseMs? }>`
|
||||
- `LimiterPresetDataSchema: z.ZodObject<{}, "strict">`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The constructor in `limiter.ts` calls `LimiterPluginOptionsSchema.safeParse(options)` and throws `AudioFxError(INVALID_OPTIONS, ...)` on failure with the Zod `issues` array attached as `context`. This keeps the surface uniform with the other plugins (eq/compressor/reverb/filter/delay/gate) and makes log triage easy: a single grep for `audio-fx.limiter-invalid-options` finds every malformed-options site.
|
||||
|
||||
`LimiterPresetDataSchema` is `.strict()` on purpose. The limiter has no preset-specific payload today, but `applyPreset` from `AbstractAudioFxPlugin` will still call the schema's `parse`. Using `.strict()` rejects accidentally-mixed payloads from sibling plugins (e.g., a `CompressorPresetData` blob applied to a `LimiterPlugin`), surfacing the mistake at the boundary rather than silently no-op'ing parameter restoration.
|
||||
Reference in New Issue
Block a user