# `bus-registry` ## Purpose The `BusRegistry` is the **project-level sidechain routing surface** that every chain hosted by an engine adapter consumes. One instance is constructed by the engine at init time and passed to every chain via `ChainContext.buses`; tracks publish their post-FX output to named buses (`registry.publish('kick', 2)`) and plugins on other tracks consume them (`ctx.buses.get('kick')?.read(scratch)`). The registry is universal: the `DefaultBusRegistry` reference implementation works headless (server renders, CLIs, tests) and in the browser; engines that need bespoke bookkeeping may swap their own implementation as long as it honours the same `BusRegistry` interface. The split between `BusRegistryReadHandle` (exposed to plugins) and the full `BusRegistry` (engine-only) is intentional: plugins can only read and watch; only the engine — which owns chain ordering — can mutate the topology. ## Features - Per-bus channel-aware `Float32Array` accumulators sized to whatever the publisher writes; consumers read into their own scratch buffer with `read(scratch)`. - Reads silence before any write and silence-pads when the scratch buffer is larger than the most recent frame, so consumers always see deterministic samples. - Hard channel-count enforcement: a bus published with 2 channels rejects read / write callers that hand it the wrong array length with `AUDIO_FX.BUS_CHANNEL_MISMATCH`. - `close()` is idempotent and reads on a closed bus handle return silence (no throw) so consumers never crash when a publisher disappears mid-render. - Republishing the same name treats the prior publisher as closed and emits a `subscribe` event with `undefined` followed by the new bus. - `subscribe(name, listener)` fires on publish + close so plugins that resolve buses lazily after `setup()` learn when they appear or disappear. ## Usage: Basic Example ```ts import { DefaultBusRegistry } from "@suaveplan/audio-fx-plugin-base/chain"; const registry = new DefaultBusRegistry(); const kick = registry.publish("kick", 2); kick.write([leftChannelBuf, rightChannelBuf]); const sidechain = registry.get("kick"); if (sidechain !== undefined) { const scratch = [new Float32Array(blockSize), new Float32Array(blockSize)]; sidechain.read(scratch); } ``` ## Usage: Advanced Example ```ts import { DefaultBusRegistry } from "@suaveplan/audio-fx-plugin-base/chain"; // One registry per engine; share across every chain it hosts. const registry = new DefaultBusRegistry(); // Track A publishes its post-FX output to "kick". const kickPub = registry.publish("kick", outputChannels); // Plugin on track B's chain subscribes so it learns when the bus appears. let kickBus: SidechainBus | undefined; const unsubscribe = registry.subscribe("kick", (bus) => { kickBus = bus; }); // In track B's compressor process(): const scratch = chainContext.chain.channels === 2 ? [new Float32Array(blockSize), new Float32Array(blockSize)] : [new Float32Array(blockSize)]; kickBus?.read(scratch); const ductReduction = computeDuct(scratch); // On chain dispose: unsubscribe(); ``` ## API Reference - `BusRegistry` (interface) — extends `BusRegistryReadHandle` (`get`, `list`) with `publish(name, channels)` and `subscribe(name, listener)`. - `SidechainPublisher` (interface) — `{ bus: SidechainBus, write(samples), close() }`. Returned by `publish`; the engine retains it to close the bus on track teardown. - `DefaultBusRegistry` (class) — reference implementation. No constructor arguments; one instance per engine. - `BusRegistryReadHandle` (in `base/`) — `{ get(name), list() }` — the surface plugins see via `ChainContext.buses`. ## Implementation Notes The registry does not synchronise reads against writes. Engines guarantee single-threaded chain processing per block and topologically order chains so publishers always write before subscribers read; the registry merely holds the per-channel accumulators. If a future engine model adds cross-thread chains, it must supply its own thread-safe implementation. `subscribe` deliberately fires on `undefined` when a publisher closes so consumers can release their local bus reference; the alternative (never firing on close) would leak the reference. Consumers that don't care about lifecycle can pass an arrow that only cares about the truthy case. `close()` on a publisher removes its entry from the registry and emits an `undefined` notification, but the local bus reference remains callable — `bus.read()` returns silence rather than throwing. This is deliberate: a consumer that already captured the bus reference and is mid-render when the publisher closes should not crash; reading silence and letting the consumer recover on its next `subscribe` cycle is the right shape. The accumulator reallocates when the publisher writes a different-size frame than the previous write, so the registry adapts to variable block sizes without per-block allocation when sizes are stable. In steady-state operation with a fixed block size, write allocates zero garbage per block.