feat(matrix): drum feel + relance actions

Add \doubleTime and \halfTime as drum-only subdivision feel toggles
(\matDrumFeel \normal/\double/\half, mutually exclusive). Engaging a
feel re-sources only drum voices (matLastColor reset + matApplyBar).
The \relanceEvolve composite action seeks to bar 0 then fires
matRelanceEvolve. \matDrumFeelPush sends /matrix/drumfeel on toggle
and on preset load (resets to \normal). OSCdef mat_drumfeel_get added.
Drum feel stretch applied in matVariationOverlay + matColorStepPattern
via txStretch local var; non-drum voices use matTxStretch unchanged.
Web vocab: relanceEvolve added, evolve relabelled Évolue.
Test file extended with sections R (feel toggle) and S (relanceEvolve).
This commit is contained in:
L'électron rare
2026-07-02 14:56:07 +02:00
parent feb32938fb
commit a569122f14
3 changed files with 64 additions and 12 deletions
+35 -6
View File
@@ -213,10 +213,14 @@ File.exists(~matDir).not.if({ ("mkdir -p " ++ ~matDir.quote).systemCmd });
[\freq, Pfunc({ |e| (e[\freq] ? 440) * freqRatio })] // pitch mod step-path only (no degree here)
}, { [] });
var modPairs = ~matModPairs.(name, spec, volOf);
// drum feel: drum voices get feel-based stretch; other voices use global transient stretch
var txStretch = (~matDrumVoices.includes(name)).if({
(~matDrumFeel == \double).if({ 0.5 }, { (~matDrumFeel == \half).if({ 2.0 }, { ~matTxStretch.value }) })
}, { ~matTxStretch.value });
Pchain(
Pbind(*([
\matGlow, Pfunc { |e| ~matEmitTrig.(vi, (e[\amp] ? spec[\amp]).clip(0, 1)); 0 },
\stretch, (spec[\stretch] * ~matTxStretch.value),
\stretch, (spec[\stretch] * txStretch),
\amp, Pfunc({ (spec[\amp] ? 1.0) * volOf.value })
] ++ instPair ++ cdPairs ++ freqPair ++ modPairs)),
base
@@ -233,6 +237,10 @@ File.exists(~matDir).not.if({ ("mkdir -p " ++ ~matDir.quote).systemCmd });
var freqPair = pitched.includes(name).if({
[\freq, Pfunc { |e| (~lpNote.notNil).if({ ~lpNote.(e[\degree] + ~matPitchOffset.(name), 0).midicps * freqRatio }, { 440 }) }]
}, { [] });
// drum feel: drum voices get feel-based stretch; other voices use global transient stretch
var txStretch = (~matDrumVoices.includes(name)).if({
(~matDrumFeel == \double).if({ 0.5 }, { (~matDrumFeel == \half).if({ 2.0 }, { ~matTxStretch.value }) })
}, { ~matTxStretch.value });
Pbind(*([
\stepIdx, Pseq((0..15), inf),
\matGlow, Pfunc { |e|
@@ -245,7 +253,7 @@ File.exists(~matDir).not.if({ ("mkdir -p " ++ ~matDir.quote).systemCmd });
\amp, Pfunc { |e|
var s = spec[\steps][e[\stepIdx]];
((spec[\amp] ? 1.0) * (s.isNil.if({ 0 }, { s[\vel] }))) * volOf.value },
\stretch, (spec[\stretch] * ~matTxStretch.value),
\stretch, (spec[\stretch] * txStretch),
\dur, ~matBeatsPerBar / 16
] ++ freqPair ++ instPair ++ cdPairs ++ modPairs))
};
@@ -371,9 +379,12 @@ File.exists(~matDir).not.if({ ("mkdir -p " ++ ~matDir.quote).systemCmd });
// -- global morceau actions: momentary transient overrides + gesture mapping --
~matTransient = ~matTransient ? nil; // (kind:, voices:[Symbol], bars:Int) or nil
~matGlobalSettings = ~matGlobalSettings ? ( transientBars: 1, breakdownKeep: [\kick, \sub] );
~matGlobalActions = ~matGlobalActions ? [\fill, \drop, \breakdown, \evolve, \muteDrums, \muteMelo, \washReverb, \next]; // 8 pinch slots 0..7
~matGlobalActions = ~matGlobalActions ? [\fill, \doubleTime, \washReverb, \muteDrums, \relanceEvolve, \halfTime, \next, \muteMelo]; // 8 pinch slots 0..7
~matDrumVoices = ~matDrumVoices ? [\kick,\hats,\clap,\ride,\rim,\sweep,\fx,\snare,\crash,\shaker,\perc,\tom];
~matMeloVoices = ~matMeloVoices ? [\sub,\acid,\arp,\lead,\stab,\pad,\reese,\bells,\melody,\chord];
// drum feel toggle (\normal/\double/\half): drum-only subdivison feel, persists as toggle, resets on preset load
~matDrumFeel = ~matDrumFeel ? \normal;
~matDrumFeelPush = { ~toscSend !? { ~toscSend.("/matrix/drumfeel", ~matDrumFeel.asString) } };
// transient param channels read by ~matVariation (1.0/0.0 when no transient or a colour-only kind)
~matTxStretch = { (~matTransient.notNil).if({ ~matTransient[\stretch] ? 1.0 }, { 1.0 }) };
~matTxRev = { (~matTransient.notNil).if({ ~matTransient[\rev] ? 0.0 }, { 0.0 }) };
@@ -432,10 +443,22 @@ File.exists(~matDir).not.if({ ("mkdir -p " ++ ~matDir.quote).systemCmd });
(act == \drop).if({ ~matSetTransient.(\drop, []) });
(act == \breakdown).if({ ~matSetTransient.(\breakdown, ~matGlobalSettings[\breakdownKeep] ? [\kick, \sub]) });
(act == \evolve).if({ ~matRelanceEvolve.() });
(act == \relanceEvolve).if({ ~matSeek.(0); ~matRelanceEvolve.() });
(act == \muteDrums).if({ ~matSetTransient.(\mute, ~matDrumVoices) });
(act == \muteMelo).if({ ~matSetTransient.(\mute, ~matMeloVoices) });
(act == \doubleTime).if({ ~matSetTransient.(\doubleTime, []) });
(act == \halfTime).if({ ~matSetTransient.(\halfTime, []) });
// drum feel: toggle \normal/\double and \normal/\half; mutually exclusive
(act == \doubleTime).if({
(~matDrumFeel == \double).if({ ~matDrumFeel = \normal }, { ~matDrumFeel = \double });
~matVoices.do { |nm, vi| (~matDrumVoices.includes(nm)).if({ ~matLastColor[vi] = -1 }) };
(~lp[\matPlaying] == true).if({ ~matApplyBar.(~lp[\matBar] ? 0) });
~matDrumFeelPush.();
});
(act == \halfTime).if({
(~matDrumFeel == \half).if({ ~matDrumFeel = \normal }, { ~matDrumFeel = \half });
~matVoices.do { |nm, vi| (~matDrumVoices.includes(nm)).if({ ~matLastColor[vi] = -1 }) };
(~lp[\matPlaying] == true).if({ ~matApplyBar.(~lp[\matBar] ? 0) });
~matDrumFeelPush.();
});
(act == \washReverb).if({ ~matSetTransient.(\washReverb, []) });
(act == \next).if({ ~matNextPreset.() });
};
@@ -886,7 +909,7 @@ OSCdef(\mat_steps_get, { |msg, time, addr|
}
});
// restore global morceau actions (default when absent/legacy)
~matGlobalActions = [\fill, \drop, \breakdown, \evolve, \muteDrums, \muteMelo, \washReverb, \next];
~matGlobalActions = [\fill, \doubleTime, \washReverb, \muteDrums, \relanceEvolve, \halfTime, \next, \muteMelo];
((raw.isArray.not) and: { raw[\globalActions].notNil and: { raw[\globalActions].isArray } }).if({
raw[\globalActions].do { |s, i| (i < 8 and: { s.notNil }).if({ ~matGlobalActions[i] = s.asSymbol }) }
});
@@ -900,6 +923,7 @@ OSCdef(\mat_steps_get, { |msg, time, addr|
});
~matPinchHoldMs = ((raw.isArray.not) and: { raw[\pinchHoldMs].notNil }).if({ raw[\pinchHoldMs].asFloat.clip(50, 2000) }, { 300 });
// fistThresh key accepted from old presets but no longer used (fist gestures removed 2026-07-02)
~matDrumFeel = \normal; // reset drum feel toggle on preset load
~lp[\matPlaying].if({ ~matApplyBar.(~lp[\matBar]) });
~matGridPush.();
~matInstrPush.();
@@ -910,6 +934,7 @@ OSCdef(\mat_steps_get, { |msg, time, addr|
~matGlobalActionsPush.();
~matGlobalSettingsPush.();
~matPinchHoldPush.();
~matDrumFeelPush.();
("[matrix] loaded " ++ path).postln;
true
}, {
@@ -1147,6 +1172,10 @@ OSCdef(\mat_pinchhold_get, { |msg, time, addr|
~toscTouch !? { ~toscTouch.(addr) };
~matPinchHoldPush.()
}, '/matrix/pinchhold/get');
OSCdef(\mat_drumfeel_get, { |msg, time, addr|
~toscTouch !? { ~toscTouch.(addr) };
~matDrumFeelPush.()
}, '/matrix/drumfeel/get');
OSCdef(\mat_bpm, { |msg, time, addr|
~toscTouch !? { ~toscTouch.(addr) };
~matSetBpm.((msg[1] ? 120))
@@ -12,7 +12,7 @@ feat.load;
~lp[\matPlaying] = false;
// -- A: defaults --
(~matGlobalActions == [\fill, \drop, \breakdown, \evolve, \muteDrums, \muteMelo, \washReverb, \next]).not.if({ pass = false; "FAIL: default mapping".postln });
(~matGlobalActions == [\fill, \doubleTime, \washReverb, \muteDrums, \relanceEvolve, \halfTime, \next, \muteMelo]).not.if({ pass = false; "FAIL: default mapping".postln });
// -- B: fill -> active voices read colour 6, silent voices stay 0 --
~matSetTransient.(\fill, []);
@@ -48,7 +48,7 @@ feat.load;
~matSavedNames = ~matSavedNames ? Set.new;
~matGlobalActions = [\evolve, \breakdown, \fill, \drop, \muteMelo, \washReverb, \muteDrums, \next]; // non-default 8-slot
~matSave.("zz_gact_test");
~matGlobalActions = [\fill, \drop, \breakdown, \evolve, \muteDrums, \muteMelo, \washReverb, \next]; // clobber, then reload
~matGlobalActions = [\fill, \doubleTime, \washReverb, \muteDrums, \relanceEvolve, \halfTime, \next, \muteMelo]; // clobber, then reload
~matLoadFile.(~matDir +/+ "zz_gact_test.matrix");
(~matGlobalActions == [\evolve, \breakdown, \fill, \drop, \muteMelo, \washReverb, \muteDrums, \next]).not.if({ pass = false; "FAIL: globalActions not round-tripped".postln });
("rm -f '" ++ (~matDir +/+ "zz_gact_test.matrix") ++ "'").systemCmd;
@@ -116,7 +116,7 @@ feat.load;
// -- P: pinch hold-to-fire (engage doesn't fire; fires past hold; release cancels) --
~matPinchRoutine !? { ~matPinchRoutine.stop };
~matGlobalActions = [\fill, \drop, \breakdown, \evolve, \muteDrums, \muteMelo, \washReverb, \next];
~matGlobalActions = [\fill, \doubleTime, \washReverb, \muteDrums, \relanceEvolve, \halfTime, \next, \muteMelo];
~matPinchHoldMs = 300; ~matTransient = nil;
~matPinchHeld = Array.fill(8, nil); ~matPinchFired = Array.fill(8, false);
OSCdef(\mat_ev_pinch).func.value(['/pose/pinch', 0, 0, 1, 1]); // L index engage -> slot 0 held
@@ -148,6 +148,28 @@ OSCdef(\mat_pinchhold).func.value(['/matrix/pinchhold', 500]);
// Q: fist gestures removed 2026-07-02 (user request; FIST_ENABLE=0 at the source)
// -- R: drum feel toggle (doubleTime/halfTime) — structure check only --
~matDrumFeel = \normal;
~matGlobalActions = [\doubleTime, \halfTime, \none, \none, \none, \none, \none, \none];
~matFireGlobal.(0); // slot 0 = doubleTime: normal->double
(~matDrumFeel == \double).not.if({ pass = false; "FAIL: doubleTime did not set \double".postln });
~matFireGlobal.(0); // toggle off: double->normal
(~matDrumFeel == \normal).not.if({ pass = false; "FAIL: doubleTime toggle did not return \normal".postln });
~matFireGlobal.(1); // slot 1 = halfTime: normal->half
(~matDrumFeel == \half).not.if({ pass = false; "FAIL: halfTime did not set \half".postln });
~matFireGlobal.(0); // doubleTime while half active: half->double (mutual exclusion)
(~matDrumFeel == \double).not.if({ pass = false; "FAIL: doubleTime did not clear halfTime".postln });
~matDrumFeel = \normal;
// -- S: relanceEvolve resets playhead to 0 then evolves --
~lp[\matBar] = 5;
~matEvolve = false; ~matBaseGrid = nil;
~lp[\matrix][0][5] = 3;
~matGlobalActions = [\relanceEvolve, \none, \none, \none, \none, \none, \none, \none];
~matFireGlobal.(0); // slot 0 = relanceEvolve
(~lp[\matBar] == 0).not.if({ pass = false; "FAIL: relanceEvolve did not seek to bar 0".postln });
(~matBaseGrid.isNil).not.if({ pass = false; "FAIL: relanceEvolve leaked baseGrid".postln });
pass.if({ "GLOBALACT PASS".postln }, { "GLOBALACT FAIL".postln });
0.exit;
)
@@ -65,7 +65,8 @@ export const POSE_EVENT_CHOICES = [
export const GLOBAL_ACTION_CHOICES = [
{ id: "none", label: "—" }, { id: "fill", label: "Fill" },
{ id: "drop", label: "Drop" }, { id: "breakdown", label: "Breakdown" },
{ id: "evolve", label: "Relance + evolve" },
{ id: "evolve", label: "Évolue" },
{ id: "relanceEvolve", label: "Relance+evolve" },
{ id: "muteDrums", label: "Mute drums" }, { id: "muteMelo", label: "Mute mélo" },
{ id: "doubleTime", label: "Double-time" }, { id: "halfTime", label: "Half-time" },
{ id: "washReverb", label: "Wash reverb" }, { id: "next", label: "Suivant" },
@@ -76,10 +77,10 @@ export const GLOBAL_SLOTS = [
{ key: 4, label: "Pince MD index" }, { key: 5, label: "Pince MD majeur" },
{ key: 6, label: "Pince MD annulaire" }, { key: 7, label: "Pince MD auriculaire" },
];
export let matGlobalActions = ["fill", "drop", "breakdown", "evolve", "muteDrums", "muteMelo", "washReverb", "next"];
export let matGlobalActions = ["fill", "doubleTime", "washReverb", "muteDrums", "relanceEvolve", "halfTime", "next", "muteMelo"];
export function setGlobalAction(slot, id) { if (slot >= 0 && slot < 8) matGlobalActions[slot] = id; }
export function resetGlobalActions() {
matGlobalActions = ["fill", "drop", "breakdown", "evolve", "muteDrums", "muteMelo", "washReverb", "next"];
matGlobalActions = ["fill", "doubleTime", "washReverb", "muteDrums", "relanceEvolve", "halfTime", "next", "muteMelo"];
}
export let matGlobalSettings = { transientBars: 1, breakdownKeep: ["kick", "sub"] };