10
$\begingroup$

With application code, I have implemented a linear ADSR envelope for shaping the amplitude of an oscillator's output. The parameters for attack, decay and release duration as well as sustain level can be set on the envelope and everything works as expected.

However, I would like to tweak the ramp shapes of the envelope to something that resembles what most synthesizers use for a more natural response: inverse exponential for the attack and exponential for the decay and release. I am having trouble getting my formulas right for calculating the envelope output values for these types of ramp shapes. To calculate the linear ramps, I am using the two-point form, plugging in the start/end $x$/$y$ values which are derived from the attack/decay/sustain/release input parameter values. I cannot seem to work out the correct formula for exponential (standard and inverse) ramps using the same start/end $x$/$y$ point values.

I have saved a Desmos Graphing Calculator session that demonstrates the approach for linear ramps that I described above.

If anyone can help point me in the right direction, it would be much appreciated.

$\endgroup$
9
$\begingroup$

I think that what confuses you is that a decreasing exponential ($e^{-x}$) does never reach 0, so an ADSR generator with truly exponential segments would stay stuck ; because it would never reach the target value. For example, if the generator is at the height of the attack phase (say $y = 1$) and has to land to a sustain value at $y = 0.5$, it can't go there with a true exponential, because the true exponential won't decay to 0.5, it'll only asymptotically go to 0.5!

If you look at an analog envelope generator (for example the 7555 based circuit everybody seems to use), you can see that during the attack phase, when the capacitor is charging, it is "aiming higher" than the threshold used to indicate the end of the attack phase. On a (7)555 based circuit powered by +15V, During the attack stage, the capacitor is charged with a +15V step, but the attack stage ends when a threshold of +10V has been reached. This is a design choice, though 2/3 is the "magic number" found in many classic envelope generator, and this might be the one musicians are familiar with.

Some ADSR shapes resulting from different "aim ratio" during capacitor charge

Thus, the functions you might want to deal with are not exponentials, but shifted/truncated/scales versions of it, and you'll have to make some choices as to how "squashed" you want them to be.

I am curious anyway as to why you are trying to get such formulas - maybe it's because of the limits of the tool you are using for synthesis ; but if you are trying to implement those using a general purpose programming language (C, java, python) with some code running for each sample of the envelope, and a notion of "state", read on... Because it's always easier to express things as "such segment will go from whatever value it has just reached to 0".

My two pieces of advice on implementing envelopes.

The first one is not to try to scale all the slopes/increments so that the envelope exactly reach start and end values. For example you want an envelope that goes from 0.8 to 0.2 in 2 seconds, so you might be tempted to compute an increment of -0.3 / second. Don't do that. Instead, break it down into two steps: getting a ramp that goes from 0 to 1.0 in 2 seconds ; and then applying a linear transform that maps 0 to 0.8 and 1.0 to 0.2. There are two advantages to work this way - the first is that it simplifies any computation you'll have relative to envelope times to a ramp from 0 to 1 ; the second is that if you change the envelope parameters (increments and start/end times) midway everything will remain well-behaved. Good if you're working on a synth, since people will ask to have envelope time parameters as modulation destinations.

