Envelope Generators
Musical instruments make sounds that vary in volume over the duration of the sound, and synthesized sounds do the same. A typical sound has an initial rise time, called the attack, a steady state portion (sustain), and a final segment that decays to silence. The final decay segment is called the release. The term release is adopted from analog synthesizers where the envelope generator was typically triggered by a key press. When the key was released, the final decay began. Unlike hardware systems, software envelope generators are not required to have a fixed number of segments, and there may not be a sustain segment at all. A typical envelope is shown in the following graph.
The graph shows the peak amplitude of the signal over time, and is referred to as the amplitude envelope. The peak amplitude has both positive and negative values, and if we were to graph the entire signal and connect the peak amplitude values, the resulting line would form a border, or "envelope" around the sound. Since our waveform values are both positive and negative, we only need to calculate the positive side of the envelope and then multiply by the waveform values.
Applying an amplitude envelope to the signal can be achieved if we set the peak amplitude with a series of line segments that calculate the amplitude based on the beginning and ending values over the duration of the segment. We can represent an envelope attack or decay with the equation for a line y = ax + b. The value for a is the range and is calculated as the end value of the segment minus the beginning value divided by the number of samples in the segment: a = (start - end) / duration. The value for b is the starting offset and is usually 0 for the attack and the peak value for the release.
totalSamples = duration * sampleRate; a = (ampEnd – ampStart) / totalSamples; for (n = 0; n < totalSamples; n++) volume = (n * a) + ampStart;
However, we can do a little better. If we convert the equation to the recursive form we can replace the multiplication with incremental addition. The recursive form is yn = x + yn-1 The value of x is calculated as the range of the segment divided by the number of samples in the segment. The value of y is initialized to the starting value. To produce our envelope generator we convert the duration of the attack and decay times into samples, and then calculate the increment value at the beginning of the respective segments. For each sample, we multiply the amplitude by the waveform value for that sample to produce our output value.
totalSamples = duration * sampleRate; attackTime = attackRate * sampleRate; decayTime = decayRate * sampleRate; decayStart = totalSamples – decayTime; peakAmp = 1.0; startAmp = 0.0; endAmp = 0.0; volume = startAmp; envInc = (peakAmp – startAmp) / attackTime; for (n = 0; n < totalSamples; n++) { if (n < attackTime || n > decayStart) volume += envInc; else if (n == decayStart) { envInc = endAmp – volume; if (decayTime > 0) envInc /= decayTime; } else volume = peakAmp; sample[n] = volume * sin(phase); }
A basic envelope can be produced with only attack, sustain and release segments, but we often need to produce more complex envelopes. For example, a traditional synthesizer uses a four segment ADSR type envelope like the one shown below.
Because we are generating the envelope in software, we can add as many segments as desired with little additional processing. One way to do this is to expand the previous examples to add more segments, comparing additional time ranges for each. A more general purpose solution is to define a set of segments where each segment is specified by a level and duration stored in an array. When the duration of the current segment is finished, we move to the next array entry.
envCount = 0; envIndex = -1; endVolume = 0; for (n = 0; n < totalSamples; n++ { if (--envCount <= 0) { volume = endVolume; if (++envIndex < maxEnvIndex) { endVolume = envLevel[envIndex]; envCount = envTime[envIndex]; envInc = endVolume - volume; if (envCount > 0) envInc /= envCount; } else { envInc = 0; } } else { volume += envInc; } sample[n] = volume * sin(phase); }
Note that this code sets the current volume to the previous ending volume at the transition to a new segment. This is done in part to adjust for any round-off error due to the incremental calculation of the volume. This is not always necessary or desired, however. If the envelope generator is desgined to respond to live performance input, some signal would be used to skip over intermediate segments when the note-off signal is received. In that case, the release segment would start at the current volume level in order to avoid a jump in the amplitude level.
Synthesis systems typically include envelope generators that produce curved segments in place of straight lines. Curved segments often have a more natural sound, although the difference can be subtle and almost imperceptible. Linear segments are faster to calculate, but, if desired, we can replace the line function with an exponential function to produce curved envelope segments.
|