Copying some stuff from web

I am copying this code from a you tube video , from an obviously verry talented user , which I think is user Nathan here on this forum
The only thing that I have not included is the safetylimiter ( which is an extension ) and a different out
The user is using a symbol as the first argument which I have no iead what that’s referring to .
Anyway , as expected the code does not work …
Here 's the og code
And he video in action at 06:13

(
SynthDef(\additive,{
	var snd,freqs,n,numPartials;
	numPartials=60;
	n=(1..numPartials);
	freqs=60*n*(1+(n*n*0.001)).sqrt;
	snd=SinOsc.ar(freqs);
	snd=snd*(log2(n)*\tilt.kr(-3)).dbamp;
	snd=snd*(1-log2(n*MouseX.kr(1,16)).sin.abs);
	snd=snd.sum!2;
	snd=snd*-20.dbamp;
	Out.ar(0,snd);
}).add;
)


Synth(\additive);

\name.kr(0) is a shortcut for NamedControl.kr(\name, 0).

Personally, I do not like this shortcut syntax at all – for exactly the reason it’s being brought up in this thread: it confuses the xxxx out of users who aren’t in the in-crowd.

Readable > clever. \xyz.kr is clever (and IMO awful).

hjh

What about it doesn’t work?

As a general rule of thumb, I’d suggest that it usually is not useful merely to say “it doesn’t work” on a tech forum. Usually it’s more useful to say “I expected x but it did y” – more specific.

hjh

NamedControls are not “awful” or “clever.” They have material advantages over the argument style, which I have explained at length in this post: NamedControl: a better way to write SynthDef arguments

NamedControl is great! And I do use it, particularly for arrayed arguments. (So I wasn’t saying that NamedControl is bad – if I did say that, I’d appreciate it being pointed out where I did. I’ll admit, however, to over-stating the case for dramatic effect – for which I’m willing to apologize.)

I was trying to say that \name.kr(default) requires explanation, and that the source of this explanation is unlikely to be obvious just from reading the code. If we take that as a hypothesis, then the beginning of this thread is evidence supporting the hypothesis: “The user is using a symbol as the first argument which I have no idea what that’s referring to.”

I’m aware of the linked thread, and I think it’s a bit of an overreach to suggest abolishing Symbol:kr as a general replacement for function argument lists to build SynthDef controls. I simply don’t agree.

It’s fine to disagree.

For another example, I also noticed in the past that many new users get confused over { |i| i * 2 } ! 10 and made it a point in my own code to write Array.fill(10, { |i| i * 2 }) instead. I fully expect a lot of users to disagree with me on that – I only maintain that Array.fill is more readable, and thus more inviting for new users. I think it’s hard to dispute that claim. It’s fine to prioritize ease of typing in one’s own code – just because I have an opinion doesn’t mean anyone else has to follow it :wink: (so likewise, the linked thread states an opinion and I’m not obligated to switch my code over, either).

For me, readability is a very high priority, so I tend to avoid shortcuts that save a few keystrokes at the expense of "what-the-heck"s down the road later.

hjh

It seems that it didnt like this line of code , I wonder why
snd=snd*-20.dbamp;

// variable snd
// operator *
// integer literal -20
// method call dbamp
snd = snd * -20.dbamp;

// variable snd
// operator *- (this doesn't exist)
// integer literal 20
// method call dbamp
snd = snd*-20.dbamp;

Removing the spaces inadvertently changed the way it parses.

Another opinion: Many users, particularly new users, I guess wish to save some typing by deleting as much white space as possible. I find that it hurts readability to have a large mass of undifferentiated characters (I strongly dislike reading such code) – and as you found here, spaces are sometimes syntactically significant.

hjh

The shortcut is also fine. It is documented and its usage is sufficiently widespread. The alternatives are to write out NamedControl.kr(\freq, 440), which is absurdly verbose for the amount SynthDef arguments are used, or to use the argument style, which has serious problems when it comes to specification of rate.

Again, agree to disagree. It’s documented, but, “I have no idea what that’s referring to.” I guess the solution to that is an RFC to change the synth control style throughout the help system.

I did this with my live coding system loaded (actually missing one file, but close enough):

