diff --git a/sound_algo/data_only/matrix.scd b/sound_algo/data_only/matrix.scd index ba7dbe6..f143e9a 100644 --- a/sound_algo/data_only/matrix.scd +++ b/sound_algo/data_only/matrix.scd @@ -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, diff --git a/sound_algo/data_only/test/test_matrix.scd b/sound_algo/data_only/test/test_matrix.scd index cfebd5e..766294c 100644 --- a/sound_algo/data_only/test/test_matrix.scd +++ b/sound_algo/data_only/test/test_matrix.scd @@ -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 };