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

3.6 KiB

PluginChain + BusRegistry + SidechainPublisher

(no submodules)

Purpose

PluginChain owns the ordered list of FX plugins attached to a single track, the per-chain ambient state, the chain-level bypass flag, and the effective plugin-delay-compensation latency for that track. DefaultBusRegistry is the project-level registry passed to every chain via ChainContext.buses; SidechainPublisher is the handle returned by BusRegistry.publish. Together they implement plugin slot 5 of the audio family's three-layer model — the "FX slot list" that the ServerTimelineAdapter hosts on each track. Every plugin runs through its BlockProcessor process() path; no createAudioNode or worklet path is used on the server.

Features

  • Ordered plugin list with add / remove / setPluginBypass.
  • Chain-level bypass that short-circuits process while preserving the hosted plugins.
  • Ambient state propagation — updateAmbient merges a patch into ChainContext.ambient and fires every plugin's onAmbient hook before the next block runs.
  • Plugin-delay-compensation via getEffectiveLatencySamples(); the chain emits latency-changed whenever the sum of un-bypassed plugin latencies changes so the host engine can recompute per-track PDC.
  • Typed event surface (plugin-added, plugin-removed, bypass-changed, ambient-changed, latency-changed) with on(event, listener) returning an unsubscribe closure.
  • DefaultBusRegistry — publish returns a SidechainPublisher, subscribe notifies listeners on registration and on every publish / close, and get / list expose the read surface plugins consume.

Usage: Basic

import { DefaultBusRegistry, PluginChain } from "@suaveplan/audio-engine-server";

const buses = new DefaultBusRegistry();
const chain = new PluginChain({
    id: "track-1",
    channels: 2,
    sampleRate: 48_000,
    blockSize: 512,
    buses,
});
chain.process([new Float32Array(512), new Float32Array(512)], 0);

Usage: Advanced

const pub = buses.publish("kick", 2);
pub.write([kickLeft, kickRight]);

const offChain = chain.on("latency-changed", ({ effectiveLatencySamples }) => {
    recomputePerTrackPdc(effectiveLatencySamples);
});

chain.updateAmbient({
    track: { pan: -0.5 },
    transport: { state: "playing", time: 1.25 },
});

offChain();
pub.close();

API Reference

  • class PluginChain — add, remove, setPluginBypass, updateAmbient, process, getEffectiveLatencySamples, on, dispose, plus read accessors id, bypassed, context, buses, list.
  • class DefaultBusRegistry implements BusRegistry — publish, subscribe, get, list.
  • interface SidechainPublisher — bus, write, close.
  • interface BusRegistry, interface ChainContext, interface ChainPlugin, interface AmbientState, interface AmbientStatePatch, plus the ChainEvent/ChainEventPayload pair.

Implementation Notes

The chain re-uses the same Float32Array[] block as both input and output for each plugin's BlockProcessor.process call, so each plugin writes into the channel arrays the next plugin reads from. Latency recomputation runs after every mutation (add / remove / setPluginBypass) and only fires latency-changed when the effective sum actually changes — repeated no-op mutations stay quiet. The default bus registry stores per-bus payloads as a Float32Array[] slot mutated by SidechainPublisher.write; close drops the slot and notifies every subscribed listener with undefined so plugins reading the bus on the next block see silence rather than stale samples.