About envelope curves using 1-pole filter, and the exponential curve setting in Env

(
x = Env([0, 1], [1], -2).discretize(256).as(Array)
-
Array.fill(256, { |i| i.lincurve(0, 255, 0, 1, -2) });

[x.minItem, x.maxItem].postln;
)

[-2.9439419124699e-08, 2.959850453621e-08]

I think the difference here is explained by single-vs-double precision floats – otherwise it’s a match (and if it were not a match, it would be a bug).

hjh

Or, another example illustrating the relationship to the formula in the website:

// we want an envelope segment from 0 to 1,
// with y = 0.25 at the midpoint
c = ~curve.(0, 0.25, 1);  // 2.1972245773362

// with an odd number of samples, (n-1)/2 is the exact midpoint
n = 32769;

// client-side
a = Array.fill(n, { |i| i.lincurve(0, n-1, 0, 1, c) });

// now... compare to the server-side EnvGen,
// with the same curve
s.boot;
b = Buffer.alloc(s, n, 1);

(
z = {
	RecordBuf.ar(
		EnvGen.ar(
			Env([0, 1], [1], c),
			timeScale: n * SampleDur.ir
		),
		b, loop: 0, doneAction: 2
	);
	Silent.ar(1)
}.play;
)

b.getToFloatArray(wait: -1, action: { |data| d = data });

// plots look the same
[a, d.as(Array)].flop.plot(numChannels: 2);

a[(n-1) div: 2]  // 0.25
d[(n-1) div: 2]  // 0.24998745322227

Server-side, the envelope segment’s phase may be slightly different from the client-side lincurve array, but practically speaking, nobody is going to notice a deviation that is 4 orders of magnitude smaller than the range.

So – the ~curve function does produce a valid curve value, and this curve value works for client-side lincurve and curve-style envelope segments. So I suspect that the assertion that it doesn’t seem to be an envelope curve is based on a misunderstanding, somewhere.

I dusted off this old function because it allows SC to implement something that is basically the same as Vital, Serum, Massive, Surge XT etc. etc. curved control-point segments. I don’t think it’s an accident that the curve in all of these environments is based on the y value halfway through the segment – lincurve involves exp(curve) ** normalized_x, which at the midpoint is exp(curve) ** 0.5, and, solving for curve, it’s significantly easier to handle this square root than it is to handle any other exponent where 0 < x < 1.

Based on those observations, I’m guessing that they are all using something similar to lincurve… which, if true, would suggest that lincurve-style calculations are quite common in digital synths.

hjh

2 Likes

Since Lag is exponential, maybe we could have a new type of env curve: “inverted exponential” (or whatever is appropriate).
Comparing exponential to Lag:

(
{
	var offset = -60.dbamp;  // 0.001
	var step = EnvGen.kr(Env([0, 0, 1, 1, 0], [0, 0.05, 0.2, 0.05], \step));
	var exp = EnvGen.kr(Env([0, 0, 1, 1, 0] + offset, [0, 0.05, 0.2, 0.05], \exp, offset: offset.neg));
	[step, Lag.kr(step, 0.05), exp]
}.plot(duration: 0.5);
)

So “inverted exponential” would work for the jump from 0 to 1.
Best,
Paul

I think the curve setting does this already. This isn’t well documented but the curvature is actually dependent on the direction…

EnvGen.kr(Env([0, 0, 1, 1, 0] + offset, [0, 0.05, 0.2, 0.05], -4, offset: offset.neg)).plot(0.5);
EnvGen.kr(Env([0, 0, 1, 1, 0] + offset, [0, 0.05, 0.2, 0.05], 4, offset: offset.neg)).plot(0.5);

EDIT: Oh, maybe that’s not what you meant, but rather this:
{EnvGen.kr(Env([0,1,0], [0.05, 0.2], [-4, 4]))}.plot(0.5);
which is more or less the “inverted exponential” that you mentioned.
(Takeaway: curve argument can be an array.)

