diff --git a/touchosc/dist/av-live-control.tosc b/touchosc/dist/av-live-control.tosc index 9827522..ae9fec4 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 3a749ab..a07a7f2 100644 --- a/touchosc/gen/layout.py +++ b/touchosc/gen/layout.py @@ -138,6 +138,14 @@ def live_page(): messages=[s.osc("/sync/beat", [s.val()], send=0, receive=1, feedback=1)], )) + # Quantize selector row (send-only momentary): 1/2 beat / 1 beat / 1 bar / 2 bars + quant_defs = [("1/2", 0.5), ("1", 1), ("1 BAR", 4), ("2 BAR", 8)] + qx_start, qy, qw, qh, qgap = 625, 8, 130, 50, 8 + for i, (lbl, beats) in enumerate(quant_defs): + qx = qx_start + i * (qw + qgap) + ch.append(button(lbl, "/launch/quant", qx, qy, qw, qh, args=[s.constf(beats)])) + ch.append(label("QUANT", qx_start, 62, 120, 20)) + # 4x4 pad grid ----------------------------------------------------------- gx, gy, pw, ph, gap = 10, 96, 283, 178, 10 for i, name in enumerate(PATTERNS): diff --git a/touchosc/gen/lua/pad.lua b/touchosc/gen/lua/pad.lua index 661f7f6..1d162dd 100644 --- a/touchosc/gen/lua/pad.lua +++ b/touchosc/gen/lua/pad.lua @@ -1,16 +1,33 @@ -- Pad GROUP: reflect armed state from SC engine onto the arm button color. -- API verified on GrosMac at fidelity gate. -- self.children[1] is the arm BUTTON; color driven by /armed/. +-- State int: 0=off, 1=playing (solid green), 2=queued-launch (blink), 3=queued-stop (blink). +local blinking = false + function onReceiveOSC(message, connections) local path = message[1] local name = self.name:gsub("pad_", "") if path == "/armed/" .. name then local args = message[2] - local on = (args and args[1] and tonumber(args[1].value)) or 0 + local state = math.floor((args and args[1] and tonumber(args[1].value)) or 0) if self.children and self.children[1] then - self.children[1].color = on > 0.5 - and Color(0.27, 0.80, 0.40, 1) - or Color(0.11, 0.11, 0.11, 1) + if state == 0 then + blinking = false + self.children[1].color = Color(0.11, 0.11, 0.11, 1) + elseif state == 1 then + blinking = false + self.children[1].color = Color(0.27, 0.80, 0.40, 1) + else + -- state 2 (queued-launch) or 3 (queued-stop): blink amber + blinking = true + end end end end + +function onFrame() + if blinking and self.children and self.children[1] then + local phase = (math.sin(getTime() * math.pi * 2.5) + 1) * 0.5 + self.children[1].color = Color(0.80 * phase, 0.60 * phase, 0.10, 1) + end +end diff --git a/touchosc/tests/test_layout.py b/touchosc/tests/test_layout.py index dff8728..40f3f3d 100644 --- a/touchosc/tests/test_layout.py +++ b/touchosc/tests/test_layout.py @@ -249,6 +249,31 @@ def test_seq_page_no_sync_amp(): assert "/sync/amp" not in addrs, "/sync/amp found — per-voice VU was supposed to be omitted" +# --------------------------------------------------------------------------- +# Page 1 — LIVE: quantize selector +# --------------------------------------------------------------------------- + +def test_live_page_quant_buttons(): + """LIVE page must have 4 /launch/quant buttons with CONSTANT/FLOAT args {0.5,1,4,8}.""" + page = layout.live_page() + quant_args = [] + for osc_el in page.findall(".//messages/osc"): + path_val = osc_el.find("path/partial/value") + if path_val is not None and path_val.text == "/launch/quant": + for part in osc_el.findall("arguments/partial"): + typ = part.find("type") + conv = part.find("conversion") + val = part.find("value") + if typ is not None and conv is not None and val is not None: + quant_args.append((typ.text, conv.text, val.text)) + assert len(quant_args) == 4, f"expected 4 /launch/quant arg partials, got {len(quant_args)}" + for typ, conv, _ in quant_args: + assert typ == "CONSTANT", f"quant arg type must be CONSTANT, got {typ}" + assert conv == "FLOAT", f"quant arg conversion must be FLOAT, got {conv}" + vals = {v for _, _, v in quant_args} + assert vals == {"0.5", "1", "4", "8"}, f"expected quant values {{0.5,1,4,8}}, got {vals}" + + # --------------------------------------------------------------------------- # Pad armed receive on GROUP — FIX 1 regression guard # ---------------------------------------------------------------------------