Is it possible to make a synthdef to deal with a variable number of oscillators?

Hello!

I know the result of running synthdef code is static.
But like a following code, I want to control the number of SinOsc with Pbind.

And I have some misc questions as commented below.

(// for sound
SynthDef(\varPolyTest, {
	arg n = 30, freq = 300, envdur = 1.0; // setting default value of n occurs "Index not an Integer" error, why?
	n = 30; // assigning value of n instead of above line is working, but this way is surely fix the value of n.
	var ratio, mixSig, env;

	ratio = Array.rand(n, 2.2, 5); 
	env = EnvGen.kr(Env.sine(envdur), doneAction:2);
	mixSig = Mix.fill(n, {|i|
			sig = SinOsc.ar(freq * ratio[i], mul:1/n * 0.5);
	});
	Out.ar(0, mixSig * env!2);
}).add;
)


(// for control
Pbind(
	\instrument, \varPolyTest,
	\n, Pwhite(5, 20, 1), // arg 'n' doesn't change.
	\freq, Pwhite(300, 700, 1),
	\envdur, Pwhite(5, 10, 1),
).play
)

I found alternative way of using a Function not a SynthDef.
But It is not possible controlled by Pbind or Pseq isn’t it?

(
~vp = {arg n = 10, freq = 200;
	var ratio = Array.rand(n, 2.2, 5);
	~sig = Array.newClear(n);
	n.do{|i|
		~sig[i] = {SinOsc.ar(freq * ratio[i], mul: 1/n * 0.5)}
	};
	~sig.sum;
};
)

~synthTest = {~vp.value(1 + 10.rand.postln, 200)}.play;

Please share your ideas.
Thanks.

Hi,

actually, the error message exactly reflects that you would like to have the synthdef structure non-static. n is fixed and used for the array which is then used for the synthdef graph, no chance to change after compilation.
The good news is that there’s an easy trick to make the structure “pseudo-dynamic”: fix a max number of sines and only take as much as you need by multiplying with 0 or 1. There’s a couple of ways to make such: you could, e.g., define a dedicated array and multiply.
In the below example I’ve chosen to make it as short as possible: use a comparison ugen (<) inside a Function and the notion { … } ! … (short for Function.dup)

Cheers

Daniel

(
SynthDef(\varPolyTest_2, {
	arg num = 30, freq = 300, envdur = 1.0; 
	var maxNum = 30, sig, env;
	env = EnvGen.kr(Env.sine(envdur), doneAction: 2);
	sig = { |i| SinOsc.ar(freq * rrand(2.2, 5), mul: 1 / maxNum * 0.5) * (i < num) } ! maxNum;
	Out.ar(0, sig.sum * env ! 2);
}).add;
)

(
Pbind(
	\instrument, \varPolyTest_2,
	\freq, Pwhite(300, 700),
	\envdur, Pwhite(3, 5),
	\dur, 2,
	\num, Pexprand(2, 30, 20).round
).trace.play
)
4 Likes

Wow, that is a nice solution!
Thank you very much.

But I am still wondering about how does it works.
When synthdef compiled, it looks like the ‘* (i < num)’ should be a fixed value.
If not, is it because that line is a function?

That makes the difference: it’s a UGen that reacts to the control input at every (control rate) sample (polymorphism: < can act as a pure language operator and can also result in a UGen, here because num is a control input). In contrast, an Array inside a SynthDef leads to a fixed size after compilation, no control input can change this size.
BTW, there’s an alternative solution: you could make a SynthDef factory, a Function that produces SynthDefs that only differ in the size of the array (e.g., named with suffices _1, _2, …). You could then call them from the Pattern. The advantage: you don’t have silent ugens running all the time (as in the suggested method). However, SinOscs are cheap, so that probably won’t be an issue.
You might take a look into miSCellaneous_lib’s tutorial “Event patterns and array args” where there are examples for this. Array args are especially useful for additive synthesis.

Wow… It is amazing.
Where can I find that from SC DOC or SC sourcecode?

And I made basic code from the tutorial you suggested.
It’s a worth tutorial to read carefully.

Thank you very much.

(
(
(1..10).do{|n|
	var name = \synth_++n;
	SynthDef(name, {|out = 0, freq = #[440, 450], amp = 0.1, gate = 1|
		var sig0, sig1, amps, env;
		env = EnvGen.kr(Env.perc(), gate, doneAction: Done.freeSelf);
		amps = (1/(1..n)).normalizeSum;
		sig0 = (SinOsc.ar(freq[0] * (1..n), mul: amp) * amps).sum;
		sig1 = (SinOsc.ar(freq[1] * (1..n), mul: amp) * amps).sum;
		Out.ar(out, [sig0, sig1] * env);
		// Out.ar(out, sig0 * env);
	}).add;
}
)

(
p = Pbind(
	\size, Prand((1..10), 100).collect{|x| x.asSymbol}, 
	\instrument, Pkey(\size).collect{|x| \synth_++x}, 
	\dur, 1,
	\amp, 0.3,
	\degree, Pshuf((0..11) + [[1.0.rand, 1.0.rand], [1.0.rand, 1.0.rand]], 100)
).trace.play
)
1 Like

Some infos:

https://doc.sccode.org/Guides/UserFAQ.html

https://doc.sccode.org/Classes/BinaryOpUGen.html

https://doc.sccode.org/Overviews/Operators.html

It follows logically from other behaviors.

Consider a function f = { |x| x + 1 };.

  • If you give it a number, the result is a number.
  • If you give it an array, the result is an array.
  • If you give it a UGen, the result is a UGen. Note that you’ve been doing math on UGens all along! So it isn’t actually surprising that aUGen + something is another UGen that represents the math operation.

+ and < are both binary operators and… guess what… < behaves in the same way! The one difference is that number < otherNumber returns a Boolean, not a number. But arrays, UGens, functions, patterns etc follow the same principle for < as for +.

hjh

Your infos and kind explanation makes me think clearer.

To sum it up,

  • The argument of SynthDef is a kind of UGen because it is an instance of Control.
  • ‘<’ operatoration with Ugen creates a BinaryOpUGen.
  • Therefore (i < num) can produce different results depending on the value of the argument even in a SynthDef.
  • This process is not different from what we have done before with various operations on UGens.

I overlooked the identity of argument of UgenGraphFunc. It was not just a SimpleNumber.

Thank you very much!