Home | Papers | Reports | Projects | Code Fragments | Dissertations | Presentations | Posters | Proposals | Lectures given | Course notes |

## Fast Exponential Envelopes
Abstract : Computing ADSR curves is a task any synth has to deal with. In this article I present a method to compute exponential patches quickly and incrementally. Namely by multiplying the last envelopevalue with a constant multiplier (r) and then adding a constant delta (d) to it. This solution is used in the Audiotool Heisenberg synth to generate ADSR curves for pitch as well as amplitude.
Keywords:
ADSR Envelopes, Exponential Patches, Incremental offsetted exponential |

The presented solution is based on exponential patches of which the linearity can be chosen as wanted.

Illustrating the various curves that can be drawn. |

The following equation

Equation main: |

can be used to draw arbitrary exponential curves through three points

This can be done by choosing correct values for r and d, as follows:

- Choose the width x2, start value y1, stop value y2 and the midpoint value
- Calculate
- Calculate

. - Set and then calculate

**Computation of **

To calculate , given d, r, and y1 values:

- odd:=0

even := d

logr := log(r)

- while x>1
- if x is odd then

odd := odd + even

even := even * r - in all cases:

x := x >> 1

even := even * ( + 1)

- if x is odd then
- y := y1 + even + odd

The above algorithm can be computed in .

Equation main describes how can be incrementally calculated. It actually was created first because it is fast and because we assumed that we could draw any exponential patch with it. Further study provided than various analytic forms.

To determine a direct form of equation main, we set up the following series of dependencies, that then provides us with the resulting equation:

Equation analytic1: |

The direct form is thus equivalent to

Equation ypx: |

The section Series of Powers describes how can be calculated. Section analytic form 4 provides the most current revision of the algorithm.

To obtain a second analytic form we can investigate the differential of and see whether the integration is sensible.

To calculate the differential we subtract y(x) from y(x+1) and name the result d(x). d(x) will thus give us at each x position the value that should be added to the current y.

Equation diff: |

This function is an exponential, indicating that we could calculate y(x) slightly different, namely by calculating the differential in an incremental fashion and accumulating these values. Section Incremental based on differential focuses on an alternate method to compute y based on this insight. At the moment however, a differential that is an exponential begs to be integrated.

Integrating the equation of the differential gives a y-translated version of y(x), namely

Equation integral: |

At x=0, a translated version of the integral should run through y1, which gives the following

Thus, another analytic form is

Equation analytical2: |

Equation analytical2 is a tricky beast to calculate because the ratio r can be 1, in which case log(r)=0, resulting in a division by 0. Secondly this equation is made using integration and differentiation which both assumed a continuous space. For integer values this will thus not work entirely correctly. See section Analytic form 4 for a better calculation.

In this section we study how we can offset and scale the curve. That will eventually allow us to write a normalized curve that goes from y1=0 to y2=1. That normalized curve can then be solved against r and d and subsequently scaled and translated back to its wanted position.

Because it is necessary below that we denote exactly which function
we are talking about we will no longer write y(x), but rather
y(x,r,d,i) in which *x* the point is where we want to know the
value of the curve. *r* specifies the ratio multiplier. *d*
specifies the delta added with each cycle and *i* specifies the
initial condition. y(x,r,d,i) will be a non-normalized curve. The normalized curve will be called

It will be normalized such that

We now aim to understand how offsetting a curve affects the values
*r* and *d*. Or rather how we can, given a type of
y-function (called
*f*), modify *r* and *d* such that the new
function *g* is shifted along the *y* axis.

Equation wish: |

