NamedControl: a better way to write SynthDef arguments

Hi again,

Only saw this now. Looks like an interesting approach! Will chew on it when I have a bit more time on my hands to see how this would fit into my overall setup…

Started using this as soon as I saw this post. Definitely a nicer way to plug controls into the SynthDef. I’m sold! Thanks!

Hi there -
This is an amazing resource - thank you for posting it.
I did want to ask a question, though, since I am currently a few months into a project with many NamedControls.
I’m finding that I have many of the same arguments, ultimately, for each Synth… and they’re all getting numbered in order to keep up with all of the controls. So, for instance, a cutoff frequency argument on different filters is becoming \cutoff1.kr, \cutoff2.kr, \cutoff3.kr…

Are there ways to make NamedControl more linked to the Synth instance directly?

hi, glad to hear you’re enjoying NamedControls.

i’m not 100% sure what you mean — do you mean “SynthDef” rather than “Synth”? if that’s the case, you can freely reuse the same NamedControl across different SynthDefs. they won’t interfere. if i guessed wrong, maybe a short code example would help clarify.

A SynthDef is an abstraction, you are going to mix the levels of abstract definition (SynthDef) and concrete instantiation (Synths), where the differentiation should be done.

To be more concrete, you could arrange your data like this:

// containers for data

~synths = […, …, …, … ]

~cutoff = […, …, …, … ]

then actions can be defined like this (and easily itereated over all indices)

~synths[0].set(\cutoff, ~cutoff[0])

// let n be the number of synths

n.do { |i| ~synths[i].set(\cutoff, ~cutoff[i]) }

Greetings

Daniel

Thank you! I got it now.

Not only that:

f = { |a = true, b = false, c = true, d = a | b | c | ... };

// vs
f = { |a = true, b = false, c = true, d = (a | b | c) | ... };

f = { |a = true, b = false, c = true, d = a | (b | c) | ... some other boolean... };

WRT NamedControls, what I would really like is a preprocessor that could associate all of these for a control:

  • name
  • default value
  • ControlSpec to be used later for GUI

E.g.,

SynthDef(\name, {
	## INPUT freq 440 \freq;
	## INPUT ffreq 2000 \freq;
	## INPUT rq 0.2 [1.0, 0.05, \exp];
	...
}).add

Now, that specific syntax is ugly and needs to be rethought, but that’s purely cosmetic. The main idea is: nobody uses specs in SynthDefs because currently you have to dump the specs into metadata at the end, separated from the input declarations. Specs, of course, would be optional but more people would use them if you include them in the same place where you write the default value. Then, if more people are using them, it’s easier to support better auto-GUI features.

IMO in freq = \freq.kr(440), I quite strongly dislike the redundancy of providing a variable name and symbol and I anticipate not using this option. But I could be persuaded to switch over if we used the opportunity of proposing a change in recommended syntactical practice to provide a currently missing feature (synth auto-GUI).

hjh

1 Like

With https://github.com/supercollider/supercollider/pull/3814 I believe you can do:

SynthDef(\foo, {
    |freq, amp|
   freq.spec = ControlSpec(20, 20000, default:440): 
   amp.spec = \db;
});

… or the equivalent via symbols - maybe more readable and straightforward than your macro example, even.

2 Likes

And - iirc you can use \symbol.kr notation for the same symbol multiple times in a SynthDef and it works as you would expect? If this doesn’t work now, it’s something worth fixing (and not so difficult, engineering-wise).

Hi! Thanks for the NamedControl tutorial! Great tool!
I wonder if it is possibe to have kind of multi type NamedControls

Starting from here

SynthDef(\sine, {
Out.ar(\out.kr(0), SinOsc.ar(\freq.kr(440)) * \amp.kr(0.1));
}).add;

Which kind of solution would be able to send an audio rate ugen to \freq.kr?
Better said how could a \freq.kr and a \freq.ar live together in the same SynthDef?
AFAIK that’s a good reason to use Instr instead SynthDef even if i know that using Quarks can be a can of worms.
I guess i’m looking for a kind of polymorphism, like sending a polymorph object as an argument to a polymorph object as almost all SC UGen are with their double kr/ar nature.

An important point is this: the structure of a synthdef on the server is totally fixed. Once you decide for kr it’s kr forever, no way to extend on that base.

