Noob: Different random behaviors in SynthDef and .play?

Hi, I am trying to learn SC, just getting my toes wet with the very first steps how to use SynthDefs. I struggle to understand different random behaviors when using SynthDefs. I will try to explain what I don’t get, and I hope to use the correct terms in my descriptions, if I fail, my apologies, please bear with me.

To me it looks like Rand() is evaluated anew each time an instance with Rand() is created from a SynthDef; whereas Array.rand() seems to be only evaluated when adding the SynthDef with Array.rand() to the server, so that all instances of the SynthDef have identical values in Array.rand().

  • Is my assumption correct?
  • If it isn’t correct, where is my mistake?
  • If it is correct, how can I achieve that also each instance of Array.rand() has different random values? And what is the reason (or benefit) why Rand() and Array.rand() behave differently?

I’ll try to explain with code examples what I mean with the above.

In the following example, I get different random notes each time I execute x=Synth(\zufall); which uses Rand().

(
SynthDef(\zufall,{
    arg freq = 400;
    var sig, env;
	sig = SinOsc.ar(Rand(300,400));
    env = EnvGen.ar(Env.perc(0.02, 2), doneAction: 2);
    Out.ar(0,sig!2*env*0.1)
}).add
)

x=Synth(\zufall);

But in this example, the pitch does not change whenever I execute x=Synth(\kein_zufall);, which uses Array.rand().

(
SynthDef(\kein_zufall,{
    arg freq = 400;
    var sig, env;
	sig = SinOsc.ar(Array.rand(2, 300, 400));
    env = EnvGen.ar(Env.perc(0.02, 2), doneAction: 2);
    Out.ar(0,sig*env*0.1)
}).add
)

x=Synth(\kein_zufall);

But if I execute the following repeatedly, I get different notes each time, although Array.rand() is being used:

(
SynthDef(\kein_zufall,{
    arg freq = 400;
    var sig, env;
	sig = SinOsc.ar(Array.rand(2, 300, 400));
    env = EnvGen.ar(Env.perc(0.02, 2), doneAction: 2);
    Out.ar(0,sig*env*0.1)
}).play
) 

Thanks much in advance!

Yes, your assumptions are correct. If you want to generate random values each time you instantiate a Synth from an existing SynthDef, using Rand() in that SynthDef is the way to go.

The confusion in your second and third example comes from how you instantiate the Synth: Calling play on a SynthDef compiles a new SynthDef behind the scenes every time from which a new Synth gets instantiated immediately. The SynthDef gets compiled with a new Array.rand each time.
In contrary calling add compiles a single SynthDef with a random array created at compile time. All Synths instantiated from that SynthDef will use exactly the same randomly created array.

Hope that makes it clear.

Stefan

2 Likes

Thanks Stefan! I think I do understand this.

However, as I am quite excited about the possibilities of Arrays (like filling it automatically with random values, then having these sorting automatically and using the result for e.g. additive synthesis), I would really like to access these possibilites each time I instantiate a Synth from a SynthDef.

Is there a way to achieve this without having to compile a new SynthDef?


8.collect{ Rand(0,10)}

or
{Rand(0,10)}.dup(8)

or

{Rand(0,10)} ! 8

will make an array of 8 Rands
1 Like

You can also pass arrays in as synth arguments, with the caveat that you have to set the size of the array when you make the synthdef. For example, this synthdef takes arrays of size 10 for frequencies and amplitudes, then generates 10 sine waves and pans them randomly across the stereo field:

(
SynthDef(\sinarray, {
  var freqs = \freqs.kr((100, 200..1000), 10);
  var amps = \amps.kr({ |i| 0.5 / (i + 1) } ! 10);
  var out = \out.kr(0);
  var sines = SinOsc.ar(freqs) * amps;
  var sig = Pan2.ar(sines, { LFDNoise3.kr(1) } ! 10).sum;
  Out.ar(out, sig);
}).add
)

