Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,84 @@
# `mixer`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`MixerPlugin` is the universal multi-channel summing mixer. Each input channel is one channel strip with its own gain, pan, and named sends; the panned outputs are summed into a stereo master bus. The output sum is internally unclipped — a caller wanting hard-cap behaviour chains a `LimiterPlugin` downstream. Per-channel and master VU levels are exposed via `getVuLevels()` so a UI can drive meters from a single source of truth.
The plugin extends `AbstractAudioFxPlugin` and runs identically across browser, server, offline, and worklet renderers. Channels live as plain configuration on the plugin (not as parameters) and survive preset round-trip via `MixerPresetData`.
## Features
- `MixerChannel[]` configured at construction with per-channel `id`, `initialGain`, `initialPan`, and `initialSends`. Up to 64 channels per instance.
- One automatable parameter: `masterGain` (0..4, default 1).
- `setChannelGain(id, gain)`, `setChannelPan(id, pan)`, `setSend(channelId, busId, amount)` — mutate channel strips at runtime. Unknown ids throw `AUDIO_FX.CHAIN_SLOT_NOT_FOUND`.
- Equal-power per-channel panning: `angle = (pan + 1) · π / 4`, `leftGain = cos(angle) · gain`, `rightGain = sin(angle) · gain`. Centre pan splits equally to `±√½`.
- Sum into a fixed-count stereo output (`io.outputs[0].count === 2`) regardless of input channel count.
- `getVuLevels()` returns per-channel RMS and master RMS over the most recent block.
- Sends are stored on the channel strip and persisted in presets. The universal block-path does not route sends through buses — the browser engine wires them up via `createAudioNode()` in a future revision.
## Usage: Basic Example
```ts
import { MixerPlugin } from "@suaveplan/audio-fx-plugin-mixer";
const mixer = new MixerPlugin({
channels: [
{ id: "drums", initialGain: 0.8, initialPan: 0 },
{ id: "bass", initialGain: 0.7, initialPan: -0.1 },
{ id: "lead", initialGain: 0.6, initialPan: 0.2 },
],
});
chain.add(mixer);
```
## Usage: Advanced Example
```ts
import { MixerPlugin } from "@suaveplan/audio-fx-plugin-mixer";
const mixer = new MixerPlugin({
channels: [
{ id: "vocal" },
{ id: "guitar" },
{ id: "drums" },
],
initialMasterGain: 0.9,
});
await mixer.setup(
{ sampleRate: 48_000, channels: 3, blockSize: 512 },
ctx
);
// Route vocal to a reverb send and a delay send.
mixer.setSend("vocal", "reverb", 0.4);
mixer.setSend("vocal", "delay", 0.2);
// Live VU metering at ~30 fps.
setInterval(() => {
const vu = mixer.getVuLevels();
meter.draw(vu);
}, 33);
```
## API Reference
- `MixerPlugin` (class extends `AbstractAudioFxPlugin`)
- `new MixerPlugin(options: MixerPluginOptions)` — throws `AUDIO_FX.INVALID_OPTIONS` on malformed options or duplicate channel ids.
- `getChannels(): ReadonlyArray<MixerChannel>` — current channel strips.
- `setChannelGain(id, gain)`, `setChannelPan(id, pan)`, `setSend(channelId, busId, amount)` — mutate channel strips.
- `getVuLevels(): MixerVuSnapshot` — per-channel RMS + master RMS for the most recent block.
- Inherited accessor for `masterGain`.
## Implementation Notes
The mixer declares `io.outputs[0].count === 2` (fixed stereo) rather than `"preserve"` because the spec calls for "a master bus" — a single output target — and downstream plugins should be able to assume a stereo bus regardless of how many input channels feed in. `io.inputs[0].count === "any"` accepts whatever the predecessor produces, with channels beyond the configured strip count silently ignored.
Per-channel sends are stored on the strip but **not routed through the `BusRegistry` in this universal implementation**. A future browser-engine build will read the `sends` map in `createAudioNode()` and wire each strip's output through the named auxiliary buses. The spec test only checks "sums channels with applied gain and pan", which the universal path satisfies; full send routing requires the browser-engine integration that lives outside this package.
The mixer's per-block VU update is RMS, not peak. RMS is what mixing engineers expect when they read a "VU" label — peak meters are typically labelled "PPM" in the same UI. Computing RMS adds one accumulator + one `Math.sqrt` per channel and one `Math.sqrt` for the master at the end of the block, which is negligible compared to the per-sample multiply-accumulate work. If a future surface needs peak metering, it can subscribe to the per-block telemetry stream and reduce locally.
The internal sum is unclipped on purpose. The spec's "sum its channels into the output block without clipping internally" requirement is verified by a test that pushes the per-channel gain to `1.5` with two channels active, producing an output of `3.0` — well past `±1` — and asserting that value is what comes out. This keeps the mixer composable: a caller can chain a `LimiterPlugin` downstream for hard-cap behaviour, or leave the clipping policy to the host renderer's master.