Comparing curve with exponential, a curve of 7/-7 seems pretty close to exponential from 0db to -60 db:

(
var offset = -60.dbamp;  // 0.001
a = Env([1, 0] + offset, [1], \exp, 
	offset: offset.neg).discretize(400);
b =  Env([1, 0] + offset, [1], -6, 
	offset: offset.neg).discretize(400);
c =  Env([1, 0] + offset, [1], -7, 
	offset: offset.neg).discretize(400);
d =  Env([1, 0] + offset, [1], -8, 
	offset: offset.neg).discretize(400);
UserView(nil, 400 @ 400)
.drawFunc_({arg view;
	[a, b, c, d].do({arg arr, i;
		Pen.color = [Color.green, Color.black, Color.red, Color.blue][i];
		Pen.moveTo(0 @ (arr[0] * view.bounds.height));
		arr.do({arg item, i; Pen.lineTo(i @ (arr[i] * view.bounds.height)); });
		Pen.stroke;	
	});
})
.front;
(a - c).maxItem; // -> 0.0047763586044312
)

(
var offset = -60.dbamp;  // 0.001
a = Array.interpolation(400,1, 0);
b = a.linexp(0, 1, offset, 1).normalize;
c = a.lincurve(0, 1, 0, 1, 7);
UserView(nil, 400 @ 400)
.drawFunc_({arg view;
	[b, c].do({arg arr, i;
		Pen.color = [Color.green, Color.red][i];
		Pen.moveTo(0 @ (arr[0] * view.bounds.height));
		arr.do({arg item, i; Pen.lineTo(i @ (arr[i] * view.bounds.height)); });
		Pen.stroke;	
	});
})
.front;
(b - c).maxItem;  // -> 0.0048290078073841

)

Best,
Paul

yup :slight_smile: that’s what I said

1 Like

The ~curve function confirms that:

// assuming Lag is a onepole, get the x = 0.5 value
(
a = {
	var trig = Impulse.ar(0);
	var t2 = TDelay.ar(trig, 0.5);
	var lag = Lag.ar(trig, 1);
	lag.poll(t2);  // x = 0.5
	Line.kr(0, 1, 0.7, doneAction: 2);
	Silent.ar(1);
}.play;
)

UGen(Lag): 0.0316277

(
~curve = { |minval, midval, maxval|
	var a, b, c, sqrterm, qresult, sgn = sign(maxval - minval);
	if(midval > ((maxval + minval) * 0.5)) {
		midval = minval + maxval - midval;
		sgn = sgn.neg;
	};
	a = midval - minval;
	b = minval - maxval;
	c = maxval - midval;
	sqrterm = sqrt(b.squared - (4 * a * c));
	qresult = (sqrterm - b) / (2 * a);
	if(qresult.abs != 1) {
		log(qresult.squared).abs * sgn
	} {
		log(((b.neg - sqrterm) / (2 * a)).squared).abs * sgn
	};
};
)

~curve.(1, 0.0316277, 0);
-> -6.8431666037493

~= ±7 :white_check_mark:

hjh

1 Like

Continuing with that…

(
var dur = 0.1;
p = {
	var trig = Impulse.ar(0);
	var lag = Lag.ar(trig, dur);
	var eg = EnvGen.ar(Env([1, 0], [dur], -6.8431666037493));
	[lag, eg, lag - eg]
}.plot(duration: dur);
)

[p.value[2].minItem, p.value[2].maxItem]
-> [-0.0028059184551239, 0.00099990924354643]

p.specs = [[0, 1], [0, 1], [-0.003, 0.003]];

Max error is about 0.28%, not bad for an envelope.

So an easy way to implement a quasi-onepole envelope segment would be to translate \onepole or \rc into a curve value of -6.843167 (and I think it’s always negative here), and have it today instead of extending EnvGen on the C++ side… an approximation, but less than a third of a percent error is probably OK for most cases.

hjh

3 Likes