That said, there is some kr / ar flexibility: e.g. you can map an audio bus to a kr control. But you must expect artefacts with non-LFO controls (with standard settings kr results in a nyquist frequency of ca 350 Hz which is often forgotten):

(
SynthDef(\test_kr, {
	Out.ar(0, LeakDC.ar(SinOsc.ar(\freq.kr(400), 0, 0.1)))
}).add;

SynthDef(\test_ar, {
	Out.ar(0, LeakDC.ar(SinOsc.ar(\freq.ar(400), 0, 0.1)))
}).add;
)

(
x = Synth(\test_kr);
a = Bus.audio(s, 1);
)

x.map(\freq, a);

// distorted FM
y = { Out.ar(a, SinOsc.ar(1000, 0, 1000)) }.play

y.free
x.free


(
x = Synth(\test_ar);
a = Bus.audio(s, 1);
)

x.map(\freq, a);

// clean FM
y = { Out.ar(a, SinOsc.ar(1000, 0, 1000)) }.play

y.free
x.free

Furthermore you can use A2K and go via a control bus (or K2A on other occasion).

But again: if you intend to go essentially into ar, e.g. do something like FM via buses you need ar args and ar buses.

1 Like

Thanks @dkmayer
that’s the reason because i was looking into Instr…as reported in help doc

" Instr SynthDefs

Unlike SynthDef, Instr can take any kind of input for its arguments, not just (including an Integer quantity, symbols or even other functions) and so while a SynthDef has a fixed architecture for a Synth, an Instr can generate multiple SynthDefs of varying architectures. For instance you could specify an Env or a fixed time duration or a quantity (how many parallel voices to create, detuned against each other) or even the name of a UGen (LFSaw, Pulse) to use for the oscillator."

This is generally good because it groups control names with their “types”, but it does have one downside: if you spread out the SynthDef inputs/controls through the code, and there’s a lot of them e.g. 20 params is not uncommon for some of my synths, it’s a real mess figuring out the full list of parameters when you look at the code. So what I do know is duplicate/move the list to the beginning of the SynthDef as vars, i.e. for a trivial example, instead of just

(SynthDef(\beep, {
	ReplaceOut.ar(\out.ir(0), \amp.kr(0.5) * SinOsc.ar(\freq.kr(440)).dup);
}).add;)

I do

(SynthDef(\beep, {
	var out = \out.ir(0), amp = \amp.kr(0.5), freq = \freq.kr(440);
	ReplaceOut.ar(out, amp * SinOsc.ar(freq).dup);
}).add;)

