4.9 KiB
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
ChannelSpecshapes 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 discriminatedIoCompatibilityresult (ok: truewith resolved channel count, orok: falsewith areasonstring suitable forthrow-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 iffinputs[0].count === 0; chain uses this to enforce the at-most-one-source-at-slot-0 rule.ioPreservesChannelCount(io)— feedsPluginCapabilities.preservesChannelCountso engines can choose render paths.
Usage: Basic Example
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
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— returnsinputChannelsfor'preserve'/'any', otherwisespec.count.isSourcePlugin(io: PluginIo): boolean— true iffinputs[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.