Files
raspberry-pi-mixer/docs/midi-controller-support.md
shawn 9cd8292acc
Lint & Validate / lint (push) Has been cancelled
Phase 1-4: Audio stack, mixer engine, MIDI, and network API
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)
2026-05-19 20:39:17 -04:00

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

  1. Architecture Overview
  2. Hardware Setup — RPi as USB Host
  3. Supported Controllers
  4. Installation
  5. MIDI Learn Mode
  6. MIDI Clock Sync
  7. JACK MIDI Integration
  8. Mapping Persistence
  9. Udev Rules — Hotplug
  10. MIDI Mapper Daemon
  11. NRPN Support
  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:

# 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:

  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

# 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

  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:

./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:

  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:

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 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)

# /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