I’m trying to program my own ADSR (because why not). I’ve done a little math on paper and figured out some formulas. Then I read through the code of the Shruthi ADSR and was surprised to find a completely different solution. Basically I’m wondering if my solution has flaws I can’t yet see.

Here’s what I did:

For the Attack phase: The goal is to have something like U (t) = t/A with A being the attack time. The derivative is: U_dot(t) = 1/A. Approximating U_dot(t) by (U (t+h) - U (t))/h I get to the solution: U (t+h) = U (t) + h/A.
For the Decay/Release phase: The goal is to have something like U (t) = U_max*e^(-t/R) with R beeing the release time. The derivative is: U_dot(t) = -U_max/R*e^(t/R). Again, approximating U_dot(t) by (U (t+h) U (t))/h I get to the solution: U (t+h) = U (t)*(1-h/R).
This formula gets unstable for any R < h, but you won’t want that anyways, because R = h is already instantly zero.

Looking at those formulas, I can see that they are easy to implement, don’t suffer from any serious numerical instabilities or errors. And they can even be modulated without problems. I don’t see any obvious problems here.

In contrast, the Shruthi code basically has a exponential curve prepared in memory and uses a phase accumulator to “play” through that curve. I wonder what’s the benefit of this approach. (I’m silently assuming that pichenettes code simply must be superiour to what I figured out in a few minutes…)

I imagine, as Bjarne says, it’s much more computationally efficient in teeny-MCU-land, to read pre-calculated data from a lookup-table than it is to do the calculations in realtime.

I think in terms of raw cycles, my solution might even be faster. But for small values of h, the numbers get too small for low-resolution (fixed or floating point) math

I’ve used a solution similar to yours for 3 or 4 months in the shruthi code before seeing the light

For numerical accuracy I think it’s better to manage a counter that goes between 0 and 0xffff (or 0xffffffff), and then scale it to make it go between whichever values you want. It makes it trivial to get any shape you want with a LUT. Bouncy envelopes, steppy envelopes, C^infty envelopes - all possible! Crossfade between two envelope shapes without any impact on the time/rates! This isn’t used in any product at the moment, but might be in the future (for fun I have implemented the e-mu “function generator” almost-turing-complete-envelope-generator).

Another advantage is that your solution would still require a division whenever the time or source/target levels are modified. If the modulation comes from a LFO rendered at the same rate as the envelope, that’s once per envelope sample!

A last thing: it is wrong to assume that what we call an exponential envelope is e^(-t). If it were the case, it would never reach the target value! If you study an analog envelope generator circuit, you’ll see that an envelope typically stops at 2/3 of its target value (the 2/3 comes from the 555 or comparators with hysteresis).

It is really hard to beat a good LUT in term of speed. If you are making a stand-alone envelope module, you will have no issues not using a LUT. In those cases, the DAC time is what will slow down the processor.

The LUT converts a boring linear curve to whatever you want. The advantage to this is that you can precalculate the answers to a complex equation and put them into the LUT. It is pretty much a wavetable for your modulators.

However, to get nice non-linear curves, I am pretty sure one will always have to divide at least once. I would love to see a mathematical proof that says other wise.

That being said, it is possible to change the rate at which you add or subtract your attack/decay/release time by changing a prescalar value. The longer the time between the steps, the more “steppy” it sounds. This means that you can correct it by reducing the size of the step every time you get closer to your target value. There are many ways to do that, but you want to make sure they do not converge at 0.

I know that envelopes can’t be “true” exponential. But 2/3 of the target value seems crazy to me. Looking at the shapes of typical envelopes, I can’t see them immediately drop to zero when they go below 1/3*sustain in the release phase. It might be different in an exponential attack phase, but AFAIK the attack is linear in most cases.
Can you point me to an example of such behaviour?

Anyways, thanks for your input. LFO Modulation would indeed require many divisions. Forgot about that.

"It is really hard to beat a good LUT in term of speed."
I think it depends. A LUT with linear interpolation requires some cycles, too. Numerical problems aside, my formula is basically a simple multiply command (1-h/R is a constant when there’s no modulation). A LUT with interpolation requires at least two additions and two multiplications. Plus some more housekeeping and shifting to get the required LUT index and the two pointer reads themselves. So yea, that can be more than the formula itself. Not very often, I must admit, espcially for comples waveforms.

> can’t see them immediately drop to zero when they go below 1/3*sustain

No that’s not what I mean. For example, during the attack phase, the exponential “aims at” 12V, but the decay starts when the envelope reaches 8V. So the slope is still quite large in the attack->decay transition.

For the release, the envelope aims at a value below 0.

I remember there’s a place in Warps’ code in which evaluating a floating point expression for a non-linearity (absolute value, low-degree polynomial, division) was faster than an interpolated LUT - same flash prefetching issues.

@pichenettes: Thanks for linking me to that video. That and the related videos will be watched when I have time.
Hearing that Warps is faster with calculations than LUTs is great as the code I want to port is much cooler when using algorithms.

Also, is the e-mu turing machine an easteregg we haven’t found yet?

@audiohoarder: Thanks for the video, very informative. Its says that the decay/release portion aims at 0.0001 below its target value. That’s practically nothing and it only makes sure that the curve will eventually hit the target value. But apart from that, I see no audible difference to an e^(-t) curve.
pichenettes mentioned “e^(-t)” and not "1-e^(t)" hence I was thinking he meant the decay/release part. Apparently he was talking about the attack part only and we had a misunderstanding there.

Yes, it completely makes sense for an attack curve. AFAIK the attack is assumed to be linear in most digital implementations.

So to sum it up: For Attack you want an exponential curve that aims roughly 1/3 above its target value (or a linear curve). For Decay and Release you want true exponential decay, but you have to make sure it eventually hits the target. It can be achieved by rounding errors or by aiming slightly below the target.

Studying the Shruthi a little more:
The VCA envelope has exponential decay/release and is fed directly to the linear VCA. This results in a linear decaying audio “volume” due to the log-response of our ears, right?
The Filter envelope has exponential decay/release as well, but its is fed through a lin-to-exp circuit in hardware before it hits the smr4 filter. This means it actually is double-exponential. Due to the relation between pitch and frequency, that would actually mean the filter has a decay with exponentially decaying pitch (not frequency!).
I can’t remember my Shruthis to behave like that. IIRC, it sweeps evenly through the octaves. I have to check that when I’m at home.

> that would actually mean the filter has a decay with exponentially decaying pitch (not frequency!).

That’s correc, but not specific to the Shruthi. Pretty much all analogue synths are working this way.

Same goes for VCO frequency by the way - the classic RC portamento circuit will not sweep linearly through notes. That’s why there are some portamento circuits with a linear rather than exponential ramp.

Yes, I’ve seen portamento being created by running the pitch through a 1-pole lowpass.
In the Shruthi, you - again - go a different route. I guess, the reason is - again - in the rounding errors?