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.
This commit is contained in:
L'électron rare
2026-06-30 11:41:55 +02:00
parent b72056ecb3
commit 0cc64194df
2 changed files with 68 additions and 1 deletions
+43 -1
View File
@@ -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|
@@ -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;
)