The above equation can be satisfied by simply ensuring that the
shape of *f* and *g* are the same. (This is possible because the
values at x=0 is for both curves already the same. Consequently
we will only be looking at *f'* and *g'*.

If we now set we obtain an expression for

This result allows us to write the following

Equation yshift: |

To scale an exponential patch we merely need to multiply *d* with
the required scaling factor *m*, as shown below

Once we know $r$ for an unscaled, zero-offset function, we can first scale it to reach and the offset it to start at . This is done as follows:

Equation offsetandscale: |

Given that we can scale and offset the curve any way we want, it is time to create a normalized curve that goes from over to .

To start at position 0, it suffices to set the fourth argument to *y* to 0.

To scale the curve such that after x2 steps is reached, it suffices to set *d* to the inverse of the value at
that position.

The calculation of>/p>

leads to

which thus means that

and that the equation for the unscaled curve is

Which is an interesting result because the normalized curve depends
only on *r* and no longer on *d*. It is also interesting because
scaling and offsetting is something we can do after the facts, leading
to the following third analytic form

The fact that we have an equation for and for the unscaled equation makes it possible to write the analytic form of before as a multiple of powers. Namely

In this section we determine values for *r* such that the
resulting curve goes through
the point . This
means that the normalized curve should go through
.
The y value of the last point will be called *b*. Given the
equation for the normalized curve we can write

Defining gives us

Given this value for *r* we can now scale the equation correctly and offset
it

This section explains how to calculate p(x) and was created in absence of a sensible solution. See section analytic form 4 for a more up to date solution

Equation ypx poses a slight computational challenge since we potentially have to perform x steps to calculate p its value (A Horner scheme should not be considered because of that). Factorization of the polynomial in prime numbers could be a worthwhile attempt, but is unnecessary, because it is easier to realize that all coefficients of the polynomial are 1. This makes it possible to split the polynomial in two when we want.

When the polynomial is of uneven order we can work with the even part of of the polynomial and remember the remainder

Combining these two branches gives (in which we assume that x/2 is performed as integer division).

To create a non-recursive loop of the above we work with a term (which accumulates the 1's form the odd part) and an accumulator (which calculates the current value of $y\left(x\right)$). We write an equation of the form

With start condition

when x=0. When x is even we have

which shows how we calculated the new T' and A' to go into the next step

In the odd case we have

In this case

This loop must finally be multiplied with *d*, as to obtain the correct value of *y*.

which leads to the two following insights regarding the odd and even cases.

By integrating d into A for the initial case we effectively multiply A throughout the entire chain, leaving

If we do the same for T (which starts of at 0), we see that

Consequently. It suffices to start the calculation of y as a calculation of p and set the initial value of A to d.

A last improvement on the algorithm is that the term , which can be rewritten as

Thus

Which means that, after some deductions

To determine r we use the bend factor b (the relative point through which the curve should go at midpoint (x1+x2)/2.

Then assuming that

gives us the solution for the quadratic equation

the other solution is s=1. The end result is thus that

Plugging r and d into the equation eventually leads to the following expression

which itself can be written as

Because the differential of y (Eq. diff is an exponential curve, we could consider calculating the delta at each step, relying on an incremental scheme to calculate the exponential and then add that delta to the current running envelope.

To calculate an exponential increasing/decreasing one merely needs to decide on the multiplier for each step, which would then result in the following:

At each step we would perform the following

Equation alternate: |

Below a breakdown of the differences in operation counts between equation main and and equation alternate.

Method | Main | Alternate |

Variable reads | 1 (y) | 2 (d, y) |

Constant read | 2 (r, d) | 1 (m) |

Variable writes | 1 (y) | 2 (d, y) |

Multiplication | 1 (y.r) | 1 (d.m) |

Additions | 1 (y.r + d) | 1 (y+d) |

The main difference between both is 1 variable write less, which in a very crude 'every operation is equally expensive' would mean that our presented methods is about 16% faster than the alternate.

The first verison of this document was written in April 2012. In may 2012 Anthony Liekens found an analytic solution for p(x), which resulted in an extension of this document (the fourth analytic variant).

The code below is provided after someone kept on polling me on what was wrong with the recipe at the start of the article. Then I figured out that a numbner of offsets were wrong and that it would have been too much work to actually rectify them. Given that the recipe at the start of the document was also already outdated, I opted for providing working code directly. Enjoy it. If you use this code please refer to this article in your code as well as in your applications about box. Thank you.

/** * To use this code incrementally, start with * * y = y1 * * At each step set * * y = y * multiplier + delta * * After x2 steps, y will be y2. The curve will go through ym at halfwidth. * * @author Werner Van Belle (werner@yellowcouch.org) */public double multiplier; public double delta; private double y1; private double y2; private double x2; private double dy; private double cstA; private double cstD; private double cstE; private int branch = 0; static final private double BEND_LINEAR_LOWER = 0.499999; static final private double BEND_LINEAR_UPPER = 0.500001;/** * Curve will run through (0,y1), (x2/2,ym) and (x2,y2) */public void setCoordinates(final double x2, final double y1, final double ym, final double y2) { if (y2 == y1) setCoordinatesWithBend(x2, y1, 0.5, y1); else { final double bend = ( ym - y1 ) / ( y2 - y1 ); assert(bend > 0 && bend < 1); setCoordinatesWithBend(x2, y1, bend, y2); } } public void setCoordinatesWithBend(double x2, double y1, double bend, double y2) { dy = y2 - y1; this.y1 = y1; this.x2 = x2; this.y2 = y2; if (bend>BEND_LINEAR_LOWER && bend<BEND_LINEAR_UPPER) { multiplier = 1; delta = dy / x2; branch=2; } else { final double onemb = 1 - bend; final double onembs = onemb * onemb; final double onem2b = 1- bend - bend; final double bends = bend * bend; final double s = onemb/bend; multiplier = Math.pow(s,2/x2); final double s2 = s * s; delta = ( y2 - y1 * s2 ) * ( multiplier - 1 ) / ( s2 - 1 ); cstA = ( y1 * onembs - y2 * bends ) / onem2b ; double B = dy * bends / onem2b; cstD = 2 * Math.log(s)/x2; cstE = Math.log(Math.abs(B)); if (B<0) branch=1; else branch=0; } }/** * Calculates y for a given x. X ranges from 0 to x2. * * @param x the position of which we want to know the y value * @return */public final double y(double x) { assert(x>=0); if (branch == 0) return cstA + Math.exp( cstD * x + cstE ); else if (branch == 1) return cstA - Math.exp( cstD * x + cstE ); else return y1 + x * dy / x2; } public double x(double y) { if (dy<0) { if (y>y1) severe("dy<0 but y>y1"); } else { if (y<y1) severe("dy>0 but y<y1"); } if ( branch == 0 ) return (Math.log( y - cstA) - cstE)/ cstD; else if ( branch == 1) return (Math.log( cstA - y ) - cstE)/ cstD; else // Use a linear approximation return ( y - y1 ) * x2 / dy; }

We presented a method to calculate an exponential patch quickly, using only 1 multiplication and 1 addition per sample. To make this possible, an algorithm was presented to calculate the required multiplier and delta, while at the same time providing a direct calculation of y(x).

Although, this is the essence of a fast ADSR envelope, it is clear that these patches need to be pieced together within a larger program, that will at appropriate times (when changing from Attack to Decay, or Sustain to Release) set new values for the multiplier and delta.

http://werner.yellowcouch.org/ werner@yellowcouch.org |