The second is to use pre-computed lookup table with envelope shapes. It is computationally lighter, it takes out many dirty details (for example you don't have to bother with an exponential not reaching 0 exactly - truncate it at your whim and rescale it so that it is mapped to [0, 1]), and it's dead easy to provide an option to alter envelope shapes, for each stage.

Here is the pseudo-code for the approach I describe.

render:
  counter += increment[stage]
  if counter > 1.0:
    stage = stage + 1
    start_value = value
    counter = 0
  position = interpolated_lookup(envelope_shape[stage], counter)
  value = start_value + (target_level[stage] - start_value) * position

trigger(state):
  if state = ON:
    stage = ATTACK
    value = 0  # for mono-style envelopes that are reset to 0 on new notes
    counter = 0
  else:
    counter = 0
    stage = RELEASE

initialization:
  target_level[ATTACK] = 1.0
  target_level[RELEASE] = 0.0
  target_level[END_OF_RELEASE] = 0.0
  increment[SUSTAIN] = 0.0
  increment[END_OF_RELEASE] = 0.0

configuration:
  increment[ATTACK] = ...
  increment[DECAY] = ...
  target_level[DECAY] = target_level[SUSTAIN] = ...
  increment[RELEASE] = ...
  envelope_shape[ATTACK] = lookup_table_exponential
  envelope_shape[DECAY] = lookup_table_exponential
  envelope_shape[RELEASE] = lookup_table_exponential
$\endgroup$
  • $\begingroup$ I seemed to solve my problem by taking my linear scale/two-point equation of y = ((y2 - y1) / (x2 - x1)) * (x - x1) + y1, rewriting it by substituting the x variables with e^x to y = ((y2 - y1) / (e^x2 - e^x1)) * (e^x - e^x1) + y1. My calculator session at link illustrates this approach. Are their any gotchas to this that I should be aware of? The results seem correct to me. $\endgroup$ – Gary DeReese Jun 8 '12 at 15:51
  • $\begingroup$ This is not envelope shape found on other synthesizers. Depending on the time / relative position of the start and end level, it can become very linear. $\endgroup$ – pichenettes Jun 8 '12 at 19:01
  • $\begingroup$ @pichenettes, might you be willing to paste the script that generated those envelopes? $\endgroup$ – P i Feb 28 '14 at 13:32
3
$\begingroup$

This is a pretty old question, but I just want to highlight a point in the answer from pichenettes:

For example you want an envelope that goes from 0.8 to 0.2 in 2 seconds [...] break it down into two steps: getting a ramp that goes from 0 to 1.0 in 2 seconds ; and then applying a linear transform that maps 0 to 0.8 and 1.0 to 0.2.

This process is sometimes known as "easing," and looks like

$$g\left(x,l,u\right)=f\left(\frac{x-l}{u-l}\right)\left(u-l\right)+l$$

where $l$ and $u$ are the lower and upper bound (possible values being $0$, $1$, and the sustain level) and $f(x)$ is something like $x^n$. Note that you don't need this for the attack phase since it already ranges from $0$ to $1$.

Here is the original Desmos session, updated to use this approach. I used a cubic shape here, but you* could use whatever shape you like, as long as $f(x)$ produces outputs ranging from zero to one given inputs ranging from zero to one.

* I guess the OP is probably long gone, but maybe this helps someone else.

$\endgroup$
  • $\begingroup$ Thank you for this, I was programming a sampler for a DAW I'm a developer for, and plugged in the formulas provided in the Desmos session and they worked perfectly. No more lame linear envelopes! :) $\endgroup$ – Douglas Aug 12 at 16:42
1
$\begingroup$

About pichenettes' comment, "During the attack stage, the capacitor is charged with a +15V step, but the attack stage ends when a threshold of +10V has been reached. This is a design choice, though 2/3 is the "magic number" found in many classic envelope generator, and this might be the one musicians are familiar with.":

Any envelope that's shooting for a 15v asymptote with a 10v target is, practically, creating a linear attack. It's just that 15v is the highest asymptote available easily, and it's close enough to linear. That is, there's nothing "magic" about it—they're just going for as linear as they can get.

I don't know how many classic synths use 15v—I'd suspect there's often a diode drop or two. My old Aries modular uses 13v for a 10v envelope, and I just looked up the a Curtis ADSR chip that uses, equivalently, 6.5v for a 5v envelope.

$\endgroup$
1
$\begingroup$

This code should generate similar plots to those of pichenettes:

def ASD_envelope( nSamps, tAttack, tRelease, susPlateau, kA, kS, kD ):
    # number of samples for each stage
    sA = int( nSamps * tAttack )
    sD = int( nSamps * (1.-tRelease) )
    sS = nSamps - sA - sD

    # 0 to 1 over N samples, weighted with w
    def weighted_exp( N, w ):
        t = np.linspace( 0, 1, N )
        E = np.exp( w * t ) - 1
        E /= max(E)
        return E

    A = weighted_exp( sA, kA )
    S = weighted_exp( sS, kS )
    D = weighted_exp( sD, kD )

    A = A[::-1]
    A = 1.-A

    S = S[::-1]
    S *= 1-susPlateau
    S += susPlateau

    D = D[::-1]
    D *= susPlateau

    env = np.concatenate( [A,S,D] )

    # plot
    tEnv = np.linspace( 0, nSamps, len(env) )
    plt.plot( tEnv, env )
    plt.savefig( "OUT/EnvASD.png" )
    plt.close()

    return env

I'm grateful for any improvements, one thing that may be a good idea is allowing the last three parameters (which determine the steepness of each of the three stages) to vary between 0 and 1, where 0.5 would be a straight-line. But I can't see offhand how to do it.

Also I haven't tested thoroughly all usage cases, for example if one stage has zero length.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.