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

hey, i would like to know how to be able to use .ar and .kr methods with this pseudo Ugen. thanks :slight_smile:

StatelessWindow : UGen {
	*ar { |lvls, durs, curves, phase|
		var x = 0;
		var window = durs.size.collect({ |i|
			var result, x2;
			x2 = x + durs[i];
			result = (phase >= x) * (phase < x2) * phase.lincurve(x, x2, lvls[i], lvls[i+1], curves[i]);
			x = x2;
			result;
		}).sum;
		window = window * (phase < 1);
		^window;
	}
}

how could this be handled if you have Ugens inside the pseudo-Ugen like in the following example and want to be able to put out .ar and .kr ?

multiChannelPhase = Array.fill(maxOverlap, { |i|
	var localTrig = PulseDivider.ar(trig, maxOverlap, i);
	var hasTriggered = PulseCount.ar(localTrig) > 0;
	var localPhase = Sweep.ar(localTrig, rate * hasTriggered);
	localPhase;
});

Presumably you’ve tried it as shown and gotten some result that is different from what you want.

One suggestion btw for testing is to begin with simpler cases. If it works for one ar or kr phase input, then you can move on to multichannel. If you start with multichannel, then it’s harder to be sure if any bugs are because of logic errors in the chain, or mishandling of the multichannel aspect. Proceed step by step and you’ll encounter fewer problems.

In this format, the *ar method is more like a function – it will operate on whatever rates that come in via the arguments. If the inputs are scalar or kr, then you’d get a kr result. If you want to force the result to be ar, you could end with ^window.asAudioRateInput.

