Envelope passing for Synths and Patterns

I work pretty heavily with Env’s passed as synth arguments - often I’ll just stick an envelope on every parameter I’d ever want to modulate, and then build a Pattern infrastructure to actually send the envelopes I want.

Because it’s come up recently on the forum, and because I think it’s super powerful - I spent an hour and came up with a pretty basic but complete distillation of how to use Env’s in a Pattern context, dynamically building them from simpler parameters to enable all kinds of nice warp-y behavior.

I think basically scrubbed any weird external dependencies and personal classes from this, so it should work out of the box. Though I’m not using it extensively here, the Env fixup step in \finish will convert scalar arguments to Envs, and can handle chords as well. I won’t go through it in depth, but please ask questions if you’re wondering about pieces of it - I hope it’s useful to learn, or at least mess around with for a bit!

(
SynthDef(\wave, {
	var sig, freq, lpf, amp, dist, gate, baseEnv, sustain, feed;
	
	baseEnv = { |default| Env(default ! 6, [1, 0, 0, 0, 0]).asArray };
	
	sustain = \sustain.kr(1);
	gate = \gate.kr(1);
	dist = \dist.kr(0.5);
	
	freq = \freq.kr(100);
	freq = freq + EnvGen.kr(\freqMod.kr(baseEnv.(0)), gate:gate);
	freq = freq + SinOsc.ar(freq*4).range(-25, 25);
	
	lpf = \lpf.kr(baseEnv.(600));
	lpf = EnvGen.kr(lpf, timeScale:sustain, gate:gate).min(20000);
	
	amp = \ampEnv.kr(baseEnv.(1));
	amp = \amp.kr(1) * EnvGen.kr(amp, timeScale: sustain, doneAction:2, gate:gate);
	
	feed = LocalIn.ar(2);
	feed = Rotate2.ar(feed[0], feed[1], SinOsc.kr(1/10).range(-1, 1), Rand(0, 2));
	feed = FreqShift.ar(feed, [0.1, -0.1]) * 0.2;
	feed = LeakDC.ar(feed).tanh;
	
	sig = LFPulse.ar(
		freq * [1, 1.01, 1.002, 2, 4],
		{Rand(0, 0.5)} ! 3,
		amp.linlin(0, 1, 0.9, 0.2)
	);
	sig = Splay.ar(sig.scramble);
	sig = LeakDC.ar(sig);
	sig = sig + feed;
	sig = LPF.ar(sig, lpf);
	sig = CombC.ar(
		sig, 
		1, 
		{ SinOsc.ar(1/Rand(20, 30), Rand(0, 0.5)).range(0, 1/100) } ! 4, 
		0.4
	);
	sig = Splay.ar(sig);
	
	sig = sig.blend(
		(sig * dist * amp).tanh,
		dist.clip(0, 1)
	);
	LocalOut.ar(sig);
	
	sig = sig * amp;
	
	Out.ar(\out.kr, sig);
}).add;

Event.addParentType(\wave, (
	instrument: \wave,
	
	freqMod: 0,
	lpfEnv: 200,
	ampEnv: Env.perc,

	// Fix up all of our env arguments when we're about to play our event
	finish: {
		var envParams = [\freqMod, \lpf, \ampEnv];
		envParams.do {
			|param|
			var value = currentEnvironment[param];
			if (value.isArray.not) { value = [value] };
			value = value.collect {
				|v|
				if (v.isRest) {
					v; // pass rests along...
				} {
					if (v.isKindOf(Env).not) {
						v = Env([v, v], [1]) // a continuous, fixed value envelope
					}
				};
				v.duration = 1.0; // fixed duration, we re-scale via timeScale in our synth
				v;
			};
			currentEnvironment[param] = value;
		}
	},


));

Pdef(\wave).clear;
Pdef(\wave, Pbind(
	\type, \wave,
	\dur, 1/4,
	
	\timingOffset, Pfunc({
		if ((thisThread.beats % 0.5) >= 0.25) 
		{
			0.02
		} {
			0
		}
	}),
	\strum, 0.05,

	
	\scale, Scale.chromatic, 
	\octave, Pseq([
		Pseq([3], 32),
		Pseq([3, 5], 32),
		Pseq([3], 32),
		Pseq([3, 6], 16),
		Pseq([3, 4], 16),
	], inf),
	
	\degree, Pseq([
		Prand([0, 2], 16),
		Prand([0, 3, 8], 16),
		Prand([0, 2], 16),
		Prand([-4, 3, 7], 16),
	], inf),
	\degree, (
		Pkey(\degree)
		+ Pif(
			Pfunc({ rrand(0.0, 1.0) < (1/18) }), 
			24, 
			0
		)
	),
	
	\dist, Pwrand([0.2, 0.6, 2, 5], [10, 5, 3, 1].normalizeSum, inf),
	
	\lpfBase, Pseg([2400, 13000, 2400], [32, 32], \exp, inf),
	\lpfSkew, { exprand(0, 5) },
	\lpfAttack, { rrand(0.0, 1.0) },
	\lpf, {
		Env(
			[~lpfBase, ~lpfBase * (1 + ~lpfSkew)],
			[~lpfAttack, 1 - ~lpfAttack] // remember, this gets reset to dur=1 later anyway
		)
	},
	
	\freqModAmt, Pwrand([0, 5, 90], [10, 2, 1].normalizeSum, inf),
	\freqModAmt, Pseg([0, 1, 0], [32, 32], [4, -6]).repeat * Pkey(\freqModAmt),
	\freqMod, Pfunc({
		|e|
		Env([0, [-1, 1].choose * e.freqModAmt], [1], -8)
	}),
	
	\longAttack, Pwnrand([0.01, 1], [10, 1].normalizeSum, inf),
	\legato, Pfunc({ |e| e.use { ( ~longAttack < 0.5).if(6, 1) } }),
	
	\ampEnv, Pfunc({
		|e|
		e.use { Env.perc(attackTime:~longAttack, releaseTime:1 - ~longAttack, curve:-2) }
	})
)).play;
)
18 Likes

