Are control signals also Ugens? What is a UGen

@zentrumsounds I’m just replying in a new thread because that topic was completed.

Continuing the discussion from Argument for multichannel expansion in a SynthDef:


I’ve deleted my previous post because on reflection I thought it was the wrong way to think about this. Here is how I’d think about it now.

A synthdef is an instruction manual on how to make to create signal processing modules and connect them together.
When you write, Out.ar(0, SinOsc.ar), you aren’t dealing with the modules nor the signals directly. Instead the UGens are representations of the modules that form a blueprint, from which, a set of instructions is made.

There are no signals, no ‘value outcomes’ in sclang. All you can do is make UGens which represent a module, through which, signals flow.

Sometimes we just refer to UGens as signals or having a value because what we really care about is the real modules and signals.

There is also some confusion in the code where the sever refers it the module as a unit generator, but in sclang, we use the same term UGen to refer to these blueprint used to created instructions that make unit generators on the server.

UGen could be called UGenBlueprint.


This conversation can actually be even more complex when talking about things like DC.ar(0), which does have a known value, and using scalar values (numbers) SinOsc.ar + 1. Demand rate is also a thing.

1 Like

Thanks for taking the time to dig deeper on this.

If i understand your reasoning correctly…

“There are no signals, no ‘value outcomes’ in sclang” … do you mean there are no virtual voltages, no independent skeuomorphic “things” that we point to as a signals. That instead, we are using the term Ugen synonymously with signal because we are simply connecting/polling the current state of a Ugen and using its current state to control aspects of something else.

But i see that you still conceptualize the state of the Ugen as a signal. And also that a static state UGen also muddies the water. Indeed what else then is a control bus if not a signal? Are we not adding together signals when we sum the outputs of Ugens to another bus. Is it then still useful to conceptualize the idea of a signal existing as data somewhere no?

Hopefully I am catching the subtlety in your post…

In sclang, no, there are no signals (with one small exception). In scsynth and supernova, yes, but that means writing C++.

Yes I think so. Consider this…

SynthDef(\a, { Out.ar(0, SinOsc.ar.poll) }).dumpUGens;

[0_SinOsc, class SinOsc, audio, [440.0, 0.0], [], [3_Poll, 1_Out], [], []]
[1_Out, class Out, audio, [0, 0_SinOsc], [0_SinOsc], [], [], []]
[2_Impulse, class Impulse, audio, [10, 0], [], [3_Poll], [], []]
[3_Poll, class Poll, audio, [2_Impulse, 0_SinOsc, -1, 12, 85, 71, 101, 110, 40, 83, 105, 110, 79, 115, 99, 41], [0_SinOsc, 2_Impulse], [], [], []]

Polling is a UGen. The sclang class Poll is a blueprint used to construct a ‘module’ on the server which prints the value to the post window.

Yes but you can’t actually get at them in sclang. The only thing you can do is create UGens, some of which, might sample the signal and return a value back to sclang (Poll, SendReply).
One little exception to this is control busses and synth inputs (also confusingly call ‘controls’), which practically you can read from and write to in sclang.

The sever does, in sclang, we create a blueprint that instructs the server to do so. In fact, if we have a SynthDef
SynthDef(\a, { Out.ar(0, SinOsc.ar + SinOsc.ar) }) we can create multiple instances of the synth 100.do { Synth(\a) } and each instance will have unique signals.

1 Like

Years ago, I was working in tech support in a company that provided a (relatively) user-friendly front end for database queries. Most of the support staff were terrified of the query-generation engine, but I got along really well with it. One of my coworkers speculated that maybe my DSP experience was related.

Later, I figured out what it was: Database queries are about manipulating abstract characteristics of large sets of data, without having to know the specific data values upfront. Signal processing also deals with large sets of data, without having to know the specific data values upfront.

Let’s get sinewave data from the server.

b = Buffer.alloc(s, 64, 1);

