Task 5 of 5

Multiplying Spectra Convolves in a Circle

Task 1 had an accident built into it: the signal occupied 20 of 64 samples and the filter 5, so the 24-sample answer had room to spare. Take the room away and the theorem bites.

Multiplying two length-n spectra does not produce the convolution you want. It produces the circular convolution — the one that treats both buffers as loops, so whatever runs off the end comes back on at the beginning:

circular[i] = linear[i] + linear[i + n]

Here sig is 32 samples and filt carries 9 taps, so the honest answer is 32 + 9 − 1 = 40 samples long. Multiply their 32-bin spectra and the last 8 samples fold back onto the first 8, corrupting them by as much as 0.37 against a signal whose largest value is 0.66. The signature is unmistakable once you have seen it: the tail of the answer appears at its head.

The fix is to give the answer somewhere to live. Zero-pad both buffers to at least n + m − 1 before transforming — 40 here, and since every real FFT wants a power of two, 64. The overhang then lands on padding instead of on your data. The starter transforms at 32 and prints the damage; fix PAD and the guard inside pad, and the error should collapse to float32 rounding — a few millionths on a GPU, a few hundred-thousandths on a software renderer.

This is also where the cost story gets honest. Padding to a power of two means transforming 64 points to convolve 32 with 9 — and 9 taps is short enough that the sliding window wins outright, on any hardware. Frequency-domain convolution starts paying somewhere around a few dozen taps, and exactly where depends on the machine. The Benchmark button will price this pipeline for you on both backends; Measuring Speed Honestly owns the rest of the methodology, including why a single timing is a rumour.

eight samples with nowhere to go, and they do not go quietly
Goal: pick a PAD long enough to hold the whole answer, and finish pad so it copies the first 32 samples and zeroes the rest.

Requirements

Hint 1 — how long is the answer?

Convolving n samples with m taps produces n + m − 1 samples: the filter's last tap is still hanging over the signal's last sample m − 1 steps after the signal has ended. With 32 and 9 that is 40. Any transform length of 40 or more works; 64 is what you would use in practice, because that is what an FFT wants.

Hint 2 — the guard

Two lines, and the if matters — a ternary would still describe an out-of-range read to the GLSL compiler:

if (this.thread.x < this.constants.src) {
  return src[this.thread.x];
}
return 0;

filt already arrives in a 32-sample buffer with 9 taps and 23 zeros, so the same kernel pads it correctly with no special case.

Hint 3 — what wrapping looks like

Before you fix it, compare the first eight samples the starter prints against the last eight of direct. They are the same numbers. That is not a coincidence and it is not rounding — it is the tail of the convolution, delivered to the front of the buffer.

Same idea elsewhere

Every FFT convolution in production carries this bookkeeping. cuFFT, FFTW and scipy.signal.fftconvolve all pad internally and hand back the n + m − 1 answer; numpy.fft does not, which is why the "why is my filtered audio clicking at the block boundaries" question outlives every generation of programmers. Streaming filters solve the same problem with overlap-add or overlap-save: pad each block, convolve, and add the overhang into the next block instead of throwing it at the start of this one.

All tasks in Filtering in the Frequency Domain

  1. Convolution Becomes Multiplication
  2. The Brick Wall Rings
  3. Trade the Ringing for a Softer Edge
  4. A Tone Buried in Noise
  5. Multiplying Spectra Convolves in a Circle

This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.