What makes UGens different

I am asking this only because I want to understand what makes UGens different from say, a function. So my question is: is there any way to insert some non-ugen in the following pan {Pan2.ar(PinkNoise.ar, SinOsc.kr(1), level: 0.1)}.play in place of the sine oscillator to do exactly the same functionality as the sinosc? I was thinking about (I know it doesn’t work) may be a function which would return a random float. Obviously the sinosc is sending it’s floats out, and the pan is eagerly looking for them. This is not the case for any thing else in that place. I thought maybe a routine:

(
f = Routine {
	inf.do {
		-1.rrand(1).yield;	
	}
}
)

{Pan2.ar(PinkNoise.ar, f.value, level: 0.1)}.play

Also doesn’t work. Please give me some hints that helps understand what makes the functionality and iteractions of UGens speciall.

Many thanks!

This won’t work as generally speaking sclang simply tells scsynth the instructions on how to build and play a synth and scysnth can only work with Ugens. When scsynth receives the instruction for

{Pan2.ar(PinkNoise.ar, f.value, level: 0.1)}.play

it doesn’t work because f.value is function within sclang that can not run on scsynth as it is not defined through UGens.
It is really worth reading the docs concerning the separation between Client and Server because many problems at the beginning originate from this

https://doc.sccode.org/Guides/ClientVsServer.html

This doesn’t mean that the proposed workflow is not possible, you simply need to build a channel from which sclang can set values on scsynth, this can be either done through a parameter

// see NamedControl in the docs
x = {Pan2.ar(PinkNoise.ar, \pan.kr(0.0), level: 0.4)}.play

(
Tdef(\panChaos, {
	100.do({|i|
		x.set(\pan, [-1.0, 1.0].choose);
		0.1.wait;
	});
}).play;
)

or through a Bus which can be shared among many Synths

// create a new bus
b = Bus.control(s, 1);
// reference it within a SynthDef by using its .kr method
x = {Pan2.ar(PinkNoise.ar, b.kr(1), level: 0.4)}.play

(
Tdef(\panChaos, {
	100.do({|i|
		b.set([-1.0, 1.0].choose);
		0.1.wait;
	});
}).play;
)

BTW, just for completion, you could write the proposed code in UGen only :slight_smile:

Either by using a Sample and Hold approach

Ndef(\noisePan, {
	Pan2.ar(
		in: PinkNoise.ar,
		pos: Latch.ar(
			in: ClipNoise.ar,
			trig: Impulse.ar(freq: \panFreq.kr(10.0)),
		),
		level: \amp.kr(0.1),
	);
}).play.gui;

or by using Demand Ugens which work a bit like patterns or a sequencer, but running as UGens on the server which have the nice benefit that they can run at any given speed :slight_smile:

(
Ndef(\noisePanDemand, {
	Pan2.ar(
		in: PinkNoise.ar,
		pos: Demand.kr(
			trig: Impulse.kr(10.0),
			reset: 0,
			demandUGens: Drand([-1.0, 1.0], repeats: inf),
		),
		level: \amp.kr(0.1),
	);
}).play;
)
2 Likes

Just to add to @dscheiba excellent answer.

This works, but it will be evaluated only once when the synthdef is defined. So it just does do what you expected.

SinOsc is not a sine wave generator. It is an instruction to make a sine wave generator.

You can have a function return a ugen, which is evaluated (only once) inside the synthdef.

What is different is the nature of the work.

In 1+2, 1 and 2 are both Right-Now values, and when you operate on them, the operation is done once and the result is immediately available.

A SinOsc isn’t a “Right-Now” value that you can operate on Right-Now.

A SinOsc produces many thousands of values – at 44.1 kHz, if it’s running for one second, you get 44100 values.

Any math operation on this SinOsc, therefore, must be done 44100 times per second. And it (obviously) must be done while the signal is being produced, which is going to be sometime after the SynthDef has been compiled.

In C or Java or whatever, you can write a big loop where you calculate the single sample values. But this gets confusing, very quickly. (I tried to do this once, and failed hard.)

I think there are two reasons why computer audio languages don’t do this. One is for the machine: It’s more efficient to do signal processing in functions that have been optimized by people who really know what they’re doing, and to package those optimizations for the rest of us idiots. (I can’t write DSP like James McCartney did.)

The other is for us. Writing sample-by-sample loops obscures the essence of the calculations that are being done – you can read the sequence of math steps, but this gives no insight into why those operations are being done. It’s easier for us to understand signal → input rather than for(sample_index) loops. This innovation dates back to the 1960s, in the Music n series of languages (which culminated in csound) – such a brilliant idea that we still use it today, some 60 years later.

If you want a random float at the start of the synth, see Rand(lo, hi).

hjh

1 Like