Mixer TOML Config Reference

shiloh-mixer uses named CLI flags for its config path:

shiloh-mixer --state-dir ~/mixer-state --config ~/mixer/shiloh-mixer.toml

Config format lifecycle: On startup, the mixer resolves config in three tiers:

  1. runtime-config.json in the state directory (primary format)
  2. shiloh-mixer.toml (legacy – auto-migrated to JSON on first run)
  3. Empty defaults (first-boot mode)

TOML is read once for migration and is never re-read. All web UI changes
persist to runtime-config.json. The TOML example below is representative
of what the legacy file should contain.

The production config lives at server/shiloh-mixer.toml in this repository.


Complete annotated example

## shiloh-mixer.toml — example production config

jack_name        = "shiloh-mixer"   # JACK client name
control_port     = 19997            # mixer_web talks here
control_bind     = "127.0.0.1"      # change to WG/LAN addr for remote web-relay
broadcast_port   = 5005             # shiloh-relay + shiloh-broadcaster connect here
broadcast_frames = 128              # frames per UDP audio packet (deployed; code default is 160)
ramp_ms          = 150              # scene transition duration (ms)
meters_file             = "/home/shiloh/mixer-state/meters.json"
gains_file              = "/home/shiloh/mixer-state/gains.json"
scenes_file             = "/home/shiloh/mixer-state/scenes.json"
relay_assignments_file  = "/home/shiloh/mixer-state/relay-assignments.json"

# ── channels ─────────────────────────────────────────────────────────────────

[[channels]]
id          = "bcast1"
label       = "CH1 — Studio"
kind        = "stereo"          # registers ports: bcast1_in_1, bcast1_in_2
main_gain   = 1.0
ingest_slot = 0                 # reads from ingest bank slots 0–1

[[channels]]
id        = "mopidy"
label     = "CH5 — Mopidy"
kind      = "stereo"
main_gain = 1.0
# no ingest_slot → reads from JACK input ports

[[channels]]
id         = "sc1"
label      = "SC1 — Scarlett 1"
kind       = "mono"             # registers one port: sc1_in
main_gain  = 1.0
main_muted = true               # muted at cold start

# ── output buses ─────────────────────────────────────────────────────────────

[buses]
main              = ["out_1",              "out_2"]
main_gain         = 1.0
monitor           = ["monitor_out_1",      "monitor_out_2"]
monitor_gain      = 1.0
cue               = ["cue_out_1",          "cue_out_2"]
cue_gain          = 1.0
main_relay        = ["main_relay_line_1",  "main_relay_line_2"]
main_relay_gain   = 1.0
main_relay_on     = true
monitor_relay     = ["monitor_relay_line_1", "monitor_relay_line_2"]
monitor_relay_gain = 1.0
monitor_relay_on  = true
cue_relay         = ["cue_relay_line_1",   "cue_relay_line_2"]
cue_relay_gain    = 1.0
cue_relay_on      = true

# ── JACK autoconnect ──────────────────────────────────────────────────────────

[[autoconnect]]
src = "Desktop Audio:monitor_FL"
dst = "shiloh-mixer:bcast1_in_1"

[[autoconnect]]
src = "Desktop Audio:monitor_FR"
dst = "shiloh-mixer:bcast1_in_2"

# ── local relay sinks ─────────────────────────────────────────────────────────

[[local_relay_sink]]
name        = "usb_codec_out"
label       = "USB Codec Out"
ports       = ["usb_out_1", "usb_out_2"]
default_bus = "main"

# ── ingest allow-list ─────────────────────────────────────────────────────────

[ingest]
slot_count = 6

[[ingest.sender]]
name       = "studio"
channels   = 2
start_slot = 0

[[ingest.sender]]
name       = "laptop"
channels   = 2
start_slot = 2

[[ingest.sender]]
name       = "guest"
channels   = 2
start_slot = 4

# ── MIDI control ──────────────────────────────────────────────────────────────

[midi]
listen_port     = 19999
ardour_osc      = "127.0.0.1:3819"
idle_timeout_ms = 3000
fader_step      = 0.05
midi_state_file = "/home/shiloh/mixer-state/midi-state.json"

