first commit
This commit is contained in:
31
docs/media/audio-fx/audio-fx-plugin-mixer/README.md
Normal file
31
docs/media/audio-fx/audio-fx-plugin-mixer/README.md
Normal 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`.
|
||||
85
docs/media/audio-fx/audio-fx-plugin-mixer/src/mixer.types.md
Normal file
85
docs/media/audio-fx/audio-fx-plugin-mixer/src/mixer.types.md
Normal 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.
|
||||
84
docs/media/audio-fx/audio-fx-plugin-mixer/src/mixer/mixer.md
Normal file
84
docs/media/audio-fx/audio-fx-plugin-mixer/src/mixer/mixer.md
Normal 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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user