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

I started working on a drum synth voice, too, a while back.

I found the best way was to simply only have the one synth per voice, and retrigger its envelopes, rather than creating multiple voices.

This approach obviates the need to check for existing voices, and potentially makes it easier to restart the envelope in different ways.

I have’t read the whole thread, so apologies if this has been mentioned already.

1 Like

IIRC this is only for envelopes that can sustain - that is, envelopes with a releaseNode defined. Env.perc works fine because it doesn’t have a releaseNode. Env.adsr does not tend to work well for re-triggering, though it’s easy enough to just build an envelope that wil - the “extra initial node” trick is good one.

1 Like

I agree. The complexity involved in forcing monophonic behavior out of polyphonic synths is a bit much for my lazy tastes.

Oh, you may be right. I remember trying to solve almost the same problem and my envelope was utterly busted until I added the “dummy” node. I’ll go check my synth code to confirm. It’s foolish of me to type SC advice on my phone :slight_smile:

Careful! This does not work (or, works very very badly…) for kicks. Kicks are usually implemented with, at least in part, a low frequency SinOsc or similar. The phase of the oscillator is not reset if you use a continuously running voice, so triggering it will start at a random place in the oscillators cycle, which can result in very different attacks. Sadly, there are not so many osc’s in SC whose phase can be reset, which makes this use case sorta difficult :frowning:

It’s also a good approach because the number of voices can quickly become out of control with fast triggers and longer release/decay times.

And, it’s pretty rare (outside of prog rock :slight_smile: ) that a drummer has 16 kicks that they play in perfect round-robin style to preserve the separate decay of each one… Monophonic percussion is what our ears are used to, in general.

2 Likes

Ah, I wasn’t aware of that gotcha. I wonder if a set of phase-resettable basic waveform custom UGens would be useful.

2 Likes