[midi.mode_keys]
"A3" = "idle"
"C3" = "channel_select"
"G3" = "transport"

[midi.channel_keys]
# leave empty for iteration-based channel selection

[midi.channel_action_keys]
"C4" = "channel_prev"
"D4" = "channel_next"
"C5" = "fader_down"
"D5" = "fader_up"
"E5" = "mute_toggle"

[midi.transport_keys]
"C6" = "play"
"D6" = "stop"
"E6" = "record"
"F6" = "goto_start"
"G6" = "goto_end"
"A6" = "prev_marker"
"B6" = "next_marker"
"C7" = "add_marker"

[midi.navigation_keys]
# optional; transport_keys now cover goto/marker actions

Top-level fields

Key Type Default Description
jack_name string "shiloh-mixer" JACK client name registered with the server
control_port integer 19997 UDP port for the control plane (mixer_web sends commands here)
control_bind string "127.0.0.1" Bind address for the control-plane socket. Set to a WireGuard or LAN address to allow remote web-relay instances to send set_relay_assignment
broadcast_port integer 5005 UDP port for relay and ingest (both shiloh-relay and shiloh-broadcaster connect here)
broadcast_frames integer 160 Frames per UDP audio packet (affects latency vs. overhead tradeoff)
meters_file string "/home/shiloh/mixer-state/meters.json" Path where peak meter snapshots are written for mixer_web
gains_file string "/home/shiloh/mixer-state/gains.json" Path where gains and current scene are persisted across restarts
scenes_file string "/home/shiloh/mixer-state/scenes.json" Path where named scenes are persisted
relay_assignments_file string "/home/shiloh/mixer-state/relay-assignments.json" Path where per-client relay feed assignments are persisted
session_delay_file string "/home/shiloh/mixer-state/session-delays.json" Path where per-session delay settings are persisted (name -> delay_ms)
ramp_ms integer 150 Fader ramp duration for scene transitions in milliseconds

[[channels]]

Defines one input channel. Repeat the block for each channel. Channel id values must be unique.

Key Type Default Required Description
id string Yes Short identifier. Used as the gain/mute key and as part of JACK port names. E.g. "net" registers net_in_1 and net_in_2 (stereo) or net_in (mono)
label string "" No Human-readable label shown in the mixer web UI
kind "stereo" or "mono" "stereo" No Stereo registers two input ports (id_in_1, id_in_2); mono registers one (id_in) and sums equally into both L and R of each bus
main_gain float 1.0 No Initial gain on the main mix bus (linear, 0.0–2.0 range typical)
main_muted bool false No Whether the channel starts muted on the main and relay buses
monitor_gain float 0.0 No Initial send level to the monitor bus
cue_gain float 0.0 No Initial send level to the cue bus
pan float 0.0 No Stereo pan / balance. Range [-1, 1]. -1 = full left, 0 = center, 1 = full right. For mono channels: constant-power pan. For stereo channels: balance (attenuates opposite side)
ingest_slot integer or absent absent No If set, this channel reads from the ingest ring-buffer bank starting at this zero-based slot index instead of JACK input ports. Stereo channels consume two contiguous slots; mono channels consume one

JACK port name convention

kind Ports registered
"stereo" <id>_in_1, <id>_in_2
"mono" <id>_in

These names appear under the mixer’s JACK client (e.g. shiloh-mixer:bcast1_in_1).


[buses]

Configures the three program buses and their corresponding relay feeds. All fields are optional — the defaults match the production config.

Program buses

Key Type Default Description
main [string, string] ["out_1", "out_2"] JACK output port names for the main bus (L, R)
main_gain float 1.0 Master gain for the main bus (linear)
monitor [string, string] ["monitor_out_1", "monitor_out_2"] JACK output port names for the monitor bus
monitor_gain float 1.0 Master gain for the monitor bus
cue [string, string] ["cue_out_1", "cue_out_2"] JACK output port names for the cue bus
cue_gain float 1.0 Master gain for the cue bus

Relay feeds