(
a = (control: 0, arrayed: 0, audio: 0, scalar: 0, total: 0);

SynthDescLib.global.synthDescs.do { |desc|
	if(desc.name.asString.beginsWith("system_").not) {
		desc.controls.do { |ctl|
			var rate = ctl.rate;
			if(ctl.name != '?') {
				a[rate] = (a[rate] ?? { 0 }) + 1;
				if(ctl.defaultValue.size > 1) {
					a[\arrayed] = a[\arrayed] + 1;
				};
				a[\total] = a[\total] + 1;
			};
		};
	};
};

a
)

-> ( 'scalar': 22, 'arrayed': 12, 'control': 1443, 'audio': 0,
  'total': 1465 )

Arrayed controls clock in at 12/1465 < 1%, and scalar rate at 22/1465 ~= 1.5% scalar. It’s hard to count tr controls this way but I don’t use them frequently. Perhaps that’s why I don’t mind to write out NamedControl in the 0.8% cases of arrayed controls (where there’s really no good other way) or the couple percent other cases.

For the majority of cases, it’s a nice optimization that function args collapse into one Control unit.

(
SynthDef(\oneControl, {
	var ctl = Control.names(Array.fill(250, { |i| ("ctl" ++ i).asSymbol }))
	.kr((1..500));
	Out.kr(0, ctl[0]);
}).add;

SynthDef(\namedControls, {
	var ctl = Array.fill(250, { |i|
		NamedControl.kr(("ctl" ++ i).asSymbol, i+1)
	});
	Out.kr(0, ctl[0]);
}).add;
)

// 10-11% after setting CPU performance mode
a = (instrument: \oneControl, ctl0: (0..100), type: \on).play;

a.put(\type, \off).play;

// 18-19%
a = (instrument: \namedControls, ctl0: (0..100), type: \on).play;

a.put(\type, \off).play;

hjh

If efficiency is a concern, ir controls are significantly better (these require NamedControl). The peak cpu of this is 0.5%, where your other two examples peaked at 18% and 42% for me:

SynthDef(\namedControlsIr, {
	var ctl = \ctl.ir((0..100));
	Out.kr(0, ctl.sum.poll);
}).add;

Somewhat unrelated but - for what it’s worth, I’m not totally sure what you’re doing with sending an array to a non-array control is valid? Setting a single control, ctl0, with an array works only because the server blindly writes all the data from that array into memory without looking at where it’s writing. It happens to be the case that the synth args are ordered in memory like ctl0, ctl1, ctl2, … etc, but I DON’T think this is guaranteed to be the case - probably this would be considered undefined behavior that just happens to work right now.

Overwriting the bounds of controls like this can cause subtle and very nasty bugs (and IIRC crashes as well) so to be honest scsynth and supernova should probably have checks in place to make sure this doesn’t occur.

That’s not an apples-to-apples comparison.

I’m using the DSP CPU testing methodology of creating the same number of a lot of something, and measuring CPU for those.

  • My test 1: 250 independently-named single-value controls, created in a single Control unit, x 101 synths = 101 Control units, 25250 active controls.

  • My test 2: 250 independently-named single-value controls, each with its own dedicated Control unit, x 101 synths = 25250 Control units, 25250 active controls.

… and found from this test that the overhead of entering and exiting 25149 additional Control_next() functions is not exactly trivial.

To compare ir, you would need two tests: test 1 architecture, and test 2 architecture, just changing the rates.

(Actually, as I was doing that, I found a mistake in the oneControl synth – it should be .kr((1..250)) – fixing this mistake reduces CPU for test 1 to about 6%! So the margin is even larger than I thought at first.)

  • kr
    • Test 1 (fixed) = 6%
    • Test 2 = 18%
  • ir
    • Test 1 = 0.2% (probably within margin of error?)
    • Test 2 = 0.2%

I can’t find the bit in the code that’s responsible for this, but I’d have to guess that scalar-rate controls simply don’t call the nextfunc at all. In that case, it wouldn’t matter how many there are.

But “make all your controls ir” isn’t really a solution, is it? :laughing: I like bus mapping…

Not true – arg i_out is used in many places in the class library. Also the construction in my test 1 is valid to create ir controls as well. (IIRC Control.names(array_of_symbols).kr(array_of_defaults) is how the function’s argument list is converted into Control channels – it is a valid usage, though not common, and not particularly user-friendly, which is why NamedControl was introduced.)

I’m not setting a non-array control to an array.

