The underlying structural surface (the session-capture provider reads it).
Open a direct source→output route — /directPath/{source}'s awaited POST door.
plugin.add on the software path — load uri into a channel's post-fader insert at
slot (append or replace), wiring it into the live head → fader → [inserts] → bus
pw-link path so it actually processes. Broadcasts the resulting plugin.chain.
allocation.apply — build the gig's buses from a console allocation.
patchbay.assignGroup — bulk-patch a whole stagebox onto channels in one action, each
port keyed by its own number (in_NN → channel[start + NN − 1]), or the inverse
(clear) / the output side. It
detects the device, plans the number-keyed mapping (planStageboxAssign), and
applies each op through the crosspoint — replacing each channel's source (inputs) or
routing each bus to a box output (outputs). Idempotent (replace / route are).
Returns the per-channel crosspoint frames + a patchbay.groupAssigned summary for the
server to broadcast so every surface converges.
Range-aware (issue #332): the assignment is bounded by the box's DECLARED width and by the 40-slot AUDIO fabric the boxes actually share — the placement comes from planStageboxFabric over every box currently detected, so a second or third box that no longer fits is REFUSED instead of being planned onto channels no REAC frame carries. Every refusal is a StageboxAssignError whose message is a translatable CODE with structured detail; nothing here puts display prose on the wire.
Clearing is inputs-only: a bus output has no unroute primitive, so clear with
direction: 'outputs' is rejected (re-route the bus elsewhere instead).
patchbay.assign — assign a source (a registered id or a raw endpoint) to a channel,
broadcast the resulting assignment so every client converges on the crosspoint.
patchbay.assign for a stereo-capable source — a NEW, additive verb alongside
assignSource (which it does not change): given a RAW endpoint that may carry
2 ports, applies the planStereoSourcePatch mapping (default: split across
channel + channel index+1; asStereoChannel: true: fold onto the one channel) and
broadcasts one patchbay.assignment frame per channel actually touched (one, or two on
the default split), so every client converges on both.
NOT YET wired to the wire protocol / UI (patchbay.assign still only carries
{ channel, source }, single-channel) — that is the follow-up: extend the
patchbay.assign client message with an optional asStereoChannel?: boolean and have
server.ts's applyPatchbay call THIS verb (instead of assignSource) whenever
the picked source endpoint has 2 ports, spreading its returned frames into the
broadcast; the picker UI gets a "keep as one stereo channel" toggle (default off) next
to the source it is about to patch.
the target channel (a registered source is not accepted here — a registered Source carries a single-port patch by contract; only a raw StereoEndpoint can be genuinely 2-port)
the source endpoint to patch
fold a stereo endpoint onto one channel instead of splitting
the assignment frame for every channel touched, in L-then-R order
safety.breakLoop — open every detected ring by cutting ONE edge from each, and report
the crosspoints that changed plus the resulting (normally all-clear) loop frame.
Which edge. Only an operator-made source patch that lies on the ring is eligible.
The rest of a howl-round is console structure — a strip's head → fader → bus path, a
bus master's chain — and cutting those to open a ring would dismantle the desk to fix
its wiring. Among the eligible edges the one recorded by noteLoopClosingPatch
wins: it is the patch that closed this ring, so cutting it restores exactly the routing
that stood a moment before, and it is re-made with the same one tap that made it.
Failing that (a ring restored from a session, or one no single patch closed) the ring's
last eligible patch is cut, deterministically.
Build the patchbay.output frame for a bus — the legacy dialect of what
/patch/output/{kind}/{index} carries, re-read from the same ledger. Public for the same
reason as channelAssignment.
The control ports of a plugin a chain processor hosts, as the served descriptor declares them — the chain parameter row's roster and each parameter's travel. The SAME catalog reading the rack's StructuralDspController.insertControlsOf makes, so a plugin declares one travel whichever door reaches it.
The curated plugin a chain processor hosts, or undefined — for a built-in stage, for an
empty slot, and for a processor id the chain does not carry.
This controller is the ONE store for that fact: it is what chain.processors reports and
what the plugin writers below act on. A row asking anywhere else would be reading a second
ledger of the same thing.
Every processor, on a strip or a bus master, that hosts a curated plugin — what the chain parameter row walks to enumerate the descriptor's ports. Read from the declared chains on every ask, never cached: a chain edit changes this the moment it lands.
Every curated-plugin control the console has COMMANDED, as addresses.
Read straight off the ledger — one pass over what was actually written, never a sweep of every channel × processor × catalog symbol. A 64-channel desk with three plugin edits enumerates three entries, which is what keeps a snapshot burst cheap.
The values commanded on one chain processor's curated plugin, keyed by control-port symbol
OR lv2:Parameter URI — the ledger the chain.processors frame carries as pluginValues.
undefined where the console has commanded nothing, which is not the same as "all zero": a
row reading this reports UNSET rather than a number nobody stated. Read-only by construction
(a fresh object), because this ledger has exactly one writer.
The chain AS READ: the ordered stages, each stage's recorded curated-plugin values, and the resolved index of every named tap preset.
Not a frame: a client reads the chain from its ROWS and resolves the taps itself — but the READING is still the server's own view of what a channel's chain is, and it is what the suites assert against after a mutation. Keeping one method for both jobs is how deleting the frame took 26 test files with it: a builder that is also an inspector cannot be retired, only replaced.
The strip's declared chain, for the send row's tap offer — the same reading chainHostedPluginOf makes, so the two rows read one chain.
The EXPECTED latency of this strip's path at the console's live rate and quantum — the read
/channel/{kind}/{index}/latency serves (2026-09-07-expected-path-latency.md).
The rack is this controller's, so the figure is computed from the ONE store that knows what
is racked; the arithmetic, the detour rule and the per-plugin hosting verdict belong to
@freemixer/catalog and are not restated here.
undefined — no instance — when there is no resolver (no catalog), no rack to walk (an
unmanaged bus, a strip the engine cannot look up), or no observed rate and quantum: exactly
the absences insertRackOf and /clock already state.
The suitability judgement for a channel's CURRENT rack — the read half of the question
insertRackRefusal answers for a PROSPECTIVE one, and the same shape
judgeChannelInserts builds for a write. /channel/{kind}/{index}/inserts/suitability
is its ONE reader: today the verdict only ever left the server as a refusal on the edit that
triggered it, so a rack that was fine at soundcheck and drifted over budget only as the rig
re-clocked (issue #337) had no way to tell the operator without a write happening to fail.
undefined — no instance — when there is no guard (no catalog) or no rack to judge (an
unmanaged bus, a strip the engine cannot look up): the same absences insertRackOf
already states, never a fabricated "fine" for a channel this console cannot see.
Build the dca.state frame for a DCA (master defaults to the read-back value) — read
fresh from the engine, so it is also the echo the verb door broadcasts after a write that
went through the fader/name ENTITY rather than this controller (mirrors muteGroupState's same reason for being public).
Optionalmaster: numberpatchbay.disconnect — the source-centric unpatch: drop every crosspoint entry
fed from sourceId (an offered node's WireIoNode.id), across EVERY channel,
without assigning a replacement. The operator counterpart to unlinkSource
(which needs a known channel + endpoint) — this instead needs only the source, so it
works from the patchbay's live source list directly.
Matches by the ${sourceId}: port-name PREFIX every enumerator emits (import('@freemixer/audio-engine').IoNode.ports are always node:port), so it finds
the source wherever it is patched (the primary slot or a multi-in extra) without a
live graph read. Idempotent: a source patched nowhere yields no frames and an empty
released set.
the source node id to disconnect (its IoNode.id)
one patchbay.links frame per channel actually touched, plus the released
endpoints (for the caller's app-stream hand-back — the same "stays audible off-desk"
treatment every other patchbay verb gives an unpatched app stream)
/safety/loop's state — the rings' channels merged, or null for the all-clear.
Channels are merged across rings rather than reporting only the first, because each one
of them really is in a loop and the wall colours them all suspect.
The routing loops the engine currently sees. Empty for a surface with no topology detector (the capability is optional) and for an acyclic mix.
Drop the values recorded against the plugin a chain processor USED to host — the old symbols do not belong to the new one (mirrors the engine's own settings-cache hygiene). Public because the chain entity is the other door onto the same plugin choice, and one cache with two writers is only honest if both invalidate it.
The plugin.chain frame for a strip's rack — the legacy dialect, read back from the ONE
store after a write that went through the insert ENTITY rather than this controller
(mirrors dcaState's same reason for being public).
Every strip and bus this console holds a rack for — what the four insert rows walk to enumerate their instances. Read from the live racks on every ask, never cached.
An EMPTY rack is in the set: it is the collection an un-rack announces on, and a client that never saw it cannot read that announcement as a change.
The control ports of a plugin, as the served descriptor declares them — the parameter row's
roster for a racked plugin and the source of each parameter's travel. undefined with no
catalog, or for a URI the catalog does not hold: the row then declares nothing.
The roster narrowed to the plugins that FIT this strip — what the rack door publishes as
plugins.values on OPTIONS. The same predicate and the same width reader as
widthRefusal, so the offer and the refusal cannot disagree; derived on every ask,
because a strip's width follows its patch. undefined with no roster: the console
publishes no vocabulary rather than an empty one it did not declare.
The rack a strip carries, or undefined when this console holds no such rack — the read
half of import('./channel-insert-rows.js').InsertEngine.
The KIND selects the rack, exactly as it does for the processing chain: a bus kind reads
the recorded OUTPUT rack (output.inserts), any other kind the live post-fader channel
inserts. An unmanaged bus and a strip the engine refuses to look up are both absent — a
defaulted empty rack would let a client "re-order" a strip that does not exist.
Why a prospective rack would be refused, or undefined when it is affordable — the
SYNCHRONOUS half of the judgement, so an entity can refuse in a code before it writes.
setInsertRack judges too (a legacy verb reaches it without passing here), and the
judgement is pure: asking twice costs two arithmetic passes and changes nothing.
Link two channels into one stereo strip (left = primary): the follower stops drawing its
own contribution and the primary meters L/R.
Announcing is /channel/{kind}/{index}/link's, not this door's — the row broadcasts the
addressed half and declares the partner as a ripple, so both halves reach every client from
one place. This is the door the RESTORE path calls, which never read a frame from it.
patchbay.link — add an input endpoint to a channel's head. GUARDED by the settled
one-physical-input-per-channel rule (see @freemixer/core's InputPatchbay): a mono
channel sources at most ONE physical input, so a second, different physical input is
REJECTED here (sum on a bus, not on a channel) — this is what stops a channel ending up
with "In 7 + In 15", where a phantom toggle would light only the first. A NON-physical
source (an app / internal stream) may still be linked, and re-linking the SAME physical
input is the idempotent no-op the engine already gives. The thrown error is surfaced to
the requesting socket by import('./server.js')'s scoped-error catch — the
crosspoint is left untouched. The landed set reaches every client on
/patch/input/{kind}/{index}, which is the fact's one address.
patchbay.devices — every patchable Device the desk sees, unified: local
interfaces (a card's Sink + Source folded by device.id), REAC stageboxes, and loose
app streams, each with its inPorts/outPorts. The superset of listStageboxes
(which stays as a kind:'stagebox'-filtered view during the migration). A read; empty
when no enumerator is wired.
patchbay.list — enumerate the live PipeWire sources + sinks the patchbay can wire.
A read (the requesting socket gets the I/O picture); empty lists when no enumerator
is wired.
plugin.move on the software path — reorder the channel's live insert chain
(splice-out-then-insert, destination clamped). Each slot's bypass state and its
commanded values ride with the plugin, not with the position. An out-of-range from
(or a no-op move) leaves the chain untouched.
Build the muteGroup.state frame for a mute group — read fresh from the engine, so it is
also the echo the verb door broadcasts after a write that went through the members ENTITY
rather than this controller.
The held panic scope, or null when none is — the same latch snapshot replays to
a joining client. The /safety/panic row's READ half; setPanic is its write door.
dca.remove — delete a DCA, broadcast a removed state so surfaces drop it.
Tear a direct path down — the converging half of the same row.
plugin.remove on the software path — drop the channel insert at slot.
muteGroup.remove — delete a mute group, broadcast a removed state so surfaces drop it.
send.remove — drop a channel's send into a bus, broadcast level 0.
chain.reorder — move a processor; broadcast the reordered chain + new tap indices.
patchbay.output — route a bus / main output to a sink (REPLACE semantics — the
single-pick dropdown path), broadcast the new routing.
Build the route.state frame for a strip — its routing mask. An input strip carries
its MAIN assignment + the mix groups it is assigned into (its unity group sends);
a bus strip carries its main fold-back (groups empty — buses don't nest).
bus.fader — set a bus master (position→linear), broadcast.
bus.mute — mute / un-mute a whole bus, broadcast (level unchanged, mute carried).
bus.pan — set a bus master L/R balance (−1..+1 canonical, 0 = centre), broadcast.
Unlike setBusFader, pan is the canonical balance the surface sends, not a
0..1 fader position, so it passes to the engine untapered. The MAIN bus's balance is
applied natively via the master-stage mirror; other buses record it. The echo carries
the current master level alongside the new pan so no client clobbers the fader.
dca.fader — set a DCA master (position→linear), broadcast.
dca.members — replace a DCA's membership, broadcast master + members.
dca.name — set / clear a DCA's operator label, broadcast the DCA state.
plugin.bypass on the software path — bypass/enable the channel insert at slot.
plugin.param on the software path — set a control on the live insert instance.
plugin.patch on the software path — set an lv2:Parameter on the live insert instance.
plugin.preset on the software path — load a preset into the live insert instance.
Replace a whole rack, in audio order — the ONE membership/order write door, which every
plugin.add / plugin.remove / plugin.move and the /inserts entity go through.
The commanded-value ledgers are realigned onto the new rack before the engine is driven: a member keeps its bypass, its control values, its properties and its preset while the same plugin is still racked, and a position whose plugin changed starts blank — values cached against the old plugin do not belong to the new one. Where the SAME uri is racked twice, position decides, which is the best a URI-addressed door can do.
Set one matrix crosspoint from a surface POSITION, converting to the linear coefficient the
DSP sums with. /channel/matrix/{index}/input/{srcKind}/{srcIndex} is the door that
announces the change; this is the engine-facing half.
muteGroup.set — activate / release a mute group, broadcast active + members.
muteGroup.assign — replace a mute group's membership, broadcast active + members.
muteGroup.name — set / clear a mute group's operator label, broadcast its state.
output.inserts — replace a bus output's insert chain, broadcast the resolved
chain. Each wire slot is a { uri, bypassed? } (a bare URI = enabled), so a
bypass toggle reaches the engine + the recorder and survives its own echo. When
an PluginChainController is wired, the chain is driven through it
(addressed by the bus id) so the echo carries descriptor-resolved slots; the
engine's own setOutputInserts is the source of truth for the audio wiring.
The applied slots are recorded per bus so snapshot replays each
configured output's rack to late joiners.
Set (merge) the per-destination output trim on a bus's route to
out (issue #158, the output-gain floor), broadcast the resulting patchbay.output with
each destination's trim so every client's crosspoint converges.
/safety/panic — engage / release the PANIC latch. The caller (the server) resolves the
session's declared panic targets and passes the whole PanicState in; this method
only forwards it to the composing engine. The row announces the resulting scope.
chain.processor.enable — enable / bypass a processor; broadcast the chain.
chain.processor.plugin — assign / clear a processor's plugin; broadcast the chain.
Set a control-port value on the CURATED plugin hosted by a chain processor (the optional
mod-host chain plugin, never the built-in DSP, whose typed writers are the /eq /gate
/dynamics rows). The .../chain/processors/{id}/params/{symbol} row's write door.
Records the value for the echo and broadcasts the resulting chain.processors (its node
carries the value in pluginValues), so all clients converge.
Set an lv2:Parameter (Patch ext) on the curated plugin hosted by a chain processor —
the .../chain/processors/{id}/properties/{property} row's write door. Same echo +
broadcast contract as StructuralDspController.setProcessorPluginParam.
route.set — toggle a routing assignment (the console MAIN / group assign buttons).
Dispatches on the target:
target = a mix group bus → assign / un-assign channel INTO the group: a
post-fader unity send (real audio summing into the group bus) on, or the send
removed off. Broadcasts the resulting send.state too, so send surfaces converge.target = the main → the MAIN assign toggle: for an input strip the channel's
direct main tap; for a bus strip (a mix sub-group) its fold-back into main.Always broadcasts the strip's resulting route.state (its routing mask).
send.set — set a channel's send into a bus (position→linear) at a tap, broadcast.
tap is a SendTapSpec: the legacy 'pre'/'post', a feed-point preset, or
an explicit { position }. The broadcast carries both the legacy pre/post
classification the engine derives and the rich tap spec the send now holds.
send.tap — move an existing send's tap (the position in the channel chain it picks
off) without changing its level, broadcast the resolved tap. Errors (via the engine)
when no send is set into the bus.
chain.swapEqDyn — swap a bus master's EQ ↔ dynamics; broadcast the chain.
Unlink a linked pair, given either half, back into two independent channels.
Remove one input endpoint from a channel. The remaining set is the patch row's answer.
Drives the structural-DSP operations onto the live mixer and produces the broadcast frame for each. The send / DCA-membership / matrix-point REST rows call the matching method after validating their own PATCH body, and the server fans out the returned StructuralBroadcast to every client.