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’s id string as configured in the mixer TOML
  • bus: one of main, monitor, cue
  • gain: linear gain, typically 0.01.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 of main, monitor, cue
  • muted: true = muted (internally stored as 1.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 of main, monitor, cue
  • on: 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’s id string as configured in the mixer TOML
  • pan: 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 of main, monitor, cue
  • plugins: array of plugin config objects. Each uses a type tag:
{ "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 (configured local_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.