s.dumpOSC(1);

a = (instrument: \oneControl, ctl0: (0..100), type: \on).play;

[ "#bundle", 16602913446784111450, 
  [ 9, "oneControl", 1404, 0, 1, "ctl0", 0 ],
  [ 9, "oneControl", 1405, 0, 1, "ctl0", 1 ],
  [ 9, "oneControl", 1406, 0, 1, "ctl0", 2 ],
  [ 9, "oneControl", 1407, 0, 1, "ctl0", 3 ],
  [ 9, "oneControl", 1408, 0, 1, "ctl0", 4 ],
  [ 9, "oneControl", 1409, 0, 1, "ctl0", 5 ],
  [ 9, "oneControl", 1410, 0, 1, "ctl0", 6 ],
  ....
]

hjh

Bus mapping works with ir, it just pulls the value once when the synth is created.

Yup, mine was a typo - but as you said the difference is still roughly the same (e.g. extremely low CPU usage for the ir case).

Got it, I misread your code - it would take another [] to do what I was thinking.

James, would you mind explaining what is actually happening here versus with the NamedControl?

Is it that an array inside of NamedControl ends up returning a Control unit per each element within an array, versus when using an array inside of an argument, Control encapsulates the entire array within one Control unit?

I use NamedControls everywhere, and really like them. I even tend to declare most of them toward the top, where I’d normally use args. One of the subtle things I appreciate is the IDE’s syntax highlighter doesn’t recognize args as such, but symbols pop right out for me.

But I also use a lot of arrays within them. And like making efficiency mods. The CPU difference is striking here!

Boris

Perhaps compare:

SynthDef(ugenGraphFunc: {
	arg bus = 0, freq = 440, phase = 0, amp = 0.1;
	Out.ar(bus, SinOsc.ar(freq, phase) * amp)
}).children.select({ arg each; each.class == Control}).size == 1

and:

SynthDef(ugenGraphFunc: {
	Out.ar('bus'.kr, SinOsc.ar('freq'.kr, 'phase'.kr) * 'amp'.kr)
}).children.select({ arg each; each.class == Control }).size == 4

There’s no particular reason the graph builder couldn’t coalesce the second case, but for the moment it doesn’t.

Interesting. Thanks for this example.

I tried this:

(
SynthDef(ugenGraphFunc: {
	var bus = Control.names(\bus).kr(0),
	freq = Control.names(\freq).kr(440),
	phase = Control.names(\phase).kr(0),
	amp = Control.names(\amp).kr(0.1);
	
	Out.ar(bus, SinOsc.ar(freq, phase) * amp)
}).children.select({ arg each; each.class == Control}).size == 4
)

So it seems like it’s the naming that’s preventing them from being bundled? Which is odd, because it seems like in Control’s source code, they’re being associated with a name, anyway…?

Another thing that I noticed:

(
SynthDef(ugenGraphFunc: {
	Out.ar('bus'.kr, SinOsc.ar('freq'.kr(440), 'phase'.kr) * 'amp'.kr(0.1))
}).children
)
// -> [ a Control, a Control, a Control, a SinOsc, a Control, a BinaryOpUGen, an Out ]

(
SynthDef(ugenGraphFunc: {
	Out.ar('bus'.kr, SinOsc.ar('freq'.kr(440), 'phase'.kr) * 'amp'.kr(0.1))
}).children[0].values
)
// -> [ 0.0, 440, 0.0, 0.1 ]

(
SynthDef(ugenGraphFunc: {
	Out.ar('bus'.kr, SinOsc.ar('freq'.kr(440), 'phase'.kr) * 'amp'.kr(0.1))
}).children[1].values
)
// -> [ 440 ]

So it seems like the first Control bundles them up anyway?

The graph description stores “parameter” meta-data as well as the Control unit generators, see:

  • int32 - number of parameters (P)
  • [float32] * P - initial parameter values
  • int32 - number of parameter names (N)
  • [ param-name ] * N

at https://doc.sccode.org/Reference/Synth-Definition-File-Format.html

Parameters can be referred to by both name and index.

If you write all of the parameters at once, for instance as pseudo-arguments, the indices can be derived from the sequence of the names.

If you write the parameters distinctly, and want to refer to them by index, then you need to know what the rule is for assigning the indexes!