That looks duplicative, but with a lot controls (20+) and 20+ lines of code in a SynthDef, it helps me to have the full list upfront in the code (which is what args`does).

The var “overloading” of the named controls does work properly, i.e. there are no compile errors and you can still change their values with messages (e.g. Node.set, .map) still work properly, as it turns out.

Oddly enough, you can also write arg instead of var there:

(SynthDef(\beep, {
	arg out = \out.ir(0), amp = \amp.kr(0.5), freq = \freq.kr(440);
	ReplaceOut.ar(out, amp * SinOsc.ar(freq).dup);
}).add;)

It seems to work too for set or map but it doesn’t for the initial values which don’t get set anymore in this latter (arg instead of var approach). The reason for that is clear if you do

SynthDescLib.global.synthDescs.at(\beep);

With the var you get

-> SynthDesc 'beep' 
Controls:
ControlName  P 0 out scalar 0.0
ControlName  P 1 amp control 0.5
ControlName  P 2 freq control 440.0
   O audio ReplaceOut out 2

but with the arg instead you get

-> SynthDesc 'beep' 
Controls:
ControlName  P 0 out control 0.0
ControlName  P 1 amp control 0.0
ControlName  P 2 freq control 0.0
   O audio ReplaceOut out 2

Likewise if you “save” the SynthDef to a client var, and perform allControlNames on that object:

-> [ ControlName  P 0 out scalar 0, ControlName  P 1 amp control 0.5, ControlName  P 2 freq control 440 ]

vs

-> [ ControlName  P 0 out control 0.0, ControlName  P 1 amp control 0.0, ControlName  P 2 freq control 0.0 ]
1 Like

There’s probably too many of these having been re-invented; at least PdefGui, Instr.gui, and VarGui. But that’s probably best left for a different discussion.

By the way, how does this named control thing this play with NodeProxy’s? Those expect function args, if I’m not mistaken… I don’t even know how to make NodeProxy type (e.g. \ar) its generated (actual SynthDef) controls.

Sure, but one needs to consider all hacky ways in which synths also get generated, e.g. NodeProxy. So a pre-processor might not be the best idea. Probably having the NamedControls in an environment, e.g. with Halos for the “extra stuff” like specs, might be the more general approach. Something like thisControls being predefined in a Synth or SynthDef like currentEnvironment is on the client. The named controls essentially define an environment like that anyway… except you cannot write (a working)

(d = SynthDef(\beep, {
	var out = ~out.ir(0), amp = ~amp.kr(0.5), freq = ~freq.kr(440);
	ReplaceOut.ar(out, amp * SinOsc.ar(freq).dup);
}).add;)

because the lookup for ~amp etc. is in currentEnvironment. Basically having a thisControls (or synthEnvironment) made explicit might be better.

Quick bullet point edition:

  • Synthesis function args look up their default values in the function’s prototypeFrame.
  • prototypeFrame contains only literal defaults. Arguments with expression defaults appear as nil in the prototypeFrame.
  • Probably (I haven’t investigated) SynthDef is building Controls for the arguments, and then \name.kr is looking for existing controls with the same name – and finding them. But \name.kr(value) shouldn’t blow away defaults from previously created control objects.

tl;dr you can use arg there, but you shouldn’t.

Part of the reason for that is that there’s no good one in the main library. (Also GUIs are somewhat personal.) By “good,” I mean general enough to handle a broad range of use cases with straightforward visual controls, and working with baseline SynthDefs. (I admit I haven’t looked at VarGui so I can’t comment on it.) There’s a makeGui for SynthDesc I think, but it’s pretty… erm… basic.

A comprehensive SynthDef GUI is a very hard design problem – I don’t really want to tackle it either!

Quite well, as I recall. I’ve used \name.ar in proxies and they do appear in the GUI.

Ok, I see your point. A preprocessor wouldn’t be good if it’s stashing specs outside of the synthesis function (as my original idea suggested).

But…

This is now merged in, so all of this is easier. (I’m still not crazy about the name duplication of var freq = \freq.kr but a preprocessor could do something about that.)

hjh

Yes, that’s correct, I’ve just tried

n = NodeProxy.audio(s, 2);
(n.source = { var out = \out.ir(0), amp = \amp.kr(0.5), freq = \freq.kr(440);
	ReplaceOut.ar(out, amp * SinOsc.ar(freq).dup); })
n.gui;

And the two params do show up. (out doesn’t but I think that’s “by design”).

Also the types get updated properly when changing the proxy source, e.g.

n = NodeProxy.audio(s, 2);
n.source = { var amp = \amp.ar(0.5), freq = \freq.ar(440); amp * SinOsc.ar(freq).dup }
n.controlNames // -> [ ControlName  P 0 amp audio 0.5, ControlName  P 1 freq audio 440 ]
n.source = { var amp = \amp.kr(0.5), freq = \freq.ar(440); amp * SinOsc.ar(freq).dup }
n.controlNames // -> [ ControlName  P 0 amp control 0.5, ControlName  P 1 freq audio 440 ]

Indeed GUIs are personal. Main lib has at least this:

SynthDescLib.global[\default].makeGui

But it’s not for controling synths and patterns at the same time. That was my motivation for writing VarGui - and I hate to write GUIs …
On the other hand I rarely work with proxies, and if, then I rather don’t want GUIs for them, so I didn’t include that in my design decisions. Maybe inevitable to tackle the own GUI concepts …

Amusingly enough perhaps, the auto-generated window title for that is “another control panel” :slight_smile:

The main advantage of NamedControls with NodeProxies seems to be not having to put the variable type in its name, i.e. one can also get typing via name prefixes (in SynthDef arg, in general), but this can be bit a annoying to carry through the code and/or change later if the type turns out not to be desired one (e.g. going from kr to ar to remove “zipper” noise.)

n = NodeProxy.audio(s, 2);
(n.source = { arg a_amp, a_freq; a_amp * SinOsc.ar(a_freq).dup; })
n.controlNames // -> [ ControlName  P 0 a_amp audio 0.0, ControlName  P 1 a_freq audio 0.0 ]