import matplotlib
if not hasattr(matplotlib.RcParams, "_get"):
matplotlib.RcParams._get = dict.get
4.3 Envelopes#
We’ve seen that we can combine scores with timbres to produce richer music. But there’s one problem. In a musical score, events are finite in duration: you pluck a string, and the sound decays away after some time. The synthesis techniques of Chapter 3, however, produce tones of theoretically infinite duration: a sum of sinusoids just keeps going. Envelopes bridge this gap, taking us from infinite sustained tones to finite sound events.
Fig. 11 A plucked guitar string (from Chapter 3). Top: the raw waveform, with a zoomed inset revealing its quasi-periodic oscillation. Bottom (upper half only): a smooth curve tracing the waveform’s peak amplitude, its envelope, alongside a piecewise-linear approximation of that envelope.#
Consider the plucked string above. The zoomed inset reveals the quasi-periodic behavior we’d expect from the synthesis of Chapter 3. But zoomed out, the waveform has a distinct shape: its peak amplitude rises sharply, then decays. If we trace an outline around the waveform’s peak amplitude, we get a curve that “envelopes” the oscillation within. If we could synthesize such a curve and multiply it by an oscillator, we could turn an infinite tone into a finite event. As the bottom panel suggests, even a simple piecewise-linear shape captures the essence.
A formal view#
If sound is a function \(x(t) : \mathbb{R} \to \mathbb{R}\) mapping time to amplitude, an envelope is a function
specifying an amplitude attenuation factor at each point in time, where 0 means silence and 1 means no attenuation. Crucially, an envelope is zero outside some finite window \((a, b)\):
We apply an envelope to a sound by simple multiplication: \(x(t) \cdot \text{Envelope}(t)\). Because the envelope is zero outside \((a, b)\), the product is also zero there, regardless of how \(x(t)\) behaves. This accomplishes our goal of turning a potentially infinite sound into a finite one.
A 220 Hz sine before and after applying an attack/decay envelope.
Fig. 12 Top: the oscillator \(x(t)\), a 220 Hz sine. Middle: an attack/decay envelope (\(a_\text{dur} = 0.1\) s, \(d_\text{dur} = 0.9\) s). Bottom: their product. The product’s amplitude is bounded by the envelope (dashed), and it fades to silence at both ends.#
Piecewise-linear envelopes#
Envelopes are often described by piecewise-linear functions, parameterized by a set of control points \((t_1, a_1), (t_2, a_2), \ldots, (t_P, a_P)\). Between consecutive control points, the envelope interpolates linearly. Outside the first and last control points, it typically is assumed to take on the edge values: \(a_1\) if \(t \leq a_1\), or \(a_P\) if \(t \geq t_P\). Accordingly, for most envelopes, \(a_1 = a_P = 0\).
The simplest useful envelope has two segments and a single interior control point: an attack that rises linearly from 0 to a peak, followed by a decay that falls back to 0. We can write it with control points \((0, 0)\), \((a_\text{dur}, 1)\), and \((a_\text{dur} + d_\text{dur}, 0)\):
In code, we can express any piecewise-linear envelope compactly with np.interp, which handles the segment-by-segment interpolation for us:
def adenv(a_dur: float, d_dur: float, N: int, n: int = 0) -> np.ndarray:
t = (n + np.arange(N)) / F_S
env = np.interp(
t, [0.0, a_dur, a_dur + d_dur], [0.0, 1.0, 0.0]
)
return env[:, np.newaxis]
The trailing [:, np.newaxis] reshapes the result to (N, 1) so that, recalling the (num_samples, num_channels) convention from Chapter 2, the envelope broadcasts cleanly across the channels of a pq.Audio when we multiply by adenv(...). Extending this to an arbitrary number of control points is left as an exercise to the reader.
Fig. 13 The output of adenv(0.1, 0.9, ...) over one second: a 0.1 s attack to the peak, then a 0.9 s decay. The three control points are marked.#
A 220 Hz sine multiplied by adenv(0.1, 0.9, ...), producing a finite note. The full code is in code/envelope.py.
The widget below builds the same envelope from its two durations. Drag the attack and the decay, and listen to how the note starts and ends.
Drag the attack and decay. The top panel is the envelope with its three control points in gold, and the bottom panel is the 220 Hz tone multiplied by it. The audio card plays the current shape. Pull the attack all the way down and you will hear the click that a sudden start produces.