See NamedControl helpfile for more on \freqs.kr type syntax. I find it much easier to work with array arguments in this way – you can also use literal arrays with function argument syntax, i.e. SynthDef(\blah, { |freq = #[100, 200, 300]|...

Anyway, to use the default values just make a synth:

Synth(\sinarray)

And to pass in arbitrary arrays, you can provide them as arguments:

Synth(\sinarray, [
  freqs: Array.rand(10, 100, 2000), 
  amps: Array.exprand(10, 0.05, 0.5)
])

And because they’re just synth arguments you can update them while the synth is running:

x = Synth(\sinarray);

x.set(\freqs, Array.rand(10, 100, 2000)); // evaluate while synth is playing

Annd, NamedControl lets you specify independent lag times for each array element, so you can do e.g.

(
x = { 
  var freqs = \freqs.kr(200 ! 10 /* values */, { rrand(1, 20) } ! 10 /* lag times */);
  var pans = { LFDNoise3.kr(1) } ! 10;
  Pan2.ar(SinOsc.ar(freqs), pans).sum * 0.2 
}.play;
)

x.set(\freqs, (100, 200..1000));
1 Like

Most array methods (such as sorting) run only on the language side. The server has no built-in capacity to sort an array on its own (though someone could write a plugin command for it in C++).

“each time I instantiate a Synth” – one of the rules in SC (the server side) is that all operations in UGen initialization must be time-bounded. If they are not time-bounded, then it may glitch the audio and this isn’t desired. Taking sorting as an example, binary comparison sorts are at best O(n log n) for quicksort and at worst O(n^2) for bubble sort, so the time bound depends on the size and hence can’t be guaranteed.

So it won’t be supported to do arbitrary array operations at synth instantiation.

Probably your best alternative is as Eric said – to make and manipulate the array on the language side, and pass the values into an arrayed synth control. Or, a buffer could serve as the conduit for array data (particularly if it’s a very large array).

hjh

1 Like

Thank you all! I’ll do some further experiments and reading!

This is where I struggle: to turn the array into an argument, so that I can manipulate array values on the language side and these on as an argument to the server. Let me explain: In this example, I have freq as a simple number as an argument, while freq_array is a variable, I can change freq at runtime:

(
SynthDef(\ton,{
	arg freq = 4;
    var sig, env, freq_array = [300,400];
	sig = SinOsc.ar(freq*freq_array);
    env = EnvGen.ar(Env.perc(0.02, 2), doneAction: 2);
    Out.ar(0,sig*env*0.1)
}).add
)

x=Synth(\ton, [\freq,2]);

Now I move freq_array into the argument section, so that I can change it after the SynthDef has been complied, but when I execute it, I get neither an error nor a sound.

(
SynthDef(\kein_ton,{
	arg freq = 4, freq_array = [300,400];
    var sig, env;
	sig = SinOsc.ar(freq*freq_array);
    env = EnvGen.ar(Env.perc(0.02, 2), doneAction: 2);
    Out.ar(0,sig*env*0.1)
}).add
)

x=Synth(\kein_ton);

Why does this happen? How can I fix this? Thanks much in advance!

The fix is simple: freq_array = #[300, 400]

The reason’s a bit obscure, details below if you’re curious…

{ arg x = [0]; x }.def.prototypeFrame == [nil] // true
{ arg x = #[0]; x }.def.prototypeFrame == [[0]] // true
{ arg x = [0]; [x] }.value == [[0]] // true
1 Like

Ah, you’ve pointed out a legitimate gap in the documentation.

NamedControl defines arrayed controls, but unfortunately I can’t see that this link is presented in SynthDef help in an obvious way – the information is there, but you’d never find it without already knowing it. That deserves a documentation bug report.

Note that this is a var, not an arg, so it can’t be controlled from outside.

Try one of the following:

    arg freq = 4, freq_array = #[300,400];

That’s Rohan’s suggestion. Or:

    var sig, env, freq_array = NamedControl.kr(\freq_array, [300, 400]);

hjh

1 Like

Thank you. I read NamedControls, but at my current level of (non) knowledge, I have to admit I did not really understand it. So I’ll try to paraphrase what my beginner’s mind understood from what you wrote. Please let me know where I got it wrong.

  • In order to control a value from the outside (i.e. once a Synth has been instantiated from a SynthDef, I can (a) either declare this value as an argument (which needs the keyword arg and must come before the var declaration), or (b) use a NamedControl, which can be declared in the var section. Correct?
  • If I want to declare an array as an arg, its values must be proceeded with a #, like freq_array = #[200, 300]. Otherwise the array will be emtpy (no idea why this is, it seems I just have to accept it). Correct?
  • I figured so much that # in front of an array indicates an array filled with “literals”, but the scary part for me (as I want to change the content of the array after the synth using it has been instantiated) is this sentence from the Literals documentation: “Literal Arrays must be used as is and may not be altered at run time.” Does this indicate that the size of a Literal Array can not be changed at run time, or that also the content of the Literal Array can not be changed at run time?

Nevertheless, I updated my little example with #:

(
SynthDef(\ton,{
	arg freq = 4, freq_array = #[300,400];
    var sig, env;
	sig = SinOsc.ar(freq*freq_array);
    env = EnvGen.ar(Env.perc(0.02, 2), doneAction: 2);
    Out.ar(0,sig*env*0.1)
}).add
)

x=Synth(\ton, [\freq,3]);

Great, this works, I hear a sound, and I can change the argument freq at run time. But how do I change the content of the argument freq_array at run time? I tried these, but alas, none of them work. Please, why don’t they work, and what do I need to change to make it work?

x=Synth(\ton, [\freq,2], [\freq_array,[200,300]]);
x=Synth(\ton, [\freq,2], [\freq_array,#[200,300]]);
x=Synth(\ton, [\freq,2], [\freq_array,200,300]);
x=Synth(\ton, [\freq,2], [\freq_array,(200,300)]);

I feel bad for asking these potentially dumb questions, but I thought I understood how arguments work (as I use them successfully to change single values at runtime), but the moment I try to transfer the same syntax to arrays, it looks to me like a different syntax is needed, for reasons I can’t grasp, which is frustrating me, I have to admit.

Hi Wolfgangschaltung,
you’re almost there, you just made a typo, try it like this:

(
SynthDef(\ton,{
	arg freq = 4, freq_array = #[300,400];
    var sig, env;
	sig = SinOsc.ar(freq*freq_array);
    env = EnvGen.ar(Env.perc(0.02, 2), doneAction: 2);
    Out.ar(0,sig*env*0.1)
}).add
)

x=Synth(\ton, [\freq,2]);

x=Synth(\ton, [\freq, 2, \freq_array, [200,300]]);

Hope this help

1 Like

You didn’t :+1:

In a SynthDef, arguments are really controls. Every control has a default value. The literal arrays provide the default values for an arrayed control. Naturally the defaults are encoded into the SynthDef and can’t be changed without building a new SynthDef. But at play time, you can override the defaults with any values you like.

The fact that the defaults were defined by an immutable array should not be taken to mean that the eventual control values are immutable. (An immutable control is pointless.)

Here, you might be overthinking.

That’s true but not directly relevant. What is relevant is that, at Synth runtime, you can’t do anything to change an array size in the SynthDef. The number and arrangement of UGens is fixed at SynthDef building time and the only way to change it is to rebuild the SynthDef. Users are always looking for exceptions to this rule, but there are none.

You can use an array to define parallel signal chains and then use a multiplier to suppress the output of some of them, dynamically. You can’t add or remove chains at runtime.

Simply a wrong inference here: if you write Synth(\ton, [\freq, 2, \amp, 0.2]), then the extension of that to an array would be to continue the pattern of name, value, name, value – as kesey says, Synth(\ton, [\freq, 2, \freq_array, [200, 300]]) – to provide a single flat list of name, value pairs where one of the value items is an array rather than a number. To split it into multiple 2-element arrays is a step too far.

You’re very close! Hang in there.

hjh

1 Like

Thank you, @kesey – so when handing over argument values on instantiating a synth, all arguments have to be enclosed in one pair of square brackets. Now it works!

Thank you for the fix, and the reason you gave. But I have to admit that the reason is indeed quite obscure, at least too obscure for me as a beginner.

I started with this one:

{ arg x = [0]; x }.def.prototypeFrame == [nil] // true

I looked up def.prototypeFrame here, but the explanatory sentence “Get the array of default values for argument and temporary variables” leaves me quite bamboozled. What is meant by that? Why is the result [nil] and not [0]?

It’s obscure, and I’m not sure it’s important to know the details, but since you ask…

SuperCollider uses functions as a framing devices for constructing UGen graphs.

{ Rand.new; SinOsc.ar * 0.1 }.asSynthDef.dumpUGens

Here the implicit output is the value of the last expression, but the Rand is placed in the graph too.

This is nice because there doesn’t need to be a notation for combining multiple graphs into one, you just write them one after another.

SuperCollider also uses the function arguments as the names and default values for Controls.

This is nice because you very often want the name of the Control and the name of the variable it is assigned to to be the same, and this way you don’t have to write the names twice.

The name and default value are looked up in the definition of the function.

The prototypeFrame has the literal default value for each argument, if there is one, else it has nil.

{ arg x = 0, y = 1; x }.def.argNames == SymbolArray[\x, \y] // true
{ arg x = 0, y = 1; x }.def.prototypeFrame == [0, 1] // true
{ arg x = 0, y = x; y }.def.prototypeFrame == [0, nil] // true
{ arg x = 0, y = x; y }.value == 0 // true
{ arg x = 0, y = 1; x }.def.inspect

SuperCollider allows non-literal default values, as you can see above, but it can’t know what value they might have.

(Apart from the lexical aspect, { arg x = 0, y = x; y } means the same as { arg x, y; x = 0; y = x; y }.)

Ps. https://github.com/supercollider/supercollider/issues/4013

1 Like

IMO at a beginner stage, it isn’t important to understand why arg x = #[0] and arg x = [0] are different – just that you need the # for arrayed controls in a SynthDef (or, use NamedControl) – just like it isn’t necessary to be able to code a gravitational attraction simulation in order to play tennis.

It’s valid to be curious about that, but not really necessary – you can always go back and pick it up later.

In short though –

The SynthDef builder reads the function’s argument list and creates a control for each one. Then the function is evaluated, passing the controls in as argument values.

Every control must have a default value.

So the SynthDef builder must be able to read defaults from the function definition without evaluating any instructions in the function.

This is possible only for literal values. If the arg default depends on any instructions to execute, then it can’t be known in advance. #[0] is known within the compiler. [0] is a list of instructions Array.new(1).add(0) and this value needs to be obtained by running the code – but the SynthDef builder can’t wait for that, so it substitutes 0.

What’s significant about literal arrays is that the “shape” of the desired array (multiple values) is different from the shape of the single value. This isn’t true of a simple numeric default. So the distinction becomes apparent only when you’re looking at arrayed controls.

Admittedly SynthDef documentation should be improved here. You’ve been sent down rabbit holes that simply weren’t necessary at this stage.

hjh

1 Like

@rdd and @jamshark70: Thank you very much for taking the time to explain this in a way that is understandable for a beginner. The point where it “clicked” for me was that [0] is not a literal value, but a set of instructions that first need to be evaluated, for which the SynthDef builder can’t wait, which is why it uses 0 instead, which leads to the “silent error”.

I am also glad to see that there already exists an issue report about these “silent errors”, with the proposed error text “Warning: SynthDef function has nil argument, default values must be literals”. This would have been a great breadcrumb for me (although I would very likely still have asked about literals :slight_smile: ).

As I am unfamiliar with Github itself and its etiquette: Is there a way to “upvote” an issue report, in the potential naive hope that this increases its chance to be fixed in the next release?

Please, how would I write such a documentation bug report?

To be honest, I’m not sure that bug report is a good idea.

SynthDef(\bufplayer, |bufnum, out, rate = 1, amp = 0.1|
    ...
}).add;

I know I will always supply a buffer number and it’s ok for out to default to zero. Currently this passes without warning. The proposed change would slap every user’s wrist for it.

There’s no way to distinguish between “I don’t need an explicit default for this control” and “I meant an array and forgot to make it literal.” The former case is more common (I think); we shouldn’t assume the latter case. I’d predict reports from hundreds of users, “My post window is filling up with this warning” and the solution would be that they would have to check every synthesis function they ever wrote to make every default an explicit literal… let’s say I have doubts whether this is a good user experience.

At the github site (which you found), create a user account if you don’t have one already. Then, from the issue report you linked above, click “Issues” and then there should be a green button for a new issue.

hjh

It’s nice when warnings are optional, i.e.

https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html#Warning-Options

There’s a short list of possibly helpful warnings at:

https://github.com/supercollider/supercollider/issues/923

There could be an Preferences.warnAll message or something similar to switch warnings on.

The check could look something like:

values.any({ arg item; item.isNil }).and(Preferences.warnOnNilArgumentAtSynthDefFunction).if(...)

&etc.

Ps. Another possible warning would be when Sc notices there are disconnected graphs and either deletes them, as in

{LFPulse.ar(LFPulse.ar); SinOsc.ar * 0.1}.asSynthDef.dumpUGens

or decides they’re required, as in

{Saw.ar(Saw.ar); SinOsc.ar * 0.1}.asSynthDef.dumpUGens

or a (somewhat eccentric) mixture of the two as in:

{LFPulse.ar(Rand.new); SinOsc.ar * 0.1}.asSynthDef.dumpUGens

A warning might be nice, any of these could be a kind of mistake.

I don’t necessarily disagree with that. I think the specific warning proposed for this issue is not particularly meaningful.

f = { |noDefault, exprDefault = ([1, 2, 3])| 0 };

f.def.prototypeFrame
-> [ nil, nil ]

For the warning to be meaningful, there should be a way to distinguish between noDefault (OK, no warning) and exprDefault (oopsy, should be literal). But… there is no way to distinguish them. You would get a lot of false-positive warnings.

I’m OK with adding a configurable warning, if it works properly. As proposed, it doesn’t work properly.

I had suggested once that “dead” DSP branches reflected a code error on the user’s part. It was swiftly pointed out to me that a user might well want to produce a number of possible branches and allow the optimizer to prune them.

One shouldn’t assume that one’s own coding style should be everybody’s style. (Except for mine – my style is optimal :laughing: joking of course.)

Also, part of the spirit of SC is, “We give you the toys and it’s up to you to be a grown-up and not step on the Legos.” So I’m a little bit hesitant about adding warnings where the spirit is, “You’re about to do something wrong, so someone is coming to stop you from stepping on the Lego.” +1 for more human-readable error messages for terminal errors; I’m not so sure about tons of nannying warnings.

hjh