From 0cc64194dfff10f7fe9781f3364548487cf0c693 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 11:41:55 +0200 Subject: [PATCH] feat(matrix): hand gesture detection + mapping io Add ~matHandFlip (hysteresis open/closed per hand), ~matGlobalActionsPush, OSCdefs /matrix/globalaction + /get, /pose/hands integration, globalActions save/load roundtrip. --- sound_algo/data_only/matrix.scd | 44 ++++++++++++++++++- .../matrix_presets/test_global_actions.scd | 25 +++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/sound_algo/data_only/matrix.scd b/sound_algo/data_only/matrix.scd index c6b1d56..2bab630 100644 --- a/sound_algo/data_only/matrix.scd +++ b/sound_algo/data_only/matrix.scd @@ -424,6 +424,25 @@ File.exists(~matDir).not.if({ ("mkdir -p " ++ ~matDir.quote).systemCmd }); (act == \evolve).if({ ~matRelanceEvolve.() }); }; +// derive open/closed per hand from openness (hysteresis); fire only on an +// established open<->closed flip. Absent hand (x<=0) resets state (no fire). +~matHandFlip = { |hand, x, openness, openSlot, closeSlot| + var present = x > 0.001; + var prev = (~matHandState[hand]) ? \unknown; + var cur = present.if({ + (openness >= ~matOpenThresh).if({ \open }, + { (openness <= ~matCloseThresh).if({ \closed }, { prev }) }) + }, { \unknown }); + ((prev == \open) and: { cur == \closed }).if({ ~matFireGlobal.(closeSlot) }); + ((prev == \closed) and: { cur == \open }).if({ ~matFireGlobal.(openSlot) }); + ~matHandState[hand] = cur; +}; + +// push the 4-slot mapping to surfaces (symbols sent as strings) +~matGlobalActionsPush = { + 4.do { |i| ~toscSend !? { ~toscSend.("/matrix/globalaction", i, (~matGlobalActions[i] ? \none).asString) } } +}; + // -- ~matPush : push playhead position to all TouchOSC clients -- ~matPush = { ~toscSend !? { ~toscSend.("/matrix/playhead", ~lp[\matBar]) } @@ -719,7 +738,8 @@ OSCdef(\mat_steps_get, { |msg, time, addr| instruments: ~matInstruments.collect({ |x| (x ? \default) }), colorDefs: ~matColorDefs.collect({ |defs| (1..6).collect({ |c| defs[c] }) }), voiceMods: ~matVoices.collect({ |name| ~matVoiceMods[name] ? [] }), - voicePoses: ~matVoices.collect({ |name| ~matVoicePoses[name] ? [] }) + voicePoses: ~matVoices.collect({ |name| ~matVoicePoses[name] ? [] }), + globalActions: ~matGlobalActions ); File.use(path, "w", { |f| f.write(payload.asCompileString) }); ~matSavedNames.add(safe.asSymbol); @@ -815,6 +835,11 @@ OSCdef(\mat_steps_get, { |msg, time, addr| }); } }); + // restore global morceau actions (default when absent/legacy) + ~matGlobalActions = [\fill, \drop, \evolve, \breakdown]; + ((raw.isArray.not) and: { raw[\globalActions].notNil and: { raw[\globalActions].isArray } }).if({ + raw[\globalActions].do { |s, i| (i < 4 and: { s.notNil }).if({ ~matGlobalActions[i] = s.asSymbol }) } + }); ~lp[\matPlaying].if({ ~matApplyBar.(~lp[\matBar]) }); ~matGridPush.(); ~matInstrPush.(); @@ -822,6 +847,7 @@ OSCdef(\mat_steps_get, { |msg, time, addr| ~matVoices.size.do { |vi| (1..6).do { |c| ~matStepsPush.(vi, c) } }; ~matVoices.size.do { |vi| ~matVoiceModPush.(vi); ~matVoicePosePush.(vi) }; ~matEvolvePush.(); + ~matGlobalActionsPush.(); ("[matrix] loaded " ++ path).postln; true }, { @@ -1004,6 +1030,20 @@ OSCdef(\mat_evolverate, { |msg, time, addr| ~matEvolvePush.() }, '/matrix/evolverate'); +OSCdef(\mat_globalaction, { |msg, time, addr| + var slot = (msg[1] ? 0).asInteger; + var sym = (msg[2] ? \none).asSymbol; + ~toscTouch !? { ~toscTouch.(addr) }; + (slot >= 0 and: { slot < 4 }).if({ + ~matGlobalActions[slot] = sym; + ~toscSend !? { ~toscSend.("/matrix/globalaction", slot, sym.asString) }; + }) +}, '/matrix/globalaction'); +OSCdef(\mat_globalaction_get, { |msg, time, addr| + ~toscTouch !? { ~toscTouch.(addr) }; + ~matGlobalActionsPush.() +}, '/matrix/globalaction/get'); + OSCdef(\mat_stop, { |msg, time, addr| ~toscTouch !? { ~toscTouch.(addr) }; ~matStop.() @@ -1141,6 +1181,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; + ~matHandFlip.(\L, msg[2] ? 0, msg[4] ? 0, 0, 1); + ~matHandFlip.(\R, msg[6] ? 0, msg[8] ? 0, 2, 3); }, '/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 3081d95..6d1278c 100644 --- a/sound_algo/data_only/matrix_presets/test_global_actions.scd +++ b/sound_algo/data_only/matrix_presets/test_global_actions.scd @@ -42,6 +42,31 @@ feat.load; ~matFireGlobal.(0); // slot 0 = fill (~matTransient.notNil and: { ~matTransient[\kind] == \fill }).not.if({ pass = false; "FAIL: fire slot0->fill".postln }); +// -- G: hand-flip detection (no fire on appear/disappear; fire on open<->closed) -- +~fired = nil; ~matFireGlobal = { |slot| ~fired = slot }; // stub to capture the slot +~matHandState[\L] = \unknown; +~matHandFlip.(\L, 0.5, 0.9, 0, 1); // appear OPEN -> baseline, no fire +(~fired.isNil).not.if({ pass = false; "FAIL: fired on appearance".postln }); +~matHandFlip.(\L, 0.5, 0.1, 0, 1); // OPEN->CLOSED -> fire closeSlot(1) +(~fired == 1).not.if({ pass = false; "FAIL: open->closed did not fire slot1".postln }); +~fired = nil; +~matHandFlip.(\L, 0.5, 0.9, 0, 1); // CLOSED->OPEN -> fire openSlot(0) +(~fired == 0).not.if({ pass = false; "FAIL: closed->open did not fire slot0".postln }); +~fired = nil; +~matHandFlip.(\L, 0.0, 0.0, 0, 1); // disappear -> reset, no fire +(~fired.isNil).not.if({ pass = false; "FAIL: fired on disappearance".postln }); + +// -- H: mapping save/load roundtrip -- +~matInstruments = ~matInstruments ? Array.fill(~matVoices.size, { \default }); +~matColorDefs = ~matColorDefs ? Array.fill(~matVoices.size, { ~matDefaultColorDefs.value }); +~matSavedNames = ~matSavedNames ? Set.new; +~matGlobalActions = [\evolve, \breakdown, \fill, \drop]; // non-default +~matSave.("zz_gact_test"); +~matGlobalActions = [\fill, \drop, \evolve, \breakdown]; // clobber, then reload +~matLoadFile.(~matDir +/+ "zz_gact_test.matrix"); +(~matGlobalActions == [\evolve, \breakdown, \fill, \drop]).not.if({ pass = false; "FAIL: globalActions not round-tripped".postln }); +("rm -f '" ++ (~matDir +/+ "zz_gact_test.matrix") ++ "'").systemCmd; + pass.if({ "GLOBALACT PASS".postln }, { "GLOBALACT FAIL".postln }); 0.exit; )