feat(matrix): per-voice additive modulation pairs
Introduce ~matVoiceMods (IdentityDictionary voice→binding list) and ~matVoicePoses. Rewrite ~matModPairs to read ~matVoiceMods[name] instead of spec[\mod], summing contributions per target additively. Disallowed targets (not in ~matModTargets[name]) are silently dropped. Test: pan-cancel, rev-depth, cutoff-gate — RED→GREEN confirmed.
This commit is contained in:
@@ -149,27 +149,47 @@ File.exists(~matDir).not.if({ ("mkdir -p " ++ ~matDir.quote).systemCmd });
|
||||
})
|
||||
};
|
||||
|
||||
// -- ~matModPairs : capture-effect keys from the color's own mod (spec[\mod]) --
|
||||
// Returns [] when mod is nil (no effect key). Used by both pattern builders.
|
||||
// -- ~matModPairs : per-voice additive binding list -> flat pair list for Pbind --
|
||||
// Reads ~matVoiceMods[name] (array of (source:,target:,depth:) events).
|
||||
// Groups by target, sums contributions, returns flat [tgt1, Pfunc1, tgt2, Pfunc2, ...].
|
||||
// Targets not in ~matModTargets[name] are silently dropped.
|
||||
~matModPairs = { |name, spec, volOf|
|
||||
var mod = spec[\mod];
|
||||
mod.isNil.if({ [] }, {
|
||||
var src = mod[\source], tgt = mod[\target], d = mod[\depth];
|
||||
var allowed = ~matModTargets[name] ? [];
|
||||
var byTarget = IdentityDictionary.new;
|
||||
var pairs = [];
|
||||
((~matVoiceMods[name] ? []).select { |b|
|
||||
b[\source].notNil and: { b[\target].notNil } and: { allowed.includes(b[\target]) }
|
||||
}).do { |b| byTarget[b[\target]] = (byTarget[b[\target]] ? []) ++ [b] };
|
||||
byTarget.keysValuesDo { |tgt, list|
|
||||
(tgt == \cutoff).if({
|
||||
var c0 = ~matModNeutralCut[name] ? 1000;
|
||||
[\cutoff, Pfunc { var s = ~matModSourceVal.(src); c0 * (s.linexp(0,1,200,6000)/c0).pow(d) }]
|
||||
pairs = pairs ++ [\cutoff, Pfunc {
|
||||
var f = c0;
|
||||
list.do { |b| var s = ~matModSourceVal.(b[\source]);
|
||||
f = f * (s.linexp(0, 1, 200, 6000) / c0).pow(b[\depth]) };
|
||||
f
|
||||
}]
|
||||
}, {
|
||||
(tgt == \pan).if({
|
||||
[\pan, Pfunc { (~matModSourceVal.(src) * 2 - 1) * d }]
|
||||
}, {
|
||||
(tgt == \rev).if({
|
||||
[\rev, Pfunc { (~matModSourceVal.(src) * d).clip(0, 1) }]
|
||||
}, {
|
||||
[\amp, Pfunc { ((spec[\amp] ? 1.0) * volOf.value) * (1 + (d * (~matModSourceVal.(src) * 2 - 1))).max(0) }]
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
(tgt == \pan).if({
|
||||
pairs = pairs ++ [\pan, Pfunc {
|
||||
var a = 0; list.do { |b| a = a + ((~matModSourceVal.(b[\source]) * 2 - 1) * b[\depth]) };
|
||||
a.clip(-1, 1)
|
||||
}]
|
||||
}, {
|
||||
(tgt == \rev).if({
|
||||
pairs = pairs ++ [\rev, Pfunc {
|
||||
var a = 0; list.do { |b| a = a + (~matModSourceVal.(b[\source]) * b[\depth]) };
|
||||
a.clip(0, 1)
|
||||
}]
|
||||
}, {
|
||||
pairs = pairs ++ [\amp, Pfunc {
|
||||
var base = (spec[\amp] ? 1.0) * volOf.value;
|
||||
list.do { |b| base = base * (1 + (b[\depth] * (~matModSourceVal.(b[\source]) * 2 - 1))).max(0) };
|
||||
base
|
||||
}]
|
||||
}) }) })
|
||||
};
|
||||
pairs
|
||||
};
|
||||
|
||||
// -- ~matVariationOverlay : assemble the Pchain overlay given a resolved spec --
|
||||
@@ -865,7 +885,9 @@ OSCdef(\mat_audition, { |msg, time, addr|
|
||||
}, '/matrix/audition');
|
||||
|
||||
// -- Capture mod source cache (nil-guarded; idempotent reload) --
|
||||
~matModCache = ~matModCache ? IdentityDictionary.new;
|
||||
~matModCache = ~matModCache ? IdentityDictionary.new;
|
||||
~matVoiceMods = ~matVoiceMods ? IdentityDictionary.new; // voice -> [ (source:,target:,depth:) ]
|
||||
~matVoicePoses = ~matVoicePoses ? IdentityDictionary.new; // voice -> [ (poseId:,action:) ]
|
||||
~matModSources = ~matModSources ?
|
||||
[\lHandY, \rHandY, \lHandX, \rHandX, \lOpen, \rOpen, \handSpeed, \handDist,
|
||||
\bodyX, \bodyY, \depth,
|
||||
|
||||
@@ -447,23 +447,50 @@ pass = pass and: { true };
|
||||
// Fix 1: fresh color pose arrays and distinct per color
|
||||
pass = pass and: { (~matColorDefs[5][1][\pose] === ~matColorDefs[5][2][\pose]).not };
|
||||
|
||||
// --- Engine reads per-color mod + per-color pose fire (Task 2 color-refactor) ---
|
||||
// --- Engine reads per-voice mod (Task 2 per-voice additive) ---
|
||||
pass = pass and: { ~matModPairs.notNil };
|
||||
// a color with a cutoff mod -> the produced event has a \cutoff key
|
||||
// a voice with a cutoff mod -> the produced event has a \cutoff key
|
||||
~matModCache = IdentityDictionary[ \rHandY -> 1.0 ];
|
||||
~matColorDefs[5][2][\mod] = (source: \rHandY, target: \cutoff, depth: 1.0);
|
||||
~matVoiceMods[\acid] = [ (source: \rHandY, target: \cutoff, depth: 1.0) ];
|
||||
~ev = ~matVariation.(\acid, 2, 5).asStream.next(());
|
||||
pass = pass and: { ~ev[\cutoff].notNil and: { ~ev[\cutoff] > 3000 } };
|
||||
~matColorDefs[5][2][\mod] = nil;
|
||||
// a color with a rev mod -> the produced event has a \rev key
|
||||
~matVoiceMods[\acid] = [];
|
||||
// a voice with a rev mod -> the produced event has a \rev key
|
||||
~matModCache = IdentityDictionary[ \rHandY -> 1.0 ];
|
||||
~matColorDefs[0][1][\mod] = (source: \rHandY, target: \rev, depth: 0.5);
|
||||
~matVoiceMods[\kick] = [ (source: \rHandY, target: \rev, depth: 0.5) ];
|
||||
~revEv = ~matVariation.(\kick, 1, 0).asStream.next(());
|
||||
pass = pass and: { ~revEv[\rev].notNil };
|
||||
~matColorDefs[0][1][\mod] = nil;
|
||||
~matVoiceMods[\kick] = [];
|
||||
// per-voice ~matMod removed
|
||||
pass = pass and: { ~matMod.isNil };
|
||||
pass = pass and: { ~matPoseBindings.isNil };
|
||||
|
||||
// --- Per-voice additive modulation model (Task 2) ---
|
||||
// pan: two sources cancel additively (rHandX=1.0 -> +0.5, bodyX=0.0 -> -0.5 -> sum 0)
|
||||
~matVoiceMods[\acid] = [ (source: \rHandX, target: \pan, depth: 0.5),
|
||||
(source: \bodyX, target: \pan, depth: 0.5) ];
|
||||
~matModCache[\rHandX] = 1.0; ~matModCache[\bodyX] = 0.0;
|
||||
pass = pass and: {
|
||||
var pairs, pf;
|
||||
pairs = ~matModPairs.(\acid, (amp: 1.0), { 1.0 });
|
||||
pf = pairs[pairs.indexOf(\pan) + 1];
|
||||
pf.notNil and: { pf.asStream.next(()).abs < 0.001 }
|
||||
};
|
||||
~matVoiceMods[\acid] = [];
|
||||
// rev: single source, cache 0.4, depth 1.0 -> value 0.4
|
||||
~matVoiceMods[\acid] = [ (source: \handDist, target: \rev, depth: 1.0) ];
|
||||
~matModCache[\handDist] = 0.4;
|
||||
pass = pass and: {
|
||||
var pairs, pf;
|
||||
pairs = ~matModPairs.(\acid, (amp: 1.0), { 1.0 });
|
||||
pf = pairs[pairs.indexOf(\rev) + 1];
|
||||
pf.notNil and: { (pf.asStream.next(()) - 0.4).abs < 0.001 }
|
||||
};
|
||||
~matVoiceMods[\acid] = [];
|
||||
// disallowed target: kick has no cutoff in ~matModTargets -> gated out
|
||||
~matVoiceMods[\kick] = [ (source: \bodyY, target: \cutoff, depth: 1.0) ];
|
||||
pass = pass and: { ~matModPairs.(\kick, (amp: 1.0), { 1.0 }).includes(\cutoff).not };
|
||||
~matVoiceMods[\kick] = [];
|
||||
// per-color pose fire: a binding under a color, firing re-sources (no raise)
|
||||
~matColorDefs[5][3][\pose] = [ (poseId: \poseX, action: \trigger) ];
|
||||
try { ~matPoseFire.(\poseX) } { |e| pass = false; ("EXC poseFire: " ++ e.class.name).postln };
|
||||
|
||||
Reference in New Issue
Block a user