{ RecordBuf.ar(SinOsc.ar(2000, 0), b, loop: 0, doneAction: 2) }.play;

b.getn(0, 64, { |sig| sig.round(0.0001).postln });

[0.0, 0.2588, 0.5, 0.7071, 0.866, 0.9659, 1.0, 0.9659, 0.866, 0.7071, 0.5, 0.2588, 0.0, -0.2588, -0.5, -0.7071, -0.866, -0.9659, -1.0, -0.9659, -0.866, -0.7071, -0.5, -0.2588, 0.0, 0.2588, 0.5, 0.7071, 0.866, 0.9659, 1.0, 0.9659, 0.866, 0.7071, 0.5, 0.2588, 0.0, -0.2588, -0.5, -0.7071, -0.866, -0.9659, -1.0, -0.9659, -0.866, -0.7071, -0.5, -0.2588, 0.0, 0.2588, 0.5, 0.7071, 0.866, 0.9659, 1.0, 0.9659, 0.866, 0.7071, 0.5, 0.2588, 0.0, -0.2588, -0.5, -0.7071]

{ RecordBuf.ar(SinOsc.ar(2000, 1.1743), b, loop: 0, doneAction: 2) }.play;

b.getn(0, 64, { |sig| sig.round(0.0001).postcs });

[0.9224, 0.9909, 0.9919, 0.9253, 0.7957, 0.6118, 0.3862, 0.1343, -0.1268, -0.3792, -0.6057, -0.791, -0.9224, -0.9909, -0.9919, -0.9253, -0.7957, -0.6118, -0.3862, -0.1343, 0.1268, 0.3792, 0.6057, 0.791, 0.9224, 0.9909, 0.9919, 0.9253, 0.7957, 0.6118, 0.3862, 0.1343, -0.1268, -0.3792, -0.6057, -0.791, -0.9224, -0.9909, -0.9919, -0.9253, -0.7957, -0.6118, -0.3862, -0.1343, 0.1268, 0.3792, 0.6057, 0.791, 0.9224, 0.9909, 0.9919, 0.9253, 0.7957, 0.6118, 0.3862, 0.1343, -0.1268, -0.3792, -0.6057, -0.791, -0.9224, -0.9909, -0.9919, -0.9253]

The numbers are concretely very different, but they are both 2000 Hz sinewaves with amplitude = 1. (Note that 1.0 and -1.0 are absent from the second series of samples! But the continuous bandlimited function that passes through each sample point will peak at 1.0, e.g. in between samples 0.9909 and 0.9919.)

When writing DSP, we are not polling anything. We are describing characteristics of signal functions that we want (“functions” in the mathematical sense, not the SC sense).

The term “UGen,” then, represents two things:

  1. In sclang, a UGen is this type of abstract functor.
  2. In scsynth, a UGen is the beast of burden that renders the final datasets.

For control signals, it may be necessary to debug specific values at specific times. This gets tricky because the language doesn’t have direct access. For audio signals, there’s quite a lot of variability that can produce sonically indistinguishable results (e.g., two bandlimited square waves, slightly phase-shifted – I was going to include graphics of this, but recent sclang changes have broken one of my demos, so I can’t do it right now), so it usually isn’t useful to look for specific data.

hjh

1 Like

Indeed, UGens often work as applicative functors. It’s so ubiquitous that we don’t even realize it.

In this example:

SinOsc.ar(440) + SinOsc.ar(880)

Applies a function used within the context of one UGen to the values in another UGen. For the same purpose, a number would be lifted as if it were a constant signal in an operation with a UGen.

In functional programming terms, it can be understood (as illustration) as:

(+) <$> SinOsc.ar 440 <*> SinOsc.ar 880
  • The <$> operator lifts the (+) function into the context of UGens
  • The <*> operator then applies this lifted function to the outputs of the two SinOsc UGens.

