Commit 361a05b9 by PLN (Algolia)

fix(midi): cut the surface's direct leg to SuperCollider, every 2s

SuperDirt's boot runs MIDIIn.connectAll (start_and_midi.scd:19), so SC
subscribes to every hardware MIDI input it can see -- the LCXL included.
The surface then fed SC twice: once raw, once through Midi Through. And
MIDIFunc.cc takes `src` and never checks it (start_and_midi.scd:51), so
every CC fired the handler twice and every button noteOn double-fired
into ~lcxlChordCheck. A double noteOn on a toggle is a no-op: that is how
a mute button silently stops working.

lcxl3-driver's prune() handled this for the v3 board, but only while it
runs -- and it was stopped today (stale binding to an unplugged mk3,
Conflicts=-killing the mk1 painter). A hand-run `aconnect -d` fixed it
live and then SC restarted and re-armed it, which is the whole argument
for putting it in the reconcile loop that already owns the wiring.

Rule 3 matches the RAW board on type=kernel, never on name alone:
lcxl3-driver publishes a virtual port called 'ParVagues LCXL3' that
matches the same name candidates, and cutting the driver's translated
feed would kill the path we actually want. Only surface->SuperCollider
links are cut; -> Midi Through and the aseqdump taps that gig-log and the
Pulsar HUD read all survive, verified live.

Tested by arming the bug (aconnect 20:0 130:3, 20:1 130:4) and watching
the loop cut both and settle: lcxl-path.py went from "LCXL -> SC direct"
back to "LCXL -> Midi Through -> SC", logged once, no per-tick spam, and
gig-log kept counting touched controls.

Also logs PLN's ear-feedback on the classic-LCXL play-test (rose_rouge
works great; midiviz landed) to the archivist's notes.
parent 84e37fec
...@@ -97,6 +97,64 @@ resolve_lcxl() { ...@@ -97,6 +97,64 @@ resolve_lcxl() {
return 1 return 1
} }
# Rule 3: the surface reaches SuperCollider through Midi Through ONLY.
#
# SuperDirt's boot runs `MIDIIn.connectAll` (start_and_midi.scd:19), which
# subscribes SC to EVERY hardware MIDI input it can see -- the LCXL included.
# That leaves the surface feeding SC twice: once raw, once via Midi Through.
# `MIDIFunc.cc` accepts `src` and never checks it (start_and_midi.scd:51), so
# every CC fires the handler twice and every button noteOn double-fires into
# ~lcxlChordCheck -- which is how a toggle silently becomes a no-op. Found
# 2026-09-05, after an SC restart re-armed it behind a hand-run `aconnect -d`.
#
# Cut ONLY hardware-surface -> SuperCollider. Everything else the surface feeds
# MUST survive: -> Midi Through (rule 1), and -> the aseqdump taps that gig-log
# and the Pulsar HUD read (cutting those blinds the session log).
#
# Match the RAW board on type=kernel, never on name alone: lcxl3-driver
# publishes a virtual port called 'ParVagues LCXL3' whose name matches the same
# candidates, and cutting the driver's own translated feed would kill the very
# path we want. type=kernel is what separates hardware from a virtual port.
#
# lcxl3-driver's prune() did this for the v3 board, but only while it runs.
# This loop already owns the wiring, so this is the generation-agnostic home.
SC_CLIENT_AWK='/^client [0-9]+: .SuperCollider./ { c = $2; sub(":", "", c); print c; exit }'
DIRECT_LEG_AWK='
/^client / {
cid = $2; sub(":", "", cid)
hw = ($0 ~ /type=kernel/ && $0 ~ /Launch Control XL|LCXL/)
next
}
hw && /^[ \t]+[0-9]+ / { port = $1; next }
hw && /Connecting To:/ {
line = $0
sub(/.*Connecting To:[ \t]*/, "", line)
n = split(line, dests, ",")
for (i = 1; i <= n; i++) {
d = dests[i]
gsub(/[ \t]/, "", d)
sub(/\[.*\]$/, "", d)
split(d, a, ":")
if (a[1] == scid) print cid ":" port, d
}
}'
cut_direct_to_sc() {
local scid pairs src dst
scid="$(aconnect -l 2>/dev/null | awk "$SC_CLIENT_AWK")"
[ -n "$scid" ] || return 0
pairs="$(aconnect -l 2>/dev/null | awk -v scid="$scid" "$DIRECT_LEG_AWK")"
[ -n "$pairs" ] || return 0
while read -r src dst; do
[ -n "$src" ] || continue
echo "midi-autoconnect: cutting direct $src -> SuperCollider $dst (Midi Through is the only hop)"
disconnect "$src" "$dst"
done <<< "$pairs"
}
reconcile() { reconcile() {
local resolved addr name local resolved addr name
if ! resolved="$(resolve_lcxl)"; then if ! resolved="$(resolve_lcxl)"; then
...@@ -113,6 +171,7 @@ reconcile() { ...@@ -113,6 +171,7 @@ reconcile() {
fi fi
connect "$addr" "$THRU:$THRU_PORT" # rule 1: controls out connect "$addr" "$THRU:$THRU_PORT" # rule 1: controls out
disconnect "$THRU:$THRU_PORT" "$addr" # rule 2: kill the loop disconnect "$THRU:$THRU_PORT" "$addr" # rule 2: kill the loop
cut_direct_to_sc # rule 3: one hop, not two
} }
echo "midi-autoconnect: reconciling every ${INTERVAL}s" echo "midi-autoconnect: reconciling every ${INTERVAL}s"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment