New envelope behavior makes it a bit harder to modulate filter frequency

Wondering if anyone has any workarounds for a side effect of the EnvGen initialization fix.

Creates a bit of a challenge for my Plug modulation approach.

So… you’ve got, for instance, a filter frequency input that is control rate.

Then you want to modulate this by a kr synth. (You don’t even have to use Plug specifically – just make a bundle playing both synths, and put a bus-mapping token in for filter frequency.)

The modulation synth maps an Env.perc onto a valid range for filter frequency, e.g. ffreq * (1 + (modFactor * eg)). And, for flexibility, there is an attack and a release time parameter, so that you could do slow swells or attack-spike envelopes.

The envelope used to start too high – which was (oddly enough) an advantage for generating attack spikes.

Now I’m getting two samples of the initial value, not just one, and then the ramp up to the peak. For filter frequency, this quite dramatically alters the timbre at note onset.

And – I guess the thing that I’m really complaining about – is that there is no way to avoid this behavior by setting attack = 0.

(
p = {
	var egs = [
		EnvGen.kr(Env([1, 0], [0.081])),
		EnvGen.kr(Env.perc(0, 0.08)),
		EnvGen.kr(Env.perc(0.001, 0.08)),
		// even A2K on an ar envelope doesn't fix this!
		A2K.kr(EnvGen.ar(Env.perc(0.001, 0.08)))
	];
	var modFactor = 8;
	var freq = 500;
	var initPulse = Impulse.kr(0);
	var freqs = freq * (1 + (modFactor * egs));
	var sig = RLPF.ar(Saw.ar(100), freqs, 0.3);
	freqs.poll(initPulse);
	[freqs, sig].lace(egs.size * 2)
}.plot(0.025);

{ p.specs = [[0, 5000], [-1, 1]].dup(4).flatten(1) }.defer(1)
)

Do I need, then, two synthdefs, one where the envelope begins at 1, and the other that begins at 0? OK, I guess, but then choosing the wrong synthdef is just another potential mistake to make in a live coding context – more user-friendly to reuse the structure and adjust parameters, than it is to have multiple structures because EnvGen ramps up too slowly.

Or I’m required to pass in the full envelope all the time, every time? Again, that kinda works against streamlining a live coding workflow.

BTW I also tried EnvGen.ar and then demoting it by A2K. Unfortunately this didn’t improve the sound.

I note in the PR comment “a one-sample phase change from previously” – I’m curious why the envelope needs to have two samples at the initial value? I think I can imagine the rationale from a programmer’s perspective, but from a musician’s perspective, this is not quite expected behavior. Any chance of improving it?

hjh

I’m not sure this is the case, or at least doesn’t seem like it should be.

Do you know if an onset time of 0 is valid? (My intuition is that it should be…) It seems this was “allowed” previously because the EnvGen’s state was prematurely advanced during initialization. If an onset of 0 should be but is not currently supported, that specific case may need be fixed in the UGen.

In the meantime is

EnvGen.kr(Env([1, 0], 0.08, curve: -4)), // -4 is the default \perc curve

a dropin replacement? (Though there still appears to be a 1-period lag)

(
p = {
	var egs = [
        EnvGen.kr(Env([1, 0], 0.08, curve: -4)), // -4 is the default \perc curve
		EnvGen.kr(Env([1, 0], [0.081])),
		EnvGen.kr(Env.perc(0, 0.08)),
		EnvGen.kr(Env.perc(0.001, 0.08)),
		// even A2K on an ar envelope doesn't fix this!
		A2K.kr(EnvGen.ar(Env.perc(0.001, 0.08))),
	];
		var modFactor = 8;
		var freq = 500;
		var initPulse = Impulse.kr(0);
		var freqs = freq * (1 + (modFactor * egs));
		var sig = RLPF.ar(Saw.ar(100), freqs, 0.3);
		freqs.poll(initPulse);
		[freqs, sig].lace(egs.size * 2)
	}.plot(0.025);//.plotMode_(\dots);

{ p.specs = [[0, 5000], [-1, 1]].dup(4).flatten(1) }.defer(1)
)

Sorry I’m unable to look more closely at the UGen under the hood atm!

Almost – with one tweak, below, it works. (The power of a MWE – while preparing a MWE, a good solution struck me.)

For a filter frequency envelope, it turns out that the duplicated initial sample doesn’t matter, because a resonant filter will always have an artifact if its frequency is modulated too quickly. One could still quibble about that one-sample phase shift, but if EnvGen didn’t output the initial sample twice, I would have still had the same problem in the sound.

