Restart Envelopes without creating new SynthDef instance

What’s the best way to restart the envelopes of a existing SynthDef instance in response to a MIDI note-on message, without creating a new instance?

On a related issue, does anyone know of any examples of simple SuperCollider monosynths, with mono legato/triggered portamento?

EnvGen’s gate parameter:

  • If you want to open the envelope on noteOn and close it on noteOff, use a sustained envelope (an Env with a releaseNode, like Env.asr) and set gate:1 on noteOn and gate:0 on noteOff. Example:
SynthDef(\asrSin){|out=0,freq=440, amp=0.1, gate=0|
    var sin = SinOsc.ar(freq) * amp;
    var env = EnvGen.kr(Env.asr, gate);
    Out.ar(out, sin * env);
}.add;

x = Synth(\asrSin);

// run this to trigger sound as many times as you want
x.set(\gate,1, \freq, rrand(40,100).midicps);
// run this to stop it
x.set(\gate,0);

// midi mappings
MIDIdef.noteOn(\testTrig,{|vel,note| 
	x.set(\gate,1,
		\freq,note.midicps, 
		\amp,vel.linexp(0,127,0.001,1)
	)
});
MIDIdef.noteOff(\testTrigOff,{ x.set(\gate,0) })

// midi test
MIDIIn.doNoteOnAction(1, 1, 70, 127); // spoof a note on
MIDIIn.doNoteOffAction(1, 1, 120); // spoof a note off, any note number will do
  • If you just want to read an envelope from start to end, releasing it automatically like in a percussive instrument, use a non-sustained Env like Env.perc. Synths can have “trigger rate” arguments, so that you just have to set them to 1 and they will be set to 0 automatically right after. Example:
SynthDef(\percSin){|out=0,freq=440, amp=0.1, dur=1|
    var sin = SinOsc.ar(freq) * amp;
    // \symbol.tr() is the easiest way to create a "trigger rate" control
    var env = EnvGen.kr(Env.perc(0.01,dur), \trigger.tr(0));
    Out.ar(out, sin * env);
}.add;

x = Synth(\percSin);
// run this to trigger sound as many times as you want
x.set(\trigger,1);

MIDIdef.noteOn(\testTrig,{|vel,note| 
	x.set(\trigger,1,
		\freq,note.midicps, 
		\amp,vel.linexp(0,127,0.001,1)
	)
});

// test your midi function
MIDIIn.doNoteOnAction(1, 1, 120, 127); // spoof a note on
3 Likes

Thanks very much, @elgiano!

Working on a percussion synthesiser, so will go with option 2 :slight_smile:

I’ve tweaked your release envelope code to make multiple envelopes.

Does this look like thee best way of reusing the same envelope code?

s.boot;

(

SynthDef(\percSin, {
	arg out = 0,
	freq = 440,
	amp = 0.1,
	dur = 1;
      
	var env = {
		arg decay;
		// \symbol.tr() is the easiest way to create a "trigger rate" control
		EnvGen.kr(Env.perc(0.01,decay), \trigger.tr(0));
	};
	
	// Amp envelope
	var envAmp = env.value(dur);
	
	// Pitch envelope
	var envPtch = linlin(env.value(0.25), 0.0, 1.0, 1, 2);
	
	// Sine wave
	var sin = SinOsc.ar(freq * envPtch) * amp;
	
	// Output
	Out.ar(out, sin * envAmp);
}).add;

x = Synth(\percSin);
// Run this to trigger sound as many times as you want
//x.set(\trigger,1);

// Connect all available MIDI input devices
MIDIIn.connectAll;

// Trigger note
MIDIdef.noteOn(\testTrig, {
	arg vel,
	note; 
	x.set(\trigger,4,
		\freq,note.midicps, 
		\amp,vel.linlin(0,127,0.001,1)
	)
});

)

At the moment they trigger at the same time, which is cool, but in some circumstances, I might want them to trigger at different times.

How could I have separate control of the two envelopes.

Try something like:

allGate = \gate.tr(0);
pitchEnv = Env.perc(0.01, 0.25).kr(gate: max(\pitchGate.tr(0), allGate));
ampEnv = Env.perc(0.01, \sustain.kr(1)).kr(gate: max(\ampGate.tr(0), allGate));

