1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# midi-daemon configuration
# Default values shown — all fields are optional.
#
# Structure:
# Top-level keys (this section) set global defaults that apply to every route.
# A [section-name] adds per-route config for routes/<section-name>.lua.
# Values in a [section-name] block are available in that route as the `config`
# Lua global (e.g. config.bpm, config.osc_receive_port).
#
# Priority order (lowest → highest):
# global defaults in this file
# → per-route [section] values
# → connect patterns returned by init() in the .lua script
# Directory containing .lua route files.
# Each .lua file gets its own virtual ALSA MIDI port pair.
# routes_dir = "~/.config/midi-daemon/routes.d"
# Default BPM for the timer passed to on_tick().
# Can be overridden at runtime from Lua with set_bpm().
= 120.0
# Auto-connect: regex matched against "ClientName:PortName" of ALSA ports.
# Applied to every route input/output that has no per-route pattern.
# Uncomment and set to your controller/synth name to wire everything at once.
# default_connect_input = ".*My Keyboard.*"
# default_connect_output = ".*My Synth.*"
# Global OSC root — a single UDP port shared by all routes.
# Incoming packets are dispatched by address prefix: /route-name/... → that route.
# Each route's send_osc() calls go to the shared destination.
# Routes can still override with their own osc = { receive, send } block in init().
# osc_receive_port = 9000
# osc_send_addr = "127.0.0.1:9001"
# How often (in seconds) the daemon sends /route/heartbeat to OSC subscribers.
# osc_heartbeat_interval = 5.0
# ── OBS connections ───────────────────────────────────────────────────────────
# Named OBS websocket connections (obs-websocket plugin, OBS 28+). Any route
# can call any connection by name via obs_call()/obs_call_sync(), regardless
# of what it declares interest in below. Multiple connections are supported —
# just add more [obs.<name>] sections.
#
# [obs.main]
# host = "127.0.0.1"
# port = 4455
# password = "changeme" # omit if authentication is disabled in OBS
#
# [obs.streaming-pc]
# host = "192.168.1.50"
# port = 4455
#
# A route opts in to receiving that connection's events (scene changed, mute
# toggled, etc.) via on_obs_event(conn, event) by declaring it in init():
#
# function init()
# return { obs = { connections = {"main"} } }
# end
#
# function on_obs_event(conn, event)
# log(conn .. ": " .. event.eventType)
# end
#
# Calling OBS (fire-and-forget, does not block the route):
# obs_call("main", "scenes.set_current", { name = "Scene 2" })
# obs_call("main", "inputs.set_mute", { name = "Mic/Aux", muted = true })
#
# Calling OBS and waiting for a result (blocks this route's thread only, up to
# timeout_ms — other routes and OBS connections keep running):
# local ok, result, err = obs_call_sync("main", "scenes.current", {}, 1000)
#
# Supported request names: scenes.set_current, scenes.current, scenes.list,
# inputs.set_mute, inputs.toggle_mute. More are added to src/obs.rs as routes
# need them.
# Pulses per quarter note — controls tick resolution.
# 24 = standard MIDI clock, 96 = high resolution.
# Can be overridden at runtime from Lua with set_ppqn().
= 24
# [keyboard-split]
# split_note = 60 # split at middle C (C4); notes below → bass, >= → lead
# connect_input = ".*KeyLab.*" # regex matched against "ClientName:PortName"
# connect_bass = ".*ZynAddSubFX.*"
# connect_lead = ".*Surge.*"
# Per-port connect strings for multi-input/output routes.
# connect_{portname}-in matches a specific named input port
# connect_{portname}-out matches a specific named output port
# Example (timing-trainer has inputs "keyboard" and "metronome"):
# [timing-trainer]
# connect_keyboard-in = ".*My Keyboard.*"
# connect_metronome-in = ".*metronome-out.*"
# OSC receive/send is declared per-route in init(), not in config.toml.
# Example init() return value (in your .lua file):
#
# function init()
# return {
# osc = {
# receive = 9000, -- UDP port to listen on
# send = { default = "127.0.0.1:9001" } -- named send targets
# }
# }
# end
#
# Use config.toml values to make the port/address configurable:
#
# function init()
# return { osc = { receive = config.osc_port or 9000 } }
# end
#
# [osc-bridge]
# osc_port = 9000
# ── Lua process state (save_state / load_state) ───────────────────────────────
# Any route can call save_state(table) and load_state() in its .lua file to
# persist/restore arbitrary state across restarts. These are the only state
# primitives — on_startup()/on_shutdown() are plain lifecycle hooks with no
# implicit state argument, so a route calls load_state() inside on_startup()
# and save_state(...) inside on_shutdown() itself if it wants persistence.
# Both can also be called from anywhere else (e.g. on_tick, on_midi) to
# checkpoint state proactively instead of waiting for shutdown.
#
# State is stored as a JSON file at <state_dir>/<route-name>/state.json.
# Routes never touch the filesystem directly — all I/O happens in the daemon.
#
# state_dir = "/var/lib/midi-daemon/state" # default: <cache_dir>/lua-state
#
# Saved only on graceful shutdown (SIGTERM / systemctl stop / kill -TERM);
# not on SIGKILL or a crash.
# ── Per-route sections ────────────────────────────────────────────────────────
# Each [section-name] below corresponds to routes.d/<section-name>.lua.
# Keys become the `config` Lua global in that route.
# Any TOML type is supported: strings, integers, floats, booleans, arrays,
# and nested tables.
[]
# Message type to match
= "cc"
# MIDI channel to match
= 1
# CC controller numbers whose values should be inverted (0–127 → 127–0)
= [7, 11] # volume, expression
[]
# Timer
= 120.0
= 24
# Optional OSC output — sends /metronome/beat and /metronome/running on each event.
# osc_send_addr = "127.0.0.1:9001"
# Optional OSC input — listens for /metronome/bpm, /start, /stop, /continue.
# osc_receive_port = 9000
# Output notes (GM percussion defaults)
= 37 # Side Stick
= 56 # Cowbell
= 10 # GM percussion channel
= 100
= 4
= 20 # fixed note duration in milliseconds (independent of BPM)
# Incoming CC that controls BPM (maps 0–127 → 20–200 BPM)
= "cc"
= 1
= 21
# Incoming CC that starts/stops the metronome (value >= 64 = start, value < 64 = stop)
# MIDI Transport Start (0xFA) resets to beat 1; Continue (0xFB) resumes; Stop (0xFC) stops.
= 1
= 22
# Set true if your controller's button reports "on" as the low CC value
# instead of high (i.e. start/stop behaves backwards from what's documented
# above). Flips the gate read from start_stop_controller only — doesn't
# affect /metronome/running's own 1=start/0=stop meaning over OSC.
# start_stop_invert = true
# Whether the metronome starts playing immediately on launch (default: true)
= true
[]
# Shared MIDI channel for all three CCs (overridden by the per-param keys below)
= 1
# Volume: CC 7 is the MIDI standard for Channel Volume (0–127 maps to OSC 0.0–1.0)
= 1
= 7
# Pan: CC 10 is the MIDI standard for Pan (0=left, 64=center, 127=right maps to OSC -1.0–1.0)
= 1
= 10
# Mute: no universal MIDI standard; set to match your DAW or hardware
# Incoming CC value ≥ 64 = muted, < 64 = unmuted; outgoing sends 127 or 0
= 1
= 118
# OSC receive/send — use the global config or override here per-route
# osc_receive_port = 9000
# osc_send_addr = "127.0.0.1:9001"
# Optional Non-Mixer-XT bridge (see routes.d/lib/nmxt.lua and
# https://github.com/Stazed/non-mixer-xt/blob/main/OSC.md). Disabled unless
# nmxt_strip is set — this route then also drives that Non-Mixer-XT strip's
# Gain (volume/mute), with changes flowing both ways.
# nmxt_strip = "Guitar" # Non-Mixer-XT strip name
# nmxt_pan = false # only set true if this strip has a Pan plugin inserted
# nmxt_osc_addr = "127.0.0.1:9500" # Non-Mixer-XT's OSC server
[]
# MIDI channel for the pan CC output
= 1
# CC number for pan output (10 = standard MIDI pan)
= 10
# ±ms window where pan reaches full left (early) or full right (late)
= 200
# Number of recent note hits to average before computing pan position
= 8
# Seconds of keyboard silence before pan resets to center
= 3
# Incoming CC that enables/disables timing training (value >= 64 = enable, < 64 = disable)
# MIDI Transport Start/Continue enable; Stop disables.
= 1
= 22
# Whether timing training is active on launch (default: true)
= true