*kr could be a bit tricky. StatelessWindow.kr(lvls, durs, curves, anAudioRatePhase) would output audio rate. In that case, you might start with var krPhase = if(phase.rate == \audio) { phase = A2K.kr(phase); and use krPhase below.

But… is it necessary to force rates? Better might be to just pass in the rate you want and not muck about with it (“prefer the simplest solution that achieves the goal”).

hjh

I don’t quite understand your question - what is the last block of code for?

…but if you wanted to write a function that produces either you can get the rate of one of the inputs which can be useful for writing more generically.

{
	|u|
	var rate = if(u.rate == \audio, \ar, \kr);
	LPF.perform(rate, u, 130); // equivalent to LPF.ar(u, 130) and LPF.kr(u, 130)
}

okay i was assuming when using:

NameOfUgen : UGen {
	*ar {
		
		^output;
	}
}

the output will always be audio rate.
I would like to have the output at control rate when using a phase/rate at control rate and the output to be audio rate when using a phase/rate at audio rate

Its for overlapping grains inside the SynthDef. The phase/windows are distributed round robin across n-channels which you define with maxOverlap. By adjusting \overlap.kr you can overlap the grains inside the SynthDef. The setup enables you to do overlap and additionally FM per grain which is not possible with the Grain Ugens because all the grain parameters are sampled and held per trigger. Im also working on a stateless multichannel phase approach with LFSaw instead of the PulseDivider, which could drive the stateless window, so you could even do FM on the Phase.

(
var statelessWindow = { |lvls, durs, curves, phase|
	var x = 0;
	var window = durs.size.collect({ |i|
		var result, x2;
		x2 = x + durs[i];
		result = (phase >= x) * (phase < x2) * phase.lincurve(x, x2, lvls[i], lvls[i+1], curves[i]);
		x = x2;
		result;
	}).sum;
	window = window * (phase < 1);
	window;
};

{
	var maxOverlap = 4;
	var tFreq = \tFreq.kr(400);
	var trig = Impulse.ar(tFreq);
	var grainRate = tFreq / \overlap.kr(2);
	
	var sig = Array.fill(maxOverlap, { |i|
		var localTrig = PulseDivider.ar(trig, maxOverlap, i);
		var hasTriggered = PulseCount.ar(localTrig) > 0;
		var phase = Sweep.ar(localTrig, grainRate * hasTriggered);
		var grainWindow = statelessWindow.(
			lvls: [0, 1, 0],
			durs: [TExpRand.ar(0.01, 0.5, localTrig), TRand.ar(0.25, 0.5, localTrig)],
			curves: [4.0, -4.0],
			phase: phase
		);
		grainWindow;
	});
	sig;
}.plot(0.02);
)

plot for \overlap.kr(2):

At the moment im trying to separate it in different blocks to make it more modular, because im not just using the StatelessWindow for the grainWindow but also for FM index etc. per grain.
One of these blocks is the multiChannelPhase.
I would like to have multiChannelPhase to output control rate phases for control rate triggers and to output audio rate phases for audio rate triggers.

Methods do what you program them to do, nothing more or less.

ar is only a name. It has only and exactly the meaning you give it. If you want it to return audio rate, then you can make it do so – but there’s no magic about this name that will do it for you.

Which means that the output rate depends on the input rate… and not really on the method that you call.

You could just write StatelessWindow { *new { ... } } and then the rate will adapt automatically to the input rate. (Actually this will happen with your ar method too, but if it’s not called ar, then you don’t have an expectation that it will force a rate.)

So the problem could be much simpler than you’re making it out to be.

hjh

this was the piece of information i was missing, thanks a lot.

But if you plug in a control rate trigger into the multiChannelPhase
PulseDivider / PulseCount and Sweep should also be .kr or?
So you cant hardcode .kr or .ar for these Ugens in the function / Pseudo-Ugen but have to use .perform?

I think this is the only way to do what you want.

	
var get_windows = {|n, trig, grainrate|
	var rate = if(trig.rate == \audio, \ar, \kr);
	n.collect{
		|i|
		var localTrig = PulseDivider.perform(rate, trig, n, i);
		var hasTriggered = PulseCount.perform(rate, localTrig) > 0;
		var phase = Sweep.perform(rate, localTrig, grainrate * hasTriggered);
		statelessWindow.(
			lvls: [0, 1, 0],
			durs: [TExpRand.perform(rate, 0.01, 0.5, localTrig), TRand.perform(rate, 0.25, 0.5, localTrig)],
			curves: [4.0, -4.0],
			phase: phase
		)
	}
};
	
	
{
	var tFreq = \tFreq.kr(400);
    // change to kr and the result of get_windows becomes kr
	var trig = Impulse.ar(tFreq);
	var grainRate = tFreq / \overlap.kr(2);
	
	get_windows.(4, trig, grainRate)
}.plot(0.02);

As far as I know this is the only way to avoid duplicating the code for each rate.

thank you very much, thats it.

i think for using the statelessWindow multiple times with different configurations for other SynthDef parameters like FM index etc. as well, all driven by the same phase get_windows should then be separated into the window part and the phase part. i will try that.

See UGen.methodSelectorForRate and its uses in the class library.

hjh

1 Like

ive ended up with this:

(
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 multiChannelTrigPhase = { |maxOverlap, trig, grainRate|
	var rate = if(trig.rate == \audio, \ar, \kr);
	var result = 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];
	};
	result.flop;
};

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 multiChannelTrig, multiChannelPhase, grainWindow, indexWindow;
	var sig, fmod, fmIndex, pan;

	# multiChannelTrig, multiChannelPhase = multiChannelTrigPhase.(maxOverlap, trig, grainRate);
			
	pan = \pan.kr(0) + channelMask.(multiChannelTrig, \chanMask.kr(1), \centerMask.kr(0));

	grainWindow = statelessWindow.(
		levels: [0, 1, 0],
		times: [TExpRand.ar(0.01, 0.5, multiChannelTrig), TRand.ar(0.25, 0.5, multiChannelTrig)],
		curve: [4.0, -4.0],
		phase: multiChannelPhase
	);
	
	indexWindow = statelessWindow.(
		levels: [0, 1, 0.8, 0],
		times: [0.25, 0.50, 0.25],
		curve: [4.0, 0.0, -4.0],
		phase: multiChannelPhase
	);
	fmIndex = \index.kr(0) + indexWindow.linlin(0, 1, 0, \iWinAmount.kr(5));
	
	fmod = SinOsc.ar(TExpRand.ar(1.0, 5.0, multiChannelTrig));
	
	sig = SinOsc.ar(\freq.kr(440) * (1 + (fmod * fmIndex)));

	sig = sig * grainWindow;

	sig = Pan2.ar(sig, pan);
	
	//sig = sig.sum;
	
	[grainWindow, indexWindow, sig];
	
}.plot(1);
)

