first commit
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
# `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.
|
||||
Reference in New Issue
Block a user