First to demonstrate the issue – when the attack time slider is below about the halfway point, the filter produces a not-clean-sounding spike. (Note also, if it’s a continuous synth that retriggers the filter envelope, then the only way to avoid the artifact is to slow down the filter envelope’s attack.)

(
a = Bus.control(s, 1);

Slider(nil, Rect(800, 200, 200, 25))
.action_({ |view| a.set(view.value.lincurve(0, 1, 0, 0.12, 4)) })
.front
.onClose_({ p.stop; a.free });

SynthDef(\test, { |out = 0, freq = 100, ffreq = 500, rq = 0.25, modFactor = 12, atk = 0, dcy = 0.12|
	var fEg = EnvGen.kr(Env.perc(atk, dcy));
	var ampEg = EnvGen.ar(Env.linen(0.001, 0.2, 0.1), doneAction: 2);
	var sig = Saw.ar(freq);
	ffreq = ffreq * (1 + (fEg * modFactor));
	sig = BLowPass4.ar(sig, ffreq, rq) * ampEg;
	Out.ar(out, (sig * 0.04).dup)
}).add;

p = Pbind(
	\instrument, \test,
	\freq, 100,
	\atk, a.asMap
).play;
)

While fiddling around with this, I realized that the Env’s init value could be calculated from atk – very short values → 1.0, atk >= 0.05 → 0.0. What I wanted was the ability to choose any filter-curve attack time and not have to switch out the whole envelope or SynthDef – this actually does it.

(
a = Bus.control(s, 1);

Slider(nil, Rect(800, 200, 200, 25))
.action_({ |view| a.set(view.value.lincurve(0, 1, 0, 0.12, 4)) })
.front
.onClose_({ p.stop; a.free });

SynthDef(\test, { |out = 0, freq = 100, ffreq = 500, rq = 0.25, modFactor = 12, atk = 0, dcy = 0.12|
	var envInit = atk.linlin(0, 0.05, 1, 0);
	var fEg = EnvGen.kr(Env([envInit, 1, 0], [atk, dcy], -4));
	var ampEg = EnvGen.ar(Env.linen(0.001, 0.2, 0.1), doneAction: 2);
	var sig = Saw.ar(freq);
	ffreq = ffreq * (1 + (fEg * modFactor));
	sig = BLowPass4.ar(sig, ffreq, rq) * ampEg;
	Out.ar(out, (sig * 0.1).dup)
}).add;

p = Pbind(
	\instrument, \test,
	\freq, 100,
	\atk, a.asMap
).play;
)

I think an example like this should go into the documentation, because probably other users will run into the same situation, and – if I had to sleep on the problem overnight and write up a MWE before finding a solution, then the solution isn’t obvious enough to go un-documented.

hjh

Ah I see why the plots were misleading…

Plotting only the frequency envelopes show the correct behavior (IMO), i.e. an “immediate” (but still non-zero) onset from perc’s initial level of 0.

(
p = {
	var egs = [
		EnvGen.kr(Env([1, 0], 0.08, curve: -4)), // -4 is the default \perc curve
		EnvGen.kr(Env([1, 0], [0.081])),
		EnvGen.kr(Env.perc(0, 0.08)),
		EnvGen.kr(Env.perc(0.001, 0.08)),
		// even A2K on an ar envelope doesn't fix this!
		A2K.kr(EnvGen.ar(Env.perc(0.001, 0.08))),
	];
	var modFactor = 8;
	var freq = 500;
	var freqs = freq * (1 + (modFactor * egs));
	var sig = RLPF.ar(Saw.ar(100), freqs, 0.3);
	// [freqs, sig].lace(egs.size*2)
	freqs
}.plot(0.025).plotMode_(\dlines);

// { p.specs = [[0, 5000], [-1, 1]].dup(p.value.size).flatten(1) }.defer(1)
)

There is no repeated sample, but because you were plotting both k-rate and a-rate signals together, it upsamples the k-rate signals using K2A, which imposes a 1-block lag on the values.

This wouldn’t be an issue if we follow through with this idea of giving K2A a transition time argument.

Great :slight_smile:

Agreed, this is a useful trick!

This reminds me also that the init-sample problem hasn’t been fixed yet on RLPF, so it could also be that it behaves nicer once that is fixed.

Ohhh I see. Ok. Good to investigate and rule it out.

It’s of course an improvement that the initial value is actually respected now (and the solution takes advantage of that).

hjh