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-widener - widener plugin for the @suaveplan/audio-fx-plugin-* family.
widener 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
- [schemas](./src/schemas/schemas.md) — Zod schemas validating the construction-time options and preset payload of `StereoWidenerPlugin`. These run on the publ…
- [schemas](./src/schemas/schemas.md) — Zod schemas validating the construction-time options and preset payload of `StereoWidenerPlugin`. These run on the publ…
- [widener](./src/widener/widener.md) — `StereoWidenerPlugin` is the universal mid/side stereo-width control. It decomposes a stereo input into mid (`(L + R) /…
- [widener](./src/widener/widener.md) — `StereoWidenerPlugin` is the universal mid/side stereo-width control. It decomposes a stereo input into mid (`(L + R) /…
<!-- end:auto-docs -->
## Composition
```ts
import { PluginChain, DefaultBusRegistry } from "@suaveplan/audio-fx-plugin-base/chain";
import { /* Plugin */ } from "@suaveplan/audio-fx-plugin-widener";
const plugin = new /* Plugin */();
const chain = new PluginChain({
buses: new DefaultBusRegistry(),
plugins: [plugin],
});
```
## Status
Replaces the `./widener` subpath of the bundled `@suaveplan/audio-plugins-fx`.

View File

@@ -0,0 +1,56 @@
# `widener/schemas`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Zod schemas validating the construction-time options and preset payload of `StereoWidenerPlugin`. 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, RPC validation, or storybook controls) without importing the audio implementation.
## Features
- `StereoWidenerPluginOptionsSchema` — validates `initialWidth ∈ [0, 2]`.
- `StereoWidenerPresetDataSchema` — empty strict object; the widener has no plugin-specific preset payload, so any extra keys are rejected.
- `z.number().finite()` blocks `NaN` and `Infinity` from sneaking through.
## Usage: Basic Example
```ts
import { StereoWidenerPluginOptionsSchema } from "@suaveplan/audio-fx-plugin-widener";
const parsed = StereoWidenerPluginOptionsSchema.parse({ initialWidth: 1.2 });
```
## Usage: Advanced Example
```ts
import {
StereoWidenerPluginOptionsSchema,
StereoWidenerPresetDataSchema,
} from "@suaveplan/audio-fx-plugin-widener";
// Driver for an editor form: pull the keys from the schema.
const fields = StereoWidenerPluginOptionsSchema.keyof().options;
for (const field of fields) {
form.addRow(field);
}
// Strict preset validation at the IPC boundary.
function isWidenerPreset(blob: unknown): boolean {
return StereoWidenerPresetDataSchema.safeParse(blob).success;
}
```
## API Reference
- `StereoWidenerPluginOptionsSchema: z.ZodObject<{ initialWidth? }>`
- `StereoWidenerPresetDataSchema: z.ZodObject<{}, "strict">`
## Implementation Notes
The maximum width of `2` is documented in `widener.md` — the cap is a perceptual choice rather than a numerical one. The M/S transform stays numerically stable up to any finite width; the cap at `2` reflects the practical ceiling at which a typical pop mix starts to lose mono fold-down compatibility. Raising the cap is a one-line change in this file, but should be made together with a perceptual review.
`StereoWidenerPresetDataSchema` is `.strict()` so unknown fields are rejected, matching the sibling-plugin policy. The widener has no plugin-specific preset payload today: every parameter (just `width`) travels through the universal parameter snapshot that `AbstractAudioFxPlugin` builds, and the empty `presetData` field marks that the plugin opts out of plugin-specific state.

View File

@@ -0,0 +1,47 @@
# `widener.types`
## Purpose
TypeScript-only type definitions for `StereoWidenerPlugin`'s construction options. Co-located with the implementation so the public type surface stays aligned with the Zod validator in `schemas/schemas.ts`. Consumers can import the type for typed form state, dependency-injection wiring, or storybook controls without pulling in the plugin runtime.
## Features
- `StereoWidenerPluginOptions` — one optional field so the default constructor (`new StereoWidenerPlugin()`) is valid.
- `readonly` property emphasises that the options object is consumed once at construction.
- Numeric range (0..2) lives in the Zod schema rather than in TypeScript types.
## Usage: Basic Example
```ts
import type { StereoWidenerPluginOptions } from "@suaveplan/audio-fx-plugin-widener";
const opts: StereoWidenerPluginOptions = { initialWidth: 1.3 };
```
## Usage: Advanced Example
```ts
import type { StereoWidenerPluginOptions } from "@suaveplan/audio-fx-plugin-widener";
const presets: Record<string, StereoWidenerPluginOptions> = {
mono: { initialWidth: 0 },
natural: { initialWidth: 1 },
wider: { initialWidth: 1.4 },
enormous: { initialWidth: 1.8 },
};
function load(name: keyof typeof presets): StereoWidenerPluginOptions {
return presets[name]!;
}
```
## API Reference
- `StereoWidenerPluginOptions`
- `readonly initialWidth?: number` — initial side-component scale factor.
## Implementation Notes
The interface deliberately exposes only `initialWidth` and omits any per-channel routing or gain compensation knob. The M/S transform is symmetric and lossless at `width = 1`, so no balance-compensation parameter is needed; gain compensation (if the user wants to keep perceived loudness flat across width sweeps) is the consumer's responsibility, typically via a downstream gain plugin. Keeping the options surface minimal makes the plugin easy to reason about in automation graphs.
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 the relevant value into the parameter list once and forgets the input.

View File

@@ -0,0 +1,61 @@
# `widener`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`StereoWidenerPlugin` is the universal mid/side stereo-width control. It decomposes a stereo input into mid (`(L + R) / 2`) and side (`(L - R) / 2`) components, scales the side by `width`, and recomposes the output. `width === 1` is exactly bit-transparent — `mid + side = L` and `mid - side = R` collapse to the original samples in IEEE arithmetic. `width === 0` collapses the stereo image to mono (both channels equal `mid`). `width > 1` exaggerates the stereo image, useful for over-narrow recordings.
The plugin is stateless on the processing path, so it adds no latency, has no `reset()` work, and runs identically across browser, server, offline, and worklet renderers.
## Features
- One parameter: `width` (ratio, 0..2).
- Pure M/S transform: `outL = mid + side·width`, `outR = mid − side·width`. No filtering, no allocation, no per-channel state.
- `width === 1` early-out: copies L/R bit-exact via `Float32Array.set`, avoiding even the M/S decomposition arithmetic when fully transparent.
- Mono input passes through unchanged because there is no side component to manipulate.
- Surround channels (third channel and beyond) pass through unchanged so the plugin can sit in a multichannel chain without disturbing the rear-channel routing.
## Usage: Basic Example
```ts
import { StereoWidenerPlugin } from "@suaveplan/audio-fx-plugin-widener";
const widener = new StereoWidenerPlugin({ initialWidth: 1.4 });
chain.add(widener);
```
## Usage: Advanced Example
```ts
import { StereoWidenerPlugin } from "@suaveplan/audio-fx-plugin-widener";
// Automate width from mono to wide over a build-up — a classic
// "stereo reveal" effect for the first chorus of a track.
const reveal = new StereoWidenerPlugin({ initialWidth: 0 });
await reveal.setup(
{ sampleRate: 48_000, channels: 2, blockSize: 512 },
ctx
);
reveal.setParameterAt("width", 0, 0);
reveal.setParameterAt("width", 1.6, 4);
```
## API Reference
- `StereoWidenerPlugin` (class extends `AbstractAudioFxPlugin`)
- `new StereoWidenerPlugin(options?: StereoWidenerPluginOptions)` — throws `AUDIO_FX.INVALID_OPTIONS` on malformed options.
- Inherited parameter accessor for `width`.
## Implementation Notes
The M/S transform is mathematically lossless: starting from `(L, R)`, computing `mid = (L+R)/2`, `side = (L-R)/2`, then recomposing as `(mid+side, mid−side)` returns `(L, R)` exactly in IEEE-754. We exploit this by early-returning at `width === 1` with a `Float32Array.set` copy. The early-out is logical, not just for performance — without it, the recomposition path would still introduce one bit of rounding error on subnormal inputs, which an audit would catch as a violation of the "width=1 is bit-transparent" contract.
Mono input (`channels === 1`) and surround channels (`channels > 2`) pass through unchanged because the M/S transform only defines L/R semantics for the first two channels. Surround applications typically run a separate widener on the front pair and leave the rear pair untouched, which is exactly what this implementation does without requiring an explicit channel-routing parameter.
The maximum width of `2` is conservative — some "extra-wide" plugins go up to 4 or 8. The cap is a phase-coherence guard: at `width = 2`, a mono signal stays mono (no side to amplify), but a typical pop track with `side / mid ≈ 0.3` produces `outL − outR ≈ 1.2 · (L − R)`, already past the point where the mono fold-down starts to lose punch. Going higher would not break the plugin, but it would surprise users; if a future surface needs `width > 2`, raise the cap in both the Zod schema and the parameter declaration in one commit.
There is no plugin-specific preset payload — `width` is a universal parameter and travels through the parameter snapshot built by `AbstractAudioFxPlugin`. `reset()` is a no-op by design and the test exercises it to lock in that contract.