A small note - the “Node not found” errors are because I have a \gate argument for my synth, but I’m not using it to actually end my synths (I’m passing them Env.perc's, which aren’t gated). I would normally factor this out in my code, but it makes the example more robust because you can pass both gated and non-gated envelopes.

3 Likes

One catch is that \sustain in the event is in beats, while the server expects seconds. I usually handle this by having a SynthDef argument time (instead of sustain) and writing in the pattern \time, Pfunc { |ev| ev.use { ~sustain.value } / thisThread.clock.tempo }.

hjh

3 Likes

cool, then it would be possible to do:

Event.addParentType(\durSeconds, (
	finish: {
	    currentEnvironment.use{
                ~durSeconds = ~sustain / thisThread.clock.tempo;
            }
	},
));

Pbindef(\test,
    \type,\durSeconds,
    \instrument,\testPoll,
    \dur,Pseq([1,2,3],inf)
).trace.play

Every synth played by a \durSecond type pattern, will get a \durSecond parameter with, well, its duration in seconds.

Is it possible for an event to have more than one parentType? If not, maybe it would be better to have this \durSeconds function as a pattern composition thing:

~durSeconds = Pfunc{|e| e.durSeconds = e.use(_.sustain) / thisThread.clock.tempo}

(
Pdef(\a,
    ~durSeconds<>
    Pbind(
        \instrument,\testPoll,
        \dur,Pseq([1,2,3],inf)
    )
).trace.play
)
1 Like

Thanks for this, there’s a lot to learn from in here!! The normalizeSum after a Pwrand is genius to me haha I’ve been sat there only playing with round figures cause I’m inputting manually. Would like to highlight there is a spelling mistake that caused the error Unknown Class to pop up! incase anyone else experiences it - there is an ‘n’ in the Pwrand.

Thanks again,

Sorry, I tried to scrub custom classes from my code, but I let Pwnrand slip by! FWIW Pwnrand is just Prand, but it automatically normalizes the weights, so you don’t need to call .normalizeSum yourself.