You can of course still use a function to wrap the Env creation - in this example case, I think it’s clearer to simply specify each envelope, but obvious it depends on the complexity of the envelope and personal preference. A few details:

  • The max(...) can be useful to allow you to have two triggers - one for individual envelopes and one for ALL envelopes. This may or may not be useful for your setup.
  • If you’re driving your synth via the Pattern/Event system, keep in mind it will be passed a \gate arg that is 1 for as long as the synth is supposed to be running (the \sustain time). You don’t HAVE to trigger things based on this, but if you ignore it completely you may be fighting against the build-in functionality of the event system.
  • When driving things via the Pattern/Event system, \dur represents the time span between two events, NOT the duration of the event itself - the expected duration of the note is \sustain. Even if you’re not using Patterns, it’s good to keep this nomenclature else your synth won’t work properly if you ever use it n a Pbind.
  • Keep in mind also that if you end up heavily modulating a bunch of parameters of your env, it may be better to simply send Env’s themselves as Synth parameters, and construct those Envs on the sclang side. For example:
SynthDef(\hit, {
	var sig, env, retrig
	
	env = \ampEnv.tr(Env.newClear(8));
	retrig = env.sum.abs > 0;
	
	env = Latch.kr(env, retrig);
	env = EnvGen.kr(env, gate:retrig);

	Out.ar(0, LFSaw.ar(100) * env);
}).add;

Pmono(
	\hit,
	\dur, 1/4,
	\ampEnv, Prand([
		[Env.perc(0.4, 0.1)],
		[Env.perc(0.01, 1)],
	], inf)
).play;

retrig and Latch are used to re-start the envelope when you send a new one - there are a few ways to do this, but I find this is the most reliable. And, there’s a gotcha when using Env’s in Pbind where you need to wrap then in either [Env(...)] or a backtick character (see Ref), else the Event system will get confused and not send them correctly.

2 Likes

Thanks very much for the tips, @scztt.

I am actually planning to modulate envelope parameters in realtime via modulation signals from a bus.

If you are not planning to change the envelope’s number of stages, so if you for example only want to modulate parameters of a fixed shape envelope like Env.perc or Env.asr, you can define those as controls for your SynthDef:

SynthDef(\percSine){|out=0,freq=440,atk=0.01,rel=1|
    var sine = SinOsc.ar(freq);
    var env = EnvGen.kr(Env.perc(atk,rel),\trig.tr(0));
    Out.ar(out,sine*env);
}.add;

x = Synth(\percSine,[freq:800, atk:1, rel: 10]);
x.set(\trig,1);
x.set(\atk,0.1,\rel,0.1,\trig,1);
x.set(\trig,1);
x.set(\atk,1,\rel,1,\trig,1);
x.set(\trig,1);
// and you can even map those to busses
x.map(\atk, ~myBus);

Otherwise, passing Envelopes as controls as @scztt suggested is a powerful advice. Just notice makes sure you noticed that \ampEnv.tr(Env.newClear(8)) in the SynthDef, that sets 8 as the maximum number of stages for envs you can pass.
That said, I wouldn’t know how to control (like to change atk or release) an envelope passed as an argument this way, nor how to map some of its parameters to a bus.

I have a morphing envelope setup, going from a conventional AR envelope to a multi-impulse “clap” envelope.

I’ll probably end up using busses for control of all parameters, just passing constants like in and out bus indices etc. as args.

I may need to come up with some way of only sampling the input bus when triggers are received, for parameters that I don’t want to change while the sound is playing.

It might not prove necessary though. I have a feeling envelopes won’t change their values while they’re playing.

You are right, parameters like attack and release are read when the segment is initialized, that is, every time the envelope reaches that segment. This means that you can change release time while the envelope is still attacking, and those changes will be effective as soon as the env reaches its release phase. On the other hand, if you change attack time while the envelope is still attacking, the current attack phase will not show the changes, and you would need to retrigger, or anyway to wait for the next time your env reaches the attack phase again.

x = Synth(\percSine,[atk:100,trig:1])
x.set(\atk,0.1) // doesn't make its attack any faster
x.set(\atk,0.1, \trig, 1) // this does

