first commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user