Newbie cannot get new code to compile

I am retrying to rewrite the following working code.

s.options.numWireBufs = (8192*12000);
s.boot;

(
SynthDef(\sap, {|freq=440, gate=1, out=0, pan=0, amp=1, ampslope=1.0|
	var sound, start, step, end;

	start = 1;
	step  = 4;
	end   = 1000;

	forBy(start, end, step) {|i|
		var phase = 0;
		i.postln;
		if(((i % 2) == 0),
			{phase = 0},
			{phase = pi});
		sound = SinOsc.ar(i * freq, phase , amp / (i**ampslope), 0.0);
		sound = Pan2.ar(sound, 0, 1);
		Out.ar(out, sound);
	};
}).add;
)

This is how I tried to rewrite it.
I tried to put the variables start, step, and end as arguments.
However, the compiler complains about the forBy statement:

ERROR: Message ‘forBy’ not understood.

s.options.numWireBufs = (8192*12000);
s.boot;

(
SynthDef(\sap, {|freq=440, start=1, step=4, end=1000, gate=1, out=0, pan=0, amp=1, ampslope=1.0|
	var sound;
	
	forBy(start, end, step) {|i|
		var phase = 0;
		i.postln;
		if(((i % 2) == 0),
			{phase = 0},
			{phase = pi});
		sound = SinOsc.ar(i * freq, phase , amp / (i**ampslope), 0.0);
		sound = Pan2.ar(sound, 0, 1);
		Out.ar(out, sound);
	};
}).add;

)

If anyone can help me with this, I would appreciate it greatly!

Here first quoting an answer in this thread

Within one SynthDef, you cannot add or destroy voices dynamically. This has never been supported in SC3, and never will be. This is what “fixed graph” means – once you build a SynthDef, its structure is set in stone and you can’t make it bigger or smaller.

You can build a new def, and replace the Synth node (but there may be some discontinuity in the output).

You can make a SynthDef for one voice, and dynamically create and destroy Synth nodes, one for each voice (but phase correlation is not guaranteed).

dietcv’s “lpf” solution keeps the same number of voices continuously, but silences some of them. All the voices exist (nothing is created or destroyed on the fly), but you can dynamically mute them.

hjh

So in other words, you cannot set the start, end and step of forBy dynamically, these values have to be fixed when compiling the SynthDef. Below I refactored you code a bit and hardwired the values.

(
SynthDef(\sap, {|freq=440, gate=1, out=0, pan=0, amp=1, ampslope=1.0, atk = 0.3, rel = 1|
	var array = (1, 5..1000); 	// these are the same values as in your forBy
	var env = Env.asr(atk, amp, rel).kr(2, gate);  // you had a gate argument so assume you want a sustaning envelope
	var sound = array.collect{|i|
		var phase = pi * (i%2);
		var sig = Pan2.ar(SinOsc.ar(i * freq, phase , amp / (i**ampslope), 0.0), 0, 1);
		sig * 0.1 // I scaled the signal down
	}.sum; // you need to sum the signal to play the signal on a stereo bus.
	Out.ar(out, sound * env);
}).add;
)

x = Synth(\sap, [freq: 220])
x.set(\gate, 0)

The SynthDef part in the following code has a similar feature of your intention:
https://sccode.org/1-5hC

(
s.options.numWireBufs = 128;
s.reboot
)

(
~numSliders = 64;
~mul = 0.1;
~freq = 220;

SynthDef(\overtone_additive, { |overtoneFirst = 1, overtoneLast = 8|
	var freq, mul, numOvertones, maxLayers, amplitudes, temp, signalArray, sum;
	mul = \mul.kr(~mul);
	freq = \freq.kr(~freq);
	maxLayers = ~numSliders;
	amplitudes = \amplitudes.kr(1 ! maxLayers);
	numOvertones = overtoneLast - overtoneFirst + 1;
	signalArray = maxLayers.collect { |i|
		var nthOvertone, minDetect, maxDetect, aliasingDetect, switch;
		nthOvertone = i + 1 * freq;
		minDetect = overtoneFirst <= (i + 1);
		maxDetect = overtoneLast >= (i + 1);
		aliasingDetect = nthOvertone < (SampleRate.ir / 2);
		switch = (minDetect * maxDetect * aliasingDetect).lag(0.1);
		SinOsc.ar(nthOvertone) * switch * numOvertones.lag(0.1).reciprocal.sqrt
	};
	signalArray = signalArray * amplitudes;
	sum = signalArray.sum * mul.lag(0.1);
	Out.ar(0, sum * [1, -1]);
}).add
)

