diff --git a/sound_algo/data_only/matrix.scd b/sound_algo/data_only/matrix.scd index b3d97be..bf91aa2 100644 --- a/sound_algo/data_only/matrix.scd +++ b/sound_algo/data_only/matrix.scd @@ -445,6 +445,24 @@ File.exists(~matDir).not.if({ ("mkdir -p " ++ ~matDir.quote).systemCmd }); 8.do { |i| ~toscSend !? { ~toscSend.("/matrix/globalaction", i, (~matGlobalActions[i] ? \none).asString) } } }; +// -- pinch hold-to-fire state + tick -- +~matPinchHoldMs = ~matPinchHoldMs ? 300; +~matPinchHeld = ~matPinchHeld ? Array.fill(8, nil); // engage time (Main.elapsedTime) or nil +~matPinchFired = ~matPinchFired ? Array.fill(8, false); +~matPinchHoldPush = { ~toscSend !? { ~toscSend.("/matrix/pinchhold", ~matPinchHoldMs) } }; +~matPinchTick = { + 8.do { |slot| + (~matPinchHeld[slot].notNil).if({ + var prog = (((Main.elapsedTime - ~matPinchHeld[slot]) * 1000) / ~matPinchHoldMs).clip(0, 1); + ~toscSend !? { ~toscSend.("/matrix/pinchprogress", slot, prog) }; + ((prog >= 1.0) and: { ~matPinchFired[slot].not }).if({ + ~matPinchFired[slot] = true; + ~matFireGlobal.(slot); + }); + }); + }; +}; + // push settings to surfaces: transientBars, keepN, keep0.. ~matGlobalSettingsPush = { var s = ~matGlobalSettings; var keep = s[\breakdownKeep] ? []; @@ -768,7 +786,8 @@ OSCdef(\mat_steps_get, { |msg, time, addr| voiceMods: ~matVoices.collect({ |name| ~matVoiceMods[name] ? [] }), voicePoses: ~matVoices.collect({ |name| ~matVoicePoses[name] ? [] }), globalActions: ~matGlobalActions, - globalSettings: ~matGlobalSettings + globalSettings: ~matGlobalSettings, + pinchHoldMs: ~matPinchHoldMs ); File.use(path, "w", { |f| f.write(payload.asCompileString) }); ~matSavedNames.add(safe.asSymbol); @@ -877,6 +896,7 @@ OSCdef(\mat_steps_get, { |msg, time, addr| (g[\breakdownKeep].notNil and: { g[\breakdownKeep].isArray }).if({ ~matGlobalSettings[\breakdownKeep] = g[\breakdownKeep].collect({ |v| v.asSymbol }) }); }); + ~matPinchHoldMs = ((raw.isArray.not) and: { raw[\pinchHoldMs].notNil }).if({ raw[\pinchHoldMs].asFloat.clip(50, 2000) }, { 300 }); ~lp[\matPlaying].if({ ~matApplyBar.(~lp[\matBar]) }); ~matGridPush.(); ~matInstrPush.(); @@ -886,6 +906,7 @@ OSCdef(\mat_steps_get, { |msg, time, addr| ~matEvolvePush.(); ~matGlobalActionsPush.(); ~matGlobalSettingsPush.(); + ~matPinchHoldPush.(); ("[matrix] loaded " ++ path).postln; true }, { @@ -1044,8 +1065,21 @@ OSCdef(\mat_ev_finger, { |msg| ~matPoseFire.(~matEventFinger.(msg[2] ? 0, msg[3] OSCdef(\mat_ev_pinch, { |msg| var hand = (msg[2] ? 0).asInteger; var finger = (msg[3] ? 1).asInteger; - ~matPoseFire.(~matEventPinch.(hand, finger)); // per-voice pinch bindings (unchanged) - ~matFireGlobal.((hand * 4) + (finger - 1)); // global action by pinch slot 0..7 + var state = (msg[4] ? 1).asInteger; + var slot = (hand * 4) + (finger - 1); + (state == 1).if({ + ~matPoseFire.(~matEventPinch.(hand, finger)); // per-voice pinch bindings on engage + (slot >= 0 and: { slot < 8 }).if({ + ~matPinchHeld[slot] = Main.elapsedTime; + ~matPinchFired[slot] = false; + }); + }, { + (slot >= 0 and: { slot < 8 }).if({ + ~matPinchHeld[slot] = nil; + ~matPinchFired[slot] = false; + ~toscSend !? { ~toscSend.("/matrix/pinchprogress", slot, 0) }; + }); + }); }, '/pose/pinch'); OSCdef(\mat_pose, { |msg, time, addr| @@ -1101,6 +1135,15 @@ OSCdef(\mat_globalsettings_get, { |msg, time, addr| ~toscTouch !? { ~toscTouch.(addr) }; ~matGlobalSettingsPush.() }, '/matrix/globalsettings/get'); +OSCdef(\mat_pinchhold, { |msg, time, addr| + ~toscTouch !? { ~toscTouch.(addr) }; + ~matPinchHoldMs = (msg[1] ? 300).asFloat.clip(50, 2000); + ~matPinchHoldPush.() +}, '/matrix/pinchhold'); +OSCdef(\mat_pinchhold_get, { |msg, time, addr| + ~toscTouch !? { ~toscTouch.(addr) }; + ~matPinchHoldPush.() +}, '/matrix/pinchhold/get'); OSCdef(\mat_bpm, { |msg, time, addr| ~toscTouch !? { ~toscTouch.(addr) }; ~matSetBpm.((msg[1] ? 120)) @@ -1237,6 +1280,9 @@ OSCdef(\mat_audition, { |msg, time, addr| ~matModValuesRoutine !? { ~matModValuesRoutine.stop }; ~matModValuesRoutine = Routine({ loop { ~matModValuesPush.(); (1/12).wait } }).play(AppClock); +~matPinchRoutine !? { ~matPinchRoutine.stop }; +~matPinchRoutine = Routine({ loop { ~matPinchTick.(); (1/30).wait } }).play(AppClock); + // own capture listeners (independent of the pose pipeline's internal dicts) OSCdef(\mat_mod_hands, { |msg| ~matModCache[\lHandY] = 1 - (msg[3] ? 0.5); // image y top-down -> up=1 diff --git a/sound_algo/data_only/matrix_presets/test_global_actions.scd b/sound_algo/data_only/matrix_presets/test_global_actions.scd index d9972c7..b92aa3b 100644 --- a/sound_algo/data_only/matrix_presets/test_global_actions.scd +++ b/sound_algo/data_only/matrix_presets/test_global_actions.scd @@ -114,18 +114,37 @@ feat.load; (~matBpm == 300).not.if({ pass = false; "FAIL: bpm not clamped to 300".postln }); ~matSetBpm.(120); // restore -// -- P: a pinch fires the mapped global action (slot = hand*4 + finger-1) -- +// -- 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]; +~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 +(~matTransient.isNil).not.if({ pass = false; "FAIL: engage fired immediately".postln }); +(~matPinchHeld[0].isNil).if({ pass = false; "FAIL: engage did not record hold".postln }); +~matPinchHeld[0] = Main.elapsedTime - 0.5; // simulate 500ms held (> 300) +~matPinchTick.(); +(~matTransient.notNil and: { ~matTransient[\kind] == \fill }).not.if({ pass = false; "FAIL: hold did not fire fill".postln }); ~matTransient = nil; -OSCdef(\mat_ev_pinch).func.value(['/pose/pinch', 0, 0, 1]); // L index -> slot 0 -> fill -(~matTransient.notNil and: { ~matTransient[\kind] == \fill }).not.if({ pass = false; "FAIL: L-index pinch != fill".postln }); -~matTransient = nil; -OSCdef(\mat_ev_pinch).func.value(['/pose/pinch', 0, 1, 1]); // R index -> slot 4 -> muteDrums (kind \mute) -(~matTransient.notNil and: { ~matTransient[\kind] == \mute }).not.if({ pass = false; "FAIL: R-index pinch != muteDrums".postln }); -~matTransient = nil; -OSCdef(\mat_ev_pinch).func.value(['/pose/pinch', 0, 1, 3]); // R ring -> slot 6 -> washReverb -(~matTransient.notNil and: { ~matTransient[\kind] == \washReverb }).not.if({ pass = false; "FAIL: R-ring pinch != washReverb".postln }); -~matTransient = nil; +~matPinchHeld = Array.fill(8, nil); ~matPinchFired = Array.fill(8, false); +OSCdef(\mat_ev_pinch).func.value(['/pose/pinch', 0, 1, 1, 1]); // R index engage -> slot 4 +OSCdef(\mat_ev_pinch).func.value(['/pose/pinch', 0, 1, 1, 0]); // release before hold +~matPinchTick.(); +(~matTransient.isNil).not.if({ pass = false; "FAIL: released-early pinch fired".postln }); +(~matPinchHeld[4].isNil).not.if({ pass = false; "FAIL: release did not clear hold".postln }); +OSCdef(\mat_pinchhold).func.value(['/matrix/pinchhold', 500]); +(~matPinchHoldMs == 500).not.if({ pass = false; "FAIL: pinchhold not set".postln }); +~matPinchHoldMs = 300; +// pinchHoldMs save/load roundtrip +~matInstruments = ~matInstruments ? Array.fill(~matVoices.size, { \default }); +~matColorDefs = ~matColorDefs ? Array.fill(~matVoices.size, { ~matDefaultColorDefs.value }); +~matSavedNames = ~matSavedNames ? Set.new; +~matPinchHoldMs = 420; +~matSave.("zz_phms_test"); +~matPinchHoldMs = 300; +~matLoadFile.(~matDir +/+ "zz_phms_test.matrix"); +(~matPinchHoldMs == 420).not.if({ pass = false; "FAIL: pinchHoldMs not restored".postln }); +("rm -f '" ++ (~matDir +/+ "zz_phms_test.matrix") ++ "'").systemCmd; pass.if({ "GLOBALACT PASS".postln }, { "GLOBALACT FAIL".postln }); 0.exit;