any recommendations for the multiChannelTrigPhase in terms of coding style?
ive also implemented the channelMask here to show one of the other building blocks which could find their way into the SynthDef and that it is probably good to have access to the multiChannelPhase and the multiChannelTrigger. The code above shows some different places where either multiChannelPhase or multiChannelTrig could be used.
For each of theses building blocks im repeating rate = if(trig.rate == \audio, \ar, \kr); together with .perform is this bad in terms of efficiency?
i think i have to be very careful with multichannel expansion here. still a lot to learn.

Typically, in a SynthDef, “efficiency” is a matter of avoiding repeated UGens – we want to avoid redundant operations in the server.

In this if expression, can you find any UGens being created?

No. Hence, no redundant server operations coming directly from the if. As far as the server is concerned, it doesn’t matter if you evaluate this if once or 100 times.

It is true that if you repeat the if a few times, the SynthDef-building process might take a couple of fractions of a millisecond longer. But, the optimization and topo-sort passes are likely to take several hundred times longer than this repetition. In other words, don’t worry about it.

You’re doing the right thing with collect, AFAICS. I often do something like this in a SynthDef, if I’m not certain that a math operator will handle the expansion exactly as I want.

hjh

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

thats a great idea, thanks alot :slight_smile:

thank you very much!

hey, im coming back to this thread because of one additional question. Whats the right way to layer multiple pulsar streams with different formant frequencies, so also multichannel expanding formantFreq without ending up with nested arrays?

(
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]);
};

var channelMask = { |trig, numChannels, channelMask, centerMask, panMax|
	var rate = if(trig.rate == \audio, \ar, \kr);
	var panChannels = Array.series(numChannels, -1 / numChannels, 2 / numChannels).wrap(-1.0, 1.0);
	var panPositions = panChannels.collect { |pos| Dser([pos], channelMask) };
	panPositions = panPositions ++ Dser([0], centerMask);
	Demand.perform(rate, trig, 0, Dseq(panPositions, inf) * panMax).lag(0.001);
};

var hanningWindow = { |phase, overlap|
	var windowRate = phase / overlap;
	(1 - (windowRate * 2pi).cos) / 2 * (windowRate < 1);
};

{
	var numChannels = 5;
	var freq = \freq.kr(10);
	var trig = Impulse.ar(freq);
	var formantCount = 3;
	var randomLFOs = { LFNoise2.ar(2) } ! formantCount;
	var formantFreq = ( randomLFOs.() ).linexp(-1, 1, 10, 200);

	var timings = timingInformation.(numChannels, trig, formantFreq);
	var chanMask = channelMask.(timings.trigger, numChannels - 1, \channelMask.kr(1), \centerMask.kr(1), \panMax.kr(0.8));
	var grainWindow = hanningWindow.(timings.phase, \overlap.kr(4));

	var sig = sin((1 - timings.phase) ** 3 * 2pi * \sineCycles.kr(8).floor);

	sig = sig * grainWindow * \amp.kr(-20.dbamp);

	// see nested channels in the post window
	sig.debug(\sig);

	sig = PanAz.ar(2, sig, chanMask);
	sig = sig.sum;

	sig;
}.play;
)