The handling is somewhat different for UGens, such as WhiteNoise. Monadic contexts involve sequencing computations and side effects, whereas applicative contexts focus on combining computations. Although WhiteNoise can be used within an applicative context in many cases, its inherent randomness often necessitates monadic handling (this was an example with graph optimization recently, since two white noise ugens can’t be “simplified” the same thing as two sinosc etc).

Some UGens are inherently stateful. For instance, in a feedback loop where the output of CombN is fed back into an LPF, the stateful nature of these UGens becomes more apparent. The interaction of their internal states and the feedback loop’s dynamics must be managed, and their stateful context becomes key to designing the sound.

While you don’t explicitly write type annotations, the system handles very different things in operations under the hood in one way or another.

All these are just a way to reason about processes on a higher level, even if not related to scsynth practical implementation.

2 Likes

Thanks for these responses. I’m still trying to come to terms with the ramifications.
So, if arguments are not static numbers but function internally as BinaryOpUgens then this can impact calculations.
The following examples are almost identical with the exception that the first version uses a variable to define the fundamental and the second uses an argument. The result is dramatically different.
How do i force the argument version to behave as a static number rather than a BinaryOpUgen?

Freq defined by array:

(
SynthDef(\addingquestion, {
	arg notearray=50;
	var notevar=50, snd, freqs, sinarray, harmamt, env;
	harmamt = 45;
	sinarray = (1..harmamt);
	freqs = notearray * sinarray;
	freqs.postln;
	freqs = 2 ** freqs.log2.blend(freqs.log2.mean, 2);
	snd = SinOsc.ar(freqs);
	snd = snd.sum !2;
	snd = snd * -24.dbamp;
	Out.ar(0, snd);
}).add)

x=Synth(\addingquestion);

Freq defined by variable.

(
SynthDef(\addingquestion, {
	arg notearray=50;
	var notevar=50, snd, freqs, sinarray, harmamt, env;
	harmamt = 45;
	sinarray = (1..harmamt);
	freqs = notevar * sinarray;
	freqs.postln;
	freqs = 2 ** freqs.log2.blend(freqs.log2.mean, 2);
	snd = SinOsc.ar(freqs);
	snd = snd.sum !2;
	snd = snd * -24.dbamp;
	Out.ar(0, snd);
}).add)

x=Synth(\addingquestion);

You can’t force the argument of the synthdef to behave differently. The synthdef compiler reads the arguments and turns them into controls. You can’t change that.

What you can do is wrap the synthdef construction in a function, that wouldn’t be a synthdef, but a synthdef generator!

~mk_my_synth = { |name, value|
   SynthDef(name, { Out.ar(\out.kr, DC.ar(value)) })
};

Apologies. I can’t quite work out how this resolves the issue. Will this option allow me to control the synth i copied above with a MIDI.def input? What seems to be happening is that only the first item in the array is being used (an Output Proxy) but then the others ignored (which are BinaryOpUgens).

Is there no way of taking the BinaryOpUgens and using them in a maths calculation? Could latch do this somehow? Basically the behavoiur i’m seeing is:

(
SynthDef(\why, {
	|fundarg = 6| 
	var array, arrayvar, arrayarg, fundvar = 6, arraysize = 6;
	array = (1..arraysize);
	arrayvar = fundvar * array;
	"variables like this: ".post; arrayvar.postln;
	" ".postln;
	arrayarg = fundarg * array;
	"which i CAN use to do calculations like .mean. to an array".postln; "".postln;
	"But then fails when using the BinaryOpUGens resulting from the args? ".post;
	" ".postln; 
	arrayarg.postln;
}).add
)

Which results in:

variables like this: [ 6, 12, 18, 24, 30, 36 ]
 
which i CAN use to do calculations like .mean. to an array

But then fails when using the BinaryOpUGens resulting from the args?  
[ an OutputProxy, a BinaryOpUGen, a BinaryOpUGen, a BinaryOpUGen, a BinaryOpUGen, a BinaryOpUGen ]

