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,83 @@
# `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.