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:
@@ -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"] };
|
||||
|
||||
Reference in New Issue
Block a user