import matplotlib
if not hasattr(matplotlib.RcParams, "_get"):
matplotlib.RcParams._get = dict.get
9.9 Subtractive synthesis#
We opened this chapter by contrasting synthesis with processing, but filters can be a synthesis tool in their own right. Subtractive synthesis starts from a harmonically rich source (often noise or a buzzy waveform like a sawtooth) and carves away frequencies with filters to shape a timbre. It is the founding principle of the classic analog synthesizer, and the complement of the additive synthesis from Chapter 3: rather than building a sound up from sinusoids, we start with everything and subtract.
Fig. 60 Subtractive synthesis: start from a harmonically rich source (a pulse wave, with many strong harmonics), pass it through a filter (often time-varying), and the filter carves the spectrum into the desired shape. Compare this to additive synthesis, which instead builds up a spectrum from individual sinusoids.#
For these examples we use a small library of ready-made filter designs, rbj.py, which implements Robert Bristow-Johnson’s widely used “Audio EQ Cookbook” formulas [BJ16]. Each function returns the feedforward and feedback coefficients (\(b\) and \(a\)) of a second-order recursive filter (a biquad), ready to apply with SciPy’s lfilter.
Our first example filters white noise, whose spectrum is flat (equal energy at all frequencies) and therefore a perfect raw material. A low-pass version keeps only the lows for a soft “thump”, and a high-pass version keeps only the highs for a crisp “tick”. Arranging the two with a pq.Score produces a simple drum-like rhythm:
# Subtractive synthesis: carve two percussion sounds out of white noise, then
# arrange them into a rhythm with a Score.
def noise_lo(duration, **kwargs):
"""A soft "thump": low-passed noise with a fast decay."""
n = int(duration * F_S)
x = np.random.uniform(-1, 1, n)
b, a = lpf(f_c=180, Q=1.0, f_s=F_S)
y = lfilter(b, a, x)
env = np.exp(-np.linspace(0, 10, n))
return pq.Audio((0.9 * y * env).astype(np.float32), F_S)
def noise_hi(duration, **kwargs):
"""A crisp "tick": high-passed noise with a very fast decay."""
n = int(duration * F_S)
x = np.random.uniform(-1, 1, n)
b, a = hpf(f_c=6000, Q=0.8, f_s=F_S)
y = lfilter(b, a, x)
env = np.exp(-np.linspace(0, 45, n))
return pq.Audio((0.9 * y * env).astype(np.float32), F_S)
def drum(voice, duration, **kwargs):
"""Dispatch each event to the right voice."""
return noise_lo(duration) if voice == "lo" else noise_hi(duration)
# A 16-step pattern: low "thump" on the strong beats, high "tick" on every step.
beat = 0.22
kicks = {0, 4, 8, 12}
events = []
for step in range(16):
events.append((step * beat, {"voice": "hi", "duration": 0.12}))
if step in kicks:
events.append((step * beat, {"voice": "lo", "duration": 0.35}))
rhythm = pq.Score(events)
pq.play(rhythm.render(drum))
Our second example is the sound most associated with subtractive synthesis: a resonant filter sweep. We start with a bright sawtooth-like tone and pass it through a resonant low-pass filter (one with a pronounced peak at its cutoff), then move the cutoff frequency over time. As the cutoff sweeps up and down, it emphasizes different harmonics in turn.
# The classic subtractive-synthesis sound: a resonant low-pass filter whose
# cutoff sweeps over time, applied to a bright sawtooth tone. We process the
# signal block by block, redesigning the filter with a new cutoff each block.
x = sawtooth(f0=110, duration=4.0)
print("Dry sawtooth (before filtering):")
pq.play(pq.Audio(x, F_S))
block = 512
y = np.zeros_like(x)
state = np.zeros(2) # biquad filter memory, carried between blocks
for i in range(0, len(x), block):
lfo = 0.5 * (1 + np.sin(2 * np.pi * 0.4 * i / F_S)) # slow 0..1 oscillation
cutoff = 150 * (4500 / 150) ** lfo # sweep 150 Hz -> 4500 Hz
b, a = lpf(f_c=cutoff, Q=6.0, f_s=F_S) # Q=6 gives an audible resonance
y[i:i + block], state = lfilter(b, a, x[i:i + block], zi=state)
y = y / np.max(np.abs(y)) # normalize (resonance can boost the level)
print("Filtered sweep:")
pq.play(pq.Audio(y, F_S))
Dry sawtooth (before filtering):
Filtered sweep: