feat: per-voice editable matrix color defs

This commit is contained in:
clement
2026-06-28 20:53:43 +02:00
parent 1df8b89ac3
commit daeb1e6c1e
2 changed files with 79 additions and 39 deletions
+54 -39
View File
@@ -35,6 +35,19 @@
~matModNeutralCut = ~matModNeutralCut ? IdentityDictionary[
\acid->700, \arp->3000, \lead->4000, \stab->2200, \pad->1500
];
// -- default color defs reproduce the historical global variation table --
~matDefaultColorDefs = {
[ nil,
(stretch: 1.0, octave: 0, amp: 1.0, inst: nil, cutoff: nil, pan: nil),
(stretch: 0.5, octave: 0, amp: 1.0, inst: nil, cutoff: nil, pan: nil),
(stretch: 1.0, octave: 1, amp: 1.0, inst: nil, cutoff: nil, pan: nil),
(stretch: 2.0, octave: 0, amp: 1.0, inst: nil, cutoff: nil, pan: nil),
(stretch: 1.0, octave: -1, amp: 1.05, inst: nil, cutoff: nil, pan: nil),
(stretch: 0.5, octave: 0, amp: 1.2, inst: nil, cutoff: nil, pan: nil) ]
};
~matColorDefs = ~matColorDefs ? Array.fill(~matVoices.size, { ~matDefaultColorDefs.value });
~lp[\matLoopStart] = ~lp[\matLoopStart] ? 0;
~lp[\matLoopEnd] = ~lp[\matLoopEnd] ? (~matBars - 1);
@@ -139,58 +152,60 @@ File.exists(~matDir).not.if({ ("mkdir -p " ++ ~matDir.quote).systemCmd });
})
};
// -- ~matVariationOverlay : assemble the Pchain overlay given a resolved spec --
// Factored out of ~matVariation so the main function stays readable.
// cdPairs (static per-color cutoff/pan) come before modPairs so a live capture
// mod on the same target overrides the static color-def value — intended.
~matVariationOverlay = { |name, vi, spec, volOf, instPair, cdPairs|
var base = ~matBaseFor.(name);
var mod = ~matMod[vi];
var modPairs = mod.isNil.if({ [] }, {
var src = mod[\source], tgt = mod[\target], d = mod[\depth];
(tgt == \cutoff).if({
var c0 = ~matModNeutralCut[name] ? 1000;
[\cutoff, Pfunc { var s = ~matModSourceVal.(src);
c0 * (s.linexp(0, 1, 200, 6000) / c0).pow(d) }]
}, {
(tgt == \pan).if({
[\pan, Pfunc { (~matModSourceVal.(src) * 2 - 1) * d }]
}, {
[\amp, Pfunc { (spec[\amp] * volOf.value) * (1 + (d * (~matModSourceVal.(src) * 2 - 1))).max(0) }]
})
})
});
Pchain(
Pbind(*([
\matGlow, Pfunc { |e| ~matEmitTrig.(vi, (e[\amp] ? spec[\amp]).clip(0, 1)); 0 },
\stretch, spec[\stretch],
\octave, spec[\octave],
\amp, Pfunc({ (spec[\amp] ? 1.0) * volOf.value })
] ++ instPair ++ cdPairs ++ modPairs)),
base
)
};
// -- ~matVariation : map color 1-6 to a Pchain overlay on the captured base --
// color 1 = base as-is, 2 = double-time, 3 = octave up, 4 = half-time,
// 5 = octave down + slight accent, 6 = accent roll (denser + louder).
// \stretch multiplies event duration; \octave shifts degree-based pitch.
// NOTE: voices that compute \freq via Pfunc (ignoring \octave) will not respond
// to the octave variation — acceptable for v1; tune the palette at the live gate.
// Single function so all variation logic lives in one place.
// Reads ~matColorDefs[vi][color] so per-voice color tables are editable at runtime.
~matVariation = { |name, color, vi = 0|
(color == 0).if({ nil }, {
var base = ~matBaseFor.(name);
base.notNil.if({
var spec = [
nil,
(stretch: 1.0, octave: 0, amp: 1.0),
(stretch: 0.5, octave: 0, amp: 1.0),
(stretch: 1.0, octave: 1, amp: 1.0),
(stretch: 2.0, octave: 0, amp: 1.0),
(stretch: 1.0, octave: -1, amp: 1.05),
(stretch: 0.5, octave: 0, amp: 1.2)
][color];
var spec = ~matColorDefs[vi][color];
spec.notNil.if({
var inst = ~matInstruments[vi];
// per-voice volume (mixer fix from main): read live, default 0.8
var inst = spec[\inst] ? ~matInstruments[vi]; // per-color wins, else voice default
var volOf = { (~lp[\vol].notNil).if({ ~lp[\vol][name.asSymbol] ? 0.8 }, { 0.8 }) };
var instPair = inst.notNil.if({ [\instrument, inst] }, { [] });
var mod = ~matMod[vi];
var modPairs = mod.isNil.if({ [] }, {
var src = mod[\source], tgt = mod[\target], d = mod[\depth];
(tgt == \cutoff).if({
var c0 = ~matModNeutralCut[name] ? 1000;
[\cutoff, Pfunc { var s = ~matModSourceVal.(src);
c0 * (s.linexp(0, 1, 200, 6000) / c0).pow(d) }]
}, {
(tgt == \pan).if({
[\pan, Pfunc { (~matModSourceVal.(src) * 2 - 1) * d }]
}, { // amp: variation amp x per-voice volume x mod factor
[\amp, Pfunc { (spec[\amp] * volOf.value) * (1 + (d * (~matModSourceVal.(src) * 2 - 1))).max(0) }]
})
})
});
Pchain(
Pbind(*([
\matGlow, Pfunc { |e|
~matEmitTrig.(vi, (e[\amp] ? spec[\amp]).clip(0, 1));
0
},
\stretch, spec[\stretch],
\octave, spec[\octave],
\amp, Pfunc({ (spec[\amp] ? 1.0) * volOf.value })
] ++ instPair ++ modPairs)),
base
)
var cdPairs = [];
(spec[\cutoff].notNil and: { (~matModTargets[name] ? []).includes(\cutoff) }).if({
cdPairs = cdPairs ++ [\cutoff, spec[\cutoff]] });
(spec[\pan].notNil and: { (~matModTargets[name] ? []).includes(\pan) }).if({
cdPairs = cdPairs ++ [\pan, spec[\pan]] });
~matVariationOverlay.(name, vi, spec, volOf, instPair, cdPairs)
}, { base })
}, { nil })
})
+25
View File
@@ -282,6 +282,31 @@ pass = pass and: { ~lp[\matBar] == 31 }; // clamped to ~matBars - 1
// Reset loop to full range
~matSetLoop.(0, 31);
// --- Color editor: per-voice color defs (Task 1) ---
~matVoices.size.do { |i| ~matMod[i] = nil }; // isolate from prior mod-persistence state
pass = pass and: { ~matColorDefs.notNil and: { ~matColorDefs.size == 16 } };
// defaults reproduce the current table: color 2 = stretch 0.5, octave 0, amp 1.0
pass = pass and: { ~matColorDefs[5][2][\stretch] == 0.5 };
pass = pass and: { ~matColorDefs[5][2][\octave] == 0 };
pass = pass and: { ~matColorDefs[5][2][\amp] == 1.0 };
pass = pass and: { ~matColorDefs[5][5][\amp] == 1.05 }; // color 5 default amp
pass = pass and: { ~matColorDefs[0][1][\inst].isNil }; // inst default nil
// ~matVariation reads the def: editing the def changes the produced event
~matColorDefs[5][2][\stretch] = 0.25;
~tv = ~matVariation.(\acid, 2, 5).asStream.next(());
pass = pass and: { ~tv[\stretch] == 0.25 };
~matColorDefs[5][2][\stretch] = 0.5; // restore default
// per-color cutoff applies for a filter voice (acid supports cutoff)
~matColorDefs[5][2][\cutoff] = 1234;
~tv2 = ~matVariation.(\acid, 2, 5).asStream.next(());
pass = pass and: { ~tv2[\cutoff] == 1234 };
~matColorDefs[5][2][\cutoff] = nil;
// per-color inst overrides the per-voice default
~matColorDefs[6][2][\inst] = \do_strike; // arp supports do_strike
~tv3 = ~matVariation.(\arp, 2, 6).asStream.next(());
pass = pass and: { ~tv3[\instrument] == \do_strike };
~matColorDefs[6][2][\inst] = nil;
pass.if(
{ "TEST PASS".postln },
{ "TEST FAIL".postln }