Each program bus has a matching relay feed — a gain-scaled, on/off-gated copy of the bus signal that is handed to the broadcast worker. UDP relay clients (headphone boxes, web-relay) receive from relay feeds, not directly from program buses.

Key Type Default Description
main_relay [string, string] ["main_relay_line_1", "main_relay_line_2"] JACK port names for the main relay tap
main_relay_gain float 1.0 Gain applied to the main relay feed
main_relay_on bool true Global on/off for the main relay feed
monitor_relay [string, string] ["monitor_relay_line_1", "monitor_relay_line_2"] JACK port names for the monitor relay tap
monitor_relay_gain float 1.0 Gain applied to the monitor relay feed
monitor_relay_on bool true Global on/off for the monitor relay feed
cue_relay [string, string] ["cue_relay_line_1", "cue_relay_line_2"] JACK port names for the cue relay tap
cue_relay_gain float 1.0 Gain applied to the cue relay feed
cue_relay_on bool true Global on/off for the cue relay feed

Per-bus DSP plugin chains

Each program bus can have a DSP plugin chain applied post-mix. Three optional keys in [buses], each taking a plugins array:

Key Type Description
main_dsp BusDspConfig DSP plugins applied to the main bus (post-mix, pre-output)
monitor_dsp BusDspConfig DSP plugins applied to the monitor bus
cue_dsp BusDspConfig DSP plugins applied to the cue bus

Each plugins entry is a tagged object with type and type-specific params:

Plugin type Params Description
echo delay_ms (default 200), feedback (default 0.3), mix (default 0.25) Stereo echo/delay with feedback
reverb wet_mix (default 0.3), decay (default 0.5) Stereo reverb
high_pass freq_hz (required), q (default 0.707) High-pass (low-cut) filter
low_pass freq_hz (required), q (default 0.707) Low-pass (high-cut) filter
mid_pass freq_hz (required), q (default 0.707) Band-pass filter
delay delay_ms (required) Simple delay (no feedback)

Example:

[buses]
main_dsp = [
    { type = "high_pass", freq_hz = 80.0 },
    { type = "echo", delay_ms = 250.0, feedback = 0.2, mix = 0.15 }
]
cue_dsp = [
    { type = "reverb", wet_mix = 0.4, decay = 0.6 }
]

[[autoconnect]]

Each entry wires one JACK port to another. The autoconnect watcher reconciles this list every ~5 seconds, reconnecting if either port disappears and comes back.

Key Type Required Description
src string Yes Source JACK port in "client:port" format
dst string Yes Destination JACK port in "client:port" format

Use JACK port names as reported by jack_lsp. Mixer input ports follow the shiloh-mixer:<id>_in_1 naming convention.

# Hardware capture → mixer input
[[autoconnect]]
src = "Scarlett Solo 4th Gen Pro:capture_AUX0"
dst = "shiloh-mixer:sc1_in"

# Mixer output → hardware playback
[[autoconnect]]
src = "shiloh-mixer:monitor_out_1"
dst = "Scarlett Solo 4th Gen Pro:playback_AUX0"

# Mono source splat to both mixer inputs (centering trick)
[[autoconnect]]
src = "mopidy-out:out_jackaudiosink0_1"
dst = "shiloh-mixer:mopidy_in_1"
[[autoconnect]]
src = "mopidy-out:out_jackaudiosink0_1"
dst = "shiloh-mixer:mopidy_in_2"

[ingest]

Configures the client→server audio ingest system (senders running shiloh-broadcaster). Omit the entire [ingest] section to disable ingest.

[ingest] top-level

Key Type Default Required Description
slot_count integer 0 Yes (if ingest is used) Total mono slots in the ingest ring-buffer bank. 0 disables ingest. Must be large enough to cover all senders

[[ingest.sender]]

One entry per allowed sender. Senders not in this list are rejected with REJECT_UNKNOWN_SENDER.

Key Type Required Description
name string Yes Must match the --name the client passes to shiloh-broadcaster connect
channels integer Yes Number of mono channels this sender will push. Must match the client’s --channels value
start_slot integer Yes Zero-based first slot in the ingest bank this sender writes to. Sender occupies slots start_slot through start_slot + channels - 1

