import matplotlib
if not hasattr(matplotlib.RcParams, "_get"):
matplotlib.RcParams._get = dict.get
13.3 Multitap delay lines#
A tapped delay line need not stop at one tap. A multitap delay line reads one delay line at several positions. For example, with taps at \(M_a < M_b < M'\):
Fig. 99 A delay line of length \(M'\) with taps at \(M_a\) and \(M_b\). The delays of the blocks between taps add up to \(M'\).#
The code below implements a multitap delay line, whose taps may vary over time and take fractional values. It returns one row of output per tap, like one channel of multichannel audio per tap:
def multitap_delay(x: np.ndarray, taps: list[np.ndarray]) -> np.ndarray:
"""Read one delay line at several time-varying, fractional delays.
Returns one row per tap, where row t is x[n - taps[t][n]]."""
L = int(np.ceil(max(M.max() for M in taps))) + 2
buffer = np.zeros(L) # the delay line, as a circular buffer
i = 0
y = np.zeros((len(taps), len(x)))
for n in range(len(x)):
buffer[i] = x[n] # write first, so that a delay of 0 reads x[n]
for t, M in enumerate(taps):
k = int(M[n])
a = M[n] - k
y[t, n] = (1 - a) * buffer[(i - k) % L] + a * buffer[(i - k - 1) % L]
i = (i + 1) % L
return y
impulse = np.zeros(8)
impulse[0] = 1.0
taps = [np.full(8, 1.0), np.full(8, 3.0), np.full(8, 6.0)] # M_a = 1, M_b = 3, M' = 6
print(multitap_delay(impulse, taps))
[[0. 1. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 1. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 1. 0.]]
Chorus#
When several people sing the same melody together, their voices never line up perfectly. Each singer’s timing, pitch, and loudness deviate slightly from the others, and these small deviations give a choir its rich, full sound. The chorus effect simulates this, by mixing a sound with several copies of itself, each delayed by a slowly varying amount:
Fig. 100 A chorus with three voices. Each voice is a copy of the input with its own time-varying delay \(M_k[n]\) and gain \(g_k\).#
A chorus is like a flanger with several delayed copies. But a chorus uses longer delays, typically \(20\) to \(30\) ms, and no feedback. A delay that changes over time also shifts the pitch of its copy slightly, because the tap moves through the delay line slightly faster or slower than the samples arrive, much like the resampling of Chapter 11. So each voice of a chorus wavers slightly in pitch, like a real singer. The code below builds a three-voice chorus from our multitap delay line, and applies it to a guitar arpeggio:
def chorus(x: np.ndarray, delays: list[float], depth: float, rates: list[float],
g: float, f_s: int) -> np.ndarray:
"""The dry signal plus one delayed copy per voice, whose delay (in seconds)
wobbles by up to `depth` seconds around its own base delay, at its own rate."""
n = np.arange(len(x))
taps = [f_s * (d + depth * np.sin(2 * np.pi * r * n / f_s)) for d, r in zip(delays, rates)]
return x + g * multitap_delay(x, taps).sum(axis=0)
guitar = pq.Audio.from_file("./assets/guitar-arpeggio.wav")
x = guitar.samples[:, 0]
wet = chorus(x, delays=[0.020, 0.025, 0.030], depth=0.003, rates=[0.35, 0.5, 0.65],
g=0.7, f_s=guitar.sample_rate)
w = wet_ratio(0.0)
pq.play(guitar, normalize=True)
pq.play(pq.Audio((1 - w) * x + w * wet, guitar.sample_rate), normalize=True)