# `capabilities` ## Purpose `PluginCapabilities` is the **explicit feature-flag declaration** every `AudioFxPlugin` carries. Engines route audio through one of three render paths (browser-native `AudioNode`, `AudioWorklet` fallback, universal `BlockProcessor`); each requires different optional members on the plugin. Rather than feature-detecting via `typeof plugin.createAudioNode === 'function'` and other opaque probes scattered across the engine adapters, plugins declare what they support up front, and the chain + engine read the typed flags directly. The two `defaultCapabilities(io)` and `capabilitiesAgreeWithMembers(cap, members)` helpers enforce honesty: plugin construction can call them to ensure the flag set matches reality. ## Features - A `PluginCapabilities` interface covering eight static flags: `hasAudioNode`, `hasWorklet`, `hasSidechain`, `sampleAccurate`, `stateful`, `asyncSetup`, `preservesChannelCount`, `latencySamples` (initial). - `defaultCapabilities(io)` returns a stateless / sample-accurate / no-async / universal-only baseline with `preservesChannelCount` derived from the declared `PluginIo`. - `capabilitiesAgreeWithMembers(cap, members)` confirms flag/method symmetry — the basis for an automated lint-style assertion in tests. - `withCapabilities(base, overrides)` produces a merged copy (no mutation) so plugin authors can express only their deltas. ## Usage: Basic Example ```ts import { defaultCapabilities, withCapabilities } from "@suaveplan/audio-fx-plugin-base/base"; const io = { inputs: [{ count: 2 }], outputs: [{ count: "preserve" as const }] }; const capabilities = withCapabilities(defaultCapabilities(io), { stateful: true, // delay lines persist across blocks latencySamples: 64, // lookahead limiter }); ``` ## Usage: Advanced Example ```ts import { capabilitiesAgreeWithMembers, defaultCapabilities, withCapabilities, } from "@suaveplan/audio-fx-plugin-base/base"; class PitchShifterPlugin { readonly io = { inputs: [{ count: 2 }], outputs: [{ count: "preserve" as const }] }; readonly capabilities = withCapabilities(defaultCapabilities(this.io), { hasWorklet: true, stateful: true, asyncSetup: true, latencySamples: 2048, // one FFT frame }); getWorkletProcessor() { return PITCH_SHIFTER_WORKLET_SOURCE; } // Constructor asserts in dev to catch drift between flags and members: constructor() { if (!capabilitiesAgreeWithMembers(this.capabilities, this)) { throw new Error("capability flags do not agree with method presence"); } } } ``` ## API Reference - `PluginCapabilities` — readonly record of eight flags. All members required at plugin construction. - `OptionalPluginMembers` — the three optional plugin members the flags cross-reference (`createAudioNode`, `getWorkletProcessor`, `setSidechain`). - `defaultCapabilities(io: PluginIo): PluginCapabilities` — returns the universal-only baseline; `preservesChannelCount` derived from `ioPreservesChannelCount(io)`. - `capabilitiesAgreeWithMembers(cap, members): boolean` — returns true iff every `has*` flag matches `typeof members. === 'function'`. - `withCapabilities(base, overrides): PluginCapabilities` — returns `{ ...base, ...overrides }`; never mutates `base`. ## Implementation Notes `latencySamples` on `PluginCapabilities` is the **initial** value — plugins whose latency changes at runtime (lookahead limiters with variable lookahead, dynamic FFT-size pitch shifters) emit `onLatencyChanged(samples)` and the chain re-reads `getLatencySamples()`. The static flag is the value an engine should assume at first `setup()` call; dynamic recomputation happens through the event surface. `capabilitiesAgreeWithMembers` is intentionally cheap (three `typeof` checks). The `AbstractAudioFxPlugin` reference class calls it in its constructor under a debug-only branch (no perf cost in production), and test suites call it once per concrete plugin to gate releases. A mismatch is a bug in the plugin author's declaration, not a runtime condition; failing fast in test is the right call. `defaultCapabilities` ties `preservesChannelCount` to `ioPreservesChannelCount(io)` so plugin authors who change their IO (e.g. add a downmix output) automatically get the right capability without restating it; the test suite catches the symmetry by running both helpers against every exported plugin.