216 lines
9.1 KiB
Markdown
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;
|
|
)
|