feat: morceau button; drop gesture + auto-chain
CI build oscope-of / build-check (push) Has been cancelled
CI build oscope-of / build-check (push) Has been cancelled
This commit is contained in:
@@ -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()
|
||||
@@ -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."
|
||||
|
||||
@@ -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;
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user