Files
skills/docs/media/audio/audio-engine-browser/src/adapter/bus-orchestrator.md
2026-05-29 08:20:56 +01:00

4.0 KiB

bus-orchestrator

Purpose

BusOrchestrator is the per-adapter owner of the project-level sidechain bus graph. It wraps a single DefaultBusRegistry instance (or any caller-supplied BusRegistry), tracks every (publisher track, bus) and (subscriber track, bus) declaration, and refuses any new declaration that would close a directed cycle in the publish-then-subscribe DAG. It also exposes a topological ordering of tracks so the adapter can process publishers before subscribers in the same render block — the property the spec calls out under "publishers process before subscribers".

Cycle detection happens at declaration time, not at first render. That keeps the failure visible at the call site that introduced the cycle and avoids a half-built render pipeline aborting mid-block. Every faulted declaration raises a BrowserAudioError(BUS_CYCLE_DETECTED).

Features

  • Wraps an injectable BusRegistry — defaults to DefaultBusRegistry from @suaveplan/audio-fx-plugin-base.
  • declarePublish(trackId, bus, channels) registers a publisher and caches the SidechainPublisher so dispose() can close it.
  • declareSubscribe(trackId, bus) records a subscriber edge.
  • Edges build a directed graph publisher → subscriber keyed on shared bus name; a cycle in this graph is rejected immediately.
  • processOrder(allTracks) returns a topologically-sorted track id list — tracks with no bus edges keep their insertion order (Kahn's algorithm).
  • dispose() closes every active publisher.

Usage: Basic Example

import { BusOrchestrator } from "@suaveplan/audio-engine-browser/adapter/bus-orchestrator";

const orch = new BusOrchestrator();
orch.declarePublish("kick", "kick-bus", 2);
orch.declareSubscribe("bass", "kick-bus");
const order = orch.processOrder(["kick", "bass"]);
// → ["kick", "bass"]

Usage: Advanced Example

import { BusOrchestrator } from "@suaveplan/audio-engine-browser/adapter/bus-orchestrator";

const orch = new BusOrchestrator();

orch.declarePublish("a", "x", 2);
orch.declareSubscribe("b", "x");

try {
    orch.declarePublish("b", "y", 2);
    orch.declareSubscribe("a", "y"); // closes a cycle
} catch (e) {
    if (e instanceof BrowserAudioError) {
        // e.code === "AUDIO_ENGINE_BROWSER.BUS_CYCLE_DETECTED"
    }
}

orch.dispose(); // closes every publisher

API Reference

new BusOrchestrator(registry?)

Constructs the orchestrator. The default registry is a fresh DefaultBusRegistry. Tests can inject any conforming BusRegistry.

declarePublish(trackId, bus, channels) → void

Opens a publisher in the registry, caches the handle, and stores the edge. Throws BUS_CYCLE_DETECTED when the new edge would close a cycle.

declareSubscribe(trackId, bus) → void

Records a subscriber edge and runs the same cycle check.

processOrder(allTracks) → ReadonlyArray<string>

Returns the topological order of allTracks honouring every recorded publisher→subscriber edge.

bus → BusRegistry

Read accessor for the underlying registry — handed to every PluginChain as ChainContext.buses.

dispose() → void

Closes every active publisher and clears internal state.

Implementation Notes

  • The DFS cycle check uses three colours (white / grey / black) so the declaration-time check is O(V + E). The processOrder step uses Kahn's algorithm and detects any residual cycle by checking that every node was emitted.
  • Edges are (publisher_track, subscriber_track) filtered on the shared bus name. Self-loops (a track publishing and subscribing to the same bus on itself) are intentionally excluded from the cycle graph because a single chain processes left-to-right within one track — self-feedback is the plugin's own responsibility to handle.
  • The cached processOrder is invalidated on every declaration so the adapter sees fresh orderings as tracks are added.
  • dispose() is best-effort about publisher closes — a misbehaving registry implementation does not prevent the rest from being released.