Supercollider tips, Q/A

Bummer. I wonder if it would be possible to write my own envelope UGen that behaves like I want…

You can modulate the parameters of an envelope while it is running. Here is the basic set up.

(
SynthDef(\a, {
	var trig = \trig.tr;
	var freq = \freq.kr(220);
	var sig = SinOsc.ar(freq);
	var aeg = Env.perc(attackTime:\atk.kr(0.01).poll).kr(gate:trig);
	sig = sig * aeg * \amp.kr(0.1);
	sig = Splay.ar(sig);
	Out.ar(\out.kr(0), sig);
}).add;
)

x = Synth(\a);
(
Pdef(\p, Pbind(
	\type, \set, 
	\id, x.nodeID, 
	\args, #[\freq, \trig, \atk], 
	\degree, Pwhite(0, 4), 
	\trig, 1,
	\atk, Pwhite(0.01, 0.5)
)).play;
)

Pdef(\p).stop;
x.free;
2 Likes

I can see that the envelope parameters in this example are changing while the envelope is running, but the envelope doesn’t seem to take the new parameters into account until it is re-triggered.

For example:

x = Synth(\a, [\atk, 10]);
x.set(\trig, 1);
x.set(\atk, 0.01);

If I execute these sequentially in the REPL I get a note with 10 seconds of attack. The envelope doesn’t change when I execute x.set(\atk, 0.01).

I was looking at some of the other envelope UGens and Decay and Decay2 seem to sort of give me what I’m looking for.


(
SynthDef(\sine, {
	arg dec=0.5, freq=440, amp=0.30;
	var sig, env;
	sig = SinOsc.ar({freq}!2);
	env = Decay2.kr(Impulse.kr(0), 0, dec);
	sig = sig * env * amp;
	Out.ar(0,sig);
}).add;
)

s = Synth.new(\sine, [\dec, 10]);
s.set(\dec, 1);

A word of warning with using Decay like this (it bites me EVERY time): decay will produce very large values if you send it triggers that are longer than 1 sample (or 1 control period for kr signals). Even triggering it before it fully decays can produce values >1. This can occasionally be valuable, but it’s easy to get in trouble if you decide later “oh, I’ll trigger this really fast, that’ll be cool” and it blows up.

I believe you’re only having a problem because you’re not sending your \attack and \trig at the same time / in the same message - the attack will only take effect when the envelope is triggered, if it’s received just after a trigger, you’ll be running through the envelope with the old value. This should work:

x = Synth(\a, [\atk, 10]);
x.set(\trig, 1, \atk, 0.01);

You can also modify the control a little by making e.g. your \atk parameter a trigger parameter, e.g. \atk.tr instead of atk.kr - this means when you send it a new value, it outputs that value for 1 sample (a trigger), and then goes back to zero. Then, you could do:

attack = \atk.tr;
Env.perc(Latch.kr(attack, attack), 1).kr(gate:attack); // latch will hold the value when triggered

and simply trigger with x.set(\atk, 1). Though to be totally honest, I would avoid this unless you’re building a really complex synth, because it’s slightly less clear at a glance what’s going on :slight_smile:

Also, there’s a pattern object that encapsulates the “run a synth once, and then send changes to it” - take a look at Pmono. Here’s an example of what that looks like, with a re-triggering envelope and different note values:

(
SynthDef(\envel, {
	var sig, env, freq;
	
	// Perc envelope, with retriggering.
	env = Env.perc(\attack.kr(0.01), \release.kr(4), curve:[-4, -20]).kr(gate:\trig.tr);
	
	// Two freqs, slightly detuned, with a lag to smooth changes
	freq = \freq.kr.lag(0.03);
	freq = [
		freq,
		freq * env.linlin(0, 1, 1.005, 1.02)
	];
	
	// Two oscillators, with a fade parameter
	sig = XFade2.ar(
		LFCub.ar(freq),
		LFSaw.ar(freq) * 0.2,
		\osc.kr(-1).lag(0.5).poll
	);
		
	// Apply envelope and a low-pass
	sig = env * sig;
	sig = LPF.ar(sig, env.linexp(0, 1, 200, 9000));
	
	// ... and some delay, with a param for delay time
	//     (and nice clicksssss)
	sig = sig + CombC.ar(sig, 4, \delay.kr(2)/[4, 3], 4);
	
	Out.ar(\out.kr(0), sig * [1, 1]);
}).add;

Pdef(\e1, Pmono(
	\envel,
	\trig, 		1,
	\delay,     	Prand([2, 4], inf),
	\dur, 		Prand([1, Pseq([1/3], 4)], inf),
	\attack, 	Prand([0.01, 0.1, 0.5], inf),
	\osc, 		Prand([-1, 0, 1], inf),
	\degree, Pseq([
		Pseq([0, 1, 3, -2], 4),
		Pseq([0, 2, 5, -1], 4)
	], inf)
)).play;
)

