ADSR Envelopes in Silverlight Audio Synthesis

posted: November 16, 2009

The latest part of the Silverlight audio synthesis library I’ve been working on is ADSR (attack-decay-sustain-release) envelopes. An ADSR envelope is used to change how an instrument sounds over time and is commonly used in digital audio synthesis to modulate different components of a waveform - including its loudness or frequency. Most commonly, this means an ADSR envelope is used to control how a sound fades in, sustains, and fades out. These envelopes can also be applied to frequency or amplitude modulation and thus alter characteristics such as timbre.

So how does one go about implementing a simple ADSR envelope in the Silverlight audio synthesis space? It’s not too difficult if you are comfortable with basic linear algebra. Before I dive in to the math, let’s look at a visual representation of an envelope that has three components: attack, sustain, and release. I’m eliminating the “decay” parameter here for simplification:

ASR

In this simple example, the x axis is “time” and the y axis is an arbitrary value - lets say the y axis is “loudness” on a scale of 0% to 100%. The envelope above describes a sound that fades in quickly, reaches and maintains its full volume, then fades out gradually.

In 16-bit digital audio synthesis, given a sample rate (e.g. 44,100 samples/sec), we can assume that each two-byte sample we create equates to a constant measure of time (1 sample ~ 0.00002226 seconds, actually). Thus, we can use a sample count to keep track of the duration of time over an envelope. Let’s look at the example graph again, but this time let’s put some concrete sample durations on the x-axis:

asr-x

The example above describes an envelope that has an attack duration of 1000 samples, a sustain duration of 2500 samples, and a release duration of 2000 samples.

Now, let’s get back to that linear algebra. Do you remember this equation from school?

y = Mx + B

This is a linear equation used to calculate the vertical position along a line at any given point. The value M is the slope, or rise over run (the vertical distance traveled by the line divided by the horizontal distance traveled by the line). The B component is the “starting value” of the function when x = 0.

The envelope in these graphs is composed of three lines. The “sustain line” we can ignore for now - it is a horizontal line that has a constant value. The interesting lines are the “attack line” and the “release line”. Using a linear equation we can define a function to calculate the attack and release values at their respective sample points.

Assume our maximum y value is 1. If A equals the attack duration, S equals the sustain duration, and R equals the release duration:

  • When x ≤ A, y = (1 / A) * x + 0
  • When x > A and x ≤ (A + S), y = 1
  • When x > (A+S), y = (-1 / R) * x + 1

Notice that the “attack” and “release” equations use the y = Mx + B forms while the “sustain” equation is a constant value of 1.

We can easily apply these functions and create an Envelope class for an Oscillator’s volume. Perhaps the most obvious difference from the easy example is that we need to account for the fact that volume is typically expressed in negative decibels (dB). For example, we want our Oscillator’s envelope to have a dynamic range from -80 dB (virtually inaudible) to 0 dB (full volume).  It’d be nice to define an Oscillator’s envelope in the c# programming language like this:

Envelope envelope = new Envelope() { 
  Attack = 1000, 
  Sustain = 2500, 
  Release = 2000, 
  Start = -80, 
  Rise = 80 
};

This would define an envelope with attack, sustain, and release values of 1000, 2500, and 2000 respectively. The envelope would start at a value of -80 and have a maximum value of zero. The “Rise” property signifies the range of envelope values. A Rise value of 70 would result in envelope values from -80 to -10.

To use the envelope within an Oscillator which has a GetSample() method that is called to generate each audio sample:

public short GetSample()
{
  ushort shortPhaseAngle = (ushort)(this.phaseAngle >> 16);
  phaseAngle += this.increment;
  short sample = 0;

  //assume a wave table of system.int16 samples
  sample = WaveTable.SineWaveForm[shortPhaseAngle];
  this.Attenuation = this.Envelope.GetNextValue();
  sample = this.Attenuate(sample);
  return sample;   
}

The sample calculation code above is adapted from Charles Petzold’s Simple SilverlightSequencer.

Notice the use of this.Envelope.GetNextValue() to automatically produce the next Attenuation (e.g. volume) value for each sample point. It’s pretty easy to use for us simple programmer-folk.

The Envelope’s GetNextValue method looks like this:

public double GetNextValue()
{
  this.counter++;
  if (counter >= (this.Attack + this.Sustain))
  {
    this.releaseCounter++;
    return this.ProcessRelease();
  }
  else
  {
    if (counter < this.Attack)
      return this.ProcessAttack();
    else
      return this.Rise + this.Start;
  }
}

double ProcessAttack()
{
  //y = mx + b
  return (double)counter 
    this.attackSlope + this.Start;
}

double ProcessRelease()
{
  //y = mx + b
  double result = (double)this.releaseCounter * 
       this.releaseSlope + this.Start + this.Rise;
  return result > this.Start ? result : this.Start;
}

The attackSlope and releaseSlope values are calculated whenever the Envelope’s Attack, Release or Rise properties are changed:

void UpdateAttackSlope()
{
  this.attackSlope = this.Rise / (double)this.Attack;
}

void UpdateReleaseSlope()
{
  this.releaseSlope = -this.Rise / (double)
    ((this.release + this.Sustain + this.Attack) - 
      (this.Attack + this.Sustain));
}

This Envelope code is able to support a variety of envelope types including ones that are percentage based (e.g. from 0.00 to 1.00), negatively ranged (e.g. -80 to 0), positively ranged (e.g. 0 to 100), or both (e.g. -50 to 50).

I’ve tested this envelope implementation with volume, frequency modulation amplitude, and amplitude modulation amplitude. The effects are pretty cool - especially once you get more than one oscillator playing at the same time. I hope to have some sample audio up sometime soon.