@jamshark70 covered the optimisation question - only thing I’d add is that you shouldn’t think about optimisation until it’s a problem.
Looks good! Channel mask is maybe not the most intuitively named - but I can’t think of a better one.
For returning multiple things in a function…
# multiChannelTrig, multiChannelPhase = multiChannelTrigPhase.(maxOverlap, trig, grainRate);
I’d recommend returning an event instead…
(\trigger : triggerUgen, \phase : phaseUgen)
… as the structured binding thing is a real mess with inline variables.
In fact you can use events for anything (and I think they are heavily underused in synthdefs), as long as you unpack them before they go into a Ugen (or use performWithEnvir).
Edit:
Here is an example with returning an Event.
(
var statelessWindow = { |levels, times, curve, phase|
var x = 0;
var window = times.size.collect({ |i|
var x2 = x + times[i];
var result = (phase >= x) * (phase < x2) * phase.lincurve(x, x2, levels[i], levels[i+1], curve[i]);
x = x2;
result;
}).sum;
window = window * (phase < 1);
window;
};
var timingInformation = { |maxOverlap, trig, grainRate|
var rate = if(trig.rate == \audio, \ar, \kr);
var arrayOfTrigsAndPhases = maxOverlap.collect{ |i|
var localTrig = PulseDivider.perform(rate, trig, maxOverlap, i);
var hasTriggered = PulseCount.perform(rate, localTrig) > 0;
var localPhase = Sweep.perform(rate, localTrig, grainRate * hasTriggered);
[localTrig, localPhase];
};
var trigsAndPhasesArray = arrayOfTrigsAndPhases.flop;
(\trigger : trigsAndPhasesArray[0], \phase: trigsAndPhasesArray[1]).postln
};
var channelMask = { |trig, channelMask, centerMask|
var rate = if(trig.rate == \audio, \ar, \kr);
Demand.perform(rate, trig, 0,
Dseq([
Dser([-0.25], channelMask),
Dser([0.25], channelMask),
Dser([0.75], channelMask),
Dser([-0.75], channelMask),
Dser([0], centerMask)
], inf)
).lag(0.001);
};
{
var maxOverlap = 4;
var tFreq = \tFreq.kr(10);
var trig = Impulse.ar(tFreq);
var grainRate = tFreq / \overlap.kr(1);
var timings = timingInformation.(maxOverlap, trig, grainRate);
var grainWindow = statelessWindow.(
levels: [0, 1, 0],
times: [TExpRand.ar(0.01, 0.5, timings.trigger), TRand.ar(0.25, 0.5, timings.trigger)],
curve: [4.0, -4.0],
phase: timings.phase
);
var indexWindow = statelessWindow.(
levels: [0, 1, 0.8, 0],
times: [0.25, 0.50, 0.25],
curve: [4.0, 0.0, -4.0],
phase: timings.phase
);
var pan = \pan.kr(0) + channelMask.(timings.trigger, \chanMask.kr(1), \centerMask.kr(0));
var fmod = SinOsc.ar(TExpRand.ar(1.0, 5.0, timings.trigger));
var fmIndex = \index.kr(0) + indexWindow.linlin(0, 1, 0, \iWinAmount.kr(5));
var freqMultiplier = 1 + (fmod * fmIndex);
var sig = SinOsc.ar(\freq.kr(440) * freqMultiplier);
sig = sig * grainWindow;
sig = Pan2.ar(sig, pan);
[grainWindow, indexWindow, sig];
}.plot(1);
)
You can do lots of cool stuff with events and Ugens - this flips the idea of multichannel expansion.
{
var voicesArgs = [
(\freq: 3, \pan: SinOsc.kr(5)),
(\freq: LFNoise2.kr(50), \pan: 0),
....
];
var voices = voicesArgs.collect{ |v|
Pan2.ar(SinOsc.ar(v.freq), v.pan)
... some longer complex function evaluated per voice
};
var stereo = Splay.ar(voices);
}
And this allows defaults and overriding only what is needed …
(
{
var default = (\foo: SinOsc.ar(220), \bar: Saw.ar(6), \amp: -10.dbamp);
var voiceArgs = [
(), // default voice
(\foo: LFNoise0.ar(5)),
(\bar: Pulse.ar(6, 0.4))
];
var voices = voiceArgs.collect { |v|
default ++ v // this is the magic overriding concat
};
voices
}.()
)
// returing
[
( 'bar': a Saw, 'amp': 0.31622776601684, 'foo': a SinOsc ),
( 'bar': a Saw, 'amp': 0.31622776601684, 'foo': a LFNoise0 ),
( 'bar': a Pulse, 'amp': 0.31622776601684, 'foo': a SinOsc )
]