import matplotlib
if not hasattr(matplotlib.RcParams, "_get"):
matplotlib.RcParams._get = dict.get
10.1 Reassembly with overlap-add#
Reassembling frames into a signal is similarly straightforward. Given frames \(x_k\) extracted at hop length \(N_H\), we reconstruct an estimate \(\hat{x}\) by adding each frame back at its original position:
Definition 28 (Overlap-add)
The overlap-add reconstruction of frames \(x_k\) at hop length \(N_H\) is
Under what conditions does this round trip give perfect reconstruction, meaning \(\hat{x} = x\)? It depends entirely on the overlap, which we can see by tracking how many frames cover each sample:
Fig. 62 How overlap-add reconstructs, as a function of hop length. Only \(N_H = N_F\) covers every sample exactly once (perfect reconstruction). Larger hops leave gaps; smaller hops double-count the overlaps, changing the amplitude.#
When \(N_H = N_F\) (no overlap), the frames tile the signal exactly once, and \(\hat{x} = x\). Perfect reconstruction.
When \(N_H > N_F\), there are gaps between frames, and the samples that fall in them are simply lost.
When \(N_H < N_F\), the frames overlap, and the overlapping samples get added together more than once, boosting the amplitude.
Both building blocks are only a few lines of code. Extraction walks the signal in hops, yielding one \(N_F\)-sample frame at a time and stopping once fewer than a full frame remains:
def iter_frames(audio: pq.Audio, N_H: int, N_F: int) -> Iterator[np.ndarray]:
for start in range(0, len(audio) - N_F + 1, N_H):
yield audio.samples[start:start + N_F]
Overlap-add takes the frames stacked into a single array and walks back the other way, adding each one into an output buffer at its hop position:
def overlap_add(frames: np.ndarray, N_H: int, sample_rate: int) -> pq.Audio:
num_frames, N_F, num_channels = frames.shape
out = np.zeros((N_H * (num_frames - 1) + N_F, num_channels), dtype=frames.dtype)
for k, frame in enumerate(frames):
out[k * N_H: k * N_H + N_F] += frame
return pq.Audio(out, sample_rate)
The full runnable versions are in code/frames.py. You can hear perfect reconstruction (and break it) by playing with \(N_H\) and \(N_F\) yourself below:
# Extract frames and glue them back together. With a rectangular window and
# N_H = N_F (0% overlap) this is perfect reconstruction. Try N_H = N_F // 2
# (overlap, doubles the amplitude) or N_H = 2 * N_F (gaps) and listen!
audio = pq.Audio.from_file("./assets/audio-trio.wav")
N_F = 1024 # frame length (samples)
N_H = 1024 # hop length (samples)
frames = np.array(list(iter_frames(audio, N_H, N_F)))
print(frames.shape) # (num_frames, N_F, num_channels)
reconstructed = overlap_add(frames, N_H, audio.sample_rate)
pq.play(reconstructed)
(344, 1024, 1)