auto-detect MIDI device, drop ALSA sequencer dependency, MPK Mini simulator

midi-bridge.py:
  - Auto-detect rawmidi device (gadget f_midi > env override > fallback)
  - Reconnect on ENODEV (device hotplug)
  - No longer needs MIDI_BRIDGE_DEV env var in common cases

start-synth.sh:
  - Remove all -alsamidi/-midiadddev flags (kernel lacks CONFIG_SND_SEQUENCER)
  - MIDI handled entirely by midi-bridge.service → UDP → netreceive
  - Host mode: same approach, no ALSA sequencer needed

mpk-mini-sim.pd:
  - New MPK Mini Mk II simulator (25 keys, 8 pads, 8 CC knobs)
  - Proper Pd #X obj format for all objects (bng, hsl, t, del, netsend, loadbang)
  - UDP output to board:8081, matching midi-bridge format

system: enable usb-gadget-audio.service at boot, load dwc3 module at boot
This commit is contained in:
2026-06-22 23:10:38 -07:00
parent 8cd796e3dd
commit dbd7c2cece
5 changed files with 642 additions and 65 deletions
+448
View File
@@ -0,0 +1,448 @@
#N canvas 0 0 950 490 10;
#X obj 10 10 loadbang ;
#X msg 10 30 ;;
#X msg 10 50 connect 192.168.9.167 8081;;
#X obj 10 80 netsend -u -b;
#X text 200 30 ← edit this line to change target IP:port;
#X text 10 440 Click keys to play. Knobs send CC on change. IPS (192.168.9.167:8081);
#X text 10 460 MIDI ch: 144=note-on ch1, 128=note-off ch1, 176=CC ch1;
#X text 20 120 -- KEYBOARD --;
#X text 30 140 C3;
#X obj 30 154 bng 15 25 0 1 0 0 empty k48 -2 -8 0 10 -262144 -1 -1;
#X obj 30 184 t b b;
#X msg 30 204 midi 144 48 100;;
#X msg 30 222 midi 128 48 0;;
#X obj 30 240 del 300;
#X text 76 140 C#3;
#X obj 76 154 bng 15 25 0 1 0 0 empty k49 -2 -8 0 10 -262144 -1 -1;
#X obj 76 184 t b b;
#X msg 76 204 midi 144 49 100;;
#X msg 76 222 midi 128 49 0;;
#X obj 76 240 del 300;
#X text 122 140 D3;
#X obj 122 154 bng 15 25 0 1 0 0 empty k50 -2 -8 0 10 -262144 -1 -1;
#X obj 122 184 t b b;
#X msg 122 204 midi 144 50 100;;
#X msg 122 222 midi 128 50 0;;
#X obj 122 240 del 300;
#X text 168 140 D#3;
#X obj 168 154 bng 15 25 0 1 0 0 empty k51 -2 -8 0 10 -262144 -1 -1;
#X obj 168 184 t b b;
#X msg 168 204 midi 144 51 100;;
#X msg 168 222 midi 128 51 0;;
#X obj 168 240 del 300;
#X text 214 140 E3;
#X obj 214 154 bng 15 25 0 1 0 0 empty k52 -2 -8 0 10 -262144 -1 -1;
#X obj 214 184 t b b;
#X msg 214 204 midi 144 52 100;;
#X msg 214 222 midi 128 52 0;;
#X obj 214 240 del 300;
#X text 260 140 F3;
#X obj 260 154 bng 15 25 0 1 0 0 empty k53 -2 -8 0 10 -262144 -1 -1;
#X obj 260 184 t b b;
#X msg 260 204 midi 144 53 100;;
#X msg 260 222 midi 128 53 0;;
#X obj 260 240 del 300;
#X text 306 140 F#3;
#X obj 306 154 bng 15 25 0 1 0 0 empty k54 -2 -8 0 10 -262144 -1 -1;
#X obj 306 184 t b b;
#X msg 306 204 midi 144 54 100;;
#X msg 306 222 midi 128 54 0;;
#X obj 306 240 del 300;
#X text 352 140 G3;
#X obj 352 154 bng 15 25 0 1 0 0 empty k55 -2 -8 0 10 -262144 -1 -1;
#X obj 352 184 t b b;
#X msg 352 204 midi 144 55 100;;
#X msg 352 222 midi 128 55 0;;
#X obj 352 240 del 300;
#X text 398 140 G#3;
#X obj 398 154 bng 15 25 0 1 0 0 empty k56 -2 -8 0 10 -262144 -1 -1;
#X obj 398 184 t b b;
#X msg 398 204 midi 144 56 100;;
#X msg 398 222 midi 128 56 0;;
#X obj 398 240 del 300;
#X text 444 140 A3;
#X obj 444 154 bng 15 25 0 1 0 0 empty k57 -2 -8 0 10 -262144 -1 -1;
#X obj 444 184 t b b;
#X msg 444 204 midi 144 57 100;;
#X msg 444 222 midi 128 57 0;;
#X obj 444 240 del 300;
#X text 490 140 A#3;
#X obj 490 154 bng 15 25 0 1 0 0 empty k58 -2 -8 0 10 -262144 -1 -1;
#X obj 490 184 t b b;
#X msg 490 204 midi 144 58 100;;
#X msg 490 222 midi 128 58 0;;
#X obj 490 240 del 300;
#X text 536 140 B3;
#X obj 536 154 bng 15 25 0 1 0 0 empty k59 -2 -8 0 10 -262144 -1 -1;
#X obj 536 184 t b b;
#X msg 536 204 midi 144 59 100;;
#X msg 536 222 midi 128 59 0;;
#X obj 536 240 del 300;
#X text 582 140 C4;
#X obj 582 154 bng 15 25 0 1 0 0 empty k60 -2 -8 0 10 -262144 -1 -1;
#X obj 582 184 t b b;
#X msg 582 204 midi 144 60 100;;
#X msg 582 222 midi 128 60 0;;
#X obj 582 240 del 300;
#X text 30 200 C#4;
#X obj 30 214 bng 15 25 0 1 0 0 empty k61 -2 -8 0 10 -262144 -1 -1;
#X obj 30 244 t b b;
#X msg 30 264 midi 144 61 100;;
#X msg 30 282 midi 128 61 0;;
#X obj 30 300 del 300;
#X text 76 200 D4;
#X obj 76 214 bng 15 25 0 1 0 0 empty k62 -2 -8 0 10 -262144 -1 -1;
#X obj 76 244 t b b;
#X msg 76 264 midi 144 62 100;;
#X msg 76 282 midi 128 62 0;;
#X obj 76 300 del 300;
#X text 122 200 D#4;
#X obj 122 214 bng 15 25 0 1 0 0 empty k63 -2 -8 0 10 -262144 -1 -1;
#X obj 122 244 t b b;
#X msg 122 264 midi 144 63 100;;
#X msg 122 282 midi 128 63 0;;
#X obj 122 300 del 300;
#X text 168 200 E4;
#X obj 168 214 bng 15 25 0 1 0 0 empty k64 -2 -8 0 10 -262144 -1 -1;
#X obj 168 244 t b b;
#X msg 168 264 midi 144 64 100;;
#X msg 168 282 midi 128 64 0;;
#X obj 168 300 del 300;
#X text 214 200 F4;
#X obj 214 214 bng 15 25 0 1 0 0 empty k65 -2 -8 0 10 -262144 -1 -1;
#X obj 214 244 t b b;
#X msg 214 264 midi 144 65 100;;
#X msg 214 282 midi 128 65 0;;
#X obj 214 300 del 300;
#X text 260 200 F#4;
#X obj 260 214 bng 15 25 0 1 0 0 empty k66 -2 -8 0 10 -262144 -1 -1;
#X obj 260 244 t b b;
#X msg 260 264 midi 144 66 100;;
#X msg 260 282 midi 128 66 0;;
#X obj 260 300 del 300;
#X text 306 200 G4;
#X obj 306 214 bng 15 25 0 1 0 0 empty k67 -2 -8 0 10 -262144 -1 -1;
#X obj 306 244 t b b;
#X msg 306 264 midi 144 67 100;;
#X msg 306 282 midi 128 67 0;;
#X obj 306 300 del 300;
#X text 352 200 G#4;
#X obj 352 214 bng 15 25 0 1 0 0 empty k68 -2 -8 0 10 -262144 -1 -1;
#X obj 352 244 t b b;
#X msg 352 264 midi 144 68 100;;
#X msg 352 282 midi 128 68 0;;
#X obj 352 300 del 300;
#X text 398 200 A4;
#X obj 398 214 bng 15 25 0 1 0 0 empty k69 -2 -8 0 10 -262144 -1 -1;
#X obj 398 244 t b b;
#X msg 398 264 midi 144 69 100;;
#X msg 398 282 midi 128 69 0;;
#X obj 398 300 del 300;
#X text 444 200 A#4;
#X obj 444 214 bng 15 25 0 1 0 0 empty k70 -2 -8 0 10 -262144 -1 -1;
#X obj 444 244 t b b;
#X msg 444 264 midi 144 70 100;;
#X msg 444 282 midi 128 70 0;;
#X obj 444 300 del 300;
#X text 490 200 B4;
#X obj 490 214 bng 15 25 0 1 0 0 empty k71 -2 -8 0 10 -262144 -1 -1;
#X obj 490 244 t b b;
#X msg 490 264 midi 144 71 100;;
#X msg 490 282 midi 128 71 0;;
#X obj 490 300 del 300;
#X text 536 200 C5;
#X obj 536 214 bng 15 25 0 1 0 0 empty k72 -2 -8 0 10 -262144 -1 -1;
#X obj 536 244 t b b;
#X msg 536 264 midi 144 72 100;;
#X msg 536 282 midi 128 72 0;;
#X obj 536 300 del 300;
#X text 680 120 -- PADS --;
#X text 680 140 C2;
#X obj 680 154 bng 15 15 0 1 0 0 empty p36 -2 -8 0 10 -262144 -1 -1;
#X obj 680 172 t b b;
#X msg 680 192 midi 144 36 100;;
#X msg 680 210 midi 128 36 0;;
#X obj 680 228 del 200;
#X text 735 140 C#2;
#X obj 735 154 bng 15 15 0 1 0 0 empty p37 -2 -8 0 10 -262144 -1 -1;
#X obj 735 172 t b b;
#X msg 735 192 midi 144 37 100;;
#X msg 735 210 midi 128 37 0;;
#X obj 735 228 del 200;
#X text 790 140 D2;
#X obj 790 154 bng 15 15 0 1 0 0 empty p38 -2 -8 0 10 -262144 -1 -1;
#X obj 790 172 t b b;
#X msg 790 192 midi 144 38 100;;
#X msg 790 210 midi 128 38 0;;
#X obj 790 228 del 200;
#X text 845 140 D#2;
#X obj 845 154 bng 15 15 0 1 0 0 empty p39 -2 -8 0 10 -262144 -1 -1;
#X obj 845 172 t b b;
#X msg 845 192 midi 144 39 100;;
#X msg 845 210 midi 128 39 0;;
#X obj 845 228 del 200;
#X text 680 195 E2;
#X obj 680 209 bng 15 15 0 1 0 0 empty p40 -2 -8 0 10 -262144 -1 -1;
#X obj 680 227 t b b;
#X msg 680 247 midi 144 40 100;;
#X msg 680 265 midi 128 40 0;;
#X obj 680 283 del 200;
#X text 735 195 F2;
#X obj 735 209 bng 15 15 0 1 0 0 empty p41 -2 -8 0 10 -262144 -1 -1;
#X obj 735 227 t b b;
#X msg 735 247 midi 144 41 100;;
#X msg 735 265 midi 128 41 0;;
#X obj 735 283 del 200;
#X text 790 195 F#2;
#X obj 790 209 bng 15 15 0 1 0 0 empty p42 -2 -8 0 10 -262144 -1 -1;
#X obj 790 227 t b b;
#X msg 790 247 midi 144 42 100;;
#X msg 790 265 midi 128 42 0;;
#X obj 790 283 del 200;
#X text 845 195 G2;
#X obj 845 209 bng 15 15 0 1 0 0 empty p43 -2 -8 0 10 -262144 -1 -1;
#X obj 845 227 t b b;
#X msg 845 247 midi 144 43 100;;
#X msg 845 265 midi 128 43 0;;
#X obj 845 283 del 200;
#X text 20 280 -- KNOBS --;
#X text 30 300 CC1 Mod;
#X obj 30 315 hsl 50 10 0 127 64 0 0 empty cc1 -2 -8 0 10 -262144 -1 -1 7200 1;
#X msg 30 330 midi 176 1 $1;;
#X text 115 300 CC71 Reson;
#X obj 115 315 hsl 50 10 0 127 64 0 0 empty cc71 -2 -8 0 10 -262144 -1 -1 7200 1;
#X msg 115 330 midi 176 71 $1;;
#X text 200 300 CC72 Release;
#X obj 200 315 hsl 50 10 0 127 64 0 0 empty cc72 -2 -8 0 10 -262144 -1 -1 7200 1;
#X msg 200 330 midi 176 72 $1;;
#X text 285 300 CC73 Attack;
#X obj 285 315 hsl 50 10 0 127 64 0 0 empty cc73 -2 -8 0 10 -262144 -1 -1 7200 1;
#X msg 285 330 midi 176 73 $1;;
#X text 370 300 CC74 Cutoff;
#X obj 370 315 hsl 50 10 0 127 100 0 0 empty cc74 -2 -8 0 10 -262144 -1 -1 7200 1;
#X msg 370 330 midi 176 74 $1;;
#X text 455 300 CC75 Decay;
#X obj 455 315 hsl 50 10 0 127 64 0 0 empty cc75 -2 -8 0 10 -262144 -1 -1 7200 1;
#X msg 455 330 midi 176 75 $1;;
#X text 540 300 CC76 LFO Rate;
#X obj 540 315 hsl 50 10 0 127 64 0 0 empty cc76 -2 -8 0 10 -262144 -1 -1 7200 1;
#X msg 540 330 midi 176 76 $1;;
#X text 625 300 CC77 LFO Depth;
#X obj 625 315 hsl 50 10 0 127 64 0 0 empty cc77 -2 -8 0 10 -262144 -1 -1 7200 1;
#X msg 625 330 midi 176 77 $1;;
#X connect 2 0 3 0;
#X connect 9 0 10 0;
#X connect 10 0 11 0;
#X connect 11 0 3 0;
#X connect 10 1 13 0;
#X connect 13 0 12 0;
#X connect 12 0 3 0;
#X connect 15 0 16 0;
#X connect 16 0 17 0;
#X connect 17 0 3 0;
#X connect 16 1 19 0;
#X connect 19 0 18 0;
#X connect 18 0 3 0;
#X connect 21 0 22 0;
#X connect 22 0 23 0;
#X connect 23 0 3 0;
#X connect 22 1 25 0;
#X connect 25 0 24 0;
#X connect 24 0 3 0;
#X connect 27 0 28 0;
#X connect 28 0 29 0;
#X connect 29 0 3 0;
#X connect 28 1 31 0;
#X connect 31 0 30 0;
#X connect 30 0 3 0;
#X connect 33 0 34 0;
#X connect 34 0 35 0;
#X connect 35 0 3 0;
#X connect 34 1 37 0;
#X connect 37 0 36 0;
#X connect 36 0 3 0;
#X connect 39 0 40 0;
#X connect 40 0 41 0;
#X connect 41 0 3 0;
#X connect 40 1 43 0;
#X connect 43 0 42 0;
#X connect 42 0 3 0;
#X connect 45 0 46 0;
#X connect 46 0 47 0;
#X connect 47 0 3 0;
#X connect 46 1 49 0;
#X connect 49 0 48 0;
#X connect 48 0 3 0;
#X connect 51 0 52 0;
#X connect 52 0 53 0;
#X connect 53 0 3 0;
#X connect 52 1 55 0;
#X connect 55 0 54 0;
#X connect 54 0 3 0;
#X connect 57 0 58 0;
#X connect 58 0 59 0;
#X connect 59 0 3 0;
#X connect 58 1 61 0;
#X connect 61 0 60 0;
#X connect 60 0 3 0;
#X connect 63 0 64 0;
#X connect 64 0 65 0;
#X connect 65 0 3 0;
#X connect 64 1 67 0;
#X connect 67 0 66 0;
#X connect 66 0 3 0;
#X connect 69 0 70 0;
#X connect 70 0 71 0;
#X connect 71 0 3 0;
#X connect 70 1 73 0;
#X connect 73 0 72 0;
#X connect 72 0 3 0;
#X connect 75 0 76 0;
#X connect 76 0 77 0;
#X connect 77 0 3 0;
#X connect 76 1 79 0;
#X connect 79 0 78 0;
#X connect 78 0 3 0;
#X connect 81 0 82 0;
#X connect 82 0 83 0;
#X connect 83 0 3 0;
#X connect 82 1 85 0;
#X connect 85 0 84 0;
#X connect 84 0 3 0;
#X connect 87 0 88 0;
#X connect 88 0 89 0;
#X connect 89 0 3 0;
#X connect 88 1 91 0;
#X connect 91 0 90 0;
#X connect 90 0 3 0;
#X connect 93 0 94 0;
#X connect 94 0 95 0;
#X connect 95 0 3 0;
#X connect 94 1 97 0;
#X connect 97 0 96 0;
#X connect 96 0 3 0;
#X connect 99 0 100 0;
#X connect 100 0 101 0;
#X connect 101 0 3 0;
#X connect 100 1 103 0;
#X connect 103 0 102 0;
#X connect 102 0 3 0;
#X connect 105 0 106 0;
#X connect 106 0 107 0;
#X connect 107 0 3 0;
#X connect 106 1 109 0;
#X connect 109 0 108 0;
#X connect 108 0 3 0;
#X connect 111 0 112 0;
#X connect 112 0 113 0;
#X connect 113 0 3 0;
#X connect 112 1 115 0;
#X connect 115 0 114 0;
#X connect 114 0 3 0;
#X connect 117 0 118 0;
#X connect 118 0 119 0;
#X connect 119 0 3 0;
#X connect 118 1 121 0;
#X connect 121 0 120 0;
#X connect 120 0 3 0;
#X connect 123 0 124 0;
#X connect 124 0 125 0;
#X connect 125 0 3 0;
#X connect 124 1 127 0;
#X connect 127 0 126 0;
#X connect 126 0 3 0;
#X connect 129 0 130 0;
#X connect 130 0 131 0;
#X connect 131 0 3 0;
#X connect 130 1 133 0;
#X connect 133 0 132 0;
#X connect 132 0 3 0;
#X connect 135 0 136 0;
#X connect 136 0 137 0;
#X connect 137 0 3 0;
#X connect 136 1 139 0;
#X connect 139 0 138 0;
#X connect 138 0 3 0;
#X connect 141 0 142 0;
#X connect 142 0 143 0;
#X connect 143 0 3 0;
#X connect 142 1 145 0;
#X connect 145 0 144 0;
#X connect 144 0 3 0;
#X connect 147 0 148 0;
#X connect 148 0 149 0;
#X connect 149 0 3 0;
#X connect 148 1 151 0;
#X connect 151 0 150 0;
#X connect 150 0 3 0;
#X connect 153 0 154 0;
#X connect 154 0 155 0;
#X connect 155 0 3 0;
#X connect 154 1 157 0;
#X connect 157 0 156 0;
#X connect 156 0 3 0;
#X connect 160 0 161 0;
#X connect 161 0 162 0;
#X connect 162 0 3 0;
#X connect 161 1 164 0;
#X connect 164 0 163 0;
#X connect 163 0 3 0;
#X connect 166 0 167 0;
#X connect 167 0 168 0;
#X connect 168 0 3 0;
#X connect 167 1 170 0;
#X connect 170 0 169 0;
#X connect 169 0 3 0;
#X connect 172 0 173 0;
#X connect 173 0 174 0;
#X connect 174 0 3 0;
#X connect 173 1 176 0;
#X connect 176 0 175 0;
#X connect 175 0 3 0;
#X connect 178 0 179 0;
#X connect 179 0 180 0;
#X connect 180 0 3 0;
#X connect 179 1 182 0;
#X connect 182 0 181 0;
#X connect 181 0 3 0;
#X connect 184 0 185 0;
#X connect 185 0 186 0;
#X connect 186 0 3 0;
#X connect 185 1 188 0;
#X connect 188 0 187 0;
#X connect 187 0 3 0;
#X connect 190 0 191 0;
#X connect 191 0 192 0;
#X connect 192 0 3 0;
#X connect 191 1 194 0;
#X connect 194 0 193 0;
#X connect 193 0 3 0;
#X connect 196 0 197 0;
#X connect 197 0 198 0;
#X connect 198 0 3 0;
#X connect 197 1 200 0;
#X connect 200 0 199 0;
#X connect 199 0 3 0;
#X connect 202 0 203 0;
#X connect 203 0 204 0;
#X connect 204 0 3 0;
#X connect 203 1 206 0;
#X connect 206 0 205 0;
#X connect 205 0 3 0;
#X connect 209 0 210 0;
#X connect 210 0 3 0;
#X connect 212 0 213 0;
#X connect 213 0 3 0;
#X connect 215 0 216 0;
#X connect 216 0 3 0;
#X connect 218 0 219 0;
#X connect 219 0 3 0;
#X connect 221 0 222 0;
#X connect 222 0 3 0;
#X connect 224 0 225 0;
#X connect 225 0 3 0;
#X connect 227 0 228 0;
#X connect 228 0 3 0;
#X connect 230 0 231 0;
#X connect 231 0 3 0;
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""Generate mpk-mini-sim.pd — MPK Mini Mk II simulator.
Architecture: each key/pad has bng→t b b→msg for note-on, del→msg for note-off.
All msgs connect to a single [netsend -u -b] via send/receive.
Knobs: hsl→msg→[s $0-cc], central [r $0-cc]→netsend.
"""
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
OUTPUT = SCRIPT_DIR.parent / "pd" / "mpk-mini-sim.pd"
def connect(c, src, sout, dst, din):
c.append((src, sout, dst, din))
def Obj(o, x, y, *args):
idx = len(o)
o.append((x, y, *args))
return idx
def gen():
o, c = [], []
# ── UDP sender (hardcoded IP — edit the msg box to change) ──
Obj(o, 10, 10, "loadbang")
Obj(o, 10, 30, "msg", ";")
msg_conn = Obj(o, 10, 50, "msg", "connect 192.168.9.167 8081;")
ns = Obj(o, 10, 80, "netsend", "-u", "-b")
connect(c, msg_conn, 0, ns, 0)
Obj(o, 200, 30, "text", "← edit this line to change target IP:port")
instr = Obj(o, 10, 440, "text", "Click keys to play. Knobs send CC on change. IPS (192.168.9.167:8081)")
instr2 = Obj(o, 10, 460, "text", "MIDI ch: 144=note-on ch1, 128=note-off ch1, 176=CC ch1")
# ── Keyboard: C3(48) to C5(72) ──
keys = [
("C3",48),("C#3",49),("D3",50),("D#3",51),("E3",52),
("F3",53),("F#3",54),("G3",55),("G#3",56),("A3",57),
("A#3",58),("B3",59),
("C4",60),("C#4",61),("D4",62),("D#4",63),("E4",64),
("F4",65),("F#4",66),("G4",67),("G#4",68),("A4",69),
("A#4",70),("B4",71),("C5",72),
]
Obj(o, 20, 120, "text", "-- KEYBOARD --")
for i, (name, note) in enumerate(keys):
x = 30 + (i % 13) * 46
y = 140 + (i // 13) * 60
Obj(o, x, y, "text", name)
b = Obj(o, x, y+14, "bng", 15, 25, 0, 1, 0, 0, "empty", f"k{note}", -2,-8,0,10,-262144,-1,-1)
trig = Obj(o, x, y+44, "t", "b b")
msg_on = Obj(o, x, y+64, "msg", f"midi 144 {note} 100;")
msg_off = Obj(o, x, y+82, "msg", f"midi 128 {note} 0;")
del300 = Obj(o, x, y+100, "del", 300)
connect(c, b, 0, trig, 0)
connect(c, trig, 0, msg_on, 0)
connect(c, msg_on, 0, ns, 0)
connect(c, trig, 1, del300, 0)
connect(c, del300, 0, msg_off, 0)
connect(c, msg_off, 0, ns, 0)
# ── Pads (C2-G2, notes 36-43) ──
pads = [("C2",36),("C#2",37),("D2",38),("D#2",39),("E2",40),("F2",41),("F#2",42),("G2",43)]
Obj(o, 680, 120, "text", "-- PADS --")
for i, (name, note) in enumerate(pads):
x = 680 + (i % 4) * 55
y = 140 + (i // 4) * 55
Obj(o, x, y, "text", name)
b = Obj(o, x, y+14, "bng", 15, 15, 0, 1, 0, 0, "empty", f"p{note}", -2,-8,0,10,-262144,-1,-1)
trig = Obj(o, x, y+32, "t", "b b")
msg_on = Obj(o, x, y+52, "msg", f"midi 144 {note} 100;")
msg_off = Obj(o, x, y+70, "msg", f"midi 128 {note} 0;")
del200 = Obj(o, x, y+88, "del", 200)
connect(c, b, 0, trig, 0)
connect(c, trig, 0, msg_on, 0)
connect(c, msg_on, 0, ns, 0)
connect(c, trig, 1, del200, 0)
connect(c, del200, 0, msg_off, 0)
connect(c, msg_off, 0, ns, 0)
# ── Knobs (CC 1, 71-77) ──
knobs = [(1,"Mod",64),(71,"Reson",64),(72,"Release",64),(73,"Attack",64),
(74,"Cutoff",100),(75,"Decay",64),(76,"LFO Rate",64),(77,"LFO Depth",64)]
Obj(o, 20, 280, "text", "-- KNOBS --")
for i, (cc, name, init) in enumerate(knobs):
x = 30 + i * 85
y = 300
Obj(o, x, y, "text", f"CC{cc} {name}")
hsl = Obj(o, x, y+15, "hsl", 50, 10, 0, 127, init, 0, 0, "empty", f"cc{cc}", -2,-8,0,10,-262144,-1,-1,7200,1)
msg = Obj(o, x, y+30, "msg", f"midi 176 {cc} $1;")
connect(c, hsl, 0, msg, 0)
connect(c, msg, 0, ns, 0)
# ── Write (Pd format: #X obj for objects, #X msg/text/floatatom/symbol for others) ──
lines = ["#N canvas 0 0 950 490 10;"]
obj_types = {"bng","t","del","netsend","loadbang","hsl","comment"}
for x, y, *args in o:
etype = args[0]
rest = args[1:]
if etype in obj_types:
# Pd stores GUI objects as: #X obj x y <name> <args>
name = etype
if name == "comment": name = "text" # Pd uses text for comments
joined = " ".join(str(a) for a in rest)
lines.append(f"#X obj {x} {y} {name} {joined};")
else:
joined = " ".join(str(a) for a in rest)
lines.append(f"#X {etype} {x} {y} {joined};")
for src, sout, dst, din in c:
lines.append(f"#X connect {src} {sout} {dst} {din};")
lines.append("")
return "\n".join(lines)
if __name__ == "__main__":
content = gen()
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
OUTPUT.write_text(content)
print(f"Wrote {OUTPUT}")
print(f" {len(content.splitlines())} lines, {content.count('#X obj')} objects, {content.count('#X connect')} connections")
+65 -39
View File
@@ -1,76 +1,102 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Bridge raw MIDI device to Pd via UDP. For host mode with MPK Mini. """Bridge raw MIDI device to Pd via UDP. Auto-detects gadget vs host mode.
Sends parsed MIDI messages as Pd-style 'midi status data1 data2;' strings Sends parsed MIDI messages as Pd-style 'midi status data1 data2;' strings
to UDP ports for Pd (8081) and LED matrix visualization (8082). to UDP ports for Pd (8081) and LED matrix visualization (8082).
""" """
import os, socket, time, select, struct import os, socket, time, select, pathlib
MIDI_DEV = os.environ.get("MIDI_BRIDGE_DEV", "/dev/snd/midiC0D0")
PD_HOST = os.environ.get("MIDI_BRIDGE_HOST", "127.0.0.1") PD_HOST = os.environ.get("MIDI_BRIDGE_HOST", "127.0.0.1")
PD_PORT = int(os.environ.get("MIDI_BRIDGE_PORT", "8081")) PD_PORT = int(os.environ.get("MIDI_BRIDGE_PORT", "8081"))
VIZ_PORT = int(os.environ.get("VIZ_PORT", "8082")) VIZ_PORT = int(os.environ.get("VIZ_PORT", "8082"))
def _find_midi_dev():
"""Auto-detect rawmidi device. Preference: gadget f_midi > env override > host mode."""
dev = os.environ.get("MIDI_BRIDGE_DEV")
if dev:
return dev
# Look for gadget f_midi card first
cards = pathlib.Path("/proc/asound").glob("card*")
for card_dir in cards:
cmi = card_dir / "id"
if cmi.exists() and "midi" in cmi.read_text().strip():
card_num = card_dir.name.replace("card", "")
midi_dev = f"/dev/snd/midiC{card_num}D0"
if os.path.exists(midi_dev):
return midi_dev
# Fallback: any rawmidi device
rawdevs = sorted(pathlib.Path("/dev/snd").glob("midiC?D0"))
if rawdevs:
return str(rawdevs[0])
return "/dev/snd/midiC0D0" # fallback for host mode
def midi_msg(status, data1, data2=None): def midi_msg(status, data1, data2=None):
if data2 is not None: if data2 is not None:
return f"midi {status} {data1} {data2};" return f"midi {status} {data1} {data2};"
return f"midi {status} {data1};" return f"midi {status} {data1};"
def main(): def _open_midi():
while not os.path.exists(MIDI_DEV): midi_dev = _find_midi_dev()
while not os.path.exists(midi_dev):
time.sleep(0.5) time.sleep(0.5)
midi_dev = _find_midi_dev()
fd = os.open(MIDI_DEV, os.O_RDWR | os.O_NONBLOCK) fd = os.open(midi_dev, os.O_RDWR | os.O_NONBLOCK)
os.set_blocking(fd, False) os.set_blocking(fd, False)
poll = select.poll() return fd
poll.register(fd, select.POLLIN)
def main():
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
running = True running = True
buf = bytearray()
while running: while running:
try: try:
events = poll.poll(500) fd = _open_midi()
for f, flags in events: poll = select.poll()
if flags & select.POLLIN: poll.register(fd, select.POLLIN)
data = os.read(fd, 1024) buf = bytearray()
if not data: while running:
continue try:
for byte in data: events = poll.poll(500)
if byte & 0x80: for f, flags in events:
if len(buf) > 0 and buf[0] >= 0x80: if flags & select.POLLIN:
_flush(sock, buf, PD_HOST, PD_PORT, VIZ_PORT) data = os.read(fd, 1024)
buf = bytearray([byte]) if not data:
else: continue
buf.append(byte) for byte in data:
if len(buf) >= 3 and (buf[0] & 0xE0) in (0x80, 0x90, 0xA0, 0xB0, 0xE0): if byte & 0x80:
_flush(sock, buf, PD_HOST, PD_PORT, VIZ_PORT) if len(buf) > 0 and buf[0] >= 0x80:
buf = bytearray() _flush(sock, buf)
elif len(buf) >= 2 and (buf[0] & 0xE0) in (0xC0, 0xD0): buf = bytearray([byte])
_flush(sock, buf, PD_HOST, PD_PORT, VIZ_PORT) else:
buf = bytearray() buf.append(byte)
except (BlockingIOError, OSError) as e: if len(buf) >= 3 and (buf[0] & 0xE0) in (0x80, 0x90, 0xA0, 0xB0, 0xE0):
if isinstance(e, OSError) and e.errno == 19: _flush(sock, buf)
time.sleep(1) buf = bytearray()
continue elif len(buf) >= 2 and (buf[0] & 0xE0) in (0xC0, 0xD0):
if not isinstance(e, BlockingIOError): _flush(sock, buf)
raise buf = bytearray()
except OSError as e:
if e.errno == 19:
os.close(fd)
time.sleep(1)
break
raise
except BlockingIOError:
pass
except KeyboardInterrupt: except KeyboardInterrupt:
running = False running = False
os.close(fd)
sock.close() sock.close()
def _flush(sock, buf, pd_host, pd_port, viz_port): def _flush(sock, buf):
status = buf[0] status = buf[0]
if (buf[0] & 0xF0) == 0xF0: if (buf[0] & 0xF0) == 0xF0:
return return
data1 = buf[1] data1 = buf[1]
data2 = buf[2] if len(buf) >= 3 else None data2 = buf[2] if len(buf) >= 3 else None
msg = midi_msg(status, data1, data2) msg = midi_msg(status, data1, data2)
sock.sendto(msg.encode(), (pd_host, pd_port)) sock.sendto(msg.encode(), (PD_HOST, PD_PORT))
sock.sendto(msg.encode(), (pd_host, viz_port)) sock.sendto(msg.encode(), (PD_HOST, VIZ_PORT))
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+7 -25
View File
@@ -56,17 +56,11 @@ if [ "$MODE" = "auto" ]; then
ALSA_DEV=$(cat /run/usb-audio-dev) ALSA_DEV=$(cat /run/usb-audio-dev)
fi fi
info "Mode: USB host (MIDI controller + USB audio interface)" info "Mode: USB host (MIDI controller + USB audio interface)"
MIDI_OPTS=(-alsamidi) # MIDI handled by midi-bridge.service → UDP → Pd netreceive
midi_dev=$(amidi -l 2>/dev/null | grep -v "^Dir" | head -1 | awk '{print $2}' | sed 's/\(hw:[0-9]*,[0-9]*\).*/\1/')
if [ -n "$midi_dev" ]; then
MIDI_OPTS+=(-midiadddev "$midi_dev")
info " MIDI device: $midi_dev"
fi
AUDIO_OPTS=(-audiooutdev 4 -audioindev 4) AUDIO_OPTS=(-audiooutdev 4 -audioindev 4)
PD_ARGS=( PD_ARGS=(
-nogui -alsa -nogui -alsa
-send "pd dsp 1" -send "pd dsp 1"
"${MIDI_OPTS[@]}"
"${AUDIO_OPTS[@]}" "${AUDIO_OPTS[@]}"
-audiobuf 20 -blocksize 64 -r 48000 -audiobuf 20 -blocksize 64 -r 48000
-channels 2 -inchannels 0 -outchannels 2 -channels 2 -inchannels 0 -outchannels 2
@@ -87,18 +81,13 @@ fi
# --- Configure per-mode options --- # --- Configure per-mode options ---
AUDIO_OPTS=() AUDIO_OPTS=()
MIDI_OPTS=(-alsamidi)
if [ "$MODE" = "usb" ]; then if [ "$MODE" = "usb" ]; then
info "Mode: USB gadget (UAC1 audio + gmidi MIDI)" info "Mode: USB gadget (UAC1 audio + gmidi MIDI)"
AUDIO_OPTS=(-audiooutdev 2 -audioindev 2) AUDIO_OPTS=(-audiooutdev 2 -audioindev 2)
if aplay -l 2>/dev/null | grep -q "UAC1Gadget"; then if aplay -l 2>/dev/null | grep -q "UAC1Gadget"; then
info " USB gadget: OK" info " USB gadget: OK"
midi_dev=$(amidi -l 2>/dev/null | grep "f_midi" | head -1 | awk '{print $2}') # MIDI handled by midi-bridge.service → UDP → Pd netreceive
if [ -n "$midi_dev" ]; then
MIDI_OPTS+=(-midiadddev "$midi_dev")
info " MIDI device: $midi_dev"
fi
else else
info " USB gadget: NOT DETECTED (kernel modules needed)" info " USB gadget: NOT DETECTED (kernel modules needed)"
fi fi
@@ -125,19 +114,12 @@ if [ "$MODE" = "loopback" ]; then
fi fi
PD_ARGS=( PD_ARGS=(
-nogui -nogui -alsa
-alsa
"${MIDI_OPTS[@]}"
"${AUDIO_OPTS[@]}"
-send "pd dsp 1" -send "pd dsp 1"
-audiobuf 20 "${AUDIO_OPTS[@]}"
-blocksize 64 -audiobuf 20 -blocksize 64 -r 48000
-r 48000 -channels 1 -inchannels 0 -outchannels 1
-channels 1 -path "$PD_DIR" "$PATCH"
-inchannels 0
-outchannels 1
-path "$PD_DIR"
"$PATCH"
) )
if [ "$VERBOSE" = true ]; then if [ "$VERBOSE" = true ]; then
+2 -1
View File
@@ -1,10 +1,11 @@
[Unit] [Unit]
Description=MIDI rawmidi to Pd UDP bridge (MPK Mini → Pd) Description=MIDI rawmidi Pd UDP bridge (auto-detects gadget/host)
After=multi-user.target After=multi-user.target
[Service] [Service]
Type=simple Type=simple
ExecStart=/usr/local/bin/midi-bridge.py ExecStart=/usr/local/bin/midi-bridge.py
User=root
Restart=on-failure Restart=on-failure
RestartSec=2 RestartSec=2
StandardOutput=journal StandardOutput=journal