first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
# @suaveplan/audio-fx-plugin-compressor - compressor plugin for the @suaveplan/audio-fx-plugin-* family.
compressor 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
- [compressor](./src/compressor/compressor.md) — `CompressorPlugin` is the universal soft-knee dynamics compressor for the project — the same plugin handles vocal level…
- [compressor](./src/compressor/compressor.md) — `CompressorPlugin` is the universal soft-knee dynamics compressor for the project — the same plugin handles vocal level…
- [schemas](./src/schemas/schemas.md) — The `compressor.schemas` module owns the Zod boundary guards for `CompressorPlugin` — both the constructor options and…
- [schemas](./src/schemas/schemas.md) — The `compressor.schemas` module owns the Zod boundary guards for `CompressorPlugin` — both the constructor options and…
<!-- end:auto-docs -->
## Composition
```ts
import { PluginChain, DefaultBusRegistry } from "@suaveplan/audio-fx-plugin-base/chain";
import { /* Plugin */ } from "@suaveplan/audio-fx-plugin-compressor";
const plugin = new /* Plugin */();
const chain = new PluginChain({
buses: new DefaultBusRegistry(),
plugins: [plugin],
});
```
## Status
Replaces the `./compressor` subpath of the bundled `@suaveplan/audio-plugins-fx`.

View File

