feat: morceau button; drop gesture + auto-chain
CI build oscope-of / build-check (push) Has been cancelled

This commit is contained in:
L'électron rare
2026-06-27 21:37:37 +02:00
parent 1a6503cb71
commit 3b3b1fe51b
3 changed files with 93 additions and 43 deletions
+73
View File
@@ -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: <venv>/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()
+9 -2
View File
@@ -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."
+11 -41
View File
@@ -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;
)