And finally, if you’re getting in to heavy envelope modulation (beyond just one or two parameters), you can also just send whole envelopes to the synth as a parameter. Here’s something I wrote up with a few tricks for doing that: https://scsynth.org/t/envelope-passing-for-synths-and-patterns/2048/6

2 Likes

Oh, I remember now that you can get closer to this behavior by using IEnvGen, which is the same as EnvGen except you can look up the envelope position yourself - but the extra bit is that when you change Env parameters, they take effect immediately:

	envPosition = Sweep.kr(\trig.tr, 1); // ramp up from 0 every time a trigger happen - this moves through the envelope
	env = IEnvGen.kr(
		Env.perc(\attack.kr(0.01).lag(0.5), \release.kr(4), curve:[-4, -20]),
		envPosition
	);

I THINK you’ll have to put a lag on parameters you’re going to modulate, otherwise you’ll get clicks when you change. I don’t recall exactly how this works, but it’s worth experimenting to see if you can get closer to what you want.

3 Likes

Thanks for the help everyone! I was finally able to get it working using IEnvGen. I used Decay to generate an end-of-rise signal which then Selects between attack and release rates. The rate for the current phase of the envelope gets sent to Sweep which controls the position of an exponential envelope with 1s rise and fall.

(
SynthDef.new(\sine, {|hz=440,amp=0.3,atk=0.01,rel=1|
	var osc = SinOsc.ar(hz);
	
	var rise = Decay.ar(Impulse.ar(0), atk);
	var env_phase = rise <= 0.001;

	var atk_rate = 1 / atk;
	var rel_rate = 1 / rel;
	var env_rate = Select.kr(env_phase, [atk_rate, rel_rate]);
	var env_pos = Sweep.ar(Impulse.ar(0), env_rate);
	
	var amp_env = Env([0.01, 1, 0.001], [1, 1], 'exp');
	var env_gen = IEnvGen.ar(amp_env, env_pos);
	var sig = osc * env_gen * amp;

	var done = env_gen <= 0.001;
	FreeSelf.kr(done);

	Out.ar(0, sig.dup);
}).add;
)

x = Synth.new(\sine, [\atk, 10, \rel, 10]);
x.set(\atk, 0.01);
x.set(\rel, 1);
5 Likes

Trying to do some additive synthesis in SC. I ran into the issue of not being able to fill an array based on an argument. I’m curious why this is and I wasn’t able to find an answer after a half an hour of googling. The REPL says it’s because the index isn’t an integer, even if it’s set as one.

If this approach is impossible, what’s another way to define an additive SynthDef?

(
SynthDef(\additive, {
	arg out = 0, 
		partials = 2,
		fundamental = 20,
		decay = 0.5;
	
	var freqs = Array.fill(partials, {
		arg i;
		fundamental * i
	});
	
	var sins = freqs.collect({
		arg freq, i;
		var mult = decay / (i+1);
		SinOsc.ar(freq)*mult
	});
	
	var mix = Mix.new(sins);

	Out.ar(out, mix)
}).add;
)

Arguments to a synth aren’t integers, they are streams of numbers that can change over time (Controls, although if you partials.postln you’ll see an OutputProxy for… reasons). Anyway, Array.fill expects an actual fixed integer that will never change.

