P2-R1: ALSA + JACK2 low-latency config (scripts, quirks, tuning) P2-R2: Carla integration (build scripts, 8ch rack config, NAM LV2 support) P2-R3: Plugin manager, categories, blacklist, NAM model support P3-R1: Mixer DSP engine (channel strip, routing matrix, bus mgr, automation) P4-R1: MIDI engine (learn mode, clock sync, HID discovery, mapping store) P4-R2: Network API (OSC server, FastAPI REST, WebSocket, auth, rate limiter) P5-R1: Touchscreen UI evaluation + main entry point docs: Audio stack, Carla integration, MIDI support, UI evaluation tests: Full test suite (292 passing)
17 KiB
MIDI Controller Support — Binding + Learn Mode
Date: 2026-05-19 Task: P4-R1 Target: Raspberry Pi 4B, RPi OS Lite 64-bit, PREEMPT_RT kernel
Comprehensive MIDI controller mapping engine with learn mode, clock sync, and JACK MIDI bridge for the RPi Audio Mixer.
Table of Contents
- Architecture Overview
- Hardware Setup — RPi as USB Host
- Supported Controllers
- Installation
- MIDI Learn Mode
- MIDI Clock Sync
- JACK MIDI Integration
- Mapping Persistence
- Udev Rules — Hotplug
- MIDI Mapper Daemon
- NRPN Support
- File Reference
1. Architecture Overview
┌──────────────────────────────────────────────────────────────┐
│ Physical World │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ X-Touch │ │ MIDImix │ │ FaderFox │ ... │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └─────────────┼─────────────┘ │
│ │ USB │
├─────────────────────┼────────────────────────────────────────┤
│ ┌──────▼──────┐ │
│ Kernel │ xHCI USB │ udev rules (99-midi.rules) │
│ │ Host Ctrl │ hotplug → ALSA seq clients │
│ └──────┬──────┘ │
│ │ /dev/snd/midi* │
├─────────────────────┼────────────────────────────────────────┤
│ ┌──────▼──────┐ │
│ ALSA │ ALSA RawMIDI│ snd-usb-audio │
│ │ / Sequencer │ snd-seq (clients 20+) │
│ └──────┬──────┘ │
│ │ │
├─────────────────────┼────────────────────────────────────────┤
│ ┌──────▼──────┐ │
│ Python │ MIDI Engine │ midi_engine.py │
│ (our code) │ │ - CC/NRPN parsing │
│ │ ┌─────────┐ │ - Mapping lookup │
│ │ │ Mapping │ │ - Value scaling │
│ │ │ Table │ │ - Learn mode │
│ │ └─────────┘ │ - Clock sync │
│ └──┬───────┬───┘ │
│ │ │ │
│ ┌─────────▼─┐ ┌──▼──────────┐ │
│ │ Parameter │ │ JACK MIDI │ │
│ │ Registry │ │ Bridge │ │
│ │ (DSP/UI) │ │ (4 ports) │ │
│ └────────────┘ └──┬──────────┘ │
│ │ JACK MIDI │
├──────────────────────────┼────────────────────────────────────┤
│ ┌──────▼──────┐ │
│ JACK2 │ JACK MIDI │ a2jmidid (ALSA→JACK) │
│ │ Ports │ │
│ └──────┬──────┘ │
│ │ │
│ ┌──────▼──────┐ │
│ Carla │ MIDI IN │ → soft synths, samplers │
│ │ Ports │ │
│ └─────────────┘ │
└──────────────────────────────────────────────────────────────┘
Data flow:
- USB MIDI controller sends CC/NRPN messages over USB
- Linux kernel (snd-usb-audio) exposes them as ALSA sequencer clients
- Python MIDI engine reads from ALSA seq / rtmidi
- Engine matches messages against the mapping table
- Matched messages are: a. Sent to the Parameter Registry (for DSP processing / UI feedback) b. Forwarded to JACK MIDI output ports (for Carla routing)
2. Hardware Setup — RPi as USB Host
The Raspberry Pi 4B acts as a USB host (not device). USB MIDI controllers plug directly into the Pi's USB ports. No OTG adapter needed.
Requirements:
- Raspberry Pi 4B with powered USB ports
- USB MIDI class-compliant controller (99% of modern controllers)
- No drivers needed — Linux UAC2 + snd-usb-audio handles everything
Verify USB MIDI is working:
# List raw MIDI devices
ls -la /dev/snd/midi*
# List ALSA sequencer clients (MIDI controllers appear as clients 20+)
cat /proc/asound/seq/clients
# Use our script
./scripts/midi-mapper --list-devices
# Or with amidi
amidi -l
Power considerations:
- RPi4B USB ports provide up to 1.2A total
- Most MIDI controllers draw <100mA — safe
- For bus-powered controllers + audio interface, use a powered USB hub
3. Supported Controllers
Pre-configured profiles
| Manufacturer | Model | VID:PID | Faders | Knobs | Buttons | Transport |
|---|---|---|---|---|---|---|
| Behringer | X-Touch Compact | 1397:00B4 | 9 | 16 | 39 | Yes |
| Behringer | X-Touch (MCU) | 1397:00B5 | 9 | 8 | 92 | Yes |
| FaderFox | UC4 | 16D0:0D27 | 8 | 0 | 32 | No |
| Akai | MIDImix | 09E8:0031 | 9 | 24 | 16 | No |
| Arturia | BeatStep | 1C75:0208 | 0 | 17 | 16 | Yes |
| Novation | Launch Control XL | 1235:0061 | 8 | 24 | 16 | No |
| Korg | nanoKONTROL2 | 0944:0117 | 8 | 8 | 32 | Yes |
Adding a new controller
Add a ControllerProfile to src/midi/controllers.py:
ControllerProfile(
manufacturer="YourBrand",
model="YourModel",
usb_vendor_id=0x1234,
usb_product_id=0x5678,
alsa_client_pattern="Your Model Name",
num_faders=8,
num_knobs=8,
num_buttons=16,
has_transport=True,
controls=[
ControllerControl("Fader 1", 0, 0, "fader"),
# ... etc
],
)
To discover a controller's CC numbers:
- Install
aseqdump:sudo apt install alsa-utils - Run:
aseqdump -p <client_id> - Wiggle each control and note the CC number
4. Installation
Prerequisites
# System packages
sudo apt update
sudo apt install -y python3 python3-pip python3-venv \
alsa-utils a2jmidid jackd2
# Python dependencies
pip install python-rtmidi # MIDI I/O via ALSA rawmidi
pip install JACK-Client # JACK MIDI port creation
pip install pyudev # Hotplug monitoring (optional)
# Install udev rules
sudo cp config/99-midi.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules
sudo udevadm trigger
Start the JACK MIDI bridge
# Start a2jmidid to bridge ALSA sequencer → JACK MIDI
a2jmidid -e &
# Or let our bridge do it directly (preferred)
./scripts/midi-mapper
5. MIDI Learn Mode
The interactive CLI lets you map physical controls to mixer parameters without editing config files.
./scripts/midi-learn-cli
Workflow
- Plug in your MIDI controller (before or during)
- Start learn mode:
./scripts/midi-learn-cli - Pick a parameter: type
l volume 0(learn volume for channel 0) - Wiggle the control on your physical controller
- Confirm: type
c(ordto discard) - Save: type
save(or justquit— auto-saves)
Commands
| Command | Description |
|---|---|
l <param> [ch] |
Start learning for a parameter |
b [ch] |
Batch learn all faders for a channel |
c |
Confirm captured mapping |
d |
Discard captured mapping |
list |
Show all mappings |
del <n> |
Delete mapping #n |
save |
Save to disk |
status |
Show learn state |
quit |
Exit (auto-saves) |
Example session
$ ./scripts/midi-learn-cli --session live-set
midi-learn> l volume 0
Listening for MIDI CC on any channel → volume CH0
Wiggle the knob/fader you want to assign, then type 'confirm' or 'c'.
[wiggle fader 1 on controller]
midi-learn> c
Confirmed: CH0 volume ← CC70
midi-learn> l pan 0
Listening for MIDI CC on any channel → pan CH0
[wiggle knob 1 on controller]
midi-learn> c
Confirmed: CH0 pan ← CC10
midi-learn> quit
Auto-saved 2 mappings to 'live-set'.
Batch learn
Maps faders sequentially without typing between each one:
midi-learn> b 0
Batch learn for CH0: wiggle each control in order.
[1/4] CH0 Vol — wiggle the fader now...
[wiggle]
[2/4] CH0 Pan — wiggle the pan knob now...
[wiggle]
[3/4] CH0 Mute — press the mute button now...
[press]
[4/4] CH0 Gain — wiggle the gain knob now...
[wiggle]
Batch learn complete: 4 mappings
6. MIDI Clock Sync
The engine accepts MIDI clock (timing clock) messages and derives tempo (BPM). This can sync backing tracks, tempo-synced delays, LFOs, and sequencers.
How it works
- MIDI clock sends 24 pulses per quarter note (PPQN)
- The engine measures inter-pulse intervals
- A moving average window (96 pulses = 4 beats) smooths jitter
- When the estimate stabilises (window half-full),
bpm_stable = True - Tempo changes are reported on beat boundaries
Usage in the daemon
The MIDI mapper daemon automatically tracks clock from any connected device:
./scripts/midi-mapper
# Output:
# 19:34:01 Transport: start
# 19:34:02 Tempo: 128.5 BPM (stable=True)
# 19:34:03 Tempo: 128.4 BPM (stable=True)
Transport control
| MIDI Message | Effect |
|---|---|
| START (0xFA) | Reset position, begin playing |
| CONTINUE (0xFB) | Resume from current position |
| STOP (0xFC) | Stop, hold position |
| Song Position Pointer | Jump to beat position |
7. JACK MIDI Integration
The engine creates JACK MIDI output ports that Carla can connect to.
Direct mode (preferred)
Uses the jack Python module to create JACK MIDI ports directly:
./scripts/midi-mapper
# Creates ports:
# rpi-mixer:midi_out_0 (channel params: volume, pan, mute, etc.)
# rpi-mixer:midi_out_1 (EQ, dynamics params)
# rpi-mixer:midi_out_2 (FX sends/returns)
# rpi-mixer:midi_out_3 (transport: play, stop, record)
In Carla, connect rpi-mixer:midi_out_* to your soft synth's MIDI input.
ALSA sequencer fallback
If jack module isn't available, the bridge uses ALSA sequencer ports.
Run a2jmidid -e to bridge them to JACK:
a2jmidid -e &
./scripts/midi-mapper --no-jack
# ALSA seq ports appear, a2jmidid auto-bridges to JACK
Routing mapped CCs
Mapped MIDI messages are forwarded to JACK ports based on parameter category:
| Parameter type | JACK port |
|---|---|
| Volume, Pan, Mute, Solo, Gain, Phase | midi_out_0 |
| EQ, Compressor, Gate | midi_out_1 |
| FX sends/returns | midi_out_2 |
| Transport (play, stop, record) | midi_out_3 |
8. Mapping Persistence
Mappings are stored as JSON in ~/.config/rpi-mixer/mappings/.
File format
{
"version": 1,
"session": "live-set",
"updated": "2026-05-19T23:45:00+00:00",
"mappings": [
{
"midi": {
"type": "CONTROL_CHANGE",
"channel": 0,
"controller": 70,
"is_nrpn": false,
"nrpn_number": 0,
"source_device": null
},
"target": {
"parameter": "volume",
"channel": 0
},
"value": {
"midi_min": 0,
"midi_max": 127,
"param_min": -60.0,
"param_max": 12.0,
"invert": false,
"curve": "linear"
},
"meta": {
"label": "CH0 Vol",
"enabled": true
}
}
]
}
Sessions
You can maintain multiple mapping configurations:
./scripts/midi-learn-cli --session studio # Studio setup
./scripts/midi-learn-cli --session live-set # Live performance setup
./scripts/midi-mapper --session live-set # Launch with specific setup
9. Udev Rules — Hotplug
The udev rules in config/99-midi.rules provide:
- Automatic permissions: MIDI devices owned by
audiogroup (mode 0660) - Persistent symlinks: Known controllers get stable names under
/dev/midi/ - Environment tagging:
MIDI_CONTROLLERenv var set for known devices - Hotplug service trigger: Systemd service can auto-restart on plug/unplug
Install:
sudo cp config/99-midi.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules
sudo udevadm trigger
The Python hotplug monitor (device_discovery.py) uses pyudev to detect
add/remove events and can re-open ports dynamically.
10. MIDI Mapper Daemon
Starting
# Basic usage
./scripts/midi-mapper
# With specific session
./scripts/midi-mapper --session live-set
# List devices
./scripts/midi-mapper --list-devices
# Without JACK (headless test)
./scripts/midi-mapper --no-jack --allow-no-device
Signal handling
SIGINT(Ctrl+C): Graceful shutdown, outputs statsSIGTERM: Same as SIGINT
Stats output
Every 10 seconds:
19:35:01 Stats: 2847 events, 1423 mapped, 71.2 ev/s, 12 mappings active
systemd service (optional)
# /etc/systemd/system/midi-mapper.service
[Unit]
Description=RPi Audio Mixer MIDI Mapper
After=jackd.service sound.target
Wants=jackd.service
[Service]
Type=simple
User=pi
ExecStart=/home/pi/rpi-mixer/scripts/midi-mapper --session default
Restart=on-failure
RestartSec=2
[Install]
WantedBy=multi-user.target
11. NRPN Support
NRPN (Non-Registered Parameter Number) is a MIDI protocol extension that provides 14-bit resolution (16,384 steps vs 128 for CC). The engine transparently handles the NRPN state machine:
- Controller sends CC 99 (NRPN MSB) + CC 98 (NRPN LSB) to select parameter
- Controller sends CC 6 (Data MSB) + CC 38 (Data LSB) for value
- Engine assembles into a 14-bit value
- Mapping lookup is on the full NRPN number
NRPN is transparent in learn mode — just wiggle the control and the engine detects the CC 99/98 preamble and captures the NRPN number.
12. File Reference
| File | Lines | Purpose |
|---|---|---|
src/midi/types.py |
~230 | MIDI message types, enums, data classes |
src/midi/controllers.py |
~350 | Known controller profiles database |
src/midi/device_discovery.py |
~250 | USB MIDI enumeration + hotplug monitor |
src/midi/midi_engine.py |
~260 | Core mapping engine + NRPN state machine |
src/midi/midi_learn.py |
~310 | Interactive learn mode state machine |
src/midi/midi_clock.py |
~210 | MIDI clock sync + tempo detection |
src/midi/mapping_store.py |
~140 | JSON persistence (save/load/list/delete) |
src/midi/jack_midi_bridge.py |
~240 | JACK MIDI port creation + ALSA fallback |
src/midi/__init__.py |
~40 | Package exports |
src/mixer/__init__.py |
~165 | Parameter registry + channel strip factory |
scripts/midi-mapper |
~290 | Main daemon entry point |
scripts/midi-learn-cli |
~295 | Interactive learn mode CLI |
config/99-midi.rules |
~60 | udev rules for USB MIDI hotplug |
docs/midi-controller-support.md |
This file | Full documentation |
Total: ~2,800 lines of Python + config + docs