x = Synth(\overtone_additive);
x.set(\overtoneFirst, 1, \overtoneLast, 1);
x.set(\overtoneFirst, 2, \overtoneLast, 2);
x.set(\overtoneFirst, 1, \overtoneLast, 2);
x.set(\overtoneFirst, 1, \overtoneLast, 3);
x.set(\overtoneFirst, 4, \overtoneLast, 7);
x.free

The following code is the revision to add “steps” to your code, but I could not configure it in SynthDef, so I used it in x.set with array for the argument \amplitudes. You will find a UGen scope with 10 channels when you evaluate the SynthDef code without modifying it, and you will see that it works as you want. After confirming its functionality, you could increase the ~maxLayers environment variable. I think 1000 is too high: More than 10 makes it hard to see the signal output via UGen Scope.

(
~maxLayers = 10;
SynthDef(\aSynth, { |freq = 440, atk = 0.3, rel = 1, amp = 0.1, pan = 0, ampSlope = 1, gate = 1|
	var maxLayers, amplitudes, numOvertones, temp, signalArray, signal;
	maxLayers = ~maxLayers;
	amplitudes = \amplitudes.kr(1 ! maxLayers);
	signalArray = maxLayers.collect { |i|
		var nthOvertone, minDetect, maxDetect, aliasingDetect, switch, phase, aSignal;
		nthOvertone = i + 1 * freq;
		aliasingDetect = nthOvertone < (SampleRate.ir / 2);
		switch = aliasingDetect.lag(0.1);
		phase = pi * (i + 1 % 2);
		aSignal = SinOsc.ar(nthOvertone, phase) / (i + 1 ** ampSlope) * switch;
	};
	signalArray = signalArray * amplitudes;
	signal = signalArray.scope.sum * amp;
	signal = signal * Env.asr(atk, 1, rel).kr(Done.freeSelf, gate);
	signal = Pan2.ar(signal, pan);
	OffsetOut.ar(0, signal);
}).add
)

x = Synth(\aSynth);
x.set(\ampSlope, 0.4)
x.set(\amplitudes, a = 10; (1..a).collect{ |item| if ((1, 4..a).includes(item)) {1} {0} })
x.set(\amplitudes, a = 10; (1..a).collect{ |item| if ((1, 3..a).includes(item)) {1} {0} })
x.release

test video

The code above and the code in the video is slightly different.

Thanks Thor_Madsen and prko for your great responses!

prko, I have some very basic language questions in your code.

I don’t know how the operators , ~, and ! are working and I am having trouble finding
their documentation. For example, in

mul = \mul.kr(~mul);

What does \ mean and what does ~ mean?

Also, in

amplitudes = \amplitudes.kr(1 ! maxLayers);

What does the ! operator doing?

Thanks!

  1. ~ in ~mul
    The tilde symbol, ~, followed by a word beginning with a lower character, is used to indicate environment variables. Environment variables act like global variables in the environment in which they are defined:
    Environment | SuperCollider 3.12.2 Help
    Except for large or specific projects, I don’t think there is usually a need to define and switch between multiple environments.

    There are three types of variables in sclang:

    • Interpreter variables
      • Global scope variables, which can be used by assigning a value to the variable:
      • A single lowercase letter from a to z.
      • s is assigned Server.local by default, and users should not reassign it with a different value, as s with the default value is used in the help documents and by many users.
      (
      a = 1;
      b = 2;
      a + b
      )
      
    • Local variables
      • Local scope variables, which should be defined with the keyword var
      • A word starting with a lowercase character.
      (
      a = 0;
      {
          var a, b;
          a = 3;
          b = 5;
          a + b
      }.().postln;
      a // This a is outside the code block above, so its value is not 3, but 0.
      )
      
    • Environmental variables:
      • can be used by assigning a value
      • See above for details.
      (
      ~freq1 = 440;
      ~freq2 = 441;
      ~freq2 - ~freq1
      )
      
  2. ! is a ‘syntactic sugar’ (a syntactic shortcut) of dup.
    The following examples are all the same:

    [1, 1, 1]
    1!3
    1.dup(3)
    
  3. \ in \amplitudes.kr(...) and \mul.kr(...)
    Please read the thread below:
    NamedControl: a better way to write SynthDef arguments

The following PDF would be very helpful for you (it is really gentle):