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-mixer - mixer plugin for the @suaveplan/audio-fx-plugin-* family.
mixer 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
- [mixer](./src/mixer/mixer.md) — `MixerPlugin` is the universal multi-channel summing mixer. Each input channel is one channel strip with its own gain,…
- [mixer](./src/mixer/mixer.md) — `MixerPlugin` is the universal multi-channel summing mixer. Each input channel is one channel strip with its own gain,…
- [schemas](./src/schemas/schemas.md) — Zod schemas validating the construction-time options, channel strips, and preset payload of `MixerPlugin`. These run on…
- [schemas](./src/schemas/schemas.md) — Zod schemas validating the construction-time options, channel strips, and preset payload of `MixerPlugin`. These run on…
<!-- end:auto-docs -->
## Composition
```ts
import { PluginChain, DefaultBusRegistry } from "@suaveplan/audio-fx-plugin-base/chain";
import { /* Plugin */ } from "@suaveplan/audio-fx-plugin-mixer";
const plugin = new /* Plugin */();
const chain = new PluginChain({
buses: new DefaultBusRegistry(),
plugins: [plugin],
});
```
## Status
Replaces the `./mixer` subpath of the bundled `@suaveplan/audio-plugins-fx`.

View File

@@ -0,0 +1,85 @@
# `mixer.types`
## Purpose
TypeScript-only type definitions for `MixerPlugin`'s channel strips, construction options, preset payload, and VU metering snapshot. Co-located with the implementation so the public type surface stays aligned with the Zod validator in `schemas/schemas.ts`.
## Features
- `MixerChannel` — a live channel strip: id, gain (0..4), pan (-1..1), sends record.
- `MixerChannelOptions` — construction-time half-twin of `MixerChannel` with optional initial values.
- `MixerPluginOptions` — top-level construction options with `channels: ReadonlyArray<MixerChannelOptions>` and optional `initialMasterGain`.
- `MixerPresetData` — preset payload carrying the full live channel strips.
- `MixerVuSnapshot` — `{ channels: Record<id, rms>; master: rms }` per-block metering view.
- All record types are `Readonly` so the consumer cannot mutate the snapshot returned by `getVuLevels()` or `getChannels()`.
## Usage: Basic Example
```ts
import type {
MixerPluginOptions,
} from "@suaveplan/audio-fx-plugin-mixer";
const opts: MixerPluginOptions = {
channels: [
{ id: "drums", initialGain: 0.8 },
{ id: "vocal", initialGain: 0.9, initialPan: 0.1 },
],
};
```
## Usage: Advanced Example
```ts
import type {
MixerChannel,
MixerPluginOptions,
MixerVuSnapshot,
} from "@suaveplan/audio-fx-plugin-mixer";
function loudest(snapshot: MixerVuSnapshot): string | null {
let best: { id: string; rms: number } | null = null;
for (const [id, rms] of Object.entries(snapshot.channels)) {
if (best === null || rms > best.rms) best = { id, rms };
}
return best?.id ?? null;
}
function asPreset(channels: ReadonlyArray<MixerChannel>): MixerPluginOptions {
return {
channels: channels.map((c) => ({
id: c.id,
initialGain: c.gain,
initialPan: c.pan,
initialSends: c.sends,
})),
};
}
```
## API Reference
- `MixerChannel`
- `readonly id: string`
- `readonly gain: number` (0..4)
- `readonly pan: number` (-1..1)
- `readonly sends: Readonly<Record<string, number>>`
- `MixerChannelOptions`
- `readonly id: string`
- `readonly initialGain?: number`
- `readonly initialPan?: number`
- `readonly initialSends?: Readonly<Record<string, number>>`
- `MixerPluginOptions`
- `readonly channels: ReadonlyArray<MixerChannelOptions>`
- `readonly initialMasterGain?: number`
- `MixerPresetData`
- `readonly channels: ReadonlyArray<MixerChannel>`
- `MixerVuSnapshot`
- `readonly channels: Readonly<Record<string, number>>`
- `readonly master: number`
## Implementation Notes
`MixerChannelOptions` mirrors `MixerChannel` with `initial*` prefixes on the mutable fields. The pattern matches the rest of this package — every plugin uses `initialX` for construction-time hydration of a runtime-mutable value. Keeping the two types distinct lets the constructor's Zod schema validate the boundary input without forcing callers to specify defaults for every field.
`MixerVuSnapshot.channels` is a `Record<string, number>` rather than an array because consumers typically look up by id. Returning an array would force a lookup-by-position contract that breaks as soon as the user reorders channels. The downside is that `Record` enumeration order is not guaranteed across runtimes; UI code that wants stable ordering should iterate over `mixer.getChannels()` and index into the snapshot map by id.

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.

View File

@@ -0,0 +1,65 @@
# `mixer/schemas`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Zod schemas validating the construction-time options, channel strips, and preset payload of `MixerPlugin`. 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
- `MixerSendsSchema` — `z.record(string.min(1), number.finite().min(0).max(2))`. Send amounts are bounded above at `2` to allow modest gain at the bus, but not unbounded amplification.
- `MixerChannelSchema` — full live strip schema: id, gain (0..4), pan (-1..1), sends.
- `MixerChannelOptionsSchema` — boundary schema with `initialX` half-twins of the live fields.
- `MixerPluginOptionsSchema` — `channels` array constrained to 1..64 plus optional `initialMasterGain`.
- `MixerPresetDataSchema` — `.strict()` object carrying the full live channel strips.
## Usage: Basic Example
```ts
import { MixerPluginOptionsSchema } from "@suaveplan/audio-fx-plugin-mixer";
const parsed = MixerPluginOptionsSchema.parse({
channels: [{ id: "drums" }, { id: "vocal" }],
});
```
## Usage: Advanced Example
```ts
import {
MixerChannelSchema,
MixerPresetDataSchema,
MixerSendsSchema,
} from "@suaveplan/audio-fx-plugin-mixer";
// Validate an external send-map blob before forwarding to the plugin.
function isSendMap(blob: unknown): boolean {
return MixerSendsSchema.safeParse(blob).success;
}
// Strict preset validation at the IPC boundary.
function isMixerPreset(blob: unknown): boolean {
return MixerPresetDataSchema.safeParse(blob).success;
}
```
## API Reference
- `MixerSendsSchema: z.ZodRecord<string, number>`
- `MixerChannelSchema: z.ZodObject<{ id, gain, pan, sends }>`
- `MixerChannelOptionsSchema: z.ZodObject<{ id, initialGain?, initialPan?, initialSends? }>`
- `MixerPluginOptionsSchema: z.ZodObject<{ channels, initialMasterGain? }>`
- `MixerPresetDataSchema: z.ZodObject<{ channels }, "strict">`
## Implementation Notes
The channel-count cap of 64 reflects two practical constraints: a typical mixing console UI struggles to render more than 32 strips legibly, and a per-channel loop over 64 entries per sample is already at the upper end of what a real-time block-processing budget can absorb on a typical CPU. Going past 64 strips is usually a sign that the caller should be using a submix architecture instead.
`MixerPresetDataSchema` uses `.strict()` so unknown fields are rejected — matching the sibling-plugin policy. The schema validates the full live `MixerChannel` shape (not just the construction-time half) because presets restore the runtime state directly, bypassing the `initialX → gain` mapping the constructor does.
The send-amount upper bound of `2` lets a bus send slightly boost the signal (useful for parallel-compression busses where the dry sum is too quiet) but blocks pathological values that would silently drown the master. The lower bound of `0` allows a "mute send" without removing the entry from the sends map, so a UI animating a fader to zero can resume from the same state.