SynthDef(ugenGraphFunc: {
	arg bus = 0, freq = 440, phase = 0, amp = 0.1;
	Out.ar(bus, SinOsc.ar(freq, phase) * amp)
}).allControlNames.collect({ arg each; each.index -> each.name })
==
SynthDef(ugenGraphFunc: {
	Out.ar('bus'.kr, SinOsc.ar('freq'.kr, 'phase'.kr) * 'amp'.kr)
}).allControlNames.collect({ arg each; each.index -> each.name })

There are various ways to write graph constructors, each with nice and less nice aspects.

First, it would be helpful to understand the purpose of the test.

I wanted to demonstrate the performance difference between a single Control unit with a large number of channels, and a large number of single-channel Controls.

Note at this point that there is no need to consider arrayed arguments for this. In fact, it would be bad to use arrayed arguments here. A good test eliminates unnecessary variation in the cases. If I want to measure the above-stated performance difference, then I want everything to be the same except for the Control structure: same number of controls, same names etc. So I do not want non-arrayed Control vs arrayed NamedControl, or vice versa.

I could have written:

(
d = SynthDef(\oneControl, {
	arg ctl0 = 1, ctl1 = 2, ctl2 = 3, ctl3 = 4, ctl4 = 5,
	ctl5 = 6, ctl6 = 7, ctl7 = 8, ctl8 = 9, ctl9 = 10,
	ctl10 = 11, ctl11 = 12, ctl12 = 13, ctl13 = 14, ctl14 = 15,
	ctl15 = 16, ctl16 = 17, ctl17 = 18, ctl18 = 19, ctl19 = 20,
	ctl20 = 21, ctl21 = 22, ctl22 = 23, ctl23 = 24, ctl24 = 25,
	ctl25 = 26, ctl26 = 27, ctl27 = 28, ctl28 = 29, ctl29 = 30,
	ctl30 = 31, ctl31 = 32, ctl32 = 33, ctl33 = 34, ctl34 = 35,
	ctl35 = 36, ctl36 = 37, ctl37 = 38, ctl38 = 39, ctl39 = 40,
	ctl40 = 41, ctl41 = 42, ctl42 = 43, ctl43 = 44, ctl44 = 45,
	ctl45 = 46, ctl46 = 47, ctl47 = 48, ctl48 = 49, ctl49 = 50,
	ctl50 = 51, ctl51 = 52, ctl52 = 53, ctl53 = 54, ctl54 = 55,
	ctl55 = 56, ctl56 = 57, ctl57 = 58, ctl58 = 59, ctl59 = 60,
	ctl60 = 61, ctl61 = 62, ctl62 = 63, ctl63 = 64, ctl64 = 65,
	ctl65 = 66, ctl66 = 67, ctl67 = 68, ctl68 = 69, ctl69 = 70,
	ctl70 = 71, ctl71 = 72, ctl72 = 73, ctl73 = 74, ctl74 = 75,
	ctl75 = 76, ctl76 = 77, ctl77 = 78, ctl78 = 79, ctl79 = 80,
	ctl80 = 81, ctl81 = 82, ctl82 = 83, ctl83 = 84, ctl84 = 85,
	ctl85 = 86, ctl86 = 87, ctl87 = 88, ctl88 = 89, ctl89 = 90,
	ctl90 = 91, ctl91 = 92, ctl92 = 93, ctl93 = 94, ctl94 = 95,
	ctl95 = 96, ctl96 = 97, ctl97 = 98, ctl98 = 99, ctl99 = 100,
	ctl100 = 101, ctl101 = 102, ctl102 = 103, ctl103 = 104, ctl104 = 105,
	ctl105 = 106, ctl106 = 107, ctl107 = 108, ctl108 = 109, ctl109 = 110,
	ctl110 = 111, ctl111 = 112, ctl112 = 113, ctl113 = 114, ctl114 = 115,
	ctl115 = 116, ctl116 = 117, ctl117 = 118, ctl118 = 119, ctl119 = 120,
	ctl120 = 121, ctl121 = 122, ctl122 = 123, ctl123 = 124, ctl124 = 125,
	ctl125 = 126, ctl126 = 127, ctl127 = 128, ctl128 = 129, ctl129 = 130,
	ctl130 = 131, ctl131 = 132, ctl132 = 133, ctl133 = 134, ctl134 = 135,
	ctl135 = 136, ctl136 = 137, ctl137 = 138, ctl138 = 139, ctl139 = 140,
	ctl140 = 141, ctl141 = 142, ctl142 = 143, ctl143 = 144, ctl144 = 145,
	ctl145 = 146, ctl146 = 147, ctl147 = 148, ctl148 = 149, ctl149 = 150,
	ctl150 = 151, ctl151 = 152, ctl152 = 153, ctl153 = 154, ctl154 = 155,
	ctl155 = 156, ctl156 = 157, ctl157 = 158, ctl158 = 159, ctl159 = 160,
	ctl160 = 161, ctl161 = 162, ctl162 = 163, ctl163 = 164, ctl164 = 165,
	ctl165 = 166, ctl166 = 167, ctl167 = 168, ctl168 = 169, ctl169 = 170,
	ctl170 = 171, ctl171 = 172, ctl172 = 173, ctl173 = 174, ctl174 = 175,
	ctl175 = 176, ctl176 = 177, ctl177 = 178, ctl178 = 179, ctl179 = 180,
	ctl180 = 181, ctl181 = 182, ctl182 = 183, ctl183 = 184, ctl184 = 185,
	ctl185 = 186, ctl186 = 187, ctl187 = 188, ctl188 = 189, ctl189 = 190,
	ctl190 = 191, ctl191 = 192, ctl192 = 193, ctl193 = 194, ctl194 = 195,
	ctl195 = 196, ctl196 = 197, ctl197 = 198, ctl198 = 199, ctl199 = 200,
	ctl200 = 201, ctl201 = 202, ctl202 = 203, ctl203 = 204, ctl204 = 205,
	ctl205 = 206, ctl206 = 207, ctl207 = 208, ctl208 = 209, ctl209 = 210,
	ctl210 = 211, ctl211 = 212, ctl212 = 213, ctl213 = 214, ctl214 = 215,
	ctl215 = 216, ctl216 = 217, ctl217 = 218, ctl218 = 219, ctl219 = 220,
	ctl220 = 221, ctl221 = 222, ctl222 = 223, ctl223 = 224, ctl224 = 225,
	ctl225 = 226, ctl226 = 227, ctl227 = 228, ctl228 = 229, ctl229 = 230,
	ctl230 = 231, ctl231 = 232, ctl232 = 233, ctl233 = 234, ctl234 = 235,
	ctl235 = 236, ctl236 = 237, ctl237 = 238, ctl238 = 239, ctl239 = 240,
	ctl240 = 241, ctl241 = 242, ctl242 = 243, ctl243 = 244, ctl244 = 245,
	ctl245 = 246, ctl246 = 247, ctl247 = 248, ctl248 = 249, ctl249 = 250;
	Out.kr(0, ctl0);
}).add;
)