The standard way to approach what you’re trying to do is compile multiple different synths, one per different argument that could happen (there are a few tutorials about this on the web, e.g. this official one from SuperCollider: https://supercollider.github.io/tutorials/error-primitive-basicnew-failed)

Here’s your code, adapted to that basic approach:

(
(1..8).do({|partials|
	SynthDef(\additive ++ partials, {
		arg out = 0, 
		fundamental = 20,
		decay = 0.5;
		
		var freqs = Array.fill(partials, {
			arg i;
			fundamental * (i+1);
		});
		
		var sins = freqs.collect({
			arg freq, i;
			var mult = decay / (i+1);
			SinOsc.ar(freq)*mult;
		});
		
		var mix = Mix.new(sins);
		
		Out.ar(out, mix)
	}).add;
});
)

Synth.new(\additive8, [\fundamental, 261.6]);

(I also fixed what I assume was a bug where you multiplied fundamental * i instead of fundamental * (i + 1), assuming you didn’t want to zero out the actual fundamental because i starts at 0)

If pre-defining the range of all possible number of partials is limiting for some reason, there’s slightly trickier approaches where you can dynamically make new synthdefs using SynthDef.wrap, but that probably isn’t needed in your case so I’ll leave it out for now :+1:

2 Likes

you can use a Ref (reference) to a (fixed-size) array [^] as a synthdef argument. see the DynKlank helpfile for a syntax example. the argument ends up being “multichannel-expanded” just as if you specified an array as a UGen argument within the SynthDef.

(or you can use the approach outlined above to dynamically create the synthdef.)


[^] so the initial argument is an array literal. i vaguely remember there are some gotchas about updating these kinds of synthdef arguments dynamically. will try and dig up a working example…

2 Likes

Thank you @21echoes, @zebra!

For a little context on my end goal, I’m trying to make an additive synthesis engine for norns that can take a bunch of different series with different growth rates, and shift the pitch and decay (and some other things maybe) dynamically.

I’m just experimenting in SC right now, examples are definitely helpful.

for doing super arbitrary/dynamic stuff, i wouldn’t disregard the option of just making a boatload of individual basic FSinOsc synths. you can easily instantiate a couple thousand on any contemporary computer.

6 Likes

I want to use the array class in SC to come up with crazy step values for my Roland MC-4.

So. like timebase 48 16 steps is 12 clocks. 12 times 16 ,192 etc.

Id like to make arrays of any length and style but have everything divided down from a value like 192.

Actually I should say create. Not rotate or scramble

More like stutter, Fibonacci sequence within the scope of 192, or 230. Whatever number, etc.

Is there a message I can use? I uninstalled SC a while ago, but I thought it would be good for this kind of thing. Thanks!

1 Like

hey y’all – just started digging into SC (really bonkers late) and ran through a buncha posts here and at the sc forums, but still stuck on what seems like it’d be a typical new user question. i bet it’ll become clear with more study, but maybe i’m also not searching the right keywords.

in this example, i’m looking for a way to interrupt each new execution of \ping, so that if a previous envelope is still running, then its level is brought back to 0 as a new one voices:

(
SynthDef.new(\ping, {
	arg atk = 0.6, rel = 2, pan = 0, hz = 300;
	var k, amp = 1;
	k = SinOsc.ar(hz) * EnvGen.kr(Env.perc(atk, rel), doneAction:2) * amp;
	k = Pan2.ar(k,pan);
	Out.ar(0,k);
}).add;
)

Synth(\ping, [\hz,rrand(30,3000)]);

any pointers to docs i missed or “interrupt a running envelope by starting a new envelope on the same voice (right??)” techniques would be :sparkles:. i tried applications of both gate and t_ and i think i’m just missing something obvious :sweat_smile:

I know less than you, but I think I actually might’ve solved this problem (which i couldn’t find documented anywhere)

var gate = (sig_Gate > 0) + ((-1.01 - (sig_Gate > 0)) * Trig1.ar((sig_Retrig <= 0), 0.011)) + (sig_Retrig > 0);

Out.ar(
	out_Out,
	EnvGen.ar(
		Env.adsr(param_Attack/1000, param_Decay/1000, param_Sustain, param_Release/1000, curve: curve),
					
		gate,
	        levelScale: 0.8
	)
);

this was earlier this year, but I think it was something about sending a single sample negative trigger right before the gate (in this context I think sig_Retrig was a copy of the gate)


from the docs:

If the gate of an EnvGen is set to -1 or below, then the envelope will cutoff immediately. The time for it to cutoff is the amount less than -1, with -1 being as fast as possible, -1.5 being a cutoff in 0.5 seconds, etc. The cutoff shape is linear.

so, yea basically, send -1 really quick at the start of a new gate. i think the code above does this, somehow,

i would not use signals for this. (the idea of “sending xyz real quick” as a signal, doesn’t hold up.)

InTrig is useful for producing triggers from sclang in a deterministic way.

(
SynthDef.new(\ping, {
	arg atk = 0.6, rel = 2, pan = 0, hz = 300,
	trig_in;
	var k, amp = 1;
	k = SinOsc.ar(hz) * EnvGen.kr(Env.perc(atk, rel), gate: InTrig.kr(trig_in), doneAction:2) * amp;
	k = Pan2.ar(k,pan);
	Out.ar(0,k);
}).add;
)

~trig_b  = Bus.control(s, 1);

x = Synth(\ping, [\trig_in, ~trig_b]);

(
~bang = {
	x.set(\hz, rrand(30, 3000));
	~trig_b.set(1);
};
)

~bang.value;

though, i might be misunderstanding the intent. code above shows how to retrigger the envelope in an arbitrary instance of a ping synth (named x).

if the intent is to immediately release the envelope on an arbitrary trigger, then the special “negative gate” would be usable, but it might be clearer to just have an additional multiplier/envelope/line with an additional DoneAction.

there is an additional factor. because you have doneAction:2, the synth will self-free when the envelope finishes. so to avoid runtime errors you should register created synths with Nodewatcher and check for .isRunning.


here’s a more complete example, assuming i understand that correctly now.

(
SynthDef.new(\ping, {
	arg atk = 0.6, amp=1, rel = 2, pan = 0, hz = 300,
	trig_in, defeat=0;
	var k, trig;

	trig = InTrig.kr(trig_in) - (defeat*1.1);
	k = SinOsc.ar(hz) * EnvGen.kr(Env.perc(atk, rel), gate: trig, doneAction:2) * amp;
	k = Pan2.ar(k,pan);
	Out.ar(0,k);
}).add;


~create = {
	var bus, synth;
	bus = Bus.control(s, 1);
	synth = Synth(\ping, [\trig_in, bus, \atk, 4.0]);
	NodeWatcher.register(synth, true);
	// returns an object prototype.
	// this could be made slicker,
	// by adding the ~reset and ~release functions below as pseudo-methods.
	(synth:synth, bus:bus)
};

~reset = {
	arg x;
	if (x[\synth].isRunning, {
		x[\synth].set(\hz, rrand(40, 60).midicps);
		x[\bus].set(1);
		true
	}, {
		false
	});
};

~release = { arg x;
	if (x[\synth].isRunning, {
		postln("releasing node: " ++ x[\synth]);
		x[\synth].set(\defeat, 1);
		true
	}, {
		postln("synth does not appear to be running.");
		false
	});
};

)

x = ~create.value;

~reset.value(x);
~release.value(x);

and here is an alternative version of the SynthDef that uses an additional envelope, instead of relying on weird magic numbers.

SynthDef.new(\ping, {
	arg atk = 0.6, amp=1, rel = 2, pan = 0, hz = 300,
	trig_in, defeat=0;
	var k, trig, defeater;

	trig = InTrig.kr(trig_in);
	defeater = 1 - EnvGen.ar(Env.new([0, 1], [0.01]), gate:defeat, doneAction:2);
	k = SinOsc.ar(hz) * EnvGen.kr(Env.perc(atk, rel), gate: trig, doneAction:2) * amp * defeater;
	k = Pan2.ar(k,pan);
	Out.ar(0,k);
}).add;
6 Likes

Array.fib(n)

.stutter(n) - repeat each element N times
.sputter(p, n) - randomly repeat elements with probability P, up to N times

3 Likes

Some good answers to this already, I think zebra covered some of the finer points… But there IS actually an existing class to cover this kind of multi-voicing: PmonoArtic. This is a relative of Pbind (which plays a sequence of Synth events), with the specific behavior that it will re-use an existing Synth and send parameters to it if one is already playing - else it will spawn a new one.

Here’s an example, with comments:

SynthDef.new(\ping, {
	arg atk = 0.01, rel = 2, pan = 0, freq = 300;
	var k, amp, env;
	
	// Just to prove the re-trig is working, lets make the \freq
	// changes smooth....
	freq = freq.lag(0.08);
	
	// A trig parameter to re-trigger your envelope.
	env = Env.perc(atk, rel).kr(
		gate: \trig.tr(1)
	);
	
	// An envelope for the *overall voice*: the doneAction here frees when gate -> 0,
	// and gives you a chance to fade the voice out smoothly (the [1]). \gate is send 
	// automatically by the pattern, but if you're spawning the synth yourself you'll 
	// need to send \gate, 1 and \gate, 0 to control the voice lifetime.
	amp = 1;
	amp = amp * Env([1, 0], [1], releaseNode:0).kr(
		gate:		\gate.kr(1), 
		doneAction:	Done.freeSelf
	);
	
	k = env * amp * SinOsc.ar(freq);
	k = Pan2.ar(k, pan);
		
	Out.ar(0,k);
}).add;
)