@@ -0,0 +1,85 @@
# `compressor`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`CompressorPlugin` is the universal soft-knee dynamics compressor for the project — the same plugin handles vocal levelling, drum-bus glue, mastering compression, and sidechain-ducked bass under a kick. It implements the `AudioFxPlugin` contract by extending `AbstractAudioFxPlugin`, so the same compressor runs identically inside the Web Audio engine, the server renderer, and the worklet wrapper.
The compressor exposes its full control surface (`threshold`, `knee`, `ratio`, `attack`, `release`) as automatable `PluginParameter`s, plus non-automatable mode flags (`midSide`, `sidechainEnabled`) and a read-only `gainReductionDb` telemetry parameter that meters can poll. A peak envelope follower with separate attack/release smoothing coefficients tracks the loudest channel across the input (or the sidechain bus when wired), and a Reiss & Stables soft-knee curve maps envelope-above-threshold to gain reduction in dB.
## Features
- Five automatable continuous parameters — `threshold`, `knee`, `ratio`, `attack`, `release` — plus two mode flags and a read-only meter.
- Soft-knee transfer curve (Reiss & Stables formulation): below `threshold - knee/2` no reduction, above `threshold + knee/2` full ratio reduction, smooth quadratic interpolation through the knee.
- Linked-channel envelope follower (one shared envelope) so stereo material stays phase-coherent under compression.
- Optional sidechain detection — call `setSidechain(bus)` to route the envelope detector from an external `SidechainBus` (e.g. a kick stem) when `sidechainEnabled` is true.
- Mid/side mode on stereo input: encode L/R → M/S, apply the linked gain to both, decode back. Mono input ignores the mode flag.
- Allocation-free per-block processing: attack/release smoothing coefficients recomputed once per block from the current parameter values.
- `gainReductionDb` parameter republished every block; consumers can either poll `getGainReductionDb()` or read the parameter directly.
## Usage: Basic Example
```ts
import { CompressorPlugin } from "@suaveplan/audio-fx-plugin-compressor";
const comp = new CompressorPlugin({
initialThresholdDb: -18,
initialRatio: 4,
initialAttackMs: 5,
initialReleaseMs: 80,
});
chain.add(comp);
```
## Usage: Advanced Example
```ts
import { CompressorPlugin } from "@suaveplan/audio-fx-plugin-compressor";
import { DefaultBusRegistry } from "@suaveplan/audio-plugins-fx/chain";
// Sidechain bass under kick:
const buses = new DefaultBusRegistry();
const kickPub = buses.publish("kick", 1);
const bass = new CompressorPlugin({
initialThresholdDb: -20,
initialRatio: 6,
initialAttackMs: 2,
initialReleaseMs: 150,
initialSidechainEnabled: true,
});
await bass.setup({ sampleRate: 48_000, channels: 1, blockSize: 256 }, ctx);
bass.setSidechain(buses.get("kick"));
// In the kick track's processBlock, publish its output:
kickPub.write([kickSamples]);
// Meter the gain reduction every UI frame:
requestAnimationFrame(function tick() {
meter.write(bass.getGainReductionDb());
requestAnimationFrame(tick);
});
```
## API Reference
- `CompressorPlugin` (class extends `AbstractAudioFxPlugin`)
- `new CompressorPlugin(options?: CompressorPluginOptions)` — throws `AUDIO_FX.INVALID_OPTIONS` on malformed options.
- `setSidechain(bus: SidechainBus | undefined): void` — attach / detach an external detection source.
- `getGainReductionDb(): number` — current frame's peak gain reduction in dB (positive = attenuation).
- Inherited parameter accessors for `threshold`, `knee`, `ratio`, `attack`, `release`, `midSide`, `sidechainEnabled`, `gainReductionDb`.
- `CompressorPluginOptions` — initial parameter values and mode flags.
- `CompressorPresetData` — `{ midSide, sidechainEnabled }`; the five continuous parameters round-trip via the universal `parameters` map.
## Implementation Notes
The envelope follower uses one-pole smoothing with separate `attack` and `release` time constants converted to per-sample coefficients via `alpha = exp(-1 / (timeSec * sampleRate))`. The peak detector takes `max(|L|, |R|, …)` across detector channels per sample so a single transient on any channel pulls the envelope up immediately. Envelope state is one number (linked) so all output channels share the same gain reduction — this keeps the stereo image stable under compression.
The soft-knee formula uses the closed-form Reiss & Stables expression: inside the knee region, `gr = (1 − 1/ratio) · (envDb − threshold + knee/2)² / (2·knee)`. At `knee = 0` the formula degenerates to a hard-knee response by skipping the knee branch entirely (the condition `knee > 0 && 2·above > -knee && 2·above < knee` is false when `knee === 0`).
Sidechain routing: `setSidechain(bus)` allocates per-channel scratch buffers sized to `bus.channels` (not the plugin's channels) so a mono sidechain can drive a stereo program. During `processBlock`, when `sidechainEnabled` and a bus is wired, the detector reads into the scratch and uses those samples as the envelope source; otherwise it falls back to the program input. Calling `setSidechain(undefined)` detaches the bus and frees the scratch.
The `gainReductionDb` parameter is non-automatable (its value is plugin output, not user input); the plugin writes it via the same `setParameter` path used by external callers, which clamps to `[0, 60]`.

View File

@@ -0,0 +1,61 @@
# `schemas`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `compressor.schemas` module owns the Zod boundary guards for `CompressorPlugin` — both the constructor options and the plugin-specific preset payload. Per the package's "Zod boundary validation" requirement, every public options object on every plugin SHALL be validated against a sibling `*.schemas.ts` before the plugin touches any state, and these schemas are how `CompressorPlugin` keeps that promise. A malformed `initialRatio`, an out-of-range `initialAttackMs`, or a missing field on `applyPreset` data all surface as `AUDIO_FX.INVALID_OPTIONS` / `AUDIO_FX.INVALID_PRESET` with the Zod issue list in `context.issues`.
Keeping the schemas in a sibling file (rather than inline in `compressor.ts`) is the package-wide convention; consumers can import the schemas directly and run their own `safeParse` against UI input, surfacing field-level errors before the plugin instance exists.
## Features
- `CompressorPluginOptionsSchema` — every constructor field is optional but, when present, must be finite and inside the per-parameter `[min, max]` range that the plugin's `parameters` declare (`threshold ∈ [−60, 0]`, `knee ∈ [0, 24]`, `ratio ∈ [1, 20]`, `attack ∈ [0.1, 100] ms`, `release ∈ [1, 1000] ms`).
- `CompressorPresetDataSchema` — the plugin-specific preset payload: just `midSide` and `sidechainEnabled` booleans. The five continuous parameters round-trip through the universal `parameters` map on the `PresetSnapshot` envelope, so they do not need to appear here.
## Usage: Basic Example
```ts
import { CompressorPluginOptionsSchema } from "@suaveplan/audio-fx-plugin-compressor";
const result = CompressorPluginOptionsSchema.safeParse(formValues);
if (!result.success) {
return showFieldErrors(result.error.issues);
}
const compressor = new CompressorPlugin(result.data);
```
## Usage: Advanced Example
```ts
import {
CompressorPluginOptionsSchema,
CompressorPresetDataSchema,
} from "@suaveplan/audio-fx-plugin-compressor";
// Server-side validation of an untrusted preset payload before it
// hits the plugin's applyPreset (which would throw with the same
// code, but the host wants to surface a friendlier error first).
const envelope = parseChainSnapshot(payload); // base envelope check
for (const entry of envelope.plugins.filter((p) => p.pluginType === "compressor")) {
const data = CompressorPresetDataSchema.parse(entry.preset.data);
if (data.sidechainEnabled && !allowSidechain) {
return reject("Sidechain compression is disabled for this tier.");
}
}
```
## API Reference
- `CompressorPluginOptionsSchema` — `z.ZodObject` matching `CompressorPluginOptions`. Every field is optional and ranged.
- `CompressorPresetDataSchema` — `z.ZodObject` matching `CompressorPresetData`. Both fields required.
## Implementation Notes
The schemas use `.finite()` so `NaN` and `±Infinity` are rejected at the boundary; the envelope follower's `exp(-1 / (τ · sr))` calculation would otherwise propagate non-finite alphas into the smoothing state and contaminate every subsequent block. Time constants use `.min(0.1)` for attack and `.min(1)` for release — values smaller than this break the one-pole smoothing assumption (alpha approaches 1 from below) and produce numerical artefacts in single-precision float.
Ratio is `.min(1)` (rather than `.positive()`) because a ratio below 1 would expand rather than compress, which the plugin's curve does not model. A future expander plugin would live as a separate plugin with its own schema. The maxima (`.max(20)` for ratio, `.max(24)` for knee, etc.) are the values the plugin's parameter declarations advertise — keeping the schema and the declaration in sync means consumers can safely take the parameter's `min` / `max` as authoritative without re-deriving them from the schema.
The preset data deliberately omits the continuous parameters because they live in `PresetSnapshot.parameters` (the universal map). Duplicating them here would risk drift if a future refactor changes one source but not the other; keeping the data slim makes the preset round-trip obviously correct.