9cd8292acc
Lint & Validate / lint (push) Has been cancelled
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)
517 lines
17 KiB
Markdown
517 lines
17 KiB
Markdown
# 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 <client_id>`
|
|
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 <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:
|
|
|
|
```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**
|