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.