So there is no means to transform the value received from an argument and to turn this into a static value? I would have thought this might be simple enough…

No. You can only apply ugens to ugens. There is a ugen called Poll which will print the current value if that is what you want. Or you can use sendreply if you want to do something else to it.

I’m not really sure I understand what you mean by a static value, there are only ugens which are instructions to make signals.

1 Like

To be honest this is normally never an issue. So whilst experimenting with some sounds this week i was surprised to hear the results of this problem when I tried to add MIDI control to my synth. I couldn’t work out why but then it seemed that the behaviour changed when using the argument instead of the variable.

Would you mind trying both of the \addingquestion Synthdefs above? It would immeadiately demonstrate the problem i think. What seems to be happening is that .mean can’t process the argument values & so we hear only the first harmonic when i do this:

freqs = notearray * sinarray;

But this works fine and i get all 45 sinewaves!

freqs = notevar * sinarray;

Of course it would be great to be able to modify the Synth externally with MIDI! But how without losing all the magic?

Try both of the SynthDefs below, and you will see where the difference is. Note that I have broken up the calculation into multiple steps, and print the values at each step (a good practice for debugging). I use .poll(inf) to print the initial values of Ugens in the first SynthDef, and .debug prints the values in the second one. These should be the same values.

(
SynthDef(\args, {
	arg notearray=50;
	var notevar=50, snd, freqs, sinarray, harmamt, env;
    var log, mean, blended;
	harmamt = 3;
	sinarray = (1..harmamt);
	freqs = notearray * sinarray;
    freqs.poll(inf, "initial freqs");
    log = freqs.log2.poll(inf, "log");
    mean = log.mean.poll(inf, "mean");
    blended = log.blend(mean, 2).poll(inf, "blended");
    freqs = 2 ** blended;
    // freqs = 2 ** freqs.log2.blend(freqs.log2.mean, 2);
    freqs.poll(inf, "final freqs");
	snd = SinOsc.ar(freqs);
	snd = snd.sum !2;
	snd = snd * -24.dbamp;
	Out.ar(0, snd);
}).add
)

x=Synth(\args);
x.free;

(
SynthDef(\vars, {
	arg notearray=50;
	var notevar=50, snd, freqs, sinarray, harmamt, env;
    var log, mean, blended;
	harmamt = 3;
	sinarray = (1..harmamt);
	freqs = notevar * sinarray;
    freqs.debug("initial freqs");
    log = freqs.log2.debug("log");
    mean = log.mean.debug("mean");
    blended = log.blend(mean, 2).debug("blended");
    freqs = 2 ** blended;
    // freqs = 2 ** freqs.log2.blend(freqs.log2.mean, 2);
    freqs.debug("final freqs");
	snd = SinOsc.ar(freqs);
	snd = snd.sum !2;
	snd = snd * -24.dbamp;
	Out.ar(0, snd);
}).add
)

// 'args' version prints:
initial freqs: 50
initial freqs: 100
initial freqs: 150
log: 5.64386
log: 6.64386
log: 7.22882
mean: 6.50551
blended: 6.50551
blended: 6.50551
blended: 6.50551
final freqs: 90.856
final freqs: 90.856
final freqs: 90.856

// vars version prints:
initial freqs: [ 50, 100, 150 ]
log: [ 5.6438561897747, 6.6438561897747, 7.2288186904959 ]
mean: 6.5055103566818
blended: [ 7.3671645235888, 6.3671645235888, 5.7822020228677 ]
final freqs: [ 165.09636244473, 82.548181222366, 55.03212081491 ]

The difference occurs after you use the .blend method. This is because the .blend expects a value between 0 and 1, but you give it a value of 2. What are you trying to achieve with blend there?

1 Like

Thanks for all these responses… Taken me a while to step through them so apologies for the delayed acknowledgement.

I think what i was looking for were the Synchronous Control Bus Methods: getnSynchronous lets me grab the current value of the bus and bring it back into the language. Perfect.