Sender slot ranges must not overlap, and start_slot + channels must not exceed slot_count. The mixer validates this at startup and exits with an error if the config is invalid.

Channels with ingest_slot set read from this bank. A stereo channel at ingest_slot = 0 reads slots 0 and 1; a mono channel at ingest_slot = 2 reads slot 2.

[ingest]
slot_count = 6

[[ingest.sender]]
name       = "studio"    # shiloh-broadcaster --name studio --channels 2
channels   = 2
start_slot = 0           # occupies slots 0, 1

[[ingest.sender]]
name       = "laptop"    # shiloh-broadcaster --name laptop --channels 2
channels   = 2
start_slot = 2           # occupies slots 2, 3

[[ingest.sender]]
name       = "guest"
channels   = 2
start_slot = 4           # occupies slots 4, 5

[[local_relay_sink]]

Each entry creates a stereo JACK output pair on the mixer client that mirrors one of the three relay feeds (main/monitor/cue). Feed assignment is switchable at runtime from the mixer web UI without restarting anything.

Local relay sinks tap the relay feeds (with per-feed gain and on/off applied), not the raw program buses.

Key Type Default Required Description
name string Yes Unique identifier. Used as the persistence key in relay_assignments_file. Must not collide with any UDP relay client names
label string name No Human-readable label shown in the mixer web UI
ports [string, string] Yes JACK output port names (L, R) to register under the mixer’s JACK client
default_bus "main" / "monitor" / "cue" "main" No Which feed to use at cold start when no persisted assignment exists
[[local_relay_sink]]
name        = "usb_codec_out"
label       = "USB Codec Out"
ports       = ["usb_out_1", "usb_out_2"]
default_bus = "main"

The ports are then wired via [[autoconnect]] to physical outputs:

[[autoconnect]]
src = "shiloh-mixer:usb_out_1"
dst = "PCM2902 Audio Codec Analog Stereo:playback_FL"
[[autoconnect]]
src = "shiloh-mixer:usb_out_2"
dst = "PCM2902 Audio Codec Analog Stereo:playback_FR"

[multicast]

Optional multicast audio relay. When enabled, the mixer publishes one AUDIO packet
per bus to a multicast group, and relays join the group to receive audio. This avoids
the N× unicast fan-out — one send reaches all LAN relays. Per-relay delay moves to
the relay side, controlled from the mixer via the JSON control plane (UDP 19997).

[multicast] top-level fields

Field Type Default Description
enabled bool false Enable multicast relay (coexists with unicast)
group_addr string "239.0.0.1" IPv4 multicast group address
group_port u16 5006 UDP port for multicast audio
ttl u32 1 Multicast packet TTL (1 = local subnet only)

Example:

[multicast]
enabled    = true
group_addr = "239.0.0.1"
group_port = 5006
ttl        = 1

Use shiloh-relay connect-multicast on the relay side instead of connect.
The relay registers via the control plane (port 19997) and receives per-relay
delay and bus assignment commands from the mixer.


[midi]

Configures the MIDI modal control system. Omit the entire [midi] block to disable MIDI — no UDP socket will be opened on listen_port.

MIDI arrives from shiloh-midi-sender (running on the machine with the keyboard) as raw MIDI bytes in UDP datagrams. The mixer interprets note-ons through a modal state machine.

[midi] top-level fields

Key Type Default Required Description
listen_port integer 19999 No UDP port to listen on for incoming MIDI datagrams
ardour_osc string "127.0.0.1:3819" No Ardour OSC target address for transport and navigation commands
idle_timeout_ms integer 3000 No Retained for backward compatibility; no longer enforced (modes no longer auto-time-out)
fader_step float 0.05 No Linear gain step per fader_down / fader_up press (on a 0.0–2.0 scale)
midi_state_file string "/home/shiloh/mixer-state/midi-state.json" No Path where the MIDI modal state (mode, current channel) is written for mixer_web
osc_feedback_port integer absent No UDP port to listen on for Ardour OSC fader feedback. Must be explicitly set; no default. Configure Ardour to send feedback to this port

