How to write pseudo-ugen with .ar and .kr method

@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 ) 
]