Files
skills/docs/media/audio-fx/audio-fx-plugin-base/src/base/plugin-io.md
2026-05-29 08:20:56 +01:00

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 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

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 — 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.