(
Pdef(\ping, PmonoArtic(
	\ping,
	
	\trig, 1, // re-trigger my perc envelope

	// A basic note pattern
	\dur, 1/6,
	\octave, 4,
	\degree, Pseq([0, -2], inf).stutter(12) + Pseq([0, 2, 4, 8, 6, 9], inf),
	
	// The time between events is ~dur. The total duration of an event is ~dur * ~legato, so 
	// when ~legato > 1.0, events will overlap. The same voice will be re-used if ~legato > 1.0 
	// (e.g. the voice is not finished playing). When ~legato < 1.0, the voice will end BEFORE your 
	// next event occurs, so a new voice will be created.
	\legato, Pseq([1.5, 1.5, 1.5, 0.2], inf),
)).play;
)

A few notes:

  • \trig.tr is roughly equivalent to an argument named t_trig, or @zebra’s InTrig.kr(trig). See this post.
  • \freq is the standard parameter name for the frequency of a synth. Likewise, \gate is assumed to be the parameter that controls the overall note lifetime. If you don’t use these names it’s okay, but you may lose some subtle nice-to-have functionality, and end up needing to re-create it yourself.
  • Also a convention: it’s assumed in many places that a \gate parameter is connected to something with doneAction: Done.freeSelf - meaning synth.set(\gate, 0) should end the synth (eventually). You don’t have to do this, but a few things may work unexpectedly if you don’t. For example, if \gate follows these rules, you don’t need NodeWatcher: you know the note will be alive as long as \gate, 1, and will end (eventually) when \gate, 0.
