ADSR Envelope

In this post, I show one way of implementing a control envelope in Kronos. It is the classic ADSR envelope, where ADSR stands for attack, decay, sustain and release.

Overview

ADSR envelopes are used to adjust the percussive characteristics of sound. While short attack time corresponds to sharp, percussive onsets, a longer attack gives a smooth, gradual impression. Conversely, release time corresponds to how suddenly the sound ends, while decay can be used to further emphasize the onset in a sustained sound.

Most often, ADSR is associated with the amplitude of the sound, but can be used with any control parameter to good effect - tone and pitch are common examples.

attack decay sustain release Attack-Decay-Sustain-Release envelope

Implementation

In Kronos, the ADSR can be implemented as a stateful unit generator. A gate signal controls the envelope: when the gate rises, the envelope is triggered, proceeding to the sustain stage. Whenever the gate is lowered, the release stage is activated.

Typically we want the release stage to interact with the earlier stages, so that if a key is released during the attack phase, the release action starts immediately, rather than going through the entire envelope.

That is why we are going to model the envelope as a product of two simple envelopes: a triggerable, infinitely sustaining ADS-envelope, and a release envelope created by slew limiting the gate signal to achieve gradual release.

envelope gate & release Attack-Decay-Sustain envelope with slew-limited Gate

Source Code

We implement the ADS envelope as an array of rates and targets; during each stage, the envelope will proceed towards the next target at a rate specific to that stage. First, we need to compute rates and targets from the control parameters:

Attack = 0.3 Decay = 0.3 Sustain = 0.4 Release = 0.3 ; use control rate for the envelope. ; substitute Audio:Signal for audio rate envelopes. tick = Control:Signal ; number of control frames per second fps = Reactive:Rate(tick(0)) ; attack and decay rate per frame ar = 1 / (Attack * fps) dr = 1 / (Decay * fps) ; build envelope segments as tuples of rate and initial y-value env = [(ar 0) (dr 1) (0 Sustain)]

Attack and decay are usually specified as durations, and here we convert them to rates according to the control signal clock rate. The y-values of the envelope are fixed to 0, 1 and the specified sustain level.

ADS

Next, we create a signal processor that iterates through the stages. As parameters, we pass a trigger signal, as well as a list of segments, such as env from the prior listing. Triggering works by pulling the envelope position back to zero whenever trigger is true-valued.

bpf(trigger segments) { ; retrieve envelope segment (rate current) = Select(segments position) (_ target) = Select(segments position + 1) ; position increases by 'rate', but is brought to ; zero by the bitwise and operator (&) when 'trigger' ; flag is high position = z-1(tick((position + rate) & Not(trigger))) ; compute current value with linear interpolation current + (position - Floor(position)) * (target - current) }

We can cook a retrig signal with an oscillator and a comparator. Please note that the gaps in the signal are not due to instant release, rather, the envelope being re-triggered on every frame and stuck at the beginning of the attack phase!

Import Gen ; Use a comparator to cook a trigger signal and resample to control rate trigger = tick(Wave:Saw(0.5) > 0) snd = Wave:Saw(110) * bpf(trigger env)

Release envelope

So far so good. We still want to add a gradual release segment overlaid on top of the ADS envelope. Please recall that our control signal specification is a rectangular pulse with the duration of the note. We want the ADS-envelope to shape that pulse, and a gradual fade after the pulse.

If we derive the total envelope as the product of the ADS-envelope and the gate pulse itself, a graceful release behavior can be obtained by simply slew-limiting the downward edge of the pulse. The upward edge of the pulse is wired to the trigger mechanism of the ADS.

We'll cook a gate signal, much like in the triggering example, but now let's use varying threshold to get notes of different durations. This time the gate signal value should also be 0 and 1 instead of false and true; we accomplish that simply by bitwise And (&) with the constant 1.

slew(gate release-rate) { ; limit the downward slope by preventing the value from ; going below previous value minus the release rate. current = Max(gate z-1(current - release-rate)) slew = current } ; varying-width pulse wave for triggering the envelope gate = tick(Wave:Saw(1) < Wave:Saw(0.1)) & 1 ; Release envelope via slew-limiting the downward slope of the gate signal rel-env = slew(gate 1 / (Release * fps)) snd = Wave:Saw(110) * rel-env ; Retrigger mask is obtained by detecting upward edges of the gate signal ads = bpf(gate > z-1(gate) env) ; Total envelope is the product of the ADS envelope and the Release envelope snd = Wave:Saw(110) * ads * rel-env

Code Summary

Finally, the ADSR envelope as a function wrapping the lower level constructs above.

; uses slew and bpf ADSR(gate attack decay sustain release) { ; use control rate for the envelope. tick = Control:Signal ; number of control frames per second fps = Reactive:Rate(tick(0)) ; attack, decay and release rate per frame (ar dr rr) = Algorithm:Map(d => 1 / (d * fps) attack decay release) ; build envelope segments as tuples of rate and initial y-value env = [(ar 0) (dr 1) (0 sustain)] ; ensure 'gate' ticks at control rate cgate = tick(gate) ; compute envelope as a product of ADS and release envelopes bpf(cgate > z-1(cgate) env) * slew(cgate rr) } ; use simple parabolic shape for the amplitude coefficient e = ADSR(gate 0.05 0.15 0.5 0.3) snd = Wave:Saw(110) * e * e