84 lines
4.9 KiB
Markdown
84 lines
4.9 KiB
Markdown
# `plugin-io`
|
|
|
|
## Purpose
|
|
|
|
The `plugin-io` module declares the **channel-topology contract** every `AudioFxPlugin` carries (`PluginIo`, `ChannelSpec`, `ChannelLayout`) and the helpers (`validateChainIo`, `resolveOutputChannels`, `isSourcePlugin`, `ioPreservesChannelCount`) the chain primitive uses to admit or reject plugin insertions at the boundary, before any audio flows. Declaring topology in data — not via per-plugin assertions in `process()` — lets the chain prove compatibility once at insert time and surface a precise, human-readable error in the UI on mismatch.
|
|
|
|
## Features
|
|
|
|
- Three `ChannelSpec` shapes for plugin authors: explicit integer (`{ count: 2 }`), inherited (`{ count: 'preserve' }`), opaque (`{ count: 'any' }`), plus the special source declaration (`{ count: 0 }`) reserved for slot-0 source plugins (synth/sampler/audio-player/music-layer-mixer).
|
|
- Layout hints (`ChannelLayout`: mono/stereo/mid-side/5.1/7.1/ambisonic-1/arbitrary) for plugins that need to express semantics, not just channel count.
|
|
- `validateChainIo(predecessorOutputChannels, candidateInput)` — returns a discriminated `IoCompatibility` result (`ok: true` with resolved channel count, or `ok: false` with a `reason` string suitable for `throw`-with-context or UI toast).
|
|
- `resolveOutputChannels(spec, inputChannels)` — used after admission to compute the effective channel count flowing out of a slot.
|
|
- `isSourcePlugin(io)` — true iff `inputs[0].count === 0`; chain uses this to enforce the at-most-one-source-at-slot-0 rule.
|
|
- `ioPreservesChannelCount(io)` — feeds `PluginCapabilities.preservesChannelCount` so engines can choose render paths.
|
|
|
|
## Usage: Basic Example
|
|
|
|
```ts
|
|
import { validateChainIo } from "@suaveplan/audio-fx-plugin-base/base";
|
|
|
|
const next = { count: 2 } as const; // stereo plugin
|
|
const compat = validateChainIo(predecessorOutputChannels, next);
|
|
if (!compat.ok) {
|
|
throw new AudioFxError(
|
|
AUDIO_FX_CODES.CHAIN_IO_INCOMPATIBLE,
|
|
"audio-fx.chain-io-incompatible",
|
|
{ context: { reason: compat.reason } }
|
|
);
|
|
}
|
|
const effectiveOut = compat.resolvedChannels; // 2
|
|
```
|
|
|
|
## Usage: Advanced Example
|
|
|
|
```ts
|
|
import {
|
|
ioPreservesChannelCount,
|
|
isSourcePlugin,
|
|
resolveOutputChannels,
|
|
} from "@suaveplan/audio-fx-plugin-base/base";
|
|
|
|
function declareCapabilities(io: PluginIo, latencySamples = 0): PluginCapabilities {
|
|
return {
|
|
hasAudioNode: false,
|
|
hasWorklet: false,
|
|
hasSidechain: false,
|
|
sampleAccurate: true,
|
|
stateful: false,
|
|
asyncSetup: false,
|
|
preservesChannelCount: ioPreservesChannelCount(io),
|
|
latencySamples,
|
|
};
|
|
}
|
|
|
|
// Walking a chain to compute per-slot effective output channel count:
|
|
let channels = chainInputChannels;
|
|
for (const slot of chain) {
|
|
if (isSourcePlugin(slot.plugin.io)) {
|
|
channels = resolveOutputChannels(slot.plugin.io.outputs[0]!, 0);
|
|
continue;
|
|
}
|
|
channels = resolveOutputChannels(slot.plugin.io.outputs[0]!, channels);
|
|
}
|
|
```
|
|
|
|
## API Reference
|
|
|
|
- `ChannelLayout` — string union of supported layouts (mono / stereo / mid-side / 5.1 / 7.1 / ambisonic-1 / arbitrary).
|
|
- `ChannelSpec` — `{ count: number | 'preserve' | 'any'; layout?: ChannelLayout }`.
|
|
- `PluginIo` — `{ inputs: ReadonlyArray<ChannelSpec>; outputs: ReadonlyArray<ChannelSpec> }`.
|
|
- `IoCompatibility` — discriminated union: `{ ok: true; resolvedChannels: number } | { ok: false; reason: string }`.
|
|
- `validateChainIo(predecessorChannels: number, candidateInput: ChannelSpec): IoCompatibility` — never throws.
|
|
- `resolveOutputChannels(spec: ChannelSpec, inputChannels: number): number` — returns `inputChannels` for `'preserve'`/`'any'`, otherwise `spec.count`.
|
|
- `isSourcePlugin(io: PluginIo): boolean` — true iff `inputs[0].count === 0`.
|
|
- `ioPreservesChannelCount(io: PluginIo): boolean` — true iff the IO claims same channel count on input and output.
|
|
|
|
## Implementation Notes
|
|
|
|
`validateChainIo` returns rather than throws because chain composition needs to choose between fail-closed insertion (`throw AUDIO_FX.CHAIN_IO_INCOMPATIBLE`) and informational dry-runs (e.g. the plugin picker UI greys out plugins whose IO would not fit the current chain). The chain itself owns the `throw`; this module owns the compatibility decision.
|
|
|
|
The `'preserve'` and `'any'` shapes look identical in `resolveOutputChannels` and `validateChainIo`, but they differ in intent for tooling. `'preserve'` declares "I will not change the channel count" (e.g. EQ, gain); `'any'` declares "I accept whatever you give me" (e.g. spectrum analyser). Both helpers treat them identically for the purposes of admission; the IDE-facing tooling may surface them differently.
|
|
|
|
A source plugin sitting at slot 0 with `inputs[0].count === 0` is the only legal placement: insertion elsewhere throws `AUDIO_FX.CHAIN_SOURCE_POSITION_INVALID` (raised by the chain, not here). Likewise, two source plugins in one chain is rejected by the chain — this module's `isSourcePlugin` only answers the per-instance question.
|