# 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 ```ts 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 ```ts 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.