Files
AV-Live/sound_algo/data_only/scene_concert.scd
T
2026-06-28 17:10:30 +02:00

216 lines
9.1 KiB
Markdown

// =====================================================================
// data_only/scene_concert.scd -- moteur de concert (setlist techno).
// Setlist sequentielle, geste bras-croises = morceau suivant, crossfade.
// Active via ~doScene.(\concert). Les morceaux s'enregistrent via
// ~concertAdd depuis morceaux/NN_*.scd (charges apres ce fichier).
// =====================================================================
// ----- NAMESPACE ~cc* (concert) vs ~cc (live) : clarification --------
// Ce fichier definit des variables ~cc* : ~ccCtlDur, ~ccCrossHold,
// ~ccCrossCool, ~ccWristFeat, ~ccPose, ~ccNote, ~ccHarmony, ~ccMaster,
// ~ccFire, ~ccGestures, ~ccCtlDur...
// Ces variables constituent le NAMESPACE DU CONCERT (mode data-only).
//
// A ne pas confondre avec ~cc du systeme LIVE (alias de ~ch, le
// dictionnaire des chains : ~chIntro, ~chDrop, ~chBreakdown, etc.,
// definis dans live/chains.scd et documentes dans live/CLAUDE.md).
//
// Ces deux namespaces ne coexistent JAMAIS dans la meme instance sclang :
// - data_only/boot.scd --> charge ~cc* concert (pas live/_load.scd)
// - boot.scd / live/_load.scd --> charge ~cc alias chains (pas ~cc*)
// Les points d'entree boot sont mutuellement exclusifs (cf. Boot modes
// dans sound_algo/CLAUDE.md).
// ---------------------------------------------------------------------
(
~ccCtlDur = 0.05; // periode du tick de controle, en BEATS
~ccCrossHold = 1.5; // duree de maintien du geste bras-croises (s)
~ccCrossCool = 3.0; // cooldown apres declenchement (s)
// -- snapshot pose (premiere personne), tout nil-garde --
// Wrist-derived hand-features. At full-body (concert) distance MediaPipe can't
// see the hands (/pose/hands all zero), but the body WRISTS are tracked. We
// shape the wrists exactly like ~handFeat so every morceau (which reads
// pose[\hands]) is driven by the arms transparently — no morceau changes.
// openness -> arm spread ; speed -> per-tick wrist motion (gain'd into a usable
// range, tune thresholds live).
~ccWristFeat = {
var w = (~poseWrist.notNil.if({ ~poseWrist.values.detect({ |v| v.notNil }) })) ? ();
var lx = (w[\lx] ? 0.5); var ly = (w[\ly] ? 0.5);
var rx = (w[\rx] ? 0.5); var ry = (w[\ry] ? 0.5);
var spread = (rx - lx).abs;
var rsp = 0; var lsp = 0; var prev = ~concert[\wprev];
if(prev.notNil) {
rsp = (((rx - prev[\rx]).squared + (ry - prev[\ry]).squared).sqrt * 8).min(1);
lsp = (((lx - prev[\lx]).squared + (ly - prev[\ly]).squared).sqrt * 8).min(1);
};
~concert[\wprev] = (lx: lx, ly: ly, rx: rx, ry: ry);
(lx: lx, ly: ly, rx: rx, ry: ry,
lopen: spread, ropen: spread, lspeed: lsp, rspeed: rsp, dist: spread);
};
~ccPose = {
var kin = ~poseKin.notNil.if({ ~poseKin.values.detect({ |v| v.notNil }) });
var st = ~poseState.notNil.if({ ~poseState.values.detect({ |v| v.notNil }) });
var ctr = ~poseCenter.notNil.if({ ~poseCenter.values.detect({ |v| v.notNil }) });
var hf = ~handFeat ? ();
var handsLive = ((hf[\rx] ? 0) > 0.001) or: { (hf[\lx] ? 0) > 0.001 };
var hands = handsLive.if({ hf }, { ~ccWristFeat.value });
(kin: (kin ? ()), state: (st ? ()), center: (ctr ? ()),
hands: hands, hasBody: kin.notNil,
skel: ((~poseSkel.notNil.if({ ~poseSkel.values.detect({ |v| v.notNil }) })) ? ()));
};
// -- registre des gestes (framework extensible) --
~ccGestures = ~ccGestures ? [];
~ccGestureAdd = { |g| ~ccGestures = ~ccGestures.add(g) };
~ccGState = IdentityDictionary.new; // name -> (holdT:, lastFire:, fired:)
~ccEvalGestures = { |pose, prev, now|
~ccGestures.do { |g|
var st = ~ccGState[g[\name]] ? (holdT: 0.0, lastFire: -100.0, fired: false);
var on = g[\detect].(pose, prev) == true;
if(on) {
if(st[\holdT] <= 0) { st[\holdT] = now };
// edge gestures fire once on the false->true transition (re-arm
// only after the detect drops); held gestures re-fire per cooldown.
if(((now - st[\holdT]) >= (g[\hold] ? 0))
and: { (now - st[\lastFire]) > (g[\cooldown] ? 0.5) }
and: { (g[\edge] != true) or: { st[\fired].not } }) {
st[\lastFire] = now; st[\holdT] = now; st[\fired] = true;
g[\fire].();
};
} {
if(st[\fired]) { g[\release] !? { |r| r.() }; st[\fired] = false };
st[\holdT] = 0;
};
~ccGState[g[\name]] = st;
};
};
// -- harmony state --
// Gammes + resolution degree->note : delegue a palette/note.scd (~noteFor).
// ~ccScales reste un alias de ~scales pour les lecteurs externes.
~ccScales = ~scales ? ~ccScales;
~ccHarmony = ~ccHarmony ? (root: 45, scale: \minor, pad: \dark, phrase: 0, octave: 0);
~ccNote = { |degree, oct=0|
~noteFor.(degree, ~ccHarmony[\scale] ? \minor,
~ccHarmony[\root] ? 45, (~ccHarmony[\octave] ? 0) + oct);
};
// -- registre des morceaux --
~concert = ~concert ? ();
~concert[\morceaux] = [];
~concertAdd = { |m| ~concert[\morceaux] = ~concert[\morceaux].add(m) };
~concert[\running] = false;
~concert[\idx] = 0;
~concert[\clock] = nil;
~concert[\routine] = nil;
~concert[\ctx] = nil; // (m: module, ctx: scratch) du morceau courant
~concert[\crossT] = 0.0;
~concert[\lastFire] = -100.0;
~concert[\startMorceau] = { |i|
var m = ~concert[\morceaux][i];
var ctx = ();
m[\start].(ctx);
~concert[\ctx] = (m: m, ctx: ctx);
("[concert] " ++ (i + 1) ++ "/" ++ ~concert[\morceaux].size
++ " : " ++ m[\name]).postln;
};
~concert[\stopMorceau] = {
~concert[\ctx] !? { |c| c[\m][\stop].(c[\ctx]) };
~concert[\ctx] = nil;
};
// Enchainement par choix ALEATOIRE (defaut) : saute vers un morceau != courant.
// ~concert[\seq] = true rebascule en mode sequentiel (idx+1 wrap).
~concert[\next] = {
var n = ~concert[\morceaux].size;
if(n > 1) {
var nxt;
if(~concert[\seq] == true) {
nxt = (~concert[\idx] + 1) % n;
} {
nxt = (0 .. (n - 1)).reject({ |i| i == ~concert[\idx] }).choose;
};
~concert[\stopMorceau].(); // fade out (stop releve les gates)
~concert[\idx] = nxt;
~concert[\startMorceau].(nxt); // fade in
} {
~concert[\stopMorceau].(); // 0/1 morceau : on relance le courant
~concert[\startMorceau].(~concert[\idx]);
};
};
// 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 } {
if(~concert[\morceaux].size < 1) {
"[concert] aucun morceau enregistre".warn;
} {
~concert[\running] = true;
~concert[\idx] = 0;
~ccGState = IdentityDictionary.new;
~concert[\clock] = TempoClock.default; // horloge unique (Lot C)
~setTempo.(126); // tempo de base concert
~concert[\startMorceau].(0);
~concert[\routine] = Routine({
var beat = 0;
var prev = ~ccPose.value;
inf.do {
var pose = ~ccPose.value;
var now = thisThread.seconds;
var c;
// 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|
cc[\m][\update].(cc[\ctx], pose);
cc[\m][\tick] !? { |t| t.(cc[\ctx], pose, beat) };
};
beat = beat + 1;
~ccCtlDur.wait;
};
}).play(~concert[\clock]);
"[concert] STARTED".postln;
};
};
};
~concert[\stop] = {
~concert[\routine] !? { |r| r.stop };
~concert[\stopMorceau].();
// NE PAS stopper l'horloge : elle alias TempoClock.default (Lot C) et la
// Routine est deja stoppee. La stopper figerait tout le systeme.
~concert[\clock] = nil;
~concert[\running] = false;
// restore the master event-FX so a stop mid-squat/spin doesn't leave the
// mix muffled / stuttering for the next set
~ccMaster !? { ~ccMaster[\filterTo].(20000) };
~ccStutter !? { |st| st.set(\mix, 0) };
"[concert] STOPPED".postln;
};
~doSceneConcert = {
~concert[\start].();
~doRegisterPriv.(\concert, { ~concert[\stop].() });
};
// /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');
"[data-only/scene_concert] engine ready".postln;
)