diff --git a/touchosc/README.md b/touchosc/README.md index 80e663a..76b4b12 100644 --- a/touchosc/README.md +++ b/touchosc/README.md @@ -93,3 +93,7 @@ Feedback (SC → surface, port 9000, all created by `touchosc_feedback.scd`): voices to bus 0, so per-voice live levels are not producible without re-routing the audio. The master RMS VU + beat + per-pad armed reflection cover the live-feedback need. +- **Sequencer playhead / step-index highlight** (spec §2/§4/§7) is + **deferred**: the data-only engine does not emit a step index over OSC, so + adding column-highlight requires a new SC step-emit plus a layout + column-highlight receiver. Not built in this pass. diff --git a/touchosc/dist/av-live-control.tosc b/touchosc/dist/av-live-control.tosc index 022c1e1..9827522 100644 Binary files a/touchosc/dist/av-live-control.tosc and b/touchosc/dist/av-live-control.tosc differ diff --git a/touchosc/gen/layout.py b/touchosc/gen/layout.py index 4ce5bdc..3a749ab 100644 --- a/touchosc/gen/layout.py +++ b/touchosc/gen/layout.py @@ -82,7 +82,6 @@ def _pad_cell(name, x, y, w, h): [s.prop("name", name, "s"), s.frame(0, 0, w - 20, btn_h)], messages=[ s.osc("/launch", [s.const(name), s.vali()]), - s.osc(f"/armed/{name}", [s.val()], send=0, receive=1, feedback=1), ], ) vol_fader = s.node( @@ -99,6 +98,7 @@ def _pad_cell(name, x, y, w, h): s.script(_lua("pad.lua")), ], children=[arm_btn, vol_fader, name_label], + messages=[s.osc(f"/armed/{name}", [s.val()], send=0, receive=1, feedback=1)], ) @@ -126,8 +126,7 @@ def live_page(): # Master VU fader (receive-only /sync/rms) ch.append(s.node( "FADER", - [s.prop("name", "rms", "s"), s.frame(490, 8, 40, 72), - s.script(_lua("feedback.lua"))], + [s.prop("name", "rms", "s"), s.frame(490, 8, 40, 72)], messages=[s.osc("/sync/rms", [s.val()], send=0, receive=1, feedback=1)], )) diff --git a/touchosc/gen/lua/melody_faders.lua b/touchosc/gen/lua/melody_faders.lua index bb6aeba..4633279 100644 --- a/touchosc/gen/lua/melody_faders.lua +++ b/touchosc/gen/lua/melody_faders.lua @@ -2,6 +2,7 @@ -- On value change: read all 8 step faders (m0..m7) -> /seq/melody/set. -- API verified on GrosMac at fidelity gate. local LO, HI = -7, 14 +local unpack = table.unpack or unpack -- Lua 5.1 / LuaJIT compat function onValueChanged(key) if key ~= "x" then return end @@ -12,5 +13,5 @@ function onValueChanged(key) local fx = (f and f.values and f.values.x) or 0 degs[i] = math.floor(LO + (HI - LO) * fx + 0.5) end - sendOSC("/seq/melody/set", table.unpack(degs)) + sendOSC("/seq/melody/set", unpack(degs)) end diff --git a/touchosc/tests/test_layout.py b/touchosc/tests/test_layout.py index 98dfd79..dff8728 100644 --- a/touchosc/tests/test_layout.py +++ b/touchosc/tests/test_layout.py @@ -247,3 +247,53 @@ def test_seq_page_no_sync_amp(): page = layout.seq_page() addrs = _addrs(page) assert "/sync/amp" not in addrs, "/sync/amp found — per-voice VU was supposed to be omitted" + + +# --------------------------------------------------------------------------- +# Pad armed receive on GROUP — FIX 1 regression guard +# --------------------------------------------------------------------------- + +def test_live_page_pad_groups_carry_armed_receive(): + """Each pad GROUP (carrying pad.lua) must have the /armed/ receive + message on itself — not only on the child arm BUTTON. pad.lua fires via + onReceiveOSC on the node that owns the matching receive message; if the + message lives only on the child BUTTON, the GROUP script never fires and + armed-state color reflection is dead.""" + page = layout.live_page() + + # Collect GROUP nodes that carry a script property (= pad GROUPs with pad.lua). + # The top-level LIVE GROUP has no script, so this selects exactly the 16 pads. + pad_groups = [] + for grp in page.findall(".//node[@type='GROUP']"): + for prop_el in grp.findall("properties/property"): + key_el = prop_el.find("key") + if key_el is not None and key_el.text == "script": + pad_groups.append(grp) + break + + assert len(pad_groups) == 16, ( + f"expected 16 pad GROUPs with a script property, got {len(pad_groups)}" + ) + + for grp in pad_groups: + # Resolve the pad name from its name property for a readable failure msg. + grp_name = "?" + for prop_el in grp.findall("properties/property"): + k = prop_el.find("key") + if k is not None and k.text == "name": + v = prop_el.find("value") + grp_name = v.text if v is not None else "?" + break + + # The GROUP's direct block (not descendants) must contain + # exactly one osc element with receive="1" whose path starts /armed/. + armed_msgs = [ + osc_el for osc_el in grp.findall("messages/osc") + if osc_el.get("receive") == "1" + and osc_el.find("path/partial/value") is not None + and (osc_el.find("path/partial/value").text or "").startswith("/armed/") + ] + assert len(armed_msgs) == 1, ( + f"pad GROUP '{grp_name}' must have exactly 1 /armed/* receive msg " + f"on the GROUP itself, got {len(armed_msgs)}" + )