Interestingly, you can both pass full envelopes as arguments AND modulate internal properties of those envelopes. This is a bit weird, because you need to roughly know the shape of the envelope that’s going to be passed in, but it can be useful in a few cases. I made a method to convert Env’s from array form back to proper Env’s a while back - this helps (https://gist.github.com/scztt/80f72111a8978062ee37360ea03fccc1). With this, my previous example can do:

(
SynthDef(\hit, {
	var sig, env, retrig, release, sharpEnv, a, b;
	
	sharpEnv = Env.adsr(0.001, 0.04, 0.1, 1);
	env = \ampEnv.tr(Env.newClear(8));

	retrig = env.sum.abs > 0;
	env = Latch.kr(env, retrig);
		
	env = Env.fromArray(env).blend(sharpEnv, SinOsc.kr(1/8).range(0, 1));
	
	Out.ar(0, WhiteNoise.ar(1) * env.kr(gate:retrig));
}).add;

Pmono(
	\hit,
	\dur, 1/8,
	\ampEnv, Prand([
		[Env.perc(0.2, 0.001)],
		[Env.perc(0.001, 0.2)]
	], inf)
).play;
)

This is synthesizing a new envelope, blended between the argument Env and sharpEnv and modulated by an LFO.

I’ve had limited success even jamming map arguments as Env values - these will generally make their way over to the Synth, as long as you don’t try to do math on them :slight_smile:

(
SynthDef(\hit, {
	var sig, env, retrig, release, sharpEnv, a, b;
	
	sharpEnv = Env.adsr(0.001, 0.04, 0.1, 1);
	env = \ampEnv.tr(Env.newClear(8));

	retrig = \envTrig.tr(0);
	env = Latch.kr(env, retrig);
			
	Out.ar(0, WhiteNoise.ar(1) * EnvGen.kr(env, gate:retrig));
}).add;

Ndef(\attack, {
	LFDNoise3.kr(4).exprange(0.01, 0.2)
});
Ndef(\curve, {
	LFDNoise3.kr(4).range(-8, 8)
});


Pmono(
	\hit,
	\dur, 1/8,
	\envTrig, 1,
	\ampEnv, Prand([
		[Env.perc(
			Ndef(\attack).asMap, 
			0.001, 
			curve:Ndef(\curve).asMap.asSymbol
		)],
		[Env.perc(
			0.001, 
			0.2, 
			curve:Ndef(\curve).asMap.asSymbol
		)]
	], inf)
).play;
)
3 Likes

That’s very cool!

I don’t think I need to get quite so fancy, for this particular project, but good to know what’s possible.

Hmm… I suspect I’m failing to grasp some basics here…

I have an envelope function within a SynthDef, and I seem to be hitting all kinds of errors, when I attempt to pre-scale the args to the function.

// Re-usable envelope function
   var env = {
   	// Input args, 0 - 1 range
   	arg atk = 0.0, spr = 0.0, dec = 0.75, crv = 0.75;

   	// Scaled env params
   	var atkScaled = LinLin.kr(atk, 0, 1, 0, 1000);
   	var sprScaled = LinLin.kr(spr, 0, 1, 0, 500);
   	var decScaled = LinLin.kr(dec, 0, 1, 0.01, 3000);
   	var crvScaled = LinLin.kr(crv, 0, 1, -5, 5);

   	// Generate envelope
   	EnvGen.kr(
   		Env(
   			levels:[0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0],
   			times: [atkScaled, sprScaled, 0.0, sprScaled, 0, sprScaled, 0, sprScaled, 0, sprScaled, 0, decScaled],
   			curve: [-5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, crvScaled]
   		),
   		gate: \trigger.tr(0)
   	);
   // End envelope
   };

   //////////
   // BODY //
   //////////

   // Body pitch envelope
   var bPDepthScaled = params[\bPDepth] * 2;
   var bPEnv = env.value( \atk, 0, \spr, 0, \dec, params[\bADec], \curve, params[\bPCurve] ) * bPDepthScaled;

I\m sure its something to do with the way I’m using LinLin, but I’m not sure what the problem is.

Your envelope generation function looks fine, but this isn’t calling that function the way you probably intend. You need either:
env.value(atk:0, spr:0, dec:params[\bADec], curve:params[\bPCurve])
or simply:
env.value(0, 0, params[\bADec], params[\bPCurve])

Also, as a matter of convenience, you can use atk.linlin(0, 1, 0, 1000) instead of using the LinLin ugen explicitly.

Ah, thank you!++++++