ADSR envelope implementation with a playable example
Four stages, and why two of them are exponential and one is not.
Anyone whose notes click on or never end.
A keyboard tells you when a note starts and stops. An envelope decides how its loudness moves between those events. Without an envelope, switching an oscillator on and off can create abrupt edges that sound like clicks.
This ADSR has four stages: attack rises to full level, decay falls toward the sustain level while the key is held, and release fades after the key is released. Sustain is a level; the other three controls are times.
Controls#
| Parameter | Default | Range | Meaning |
|---|---|---|---|
attack | 0.010 s | 0.001–2 s | Linear rise time from zero to full level. |
decay | 0.25 s | 0.01–4 s | Time for the distance from sustain to fall by 60 dB. |
sustain | 0.40 | 0–1 | Level approached while a key remains held. |
release | 0.45 s | 0.01–4 s | Time for the released level to fall by 60 dB. |
The input streams follow the CLI note layout: pitch in Hz, vel, modWheel
and gate. The mod wheel is declared to preserve that slot order; this patch
does not use it. The browser keyboard finds pitch, velocity and gate by name.
out is the mono voice.
The complete patch#
processor AdsrVoice
{
input stream float pitch;
input stream float vel;
input stream float modWheel;
input stream float gate;
param float attack = 0.010f [0.001f, 2.0f] smooth 5.0f;
param float decay = 0.25f [0.01f, 4.0f] smooth 5.0f;
param float sustain = 0.40f [0.0f, 1.0f] smooth 10.0f;
param float release = 0.45f [0.01f, 4.0f] smooth 5.0f;
output stream float out;
float env = 0.0f;
float attacking = 0.0f;
float prevGate = 0.0f;
float phase = 0.0f;
float freq = 220.0f;
float amp = 0.8f;
void main()
{
loop
{
let sr = processor.frequency;
let attackTime = max(attack, 0.0005f);
let decayTime = max(decay, 0.005f);
let releaseTime = max(release, 0.005f);
let attackInc = 1.0f / (attackTime * sr);
let decayCoef = exp(-6.907755f / (decayTime * sr));
let releaseCoef = exp(-6.907755f / (releaseTime * sr));
if (gate > 0.5f)
{
freq = pitch;
amp = vel;
}
let rising = gate * (1.0f - prevGate);
prevGate = gate;
attacking = rising > 0.5f ? 1.0f : attacking;
if (gate > 0.5f)
{
if (attacking > 0.5f)
{
env = env + attackInc;
if (env > 1.0f)
{
env = 1.0f;
attacking = 0.0f;
}
}
else
{
env = sustain + (env - sustain) * decayCoef;
}
}
else
{
env = env * releaseCoef;
attacking = 0.0f;
}
let step = freq / sr;
let p = phase + step;
phase = p > 1.0f ? p - 1.0f : p;
out <- sin(phase * 6.2831853f) * env * amp * 0.7f;
advance();
}
}
}Follow a note from press to release#
On a rising gate, attacking becomes 1. The envelope adds
1 / (attackTime * sampleRate) every frame, reaching full level in the
requested attack time when it starts from zero. Retriggering during an existing
tail starts from the current level, so the remaining attack is shorter.
Once it reaches 1, the envelope approaches sustain. The equation
sustain + (env - sustain) * decayCoef shrinks the remaining distance each
frame. On release, env * releaseCoef instead approaches zero. A 60 dB fall
means one thousandth of the initial amplitude, not mathematical zero.
The oscillator keeps running while the envelope falls. The patch retains its
last frequency and velocity after the gate drops, so the release is a fading
tone. Testing pitch > 0 for note-off would be wrong for a host that holds
pitch through release; that is why the gate is a separate input.
Try it#
To compare decay, set attack to 0.01 and sustain to 0.1, then play a fresh note with decay at 0.05 and another at 2.0. Decay changes the fall after the attack, not a note that has already settled at sustain. If sustain is 1, there is no fall to hear at all. Use a held computer key so your mouse is free to adjust the sliders.
Release is different: compare its settings by letting go of the note. Keep sustain above zero so there is some level left to fade. The Stop button cuts the entire runner immediately; it does not trigger release.
Hold a note with a two-second attack and a low sustain level. You can hear the
rise, then the fall to sustain. Release it with release at two seconds and
listen for the tail. Next, retrigger before the tail ends and notice that the
attack begins from the remaining level.
If a note never releases: check that your host updates the gate input.
Changing pitch alone is not a note-off message. This patch is monophonic; use
node arrays to give several voices separate
envelopes.
Next: a delay, which is the same idea applied to time instead of amplitude.