openmixer — generated API reference
    Preparing search index...

    Class StructuralDspController

    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.

    Index
    • Open a direct source→output route — /directPath/{source}'s awaited POST door.

      Parameters

      • sourceRef: string
      • out: StereoEndpoint

      Returns Promise<void>

    • 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.

      Parameters

      • channel: ChannelId
      • slot: number
      • uri: string
      • override: boolean = false

      Returns Promise<PluginChainState>

    • allocation.apply — build the gig's buses from a console allocation.

      Parameters

      Returns Promise<void>

    • 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).

      Parameters

      Returns Promise<GroupAssignedReport>

      unknown device, absent side, clear-on-outputs, an assignment wider than the box, or a placement crossing the audio fabric.

    • 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.

      Parameters

      • channel: ChannelId
      • source: string | StereoEndpoint

      Returns Promise<void>

    • 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.

      Parameters

      • channel: ChannelId

        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)

      • endpoint: StereoEndpoint

        the source endpoint to patch

      • asStereoChannel: boolean = false

        fold a stereo endpoint onto one channel instead of splitting

      Returns Promise<void>

      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.

      Returns Promise<{ cut: readonly ChannelId[]; loop: FeedbackLoopFrame | null }>

      CodedError when a ring carries no operator-made patch at all — the console does not guess at cutting its own structure, and says so instead of silently doing nothing.

    • 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.

      Parameters

      Returns OutputStateReport

    • 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.

      Parameters

      • uri: string

      Returns readonly PluginParam[] | undefined

    • 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.

      Parameters

      • target: "channel" | "bus"
      • channel: ChannelId
      • processorId: string

      Returns string | undefined

    • 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.

      Returns readonly { channel: ChannelId; processorId: string; target: "channel" | "bus" }[]

    • 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.

      Returns readonly {
          channel: ChannelId;
          key: string;
          processorId: string;
          target: "channel" | "bus";
      }[]

    • 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.

      Parameters

      • target: "channel" | "bus"
      • channel: ChannelId
      • processorId: string

      Returns Readonly<Record<string, string | number>> | undefined

    • 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.

      Parameters

      • target: "channel" | "bus"
      • channel: ChannelId

      Returns ChainReading

    • The strip's declared chain, for the send row's tap offer — the same reading chainHostedPluginOf makes, so the two rows read one chain.

      Parameters

      Returns readonly Processor[] | undefined

    • 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.

      Parameters

      Returns ExpectedLatency | undefined

    • 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.

      Parameters

      Returns InsertSuitabilityReport | undefined

    • 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).

      Parameters

      Returns DcaStateReport

    • patchbay.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.

      Parameters

      • sourceId: string

        the source node id to disconnect (its IoNode.id)

      Returns Promise<{ released: StereoEndpoint[] }>

      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.

      Returns FeedbackLoopFrame | null

    • The routing loops the engine currently sees. Empty for a surface with no topology detector (the capability is optional) and for an acyclic mix.

      Returns FeedbackLoop[]

    • 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.

      Parameters

      • target: "channel" | "bus"
      • channel: ChannelId
      • processorId: string

      Returns void

    • 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).

      Parameters

      Returns PluginChainState

    • 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.

      Returns readonly ChannelId[]

    • 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.

      Parameters

      • uri: string

      Returns readonly PluginParam[] | undefined

    • 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.

      Parameters

      Returns readonly string[] | undefined

    • 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.

      Parameters

      Returns readonly InsertSlotView[] | undefined

    • 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.

      Parameters

      • channel: ChannelId
      • plugins: readonly string[]

      Returns Refusal | undefined

    • 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.

      Parameters

      Returns Promise<void>

    • 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.

      Parameters

      Returns Promise<void>

    • 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.

      Returns Promise<readonly Device[]>

    • 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.

      Returns Promise<PatchbayIoView>

    • 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.

      Parameters

      Returns Promise<PluginChainState>

    • 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.

      Parameters

      Returns MuteGroupStateReport

    • 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.

      Returns PanicScope | null

    • Tear a direct path down — the converging half of the same row.

      Parameters

      • sourceRef: string
      • out: StereoEndpoint

      Returns Promise<void>

    • chain.reorder — move a processor; broadcast the reordered chain + new tap indices.

      Parameters

      • target: "channel" | "bus"
      • channel: ChannelId
      • from: number
      • to: number

      Returns Promise<void>

    • 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).

      Parameters

      Returns RouteStateReport

    • bus.fader — set a bus master (position→linear), broadcast.

      Parameters

      Returns Promise<void>

    • bus.mute — mute / un-mute a whole bus, broadcast (level unchanged, mute carried).

      Parameters

      Returns Promise<void>

    • 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.

      Parameters

      Returns Promise<void>

    • plugin.bypass on the software path — bypass/enable the channel insert at slot.

      Parameters

      • channel: ChannelId
      • slot: number
      • bypassed: boolean
      • override: boolean = false

      Returns Promise<PluginChainState>

    • plugin.param on the software path — set a control on the live insert instance.

      Parameters

      • channel: ChannelId
      • slot: number
      • symbol: string
      • value: number
      • override: boolean = false

      Returns Promise<PluginChainState>

    • plugin.patch on the software path — set an lv2:Parameter on the live insert instance.

      Parameters

      • channel: ChannelId
      • slot: number
      • uri: string
      • value: string | number

      Returns Promise<PluginChainState>

    • plugin.preset on the software path — load a preset into the live insert instance.

      Parameters

      • channel: ChannelId
      • slot: number
      • presetUri: string

      Returns Promise<PluginChainState>

    • 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.

      Parameters

      • channel: ChannelId
      • plugins: readonly string[]
      • override: boolean = false

      Returns Promise<PluginChainState>

    • 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.

      Parameters

      Returns Promise<void>

    • 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.

      Parameters

      • bus: ChannelId
      • inserts: readonly (string | { bypassed?: boolean; uri: string })[]
      • override: boolean = false

      Returns Promise<void>

    • 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.

      Parameters

      Returns Promise<OutputStateReport>

    • /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.

      Parameters

      Returns Promise<void>

    • chain.processor.enable — enable / bypass a processor; broadcast the chain.

      Parameters

      • target: "channel" | "bus"
      • channel: ChannelId
      • processorId: string
      • enabled: boolean

      Returns Promise<void>

    • chain.processor.plugin — assign / clear a processor's plugin; broadcast the chain.

      Parameters

      • target: "channel" | "bus"
      • channel: ChannelId
      • processorId: string
      • uri: string | undefined

      Returns Promise<void>

    • 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.

      Parameters

      • target: "channel" | "bus"
      • channel: ChannelId
      • processorId: string
      • symbol: string
      • value: number

      Returns Promise<void>

    • 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.

      Parameters

      • target: "channel" | "bus"
      • channel: ChannelId
      • processorId: string
      • uri: string
      • value: string | number

      Returns Promise<void>

    • 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).

      Parameters

      Returns Promise<{ route: RouteStateReport; send?: SendStateReport }>

    • 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.

      Parameters

      Returns Promise<SendStateReport>

    • chain.swapEqDyn — swap a bus master's EQ ↔ dynamics; broadcast the chain.

      Parameters

      Returns Promise<void>

    • Unlink a linked pair, given either half, back into two independent channels.

      Parameters

      Returns Promise<void>

    • Remove one input endpoint from a channel. The remaining set is the patch row's answer.

      Parameters

      Returns Promise<void>