fix: pad armed receive on group, lua guards
CI build oscope-of / build-check (push) Has been cancelled

Move /armed/<name> receive msg from arm BUTTON to parent GROUP so
pad.lua onReceiveOSC fires on the node that owns the message (FIX 1).
Remove feedback.lua from master RMS fader; native receive already
drives the VU, onFrame decay was pure downside (FIX 3).
Add Lua 5.1/LuaJIT compat shim for table.unpack in melody_faders
(FIX 2). Document deferred sequencer playhead in README (FIX 4).
Regression test: 16 pad GROUPs carry /armed/* receive; 21 tests pass.
Rebuild dist/av-live-control.tosc.
This commit is contained in:
L'électron rare
2026-06-28 13:49:00 +02:00
parent c90cbb103c
commit 67fd253de5
5 changed files with 58 additions and 4 deletions
+4
View File
@@ -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.
Binary file not shown.
+2 -3
View File
@@ -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)],
))
+2 -1
View File
@@ -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
+50
View File
@@ -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/<name> 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 <messages> 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)}"
)