From dbd7c2cecee472b0bec996c45032568b4f82dda7 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Mon, 22 Jun 2026 23:10:38 -0700 Subject: [PATCH] auto-detect MIDI device, drop ALSA sequencer dependency, MPK Mini simulator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- pd/mpk-mini-sim.pd | 448 +++++++++++++++++++++++++++++++++++++ scripts/gen_mpk_sim.py | 120 ++++++++++ scripts/midi-bridge.py | 104 +++++---- scripts/start-synth.sh | 32 +-- system/midi-bridge.service | 3 +- 5 files changed, 642 insertions(+), 65 deletions(-) create mode 100644 pd/mpk-mini-sim.pd create mode 100644 scripts/gen_mpk_sim.py diff --git a/pd/mpk-mini-sim.pd b/pd/mpk-mini-sim.pd new file mode 100644 index 0000000..d05950a --- /dev/null +++ b/pd/mpk-mini-sim.pd @@ -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; diff --git a/scripts/gen_mpk_sim.py b/scripts/gen_mpk_sim.py new file mode 100644 index 0000000..a4b63f0 --- /dev/null +++ b/scripts/gen_mpk_sim.py @@ -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 = 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") diff --git a/scripts/midi-bridge.py b/scripts/midi-bridge.py index fa67938..e5375bf 100644 --- a/scripts/midi-bridge.py +++ b/scripts/midi-bridge.py @@ -1,76 +1,102 @@ #!/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 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_PORT = int(os.environ.get("MIDI_BRIDGE_PORT", "8081")) 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): if data2 is not None: return f"midi {status} {data1} {data2};" return f"midi {status} {data1};" -def main(): - while not os.path.exists(MIDI_DEV): +def _open_midi(): + midi_dev = _find_midi_dev() + while not os.path.exists(midi_dev): time.sleep(0.5) - - fd = os.open(MIDI_DEV, os.O_RDWR | os.O_NONBLOCK) + midi_dev = _find_midi_dev() + fd = os.open(midi_dev, os.O_RDWR | os.O_NONBLOCK) os.set_blocking(fd, False) - poll = select.poll() - poll.register(fd, select.POLLIN) + return fd +def main(): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) running = True - buf = bytearray() while running: try: - events = poll.poll(500) - for f, flags in events: - if flags & select.POLLIN: - data = os.read(fd, 1024) - if not data: - continue - for byte in data: - if byte & 0x80: - if len(buf) > 0 and buf[0] >= 0x80: - _flush(sock, buf, PD_HOST, PD_PORT, VIZ_PORT) - buf = bytearray([byte]) - else: - buf.append(byte) - if len(buf) >= 3 and (buf[0] & 0xE0) in (0x80, 0x90, 0xA0, 0xB0, 0xE0): - _flush(sock, buf, PD_HOST, PD_PORT, VIZ_PORT) - buf = bytearray() - elif len(buf) >= 2 and (buf[0] & 0xE0) in (0xC0, 0xD0): - _flush(sock, buf, PD_HOST, PD_PORT, VIZ_PORT) - buf = bytearray() - except (BlockingIOError, OSError) as e: - if isinstance(e, OSError) and e.errno == 19: - time.sleep(1) - continue - if not isinstance(e, BlockingIOError): - raise + fd = _open_midi() + poll = select.poll() + poll.register(fd, select.POLLIN) + buf = bytearray() + while running: + try: + events = poll.poll(500) + for f, flags in events: + if flags & select.POLLIN: + data = os.read(fd, 1024) + if not data: + continue + for byte in data: + if byte & 0x80: + if len(buf) > 0 and buf[0] >= 0x80: + _flush(sock, buf) + buf = bytearray([byte]) + else: + buf.append(byte) + if len(buf) >= 3 and (buf[0] & 0xE0) in (0x80, 0x90, 0xA0, 0xB0, 0xE0): + _flush(sock, buf) + buf = bytearray() + elif len(buf) >= 2 and (buf[0] & 0xE0) in (0xC0, 0xD0): + _flush(sock, buf) + buf = bytearray() + except OSError as e: + if e.errno == 19: + os.close(fd) + time.sleep(1) + break + raise + except BlockingIOError: + pass except KeyboardInterrupt: running = False - os.close(fd) sock.close() -def _flush(sock, buf, pd_host, pd_port, viz_port): +def _flush(sock, buf): status = buf[0] if (buf[0] & 0xF0) == 0xF0: return data1 = buf[1] data2 = buf[2] if len(buf) >= 3 else None msg = midi_msg(status, data1, data2) - sock.sendto(msg.encode(), (pd_host, pd_port)) - sock.sendto(msg.encode(), (pd_host, viz_port)) + sock.sendto(msg.encode(), (PD_HOST, PD_PORT)) + sock.sendto(msg.encode(), (PD_HOST, VIZ_PORT)) if __name__ == "__main__": main() diff --git a/scripts/start-synth.sh b/scripts/start-synth.sh index bf72fe0..918519e 100755 --- a/scripts/start-synth.sh +++ b/scripts/start-synth.sh @@ -56,17 +56,11 @@ if [ "$MODE" = "auto" ]; then ALSA_DEV=$(cat /run/usb-audio-dev) fi info "Mode: USB host (MIDI controller + USB audio interface)" - MIDI_OPTS=(-alsamidi) - 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 + # MIDI handled by midi-bridge.service → UDP → Pd netreceive AUDIO_OPTS=(-audiooutdev 4 -audioindev 4) PD_ARGS=( -nogui -alsa -send "pd dsp 1" - "${MIDI_OPTS[@]}" "${AUDIO_OPTS[@]}" -audiobuf 20 -blocksize 64 -r 48000 -channels 2 -inchannels 0 -outchannels 2 @@ -87,18 +81,13 @@ fi # --- Configure per-mode options --- AUDIO_OPTS=() -MIDI_OPTS=(-alsamidi) if [ "$MODE" = "usb" ]; then info "Mode: USB gadget (UAC1 audio + gmidi MIDI)" AUDIO_OPTS=(-audiooutdev 2 -audioindev 2) if aplay -l 2>/dev/null | grep -q "UAC1Gadget"; then info " USB gadget: OK" - midi_dev=$(amidi -l 2>/dev/null | grep "f_midi" | head -1 | awk '{print $2}') - if [ -n "$midi_dev" ]; then - MIDI_OPTS+=(-midiadddev "$midi_dev") - info " MIDI device: $midi_dev" - fi + # MIDI handled by midi-bridge.service → UDP → Pd netreceive else info " USB gadget: NOT DETECTED (kernel modules needed)" fi @@ -125,19 +114,12 @@ if [ "$MODE" = "loopback" ]; then fi PD_ARGS=( - -nogui - -alsa - "${MIDI_OPTS[@]}" - "${AUDIO_OPTS[@]}" + -nogui -alsa -send "pd dsp 1" - -audiobuf 20 - -blocksize 64 - -r 48000 - -channels 1 - -inchannels 0 - -outchannels 1 - -path "$PD_DIR" - "$PATCH" + "${AUDIO_OPTS[@]}" + -audiobuf 20 -blocksize 64 -r 48000 + -channels 1 -inchannels 0 -outchannels 1 + -path "$PD_DIR" "$PATCH" ) if [ "$VERBOSE" = true ]; then diff --git a/system/midi-bridge.service b/system/midi-bridge.service index 8fe51d5..3337a97 100644 --- a/system/midi-bridge.service +++ b/system/midi-bridge.service @@ -1,10 +1,11 @@ [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 [Service] Type=simple ExecStart=/usr/local/bin/midi-bridge.py +User=root Restart=on-failure RestartSec=2 StandardOutput=journal