midi-daemon 0.9.5

A Lua-scriptable MIDI routing daemon
# 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().
default_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().
default_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.

[invert_controllers]
# Message type to match
type = "cc"
# MIDI channel to match
channel = 1
# CC controller numbers whose values should be inverted (0–127 → 127–0)
controllers = [7, 11]   # volume, expression

[metronome]
# Timer
bpm  = 120.0
ppqn = 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)
beat_1_note = 37   # Side Stick
beat_n_note = 56   # Cowbell
channel     = 10   # GM percussion channel
velocity    = 100
beats_per_bar = 4
note_len_ms   = 20   # fixed note duration in milliseconds (independent of BPM)

# Incoming CC that controls BPM (maps 0–127 → 20–200 BPM)
cc_type       = "cc"
cc_channel    = 1
cc_controller = 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.
start_stop_channel    = 1
start_stop_controller = 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)
start_running = true

[VolumePanMuteControl]
# Shared MIDI channel for all three CCs (overridden by the per-param keys below)
channel = 1

# Volume: CC 7 is the MIDI standard for Channel Volume (0–127 maps to OSC 0.0–1.0)
volume_channel    = 1
volume_controller = 7

# Pan: CC 10 is the MIDI standard for Pan (0=left, 64=center, 127=right maps to OSC -1.0–1.0)
pan_channel    = 1
pan_controller = 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
mute_channel    = 1
mute_controller = 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

[timing-trainer]
# MIDI channel for the pan CC output
pan_channel   = 1
# CC number for pan output (10 = standard MIDI pan)
pan_controller = 10

# ±ms window where pan reaches full left (early) or full right (late)
max_error_ms = 200

# Number of recent note hits to average before computing pan position
history_size = 8

# Seconds of keyboard silence before pan resets to center
idle_seconds = 3

# Incoming CC that enables/disables timing training (value >= 64 = enable, < 64 = disable)
# MIDI Transport Start/Continue enable; Stop disables.
start_stop_channel    = 1
start_stop_controller = 22

# Whether timing training is active on launch (default: true)
start_running = true