From e53ae8d94d0461c746c611f9fe5bdbafcfefd9f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=27=C3=A9lectron=20rare?= <108685187+electron-rare@users.noreply.github.com> Date: Mon, 29 Jun 2026 18:12:21 +0200 Subject: [PATCH] feat(matrix): pitch modulation target --- sound_algo/data_only/matrix.scd | 30 ++++++++++++------ sound_algo/data_only/test/test_matrix.scd | 37 +++++++++++++++++++++++ 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/sound_algo/data_only/matrix.scd b/sound_algo/data_only/matrix.scd index 3ef646b..9d2e673 100644 --- a/sound_algo/data_only/matrix.scd +++ b/sound_algo/data_only/matrix.scd @@ -27,12 +27,12 @@ ~matInstruments = ~matInstruments ? Array.fill(~matVoices.size, nil); ~matModTargets = ~matModTargets ? IdentityDictionary[ \kick->[\amp,\pan,\rev], \hats->[\amp,\pan,\rev], \clap->[\amp,\pan,\rev], - \perc->[\amp,\cutoff,\pan,\rev], - \sub->[\amp,\pan,\rev,\res], \acid->[\amp,\cutoff,\pan,\rev,\res], \arp->[\amp,\cutoff,\pan,\rev,\res], - \lead->[\amp,\cutoff,\pan,\rev,\res], \stab->[\amp,\cutoff,\pan,\rev,\res], \pad->[\amp,\cutoff,\pan,\rev,\res], - \ride->[\amp,\pan,\rev], \rim->[\amp,\pan,\rev], \tom->[\amp,\pan,\rev], - \reese->[\amp,\cutoff,\pan,\rev,\res], - \bells->[\amp,\pan,\rev,\res], \sweep->[\amp,\pan,\rev], + \perc->[\amp,\cutoff,\pan,\rev,\pitch], + \sub->[\amp,\pan,\rev,\res,\pitch], \acid->[\amp,\cutoff,\pan,\rev,\res,\pitch], \arp->[\amp,\cutoff,\pan,\rev,\res,\pitch], + \lead->[\amp,\cutoff,\pan,\rev,\res,\pitch], \stab->[\amp,\cutoff,\pan,\rev,\res,\pitch], \pad->[\amp,\cutoff,\pan,\rev,\res,\pitch], + \ride->[\amp,\pan,\rev], \rim->[\amp,\pan,\rev], \tom->[\amp,\pan,\rev,\pitch], + \reese->[\amp,\cutoff,\pan,\rev,\res,\pitch], + \bells->[\amp,\pan,\rev,\res,\pitch], \sweep->[\amp,\pan,\rev], \melody->[\amp,\cutoff,\pan,\rev,\res], \chord->[\amp,\cutoff,\pan,\rev,\res], \fx->[\amp,\pan,\rev], \snare->[\amp,\pan,\rev], \crash->[\amp,\pan,\rev], \shaker->[\amp,\pan,\rev] @@ -158,7 +158,8 @@ File.exists(~matDir).not.if({ ("mkdir -p " ++ ~matDir.quote).systemCmd }); var byTarget = IdentityDictionary.new; var pairs = []; ((~matVoiceMods[name] ? []).select { |b| - b[\source].notNil and: { b[\target].notNil } and: { allowed.includes(b[\target]) } + b[\source].notNil and: { b[\target].notNil } + and: { b[\target] != \pitch } and: { allowed.includes(b[\target]) } }).do { |b| byTarget[b[\target]] = (byTarget[b[\target]] ? []) ++ [b] }; byTarget.keysValuesDo { |tgt, list| (tgt == \cutoff).if({ @@ -208,7 +209,7 @@ File.exists(~matDir).not.if({ ("mkdir -p " ++ ~matDir.quote).systemCmd }); var pitched = [\sub, \acid, \arp, \lead, \stab, \pad, \reese, \bells, \perc, \tom]; var freqRatio = 2 ** (spec[\octave] ? 0); var freqPair = (pitched.includes(name) and: { freqRatio != 1.0 }).if({ - [\freq, Pfunc({ |e| (e[\freq] ? 440) * freqRatio })] + [\freq, Pfunc({ |e| (e[\freq] ? 440) * freqRatio })] // pitch mod step-path only (no degree here) }, { [] }); var modPairs = ~matModPairs.(name, spec, volOf); Pchain( @@ -229,7 +230,7 @@ File.exists(~matDir).not.if({ ("mkdir -p " ++ ~matDir.quote).systemCmd }); var pitched = [\sub, \acid, \arp, \lead, \stab, \pad, \reese, \bells, \perc, \tom]; var modPairs = ~matModPairs.(name, spec, volOf); var freqPair = pitched.includes(name).if({ - [\freq, Pfunc { |e| (~lpNote.notNil).if({ ~lpNote.(e[\degree], 0).midicps * freqRatio }, { 440 }) }] + [\freq, Pfunc { |e| (~lpNote.notNil).if({ ~lpNote.(e[\degree] + ~matPitchOffset.(name), 0).midicps * freqRatio }, { 440 }) }] }, { [] }); Pbind(*([ \stepIdx, Pseq((0..15), inf), @@ -950,6 +951,17 @@ OSCdef(\mat_audition, { |msg, time, addr| // reader: clamp to 0..1, default 0 when absent ~matModSourceVal = { |key| (~matModCache[key] ? 0).clip(0, 1) }; +// +/- scale-degree offset at full deflection (env-tunable; 7 ~= 1 diatonic octave) +~matPitchRange = ~matPitchRange ? 7; +// bipolar additive sum of \pitch bindings -> integer degree offset applied in the freq Pfunc +~matPitchOffset = { |name| + var off = 0; + ((~matVoiceMods[name] ? []).select { |b| b[\target] == \pitch }).do { |b| + off = off + (((~matModSourceVal.(b[\source]) * 2 - 1) * b[\depth] * ~matPitchRange).round.asInteger) + }; + off +}; + // -- Live modvalues push (~12 Hz) -- ~matModValuesPush = { ~toscSend !? { diff --git a/sound_algo/data_only/test/test_matrix.scd b/sound_algo/data_only/test/test_matrix.scd index 7146271..9c26d7f 100644 --- a/sound_algo/data_only/test/test_matrix.scd +++ b/sound_algo/data_only/test/test_matrix.scd @@ -766,6 +766,43 @@ File.delete(~matDir +/+ "t6_tmp.matrix"); ~matVoicePoses[\acid] = []; }.value; +// --- pitch target (cycle 1): bipolar scale-degree offset --- +~matPitchRange = 7; // make the assertion deterministic regardless of env +// +full deflection: source 1.0, depth 1.0 -> +7 +~matVoiceMods[\acid] = [ (source: \rHandY, target: \pitch, depth: 1.0) ]; +~matModCache[\rHandY] = 1.0; +pass = pass and: { ~matPitchOffset.(\acid) == 7 }; +// -full deflection: source 0.0 -> -7 +~matModCache[\rHandY] = 0.0; +pass = pass and: { ~matPitchOffset.(\acid) == -7 }; +// neutral: source 0.5 -> 0 +~matModCache[\rHandY] = 0.5; +pass = pass and: { ~matPitchOffset.(\acid) == 0 }; +// additive: two bindings sum (1.0*+7 and 0.5-source*0 -> +7) +~matVoiceMods[\acid] = [ (source: \rHandY, target: \pitch, depth: 1.0), + (source: \bodyY, target: \pitch, depth: 1.0) ]; +~matModCache[\rHandY] = 1.0; ~matModCache[\bodyY] = 0.5; // +7 + 0 = +7 +pass = pass and: { ~matPitchOffset.(\acid) == 7 }; +~matVoiceMods[\acid] = []; +// gating: pitch allowed on acid (melodic) but NOT on kick (percussive) +pass = pass and: { (~matModTargets[\acid] ? []).includes(\pitch) }; +pass = pass and: { (~matModTargets[\kick] ? []).includes(\pitch).not }; +// pitch is NOT emitted as a ~matModPairs pair (handled in the freq Pfunc) +~matVoiceMods[\acid] = [ (source: \rHandY, target: \pitch, depth: 1.0) ]; +pass = pass and: { ~matModPairs.(\acid, (amp: 1.0), { 1.0 }).includes(\pitch).not }; +~matVoiceMods[\acid] = []; +// freq integration: with ~lpNote stubbed, a +7 offset transposes the played note up +~origLpNote = ~lpNote; +~lpNote = { |deg, oct| 60 + deg }; // simple midi-note stub +~matVoiceMods[\acid] = [ (source: \rHandY, target: \pitch, depth: 1.0) ]; +~matModCache[\rHandY] = 1.0; // +7 +~matColorDefs[5][2][\steps] = Array.fill(16, nil).put(0, (degree: 0, vel: 0.8)); +~pEv = ~matVariation.(\acid, 2, 5).asStream.next(()); +pass = pass and: { (~pEv[\freq] - ((60 + 0 + 7).midicps * (2 ** (~matColorDefs[5][2][\octave] ? 0)))).abs < 1.0 }; +~lpNote = ~origLpNote; +~matVoiceMods[\acid] = []; +~matColorDefs[5][2][\steps] = Array.fill(16, nil); + pass.if( { "TEST PASS".postln }, { "TEST FAIL".postln }