Control Plane Protocol
The control plane is a JSON-over-UDP protocol. The mixer listens on
127.0.0.1:19997 (loopback only). Each UDP datagram is one JSON object.
Replies, where documented, are sent back to the sender’s ephemeral port as
a single JSON datagram.
Maximum datagram size accepted: 8192 bytes.
Message format
Every request has an "op" string field that identifies the operation:
{ "op": "<operation_name>", ... }
All field names are snake_case. All bus / feed identifiers are lowercase
strings. The Rust side uses #[serde(tag = "op", rename_all = "snake_case")]
so the op string maps directly to the enum variant name.
Operations
set_fader
Set a single channel’s send level to one bus.
{
"op": "set_fader",
"channel": "<channel_id>",
"bus": "<bus_id>",
"gain": <f32>
}
channel: the channel’sidstring as configured in the mixer TOMLbus: one ofmain,monitor,cuegain: linear gain, typically0.0–1.0
No reply.
set_mute
Mute or unmute a channel on a bus.
{
"op": "set_mute",
"channel": "<channel_id>",
"bus": "<bus_id>",
"muted": <bool>
}
bus: one ofmain,monitor,cuemuted:true= muted (internally stored as1.0),false= active (0.0)
No reply.
set_master
Set a bus master gain.
{
"op": "set_master",
"bus": "<bus_id>",
"gain": <f32>
}
bus accepts the full set of bus identifiers:
bus value |
Description |
|---|---|
main |
Main bus master gain |
monitor |
Monitor bus master gain |
cue |
Cue bus master gain |
main_relay |
Relay send level from the main bus |
monitor_relay |
Relay send level from the monitor bus |
cue_relay |
Relay send level from the cue bus |
No reply.
set_relay_on
Enable or disable the relay output for a feed.
{
"op": "set_relay_on",
"feed": "<feed_id>",
"on": <bool>
}
feed: one ofmain,monitor,cueon:true= relay is sending audio for this feed;false= relay is
silenced (sessions remain registered)
No reply.
set_relay_assignment
Assign a named relay session to a feed. Overwrites any existing assignment.
Persisted to the assignments JSON file so it survives mixer restarts.
{
"op": "set_relay_assignment",
"name": "<session_name>",
"feed": "<feed_or_off>"
}
feed values:
| Value | Effect |
|---|---|
main |
Session receives the main bus relay feed |
monitor |
Session receives the monitor bus relay feed |
cue |
Session receives the cue bus relay feed |
off |
Session is parked — mixer forwards no audio. Session stays visible in the UI and can be flipped back without re-registering. |
No reply.
This operation is typically issued by mixer_web when an operator changes a
relay session’s feed assignment in the UI.
set_relay_assignment_default
Set a relay assignment only if no assignment for name exists yet
(set-if-absent semantics). Used by relay clients on startup to seed their
default feed without clobbering an operator-set value — particularly off,
which is how bandwidth-parking is persisted across restarts.
{
"op": "set_relay_assignment_default",
"name": "<session_name>",
"feed": "<feed_or_off>"
}
Same feed values as set_relay_assignment. No reply.
shiloh-web-relay sends this immediately after each ACCEPT from the mixer
(one per feed: main, monitor, cue). shiloh-relay on Pi devices sends it
with their configured default feed.
set_scene
Atomically replace the mixer’s live fader state with a full Scene object.
Triggers a linear ramp to the new values over ramp_ms milliseconds
(typically 150 ms). mixer_web owns scene persistence; the Rust mixer is a
pure applier.
{
"op": "set_scene",
"scene": { ... scene object ... }
}
The scene object structure (all fields required):
{
"name": "<string>",
"channels": [
{
"main": <f32>,
"monitor": <f32>,
"cue": <f32>,
"main_muted": <f32>,
"monitor_muted": <f32>,
"cue_muted": <f32>,
"pan": <f32>
},
...
],
"main_gain": <f32>,
"monitor_gain": <f32>,
"cue_gain": <f32>,
"main_relay_gain": <f32>,
"main_relay_on": <bool>,
"monitor_relay_gain": <f32>,
"monitor_relay_on": <bool>,
"cue_relay_gain": <f32>,
"cue_relay_on": <bool>,
"main_dsp_plugins": [],
"monitor_dsp_plugins": [],
"cue_dsp_plugins": []
}
channels array length must match the mixer’s configured channel count. Index
i in channels corresponds to Config.channels[i].
Mute values are stored as floats: 1.0 = muted, 0.0 = active. This allows
partial mutes in future but currently only 0.0 and 1.0 are used.
pan is a per-channel stereo pan/balance value in [-1.0, 1.0]. It is
optional (defaults to 0.0).
DSP plugin arrays (main_dsp_plugins, monitor_dsp_plugins, cue_dsp_plugins)
are optional (default to empty). They snap immediately with no ramp.
No reply.
set_pan
Set a single channel’s stereo pan / balance.
{
"op": "set_pan",
"channel": "<channel_id>",
"pan": <f32>
}
channel: the channel’sidstring as configured in the mixer TOMLpan: range[-1.0, 1.0].-1.0= full left,0.0= center,1.0= full right.
For mono channels this is a constant-power pan; for stereo channels it is a balance
(attenuates the opposite side).
No reply.
reload_config
Dynamically reload the mixer configuration from a JSON object. This triggers
a JACK deactivate/reactivate cycle and re-registers all ports. The operation
blocks until the reload completes or times out (10 second timeout).
{
"op": "reload_config",
"config": { ... full Config JSON object ... }
}
config must be a valid Config object (matching the TOML schema). On success,
the mixer’s JACK client is deactivated and reactivated with the new channel
layout, bus ports, and ingest slot assignments.
Reply:
{
"kind": "reload_result",
"success": <bool>,
"error": "<string or null>"
}
set_bus_dsp
Replace the DSP plugin chain for a bus. Plugins are validated (delay ranges,
feedback caps, Q limits) before being applied. DSP chains snap immediately
(no ramp).
{
"op": "set_bus_dsp",
"bus": "<bus_id>",
"plugins": [ <plugin_config>, ... ]
}
bus: one ofmain,monitor,cueplugins: array of plugin config objects. Each uses atypetag:
{ "type": "echo", "delay_ms": <f32>, "feedback": <f32>, "mix": <f32> }
{ "type": "reverb", "wet_mix": <f32>, "decay": <f32> }
{ "type": "high_pass", "freq_hz": <f32>, "q": <f32> }
{ "type": "low_pass", "freq_hz": <f32>, "q": <f32> }
{ "type": "mid_pass", "freq_hz": <f32>, "q": <f32> }
{ "type": "delay", "delay_ms": <f32> }
If any plugin fails validation, an error reply is returned and the DSP chain
is unchanged.
Reply (on error):
{ "kind": "error", "message": "<string>" }
No reply on success.
set_session_delay
Set a per-session delay for a named relay client. Delay is applied to the
audio stream sent to that client (feed-independent). Must be in the range
[0, MAX_DELAY_MS].
{
"op": "set_session_delay",
"name": "<session_name>",
"delay_ms": <f32>
}
Reply (on error, e.g. out of range):
{ "kind": "error", "message": "<string>" }
No reply on success.
get_state
Query the current live scene target (the scene the mixer is ramping toward,
not necessarily the current instantaneous gains).
{ "op": "get_state" }
Reply:
{
"kind": "state",
"name": "<string>",
"channels": [ ... ],
"main_gain": <f32>,
...
}
The reply is the full scene object wrapped in a kind: "state" envelope (from
the Reply::State(Scene) serde enum serialization).
get_config
Query the mixer’s static channel and bus configuration. mixer_web calls this
on startup to render channel strips without duplicating the TOML config.
{ "op": "get_config" }
Reply:
{
"kind": "config",
"channels": [
{ "id": "<string>", "label": "<string>", "kind": "stereo" | "mono" },
...
],
"buses": [
{ "id": "main", "label": "Main" },
{ "id": "monitor", "label": "Monitor" },
{ "id": "cue", "label": "Cue" },
{ "id": "main_relay", "label": "Main Relay" },
{ "id": "monitor_relay", "label": "Monitor Relay" },
{ "id": "cue_relay", "label": "Cue Relay" }
],
"ramp_ms": <u32>
}
ramp_ms is the fader ramp duration in milliseconds (typically 150).
get_full_config
Query the complete runtime mixer configuration (all fields including channels,
buses, ingest, autoconnect, MIDI, etc.), not just the UI-facing channel/bus
description. Used by mixer_web for the channel configuration editor modal.
{ "op": "get_full_config" }
Reply:
{
"kind": "full_config",
... full Config object (channels, buses, ingest, autoconnect, midi, etc.) ...
}
get_available_ports
Query JACK for available capture and playback ports on the system. Used by
mixer_web when creating or editing channels to present a port picker.
{ "op": "get_available_ports" }
Reply:
{
"kind": "available_ports",
"capture": [
{ "client": "<jack_client_name>", "ports": ["<port_name>", ...] },
...
],
"playback": [
{ "client": "<jack_client_name>", "ports": ["<port_name>", ...] },
...
]
}
Ports owned by shiloh-mixer itself are excluded from the results.
get_midi_state
Query the current MIDI controller state for display in the mixer UI. Reads
from /home/shiloh/mixer-state/midi-state.json; returns a synthetic idle
response if the file is absent.
{ "op": "get_midi_state" }
Reply:
{
"kind": "midi_state",
"mode": "idle" | "<mode string>",
"channel": null | "<channel_id>"
}
get_jack_graph
Return the full JACK graph: all clients, their ports, and current connections.
Used by mixer_web to render the JACK patchbay view.
{ "op": "get_jack_graph" }
Reply:
{
"kind": "jack_graph",
"clients": [
{
"name": "<client_name>",
"ports": [
{
"name": "<port_name>",
"direction": "output" | "input",
"connections": ["<connected_port_name>", ...]
},
...
]
},
...
]
}
direction:"output"= source port (readable);"input"= sink port (writable).
jack_connect
Connect two JACK ports. Wraps jack_connect CLI command on the mixer host.
{
"op": "jack_connect",
"src": "<source_port>",
"dst": "<destination_port>"
}
Both src and dst must be full JACK port names (e.g. "shiloh-mixer:out_1").
No reply on success. On error:
{ "kind": "error", "message": "<string>" }
jack_disconnect
Disconnect two JACK ports. Wraps jack_disconnect CLI command on the mixer host.
{
"op": "jack_disconnect",
"src": "<source_port>",
"dst": "<destination_port>"
}
No reply on success. On error:
{ "kind": "error", "message": "<string>" }
Scenes and state persistence
The control plane does not persist scenes to disk — that is mixer_web’s
responsibility. The mixer only holds one live scene in memory (wrapped in
ArcSwap for lock-free RT access). On startup the mixer initializes from the
defaults in its TOML config; mixer_web is expected to push a set_scene
with the persisted state immediately after connecting.
Relay assignments are persisted by the mixer itself to a JSON sidecar file
(path configured as relay_assignments_file in the TOML, default
/home/shiloh/mixer-state/relay-assignments.json).
Fader ramp
All gain changes (via set_fader, set_master, or set_scene) are applied
to the target scene atomically. The RT thread detects a changed target and
linearly interpolates from the current live gains to the target over ramp_ms
milliseconds (~150 ms default). If the target changes mid-ramp, a fresh ramp
starts from the current interpolated position. No allocations occur in the RT
path.
Sessions sidecar
The mixer writes a sessions JSON file to
/home/shiloh/mixer-state/sessions.json at ~5 Hz (every 200 ms). This file
is read by mixer_web to render the relay session list. It is not part of the
control-plane protocol but is documented here for completeness.
{
"sessions": [
{
"kind": "udp" | "local" | "broadcaster",
"session_id": <u32>,
"name": "<string>",
"label": "<string>" | null,
"peer": "<ip:port>" | "local",
"version": <u8>,
"bus": "<feed_id>" | "<channel_id>",
"seconds_since_ping": <f64>
},
...
]
}
kind values:
"udp"— a connected UDP relay client (Pi relay, web-relay)"local"— a JACK output sink on the mixer host (configuredlocal_relay_sinks)"broadcaster"— an active ingest sender (shiloh-broadcaster)
For "broadcaster" rows, bus is repurposed as the target channel id (e.g.
"bcast1"), and label carries the channel’s human-readable label. mixer_web
branches on kind to render these rows differently.