EnvGen doesn't keep envelope constant

In the EnvGen help I read “Plays back break point envelopes. The envelopes are instances of the Env class. The envelope and the arguments for levelScale, levelBias, and timeScale are polled when the EnvGen is triggered and remain constant for the duration of the envelope.”

The following code demonstrates that a change to the Env object after the EvnGen has been triggered leads to a change in the generated envelope. It seems that only the current segment is kept constant. What would be a good workaround? I tried latching the fade value with the same trigger starting the envelope, but this led to even less predictable behaviour. I can also provide an example, but maybe somebody can already shade some light on what I am doing (wrong) here and what I should be doing to keep the envelope specification constant until the next trigger to EnvGen.

(
fork {
	x = { |fade = 0.5|
		var env = Env.new([0, 1, 1, 0], [fade, 1 - (fade * 2), fade], \sin);
		EnvGen.ar(env, 1, timeScale:6).poll();
	}.play();
	2.wait();
	x.set(\fade, 0);
	4.wait();
	"************* 6 seconds".postln();
}
)

if you append i_ to your argument name (so i_fade) SynthDef will create an initialization rate Control in the unit generator graph which will be static.

see SynthDef | SuperCollider 3.12.2 Help for all the gory details

You could also define the Env outside your SynthDef function.

(
~testMe = { | fade = 0.5 |
	fork {
		var env = { Env.new([0, 1, 1, 0], [fade, 1 - (fade * 2), fade], \sin) };
		var x = { 
		    EnvGen.ar(env.(), 1, timeScale:6).poll();
		}.play;
		2.wait;
		x.set(\fade, 0);
		4.wait;
		"************* 6 seconds".postln();
	}
};
~testMe.(0.5)
~testMe.(1)

Thank’s a lot, semiquaver!

I would like to be able to dynamically parametrise the envelope while keeping it constant between triggers to EnvGen, like I understand the description in the help file: "The envelope … [is] polled when the EnvGen is triggered and remain[s] constant for the duration of the envelope.”. Any ideas how to achieve this?

I have tried this workaround, but without success:

(
{
	var fade, trigger, times, env, envgen;
	
	fade = WhiteNoise.ar().range(0, 0.5);
	trigger = Impulse.ar(1);
	fade = Latch.ar(fade, trigger);
	times = [fade, 1 - (fade * 2), fade];
	env = Env.new([0, 1, 1, 0], times, \sin);
	envgen = EnvGen.ar(env, trigger);
	[fade, envgen];
}.scope()
)

While poking around this, I discovered a behaviour I cannot explain at all.

The code below works, but if I want to also scope the trigger like in the line commented out, then the envelope doesn’t seem to trigger any longer. It is as if using the trigger signal past the EnvGen makes some kind of difference. This may be another issue all together!

(
{
	var trigger, fade, times, env, envgen;

	trigger = Impulse.ar(1);
	fade = DC.ar(1/3);
	times = [fade, 1 - (fade * 2), fade];
	env = Env.new([0, 1, 1, 0], times, \sin);
	envgen = EnvGen.ar(env, trigger);
	[fade, envgen];
	// [fade, trigger, envgen];
}.scope()
)

BTW, I am still using SC 3.11.1. Could that problem have been fixed since? Does Env maybe not allow audio signals to specify levels and times?

hmmm

according to the current help the arguments are polled at the start of each envelope segment.

not sure what’s happening in the second example!

Thank you! I switched to 3.12.2 now and see that the help has been updated. This explains some of my issues.

Unfortunately, your suggestion to fix my workaround doesn’t work for me. I recorded the output of the following code in 3.12.2:

(
{
	var fade, trigger, times, env, envgen;
	
	fade = WhiteNoise.ar().range(0, 0.5);
	trigger = Impulse.ar(1) - 0.1;
	fade = Latch.ar(fade, trigger);
	times = [fade, 1 - (fade * 2), fade];
	env = Env.new([0, 1, 1, 0], times, \sin);
	envgen = EnvGen.ar(env, trigger);
	[fade, envgen];
}.scope()
)

And get this:

An audio rate trigger doesn’t work in this context because the envelope parameters update only at control rate.

Unfortunately there’s no solution at present.

You’d be fine with a control rate trigger.

Edit 1: The - 0.1 is not necessary – let’s not introduce folklore here. semiquaver did give the correct definition of a trigger (transition from non-positive to positive), but zero is non-positive, so Latch does latch when Impulse goes from 0 to 1.

Edit 2: Try this:

(
{
	var fade, trigger, times, env, envgen;
	
	fade = WhiteNoise.kr().range(0, 0.5);
	trigger = Impulse.kr(1);
	fade = Latch.kr(fade, trigger);
	times = [fade, 1 - (fade * 2), fade];
	env = Env.new([0, 1, 1, 0], times, \sin);
	// note that EnvGen.ar with kr inputs is OK
	envgen = EnvGen.ar(env, trigger);
	[K2A.ar(fade), envgen];
}.scope()
)

hjh

Thank you very much jamshark70!
Do you know if there are plans to make it work with audio rate?
Just checked and it seems to work fine with a blockSize of 1.
Cheers,
Gerhard

I’m not aware of any plans.

The technical problem is that audio rate inputs need to advance a pointer through the wire buffer, while control rate inputs don’t use a wire buffer (being only a single value). Most UGens handle this by having different functions for different combinations of rates, but EnvGen has an arbitrary number of inputs, and any of them could be ar or kr. So the code would need to add conditionals for every input (4 inputs per breakpoint!) which may affect performance.

A not very nice solution may be to require all inputs to be audio rate, for audio rate processing. But it’s typical for many EnvGen inputs to be scalar; requiring all of those to be wrapped in K2A units just to enable audio rate updates would be an extraordinary demand on the user and likely to be unpopular.

So I think it’s better to do it in the user friendly way and allow mixed rates (although this isn’t as easy as you might have thought).

This limitation is especially nasty for VarLag, which depends on EnvGen and thus is not reliable at audio rate.

hjh

I believe it’s still possible to write a single process function that can handle both audio and control rates for any given input - it’s just that you have to check before reading from them. Normally this would be a performance problem, but in case of EnvGen you only need to check once every envelope segment so efficiency shouldn’t be an issue. In psuedo-code it should look something like:

if (rate(in5) == Audio) {
    in5_value = in5[sampleOffset]
} else {
    in5_value = *in5
}

Hm, right, that shouldn’t be a big issue.

Next question: in EnvGen.ar with an ar trigger, and some kr breakpoint parameters, in case of a trigger occurring mid-control-block, should it interpolate the kr inputs? The suggested code snippet does a zero-order hold.

hjh

Hello Gerhard,

to get ar you can work with linear segments and then map to sine.


(
{
	var envDur, fade, trig_1, trig_2, trigSum, ramp, toggle, envLin, env;

	envDur = 0.1;
	fade = WhiteNoise.ar().range(0.005, 0.05);
	trig_1 = Impulse.ar(1 / envDur);
	fade = Latch.ar(fade, trig_1);
	
	// trigger for release
	trig_2 = DelayC.ar(trig_1, envDur, envDur - fade);
	trigSum = trig_1 + trig_2;
	
	// basic ramp for fade-in and -out
	ramp = Sweep.ar(trigSum, 1 / fade);
	
	// toggle starts with 0 that way
	toggle = 1 - ToggleFF.ar(trigSum);
	
	// distinguish ramp up and down
	envLin = Clip.ar(toggle + ((1 - (toggle * 2)) * ramp), 0, 1);

	// map to sine
	env = sin(-pi/2 + (envLin * pi));
	[fade * 10, Trig1.ar(trig_1, 0.005), Trig1.ar(trig_2, 0.005), toggle, envLin, env]
}.plot(1)
)


It’s a bit fiddling, so for reusability a pseudo ugen might make sense. In fact, miSCellaneous_lib’s DX suite does something quite similar, transitions being based on PanAz. However, it’s aim is slightly different and so the interface is not perfectly suited for this example, the above solution looks easier.

1 Like

Thank you very much Daniel. This is super useful and very clever!

Hello Daniel,

in order to avoid large delay memory for longer envelopes caused by DelayC.ar, I tried with TDelay.ar, but this doesn’t work all the time. Do you have any idea why? As far as I understand, we don’t ask for a new trigger to be delayed before the last one has been returned from TDelay.ar, which swallows triggers arriving in the time between its input and output trigger. Also, wouldn’t DelayN.ar be good enough in your original formulation?

(
{
	var envDur, fade, trig_1, trig_2, trigSum, ramp, toggle, envLin, env;

	envDur = 0.1;
	fade = WhiteNoise.ar().range(0.005, 0.05);
	trig_1 = Impulse.ar(1 / envDur);
	fade = Latch.ar(fade, trig_1);
	
	// trigger for release
	trig_2 = TDelay.ar(trig_1, envDur - fade);
	trigSum = trig_1 + trig_2;
	
	// basic ramp for fade-in and -out
	ramp = Sweep.ar(trigSum, 1 / fade);
	
	// toggle starts with 0 that way
	toggle = 1 - ToggleFF.ar(trigSum);
	
	// distinguish ramp up and down
	envLin = Clip.ar(toggle + ((1 - (toggle * 2)) * ramp), 0, 1);

	// map to sine
	env = sin(-pi/2 + (envLin * pi));
	[fade * 10, Trig1.ar(trig_1, 0.005), Trig1.ar(trig_2, 0.005), toggle, envLin, env]
}.plot(1)
)

It seems we then again run into kr/ar issues. I tried the following variant with TDelay and fade times below 1 ms. With blockSize == 64 it shows inaccuracies but with blockSize == 1 it looks ok again (which then would be a similar situation as with the EnvGen solution).

(
{
	var envDur, fade, trig_1, trig_2, trigSum, ramp, toggle, envLin, env;

	envDur = 0.01;
	fade = WhiteNoise.ar().range(0.0005, 0.001);
	trig_1 = Impulse.ar(1 / envDur);
	fade = Latch.ar(fade, trig_1);
	
	// trigger for release
	trig_2 = TDelay.ar(trig_1, envDur - fade);
	trigSum = trig_1 + trig_2;
	
	// basic ramp for fade-in and -out
	ramp = Sweep.ar(trigSum, 1 / fade);
	
	// toggle starts with 0 that way
	toggle = 1 - ToggleFF.ar(trigSum);
	
	// distinguish ramp up and down
	envLin = Clip.ar(toggle + ((1 - (toggle * 2)) * ramp), 0, 1);

	// map to sine
	env = sin(-pi/2 + (envLin * pi));
	[fade * 10, Trig1.ar(trig_1, 0.005), Trig1.ar(trig_2, 0.005), toggle, envLin, env]
}.plot(0.1)
)

Probably for most practical applications. I didn’t think about the memory usage of Delay as this rarely makes troubles for me. If it does you can always increase the memSize via server options. In some cases (parallel usages) the BufDelay variants can also reduce memory usage.

… maybe worth noting that there’s also lincurve and related operators which let’s one choose resp. sequence any kind of envelope shape (being an alternative to DemandEnvGen).

It would also be possible to replace Delay with a demand rate UGen solution:

(
{
	var envDur, fade, trig_1, trig_2, trigSum, ramp, toggle, envLin, env;

	envDur = 0.01;
	fade = WhiteNoise.ar().range(0.0005, 0.003);
	trig_1 = Impulse.ar(1 / envDur);
	fade = Latch.ar(fade, trig_1);
	
	// trigger for attack and release
	trigSum = TDuty.ar(Dseq([envDur - fade, fade], inf), reset: trig_1);
	
	// basic ramp for fade-in and -out
	ramp = Sweep.ar(trigSum, 1 / fade);
	
	// toggle starts with 0 that way
	toggle = 1 - ToggleFF.ar(trigSum);
	
	// distinguish ramp up and down
	envLin = Clip.ar(toggle + ((1 - (toggle * 2)) * ramp), 0, 1);

	// map to sine
	env = sin(-pi/2 + (envLin * pi));
	[fade * 10, Trig1.ar(trigSum, 0.0005), toggle, envLin, env]
}.plot(0.1)
)

Though this construct of ugens inside the Dseq requires resetting … without further testing I’d be more confident with a Delay here but this is probably also ok.