Modal state machine

The mixer interprets notes through a vim-style modal machine:

Idle ──► ChannelSelect ──► ChannelControl{id}
  │             ▲                │
  │         C3 key           action keys
  │                              │
  └──► Transport (sticky)        │
         G3 key                  │
                                 │
  A3 from any mode ──────────────┴──► Idle (universal reset)

Modes stay active until a mode key is pressed. There is no auto-timeout in normal operation.

[midi.mode_keys]

Maps note names to mode transitions. Valid mode values:

Value Effect
"idle" Return to Idle from any mode
"channel_select" Enter ChannelSelect mode
"transport" Enter Transport mode (sticky)
"navigation" Enter Navigation mode

Note names use standard MIDI convention: C4 = 60 (middle C). C3 = 48, D3 = 50, etc. Sharps use # suffix: "C#3".

[midi.mode_keys]
"A3" = "idle"           # MIDI note 57 — universal reset
"C3" = "channel_select" # MIDI note 48
"G3" = "transport"      # MIDI note 55

[midi.channel_keys]

Maps note names to channel ids (in ChannelSelect mode). Values must match an id in [[channels]].

[midi.channel_keys]
"C4" = "bcast1"   # selects the Studio channel
"D4" = "bcast2"
"E4" = "bcast3"

Leave empty to use iteration-based channel selection (navigate with channel_prev / channel_next actions in channel_action_keys).

[midi.channel_action_keys]

Maps note names to actions available in ChannelControl mode. Valid actions:

Action Effect
"fader_down" Decrease the channel’s main gain by fader_step
"fader_up" Increase the channel’s main gain by fader_step
"mute_toggle" Toggle mute on the main bus
"channel_prev" Move to the previous channel in config order (wraps)
"channel_next" Move to the next channel in config order (wraps)
[midi.channel_action_keys]
"C4" = "channel_prev"
"D4" = "channel_next"
"C5" = "fader_down"
"D5" = "fader_up"
"E5" = "mute_toggle"

[midi.transport_keys]

Maps note names to transport actions (in Transport mode). Commands are sent as OSC messages to ardour_osc. Valid actions:

Action OSC message
"play" /transport_play
"stop" /transport_stop
"play_stop" Toggle play/stop
"record" /rec_enable_toggle
"goto_start" /goto_start
"goto_end" /goto_end
"prev_marker" /prev_marker
"next_marker" /next_marker
"add_marker" /add_marker
[midi.transport_keys]
"C6" = "play"
"D6" = "stop"
"E6" = "record"
"F6" = "goto_start"
"G6" = "goto_end"
"A6" = "prev_marker"
"B6" = "next_marker"
"C7" = "add_marker"

[midi.navigation_keys]

Maps note names to navigation actions. Valid actions: "goto_start", "goto_end", "add_marker", "prev_marker", "next_marker". In the production config this table is typically left empty — the transport_keys table now covers these actions in Transport (sticky) mode.


Validation rules

The mixer validates the config at startup and exits with an error if any of the following hold:

  • Duplicate id values in [[channels]]
  • A channel’s ingest_slot + slot width exceeds ingest.slot_count
  • Duplicate name values in [[ingest.sender]]
  • A sender’s start_slot + channels exceeds ingest.slot_count
  • Duplicate name values in [[local_relay_sink]]
  • A [[local_relay_sink]] has a default_bus other than "main", "monitor", or "cue"

Default paths and ports summary

Setting Default
broadcast_port 5005
control_port 19997
control_bind 127.0.0.1
broadcast_frames 128 frames (deployed; code default is 160)
ramp_ms 150 ms
meters_file /home/shiloh/mixer-state/meters.json
gains_file /home/shiloh/mixer-state/gains.json
scenes_file /home/shiloh/mixer-state/scenes.json
relay_assignments_file /home/shiloh/mixer-state/relay-assignments.json
midi.listen_port 19999
midi.ardour_osc 127.0.0.1:3819
midi.fader_step 0.05
midi.midi_state_file /home/shiloh/mixer-state/midi-state.json