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

4.4 KiB

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

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

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.<m> === '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.