# 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 1. [Architecture Overview](#1-architecture-overview) 2. [Hardware Setup — RPi as USB Host](#2-hardware-setup) 3. [Supported Controllers](#3-supported-controllers) 4. [Installation](#4-installation) 5. [MIDI Learn Mode](#5-midi-learn-mode) 6. [MIDI Clock Sync](#6-midi-clock-sync) 7. [JACK MIDI Integration](#7-jack-midi-integration) 8. [Mapping Persistence](#8-mapping-persistence) 9. [Udev Rules — Hotplug](#9-udev-rules) 10. [MIDI Mapper Daemon](#10-midi-mapper-daemon) 11. [NRPN Support](#11-nrpn-support) 12. [File Reference](#12-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:** 1. USB MIDI controller sends CC/NRPN messages over USB 2. Linux kernel (snd-usb-audio) exposes them as ALSA sequencer clients 3. Python MIDI engine reads from ALSA seq / rtmidi 4. Engine matches messages against the mapping table 5. 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:** ```bash # 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`: ```python 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: 1. Install `aseqdump`: `sudo apt install alsa-utils` 2. Run: `aseqdump -p ` 3. Wiggle each control and note the CC number --- ## 4. Installation ### Prerequisites ```bash # 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 ```bash # 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. ```bash ./scripts/midi-learn-cli ``` ### Workflow 1. **Plug in your MIDI controller** (before or during) 2. **Start learn mode:** `./scripts/midi-learn-cli` 3. **Pick a parameter:** type `l volume 0` (learn volume for channel 0) 4. **Wiggle the control** on your physical controller 5. **Confirm:** type `c` (or `d` to discard) 6. **Save:** type `save` (or just `quit` — auto-saves) ### Commands | Command | Description | |---|---| | `l [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 ` | 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: ```bash ./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: ```bash ./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: ```bash 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 ```json { "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: ```bash ./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: 1. **Automatic permissions:** MIDI devices owned by `audio` group (mode 0660) 2. **Persistent symlinks:** Known controllers get stable names under `/dev/midi/` 3. **Environment tagging:** `MIDI_CONTROLLER` env var set for known devices 4. **Hotplug service trigger:** Systemd service can auto-restart on plug/unplug Install: ```bash 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 ```bash # 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 stats - `SIGTERM`: 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) ```ini # /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: 1. Controller sends CC 99 (NRPN MSB) + CC 98 (NRPN LSB) to select parameter 2. Controller sends CC 6 (Data MSB) + CC 38 (Data LSB) for value 3. Engine assembles into a 14-bit value 4. 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**