But this is boring and noisy. So instead I wrote:

(
e = SynthDef(\oneControl, {
	var ctl = Control.names(Array.fill(250, { |i| ("ctl" ++ i).asSymbol }))
	.kr((1..250));  // fixed the earlier mistake
	Out.kr(0, ctl[0]);
}).add;
)

… which is exactly the same. We can prove that:

d.asBytes == e.asBytes
-> true

This demonstrates that function arguments in a SynthDef will be collapsed down to the smallest number of Control UGens. That’s all.

For \namedControls, I could have written:

(
SynthDef(\namedControls, {
	var ctl0 = \ctl0.kr(1);
	var ctl1 = \ctl1.kr(2);
	var ctl2 = \ctl2.kr(3);
	var ctl3 = \ctl3.kr(4);
	var ctl4 = \ctl4.kr(5);
	var ctl5 = \ctl5.kr(6);
	var ctl6 = \ctl6.kr(7);
	var ctl7 = \ctl7.kr(8);
	var ctl8 = \ctl8.kr(9);
	var ctl9 = \ctl9.kr(10);
	
	// ... OK, here I won't be 250-lines level of pedantic
	// ... snipped
	
	var ctl245 = \ctl245.kr(246);
	var ctl246 = \ctl246.kr(247);
	var ctl247 = \ctl247.kr(248);
	var ctl248 = \ctl248.kr(249);
	var ctl249 = \ctl249.kr(250);
	
	Out.kr(0, ctl0);
}).add;
)

