From 3b3b1fe51bb0ac58abd68e633a6e17d241def703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=27=C3=A9lectron=20rare?= <108685187+electron-rare@users.noreply.github.com> Date: Sat, 27 Jun 2026 21:37:37 +0200 Subject: [PATCH] feat: morceau button; drop gesture + auto-chain --- launcher/concert/concert_control.py | 73 ++++++++++++++++++++++++++ launcher/concert/launch_concert.sh | 11 +++- sound_algo/data_only/scene_concert.scd | 52 ++++-------------- 3 files changed, 93 insertions(+), 43 deletions(-) create mode 100644 launcher/concert/concert_control.py diff --git a/launcher/concert/concert_control.py b/launcher/concert/concert_control.py new file mode 100644 index 0000000..a7f14ef --- /dev/null +++ b/launcher/concert/concert_control.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Floating concert control panel. + +A small always-on-top panel with a big "Morceau suivant" button that advances +the concert by sending OSC /control/concertNext (-> ~concert[\\next], random). + +It is a NON-ACTIVATING NSPanel: clicking the button does NOT steal keyboard +focus from the viz window, so Space keeps working there too; and the click works +regardless of which window is key (so it is the reliable, focus-independent way +to change morceau). Launched by AV-Live Concert.app alongside the stack. + +Run: /bin/python launcher/concert/concert_control.py +""" +from Foundation import NSObject, NSMakeRect +from AppKit import ( + NSApplication, NSPanel, NSButton, NSFont, NSScreen, + NSBackingStoreBuffered, NSFloatingWindowLevel, + NSApplicationActivationPolicyAccessory, + NSWindowStyleMaskTitled, NSWindowStyleMaskClosable, + NSWindowStyleMaskUtilityWindow, NSWindowStyleMaskNonactivatingPanel, + NSBezelStyleRegularSquare, +) +from pythonosc.udp_client import SimpleUDPClient + +SC = SimpleUDPClient("127.0.0.1", 57121) + + +class FirstClickButton(NSButton): + """Trigger on the first click even when the panel is not the key window.""" + def acceptsFirstMouse_(self, event): # noqa: N802 + return True + + +class Ctl(NSObject): + def nextMorceau_(self, sender): # noqa: N802 + SC.send_message("/control/concertNext", [1]) + + +def main(): + app = NSApplication.sharedApplication() + app.setActivationPolicy_(NSApplicationActivationPolicyAccessory) + ctl = Ctl.alloc().init() + + style = (NSWindowStyleMaskTitled | NSWindowStyleMaskClosable + | NSWindowStyleMaskUtilityWindow | NSWindowStyleMaskNonactivatingPanel) + panel = NSPanel.alloc().initWithContentRect_styleMask_backing_defer_( + NSMakeRect(0, 0, 300, 140), style, NSBackingStoreBuffered, False) + panel.setTitle_("Concert") + panel.setFloatingPanel_(True) + panel.setBecomesKeyOnlyIfNeeded_(True) + panel.setHidesOnDeactivate_(False) + panel.setLevel_(NSFloatingWindowLevel) + panel.setReleasedWhenClosed_(False) + + btn = FirstClickButton.alloc().initWithFrame_(NSMakeRect(16, 18, 268, 96)) + btn.setTitle_("Morceau suivant ▶") + btn.setBezelStyle_(NSBezelStyleRegularSquare) + btn.setFont_(NSFont.boldSystemFontOfSize_(22)) + btn.setTarget_(ctl) + btn.setAction_("nextMorceau:") + panel.contentView().addSubview_(btn) + + scr = NSScreen.mainScreen().frame() + panel.setFrameOrigin_((scr.size.width - 320, scr.size.height - 190)) + panel.orderFrontRegardless() + + # keep strong refs so they survive the run loop + globals()["_keep"] = (panel, btn, ctl) + app.run() + + +if __name__ == "__main__": + main() diff --git a/launcher/concert/launch_concert.sh b/launcher/concert/launch_concert.sh index c3297b0..05e2082 100755 --- a/launcher/concert/launch_concert.sh +++ b/launcher/concert/launch_concert.sh @@ -53,8 +53,15 @@ nohup "$PY" -m data_only_viz.main --pose --iphone-usb > /tmp/concert_viz.log 2>& disown sleep 11 -# 4) switch to the concert scene +# 4) floating "Morceau suivant" button panel (non-activating: keeps the viz the +# key window so Space still works, while the button click changes morceau +# regardless of focus). +nohup "$PY" "$REPO/launcher/concert/concert_control.py" > /tmp/concert_control.log 2>&1 & +disown +sleep 1 + +# 5) switch to the concert scene "$PY" -c "from pythonosc.udp_client import SimpleUDPClient; SimpleUDPClient('127.0.0.1',57121).send_message('/control/doScene','concert')" sleep 1 -notify "Concert lance. Espace = morceau suivant. Bras croises = morceau au hasard." +notify "Concert lance. Bouton ou Espace = morceau suivant." diff --git a/sound_algo/data_only/scene_concert.scd b/sound_algo/data_only/scene_concert.scd index 3c00da9..c071a83 100644 --- a/sound_algo/data_only/scene_concert.scd +++ b/sound_algo/data_only/scene_concert.scd @@ -128,29 +128,9 @@ }; }; -// geste bras-croises : main D a gauche de main G, a mi-hauteur, maintenu -~concert[\detectCross] = { |pose, now| - // Detect via BODY WRISTS (skeleton), not hands: at full-body distance the - // hands aren't detected and crossing occludes them. Clear X cross = the - // right wrist swaps to the LEFT of the left wrist. Require the performer - // centered (not at a frame edge) + wrists at chest height. - var w = (~poseWrist.notNil.if({ ~poseWrist.values.detect({ |v| v.notNil }) })) ? (); - var lx = (w[\lx] ? 0.5); var rx = (w[\rx] ? 0.5); - var ly = (w[\ly] ? 0.5); var ry = (w[\ry] ? 0.5); - var centered = (lx > 0.15) and: { lx < 0.85 } and: { rx > 0.15 } and: { rx < 0.85 }; - var midH = (ly > 0.2) and: { ly < 0.85 } and: { ry > 0.2 } and: { ry < 0.85 }; - var crossed = pose[\hasBody] and: { centered } and: { midH } - and: { rx < (lx - 0.10) }; - if(crossed) { - if(~concert[\crossT] <= 0) { ~concert[\crossT] = now }; - if(((now - ~concert[\crossT]) >= ~ccCrossHold) - and: { (now - ~concert[\lastFire]) > ~ccCrossCool }) { - ~concert[\lastFire] = now; - ~concert[\crossT] = 0; - ~concert[\next].(); - }; - } { ~concert[\crossT] = 0 }; -}; +// Morceau advance is MANUAL only: the clickable "Morceau suivant" button (and +// Space) send /control/concertNext -> ~concert[\next]. The gesture-cross advance +// and the auto-chain were removed on request. ~concert[\start] = { if(~concert[\running]) { "[concert] deja en cours".postln } { @@ -159,7 +139,6 @@ } { ~concert[\running] = true; ~concert[\idx] = 0; - ~concert[\lastChain] = 0; ~ccGState = IdentityDictionary.new; ~concert[\clock] = TempoClock(126/60).permanent_(true); ~concert[\startMorceau].(0); @@ -170,7 +149,8 @@ var pose = ~ccPose.value; var now = thisThread.seconds; var c; - ~concert[\detectCross].(pose, now); + // FX / harmony / melody gestures only; morceau advance is + // manual (button / Space). No gesture-cross, no auto-chain. ~ccEvalGestures.(pose, prev, now); prev = pose; c = ~concert[\ctx]; // relire (next a pu permuter) c !? { |cc| @@ -178,12 +158,6 @@ cc[\m][\tick] !? { |t| t.(cc[\ctx], pose, beat) }; }; beat = beat + 1; - // auto-chaine (mains libres) : avance vers un morceau au - // hasard toutes les ~chainTicks ticks quand actif. - if((~concert[\autoChain] == true) - and: { (beat - (~concert[\lastChain] ? 0)) >= (~concert[\chainTicks] ? 1280) }) { - ~concert[\lastChain] = beat; ~concert[\next].(); - }; ~ccCtlDur.wait; }; }).play(~concert[\clock]); @@ -210,20 +184,16 @@ ~doRegisterPriv.(\concert, { ~concert[\stop].() }); }; -// Bascules live : /control/concertChain <0|1> (auto-chaine aleatoire mains -// libres) ; /control/concertSeq <0|1> (avance sequentielle au lieu d'aleatoire). -OSCdef(\concertChain, { |msg| - ~concert[\autoChain] = (msg[1] ? 0) > 0; - ("[concert] autoChain = " ++ ~concert[\autoChain]).postln; -}, '/control/concertChain'); +// /control/concertNext : avance vers un morceau (bouton "Morceau suivant" + +// touche espace dans data_only_viz). /control/concertSeq <0|1> : avance +// sequentielle au lieu d'aleatoire. (Auto-chaine retiree.) +OSCdef(\concertNext, { |msg| + if(~concert[\running] == true) { ~concert[\next].() }; +}, '/control/concertNext'); OSCdef(\concertSeq, { |msg| ~concert[\seq] = (msg[1] ? 0) > 0; ("[concert] seq = " ++ ~concert[\seq]).postln; }, '/control/concertSeq'); -// /control/concertNext : avance (touche espace dans data_only_viz). -OSCdef(\concertNext, { |msg| - if(~concert[\running] == true) { ~concert[\next].() }; -}, '/control/concertNext'); "[data-only/scene_concert] engine ready".postln; )