Understanding ~variables not working as default argument values for SynthDefs

I am puzzled. As usual, it is most probably me who has strange assumptions, but can anyone explain me what I don’t get there, and where in the doc I should read about this?

Thanks!

// set a value
~val = 1;

// when instantiating a synth that value is replaced in the code
{Out.ar(~val, SinOsc.ar(222,mul: 0.1))}.play

// but not when it is a function default value...
{arg x = ~val; Out.ar(x, SinOsc.ar(222,mul: 0.1))}.play

My guess is that the .play method on a function doesn’t actually pass arguments to the receiver the same way that .value does? I.e., the function might get a 0 passed in that replaces the the default value…

Doesn’t really answer the question, but you can of course do:
{arg x ; Out.ar(x, SinOsc.ar(222,mul: 0.1))}.play(args: [\x, ~val])

this works:

~freq = 300;

{ { arg x = ~freq; SinOsc.ar(x, mul: 0.3) }.value }.play
1 Like

Right… seems like Function.play has a default bus, but not a default freq, and that method’s default overrides whatever you set as the default in your function.

in Core/Control/asDefName.sc, there’s this line inside the Function.play method:

synthMsg = synth.newMsg(target, [\i_out, outbus, \out, outbus] ++ args, addAction)

and from what I can tell, the code does not look inside the receiver Function to see what args it might have.(the args variable in that line is the args to the .play call, not the Function args) So it will just overwrite whatever default you set for the bus.

Seems like this is one of those things where it’s faster to look in the source than to look in the docs…

1 Like

In my actual usecase, I use SynthDef and I get the same strange behaviour. I would have expected the SynthDef code to resolve the value of the ~variable as default when it sends the code over…

~val = 1

SynthDef(\simple, {Out.ar(~val, SinOsc.ar(222,mul: 0.1))}).send

Synth(\simple) // on the right

SynthDef(\simple, {arg x = ~val; Out.ar(x, SinOsc.ar(222,mul: 0.1))}).send

Synth(\simple) // on the left

well this is beyond me. Apparently variables (of any kind) just don’t make it into the FunctionDef.prototypeFrame from which the Synthdef is built. (That’s more or less a list of default arguments).
e.g.

~val = 1;
u = {arg a = ~val, x = 2, y = 3;  x.postln}
u.def.prototypeFrame // -> [nil, 2,3]
//but
v = {arg a = 1, x = 2, y = 3;  x.postln}
v.def.prototypeFrame // -> [1, 2,3]

this would explain why this only affects the default values, not regular variables.
Maybe you can work around this with a NamedControl or some such?

The prototypeFrame is just copied onto the variable slots when entering a function or method, without any further evaluation being done. So it can handle only literal values.

I’m pretty sure that, in SC 2 and early versions of SC Server (now 3.x), it was impossible to write arg x = 1/2 and var x = 1/2 in any context. My memory is a bit fuzzy on this but I have a vague recollection of expression-defaults being added sometime after I started using SC.

As it’s a later addition, there are other inconsistencies to be found, e.g.:

a = { |x = 1, y = (2/2)| [x, y] };
a.value(nil, nil);
-> [nil, 1]

… because a is really { |x = 1, y| y ?? { y = 2/2 }; [x, y] } so y cannot distinguish between a nil value that was passed in vs a nil value from the prototype frame, and it replaces both with the expression default.

For all SynthDef inputs, expression defaults can never be written into a function argument list. This was one of the drivers for the idea of writing all synth inputs as NamedControls, always.

Alternately, I wrote a synth arg preprocessor quark – A new approach to SynthDef control input syntax – designed to meet as many of the requirements in a consistent way as possible. (Not at the computer now so I haven’t double checked it, but it should be fine.)

// not ok
{arg x = ~val; Out.ar(x, SinOsc.ar(222,mul: 0.1))}.play

// ok
{ ##x = ~val; Out.ar(x, SinOsc.ar(222,mul: 0.1))}.play

But nobody has really adopted it :man_shrugging:

hjh

1 Like

ok first I am reassured it is not just me :slight_smile:

NamedControls it will be in the next project. For now, setting args at synth instantiation it is.

What puzzled me most was the idea it might be scope. as the ~variables were accessible inside the function, I was freaking out as to why it wasn’t in its “definition” (see the “” as i am pretty sure it is the wrong word here)

I will think of a place in the docs where I think a note on this would make sense. maybe in Functions helper…

I’d suggest SynthDef help…? Because it isn’t true that expression defaults don’t work for functions in general. It is true that an expression default won’t be evaluated when the SynthDef builder “lifts” function arguments into SynthDef controls – this is one specific Function usage case.

hjh

indeed, and just for completion because I had to test to get my head around it:

~val = 1;

f = {~val.postln}

f.value

f = {arg x = ~val; x.postln}

f.value // x is ~val indeed

i think that this is why my assumption of the SynthDef default would work actually - I probably tested that long ago… in all cases, happy to find work arounds.

A SynthDef function never receives nil in its arguments – the arguments receive OutputProxies from Control UGens. So there’s no chance for ~val to evaluate in a SynthDef. This may be the disconnect here – the assumption that a SynthDef function is value’d with nil arguments, just like when you .value it by hand.

hjh

2 Likes