6 Likes

thanks so much @andrew, @zebra + @scztt! super helpful directions and really useful insights. and holy hell, scott, that example is really gorgeous!!

it’s clear that there’s a bit of homework ahead of me :sweat_smile:. i’m structuring my learning by building a drum synth engine, where a trig will just kill any previous envelope on a specified voice and start a new one from scratch, eg.:

image

@scztt, your example makes a lot of sense – from a place of ignorance, i was looking for a command that would force a Done Action on any synth with the same name (eg. \kick or \snare) that’s still running. or like, a “monophonic mode” that checks to see if a \kick is active and frees the old one before making a new one. it seems the \gate 0 can help achieve this (in a localized way). thank you!

i’m wondering / getting ahead of myself: as i build this with an eye toward bringing it into norns, is the \gate approach enough to avoid complications + overlap between steps? or would busses be best way to handle this? (busses seem to be what’s outlined in drumf)

this version most directly aligns with my current depth of understanding SC but i’m not grokking how to actually spawn it. x = Synth(\ping, [\trig_in, 1]); doesn’t seem to do it :confused:

Read the Env docs carefully. There is a major gotcha with envelopes, and it bit me recently. That is, the first node in the envelope is played exactly once in the lifetime of the Env UGen.

So if you want the retrigger behavior illustrated in your post, and you want a monophonic Synth, you will need to start your envelope with 2 nodes with the value 0 and a duration of 0 between them. I don’t believe Env.perc provides this extra node, so you may have to roll your own envelope path by declaring the level and duration lists.

2 Likes

You can create a group for each synth (\kick, \snare, etc) and choke all voices with e.g. ~group.set(\gate, 0).

You might consider avoiding some of the complexity here by simply running a fixed number of voices for each Synth at all times - run them all at once, and just trigger them. This is generally how software instruments (non-SuperCollider) handle voices. Then switching to a new voice (vs. retriggering) would involve sending e.g. a [\gate, -1] to the current voice, incrementing your voice by 1 % numVoices, and sending a new [\trig, 1, \gate, 1] to the next one.

1 Like