From 24078111b4dfba9b2e8441a3de8c00434e7c98b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=27=C3=A9lectron=20rare?= <108685187+electron-rare@users.noreply.github.com> Date: Tue, 30 Jun 2026 16:42:58 +0200 Subject: [PATCH] feat(matrix): fist-held gestures (reload/next) Held closed-fist (low openness, hysteresis+presence guard) per hand fires an action after ~matPinchHoldMs: left hand reloads the current preset, right hand advances to the next. Progress emitted on /matrix/fistprogress. - ~matFistEdge edge detector (engage/release/absent + hysteresis) - ~matFistTick fires once past hold threshold (guarded by ~matFistFired) - ~matFistReloadCurrent and ~matNextPreset wired to hand 0/1 - /matrix/fisthresh[/get] OSCdefs + ~matFistThreshPush - fistThresh saved and restored per .matrix file - 30 Hz poller now calls ~matFistTick alongside ~matPinchTick - Harness group Q: A-Q all green --- sound_algo/data_only/matrix.scd | 60 ++++++++++++++++++- .../matrix_presets/test_global_actions.scd | 32 ++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/sound_algo/data_only/matrix.scd b/sound_algo/data_only/matrix.scd index bf91aa2..243c1cc 100644 --- a/sound_algo/data_only/matrix.scd +++ b/sound_algo/data_only/matrix.scd @@ -463,6 +463,48 @@ File.exists(~matDir).not.if({ ("mkdir -p " ++ ~matDir.quote).systemCmd }); }; }; +// -- fist hold-to-fire state + edge detector + tick -- +~matFistThresh = ~matFistThresh ? 0.15; +~matFistState = ~matFistState ? Array.fill(2, \unknown); +~matFistHeld = ~matFistHeld ? Array.fill(2, nil); +~matFistFired = ~matFistFired ? Array.fill(2, false); +~matFistThreshPush = { ~toscSend !? { ~toscSend.("/matrix/fisthresh", ~matFistThresh) } }; +~matFistReloadCurrent = { + ~matCurrentName.notNil.if({ + ~matLoad.(~matCurrentName); + ~toscSend !? { ~toscSend.("/matrix/loaded", ~matCurrentName.asString) }; + }); +}; +// fist edge from openness (hysteresis + presence guard). hand 0 = L, 1 = R. +~matFistEdge = { |hand, x, openness| + var present = x > 0.001; + var prev = ~matFistState[hand] ? \unknown; + var cur = present.if({ + (openness <= ~matFistThresh).if({ \fist }, + { (openness >= (~matFistThresh + 0.15)).if({ \open }, { prev }) }) + }, { \unknown }); + ((prev != \fist) and: { cur == \fist }).if({ + ~matFistHeld[hand] = Main.elapsedTime; ~matFistFired[hand] = false; + }); + ((prev == \fist) and: { cur != \fist }).if({ + ~matFistHeld[hand] = nil; ~matFistFired[hand] = false; + ~toscSend !? { ~toscSend.("/matrix/fistprogress", hand, 0) }; + }); + ~matFistState[hand] = cur; +}; +~matFistTick = { + 2.do { |hand| + (~matFistHeld[hand].notNil).if({ + var prog = (((Main.elapsedTime - ~matFistHeld[hand]) * 1000) / ~matPinchHoldMs).clip(0, 1); + ~toscSend !? { ~toscSend.("/matrix/fistprogress", hand, prog) }; + ((prog >= 1.0) and: { ~matFistFired[hand].not }).if({ + ~matFistFired[hand] = true; + (hand == 0).if({ ~matFistReloadCurrent.() }, { ~matNextPreset.() }); + }); + }); + }; +}; + // push settings to surfaces: transientBars, keepN, keep0.. ~matGlobalSettingsPush = { var s = ~matGlobalSettings; var keep = s[\breakdownKeep] ? []; @@ -787,7 +829,8 @@ OSCdef(\mat_steps_get, { |msg, time, addr| voicePoses: ~matVoices.collect({ |name| ~matVoicePoses[name] ? [] }), globalActions: ~matGlobalActions, globalSettings: ~matGlobalSettings, - pinchHoldMs: ~matPinchHoldMs + pinchHoldMs: ~matPinchHoldMs, + fistThresh: ~matFistThresh ); File.use(path, "w", { |f| f.write(payload.asCompileString) }); ~matSavedNames.add(safe.asSymbol); @@ -897,6 +940,7 @@ OSCdef(\mat_steps_get, { |msg, time, addr| ~matGlobalSettings[\breakdownKeep] = g[\breakdownKeep].collect({ |v| v.asSymbol }) }); }); ~matPinchHoldMs = ((raw.isArray.not) and: { raw[\pinchHoldMs].notNil }).if({ raw[\pinchHoldMs].asFloat.clip(50, 2000) }, { 300 }); + ~matFistThresh = ((raw.isArray.not) and: { raw[\fistThresh].notNil }).if({ raw[\fistThresh].asFloat.clip(0.05, 0.5) }, { 0.15 }); ~lp[\matPlaying].if({ ~matApplyBar.(~lp[\matBar]) }); ~matGridPush.(); ~matInstrPush.(); @@ -907,6 +951,7 @@ OSCdef(\mat_steps_get, { |msg, time, addr| ~matGlobalActionsPush.(); ~matGlobalSettingsPush.(); ~matPinchHoldPush.(); + ~matFistThreshPush.(); ("[matrix] loaded " ++ path).postln; true }, { @@ -1144,6 +1189,15 @@ OSCdef(\mat_pinchhold_get, { |msg, time, addr| ~toscTouch !? { ~toscTouch.(addr) }; ~matPinchHoldPush.() }, '/matrix/pinchhold/get'); +OSCdef(\mat_fisthresh, { |msg, time, addr| + ~toscTouch !? { ~toscTouch.(addr) }; + ~matFistThresh = (msg[1] ? 0.15).asFloat.clip(0.05, 0.5); + ~matFistThreshPush.() +}, '/matrix/fisthresh'); +OSCdef(\mat_fisthresh_get, { |msg, time, addr| + ~toscTouch !? { ~toscTouch.(addr) }; + ~matFistThreshPush.() +}, '/matrix/fisthresh/get'); OSCdef(\mat_bpm, { |msg, time, addr| ~toscTouch !? { ~toscTouch.(addr) }; ~matSetBpm.((msg[1] ? 120)) @@ -1281,7 +1335,7 @@ OSCdef(\mat_audition, { |msg, time, addr| ~matModValuesRoutine = Routine({ loop { ~matModValuesPush.(); (1/12).wait } }).play(AppClock); ~matPinchRoutine !? { ~matPinchRoutine.stop }; -~matPinchRoutine = Routine({ loop { ~matPinchTick.(); (1/30).wait } }).play(AppClock); +~matPinchRoutine = Routine({ loop { ~matPinchTick.(); ~matFistTick.(); (1/30).wait } }).play(AppClock); // own capture listeners (independent of the pose pipeline's internal dicts) OSCdef(\mat_mod_hands, { |msg| @@ -1293,6 +1347,8 @@ OSCdef(\mat_mod_hands, { |msg| ~matModCache[\rOpen] = msg[8] ? 0; ~matModCache[\handSpeed] = ((msg[5] ? 0) max: (msg[9] ? 0)); ~matModCache[\handDist] = msg[10] ? 0; + ~matFistEdge.(0, msg[2] ? 0, msg[4] ? 0); + ~matFistEdge.(1, msg[6] ? 0, msg[8] ? 0); }, '/pose/hands'); OSCdef(\mat_mod_center, { |msg| 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 b92aa3b..1f1151b 100644 --- a/sound_algo/data_only/matrix_presets/test_global_actions.scd +++ b/sound_algo/data_only/matrix_presets/test_global_actions.scd @@ -146,6 +146,38 @@ OSCdef(\mat_pinchhold).func.value(['/matrix/pinchhold', 500]); (~matPinchHoldMs == 420).not.if({ pass = false; "FAIL: pinchHoldMs not restored".postln }); ("rm -f '" ++ (~matDir +/+ "zz_phms_test.matrix") ++ "'").systemCmd; +// -- Q: fist hold-to-fire (engage records, tick fires, release/absent cancel, threshold, save) -- +~matPinchRoutine !? { ~matPinchRoutine.stop }; +~matPinchHoldMs = 300; ~matFistThresh = 0.15; +~matFistState = Array.fill(2, \unknown); ~matFistHeld = Array.fill(2, nil); ~matFistFired = Array.fill(2, false); +~loadedFist = nil; ~matCurrentName = \zztest_morceau; +~matLoadReal = ~matLoad; ~matLoad = { |n| ~loadedFist = n.asSymbol }; +~matFistEdge.(0, 0.5, 0.05); // L fist engage -> held, no fire +(~matFistHeld[0].isNil).if({ pass = false; "FAIL: L fist engage not held".postln }); +~matFistTick.(); +(~loadedFist.notNil).if({ pass = false; "FAIL: L fist fired before hold".postln }); +~matFistHeld[0] = Main.elapsedTime - 0.5; // 500ms held (> 300) +~matFistTick.(); +(~loadedFist == \zztest_morceau).not.if({ pass = false; "FAIL: L fist hold did not reload current".postln }); +~loadedFist = nil; ~matFistState = Array.fill(2, \unknown); ~matFistHeld = Array.fill(2, nil); ~matFistFired = Array.fill(2, false); +~matFistEdge.(1, 0.5, 0.05); // R fist engage +~matFistEdge.(1, 0.5, 0.40); // open (>= 0.30) -> release +~matFistTick.(); +(~matFistHeld[1].isNil).not.if({ pass = false; "FAIL: R fist release did not clear".postln }); +~matFistState = Array.fill(2, \unknown); ~matFistHeld = Array.fill(2, nil); +~matFistEdge.(0, 0.0, 0.0); // absent hand (x=0, openness 0) must NOT engage +(~matFistHeld[0].isNil).not.if({ pass = false; "FAIL: absent hand engaged a fist".postln }); +OSCdef(\mat_fisthresh).func.value(['/matrix/fisthresh', 0.25]); +(~matFistThresh == 0.25).not.if({ pass = false; "FAIL: fisthresh not set".postln }); +~matFistThresh = 0.15; ~matLoad = ~matLoadReal; +~matInstruments = ~matInstruments ? Array.fill(~matVoices.size, { \default }); +~matColorDefs = ~matColorDefs ? Array.fill(~matVoices.size, { ~matDefaultColorDefs.value }); +~matSavedNames = ~matSavedNames ? Set.new; +~matFistThresh = 0.22; ~matSave.("zz_fth_test"); ~matFistThresh = 0.15; +~matLoadFile.(~matDir +/+ "zz_fth_test.matrix"); +(~matFistThresh == 0.22).not.if({ pass = false; "FAIL: fistThresh not restored".postln }); +("rm -f '" ++ (~matDir +/+ "zz_fth_test.matrix") ++ "'").systemCmd; + pass.if({ "GLOBALACT PASS".postln }, { "GLOBALACT FAIL".postln }); 0.exit; )