Instead, I constructed the identical NamedControl structure in a loop.

The coding style in that test was simply because, why should I post 300+ lines of repetitive declarations, when I can do it in about 15 lines?

Anyway, NamedControl produces one Control UGen per NamedControl object. An arrayed control has one name, one NamedControl, and multiple values. So, no, the array will not be split out into multiple Controls.

I think this is a bug actually.

Let’s add one more control:

(
d = SynthDef(\test, {
	var extraCtl = \extraCtl.kr(10000);
	Out.ar('bus'.kr, SinOsc.ar('freq'.kr(440), 'phase'.kr) * 'amp'.kr(0.1))
}).children[0].values
)

-> [ 10000, 0.0, 440, 0.0 ]

Oopsy, five controls were declared but this array has only four values.

  1. That means this array is not reliable for anything (i.e. “the first Control bundles them up anyway?” = no).
  2. When an array stops at size 4, usually that means something is .add-ing to it without reassigning the result back into the variable. I can’t find where in the code this is happening, and I don’t know why this is being done only for the first Control to be created. It does look a bit like unintentional behavior, though.

I suspect this is likely to be trickier than one might expect.

Because Control outputs will be used in many, many other units’ inputs, you would need to take each of these OutputProxy objects and mutate them so that they belong to a single Control. (It’s too risky to try to make new OutputProxies and substitute them in other units’ inputs.) I suppose it’s possible, but mutating the graph after the fact… I would expect pitfalls. Probably all of the problems could be solved though.

Note that the function-argument “collapse to one” happens before the function is evaluated, so in that case, there is no need to mutate anything afterward.

hjh

And the performance differences are substantial!

In my post above, when I explicitly called Control to create arguments, instead of using function-arguments, the results are similar to using NamedControls: They did not collapse down into a single Control unit. In your example, this worked out a bit differently.

These all did collapse down into one -

Actually, I’m a bit surprised by this…

Is there anything preventing from it being possible to allow for this behavior for NamedControls?

Looking through the source code, it seems like Control.names can/does process names as an array, thus is able to collapse them down into one, like your earlier example.

NamedControl has a name method instead, which works with only a single entity.

If you do SinOsc.kr 4 times, you would expect 4 SinOsc units in the SynthDef.

It’s actually the same for Control: you have an example which called kr on Control 4 times, and you got 4 Control units. My example calls Control...stuff...kr once, and it produces one Control unit.

Control itself does nothing to collapse them. It allows you to prepare the names and values in a way that can handle multiple differently-named controls within one Control unit. But it is up to you to prepare it that way.

For function arguments, the preparation happens before even calling into the function.

In my opinion, it may be solvable, but difficult.

Understand the process first. The SynthDef builder reads the function arguments and groups them by rate and lag, clumping similar inputs together. Then it makes a UGen for each group, and parcels out these units’ OutputProxy channels into the function’s argument list. (Inside the function, every argument will receive either an OutputProxy or an array of OutputProxies.)

The key here is that these Control / AudioControl / TrigControl / LagControl objects are constructed before function evaluation, and these objects do not change during function evaluation.

To collapse NamedControls – this would be working with objects that are created during function evaluation, not before. So there is no way to prepare static objects in advance. There are only two ways to fold the second NamedControl into the first:

  • At the moment of creating the second one, go back and find the first, and mutate its data structure to contain two channels with different names. (Then the third would do this again, and so on.)
  • Or, during SynthDef post-processing, analyze the NamedControl collection and replace the individual units with a smaller number of units. (IMO this may be riskier – it would touch not only the Controls but perhaps most other units’ index etc.)

Mutating UGens isn’t impossible – the math operator optimizations do this, on a small scale – but it is a bit delicate and the code would need to be tested carefully. Every time a UGen’s internals are modified after it’s already integrated into the graph is another source of risk.

Note that SynthDef.wrap also creates additional Control units, instead of folding the new ones in with the old ones. That’s James McCartney’s code, and he didn’t want to tackle the problem of after-the-fact UGen manipulation. This is probably why NamedControl just says “yikes, that’s too hard (and besides, it’s not 1996 anymore, we don’t have to worry about CPU so much)” and goes with the less efficient option.

It would be nice if someone tackled this. It won’t be me, though (since I’m mainly satisfied with function arguments).

hjh