99 lines
3.6 KiB
Markdown
99 lines
3.6 KiB
Markdown
# PluginChain + BusRegistry + SidechainPublisher
|
|
|
|
<!-- begin:auto-docs -->
|
|
_(no submodules)_
|
|
<!-- end:auto-docs -->
|
|
|
|
## 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.
|