Task 2 of 5

The Brick Wall Rings

If multiplying by a spectrum is a filter, then just choose the spectrum. Want a low-pass? Set every bin above a cutoff to zero and transform back. Nothing in signal processing is simpler, and nothing punishes simplicity faster.

What comes back is not a smoothed square wave. It is a square wave with ringing: an overshoot at every edge, plus ripples running the whole length of the buffer. Widening the cutoff does not help: it squeezes the ripple closer to the edge, but the height of that first overshoot sits at about 9% of the jump however many bins you keep — right up until you keep them all and there is no filter left. This is the Gibbs phenomenon, and it is not a bug in your arithmetic: a rectangle in frequency is a sinc in time, and a sinc rings forever.

One structural detail first, because it is the most common way this task goes wrong. The signal is real, which makes its spectrum conjugate-symmetric: bin n − k always holds the conjugate of bin k. Those upper bins are not extra high frequencies — they are the mirror halves of the low ones. Bin 250 of 256 is six bins from DC. Zero a bin without zeroing its mirror and the inverse transform comes back complex, so fold the index before you compare it to the cutoff:

const k = this.thread.x;
const dist = Math.min(k, this.constants.n - k);

The starter below forgets that fold, on purpose. Run it and look at the second log line: a real signal has come back with an imaginary part bigger than 1.

you cut the spectrum with a cliff; the cliff hands you back a ripple
Goal: finish lowPass so that every bin more than 20 bins from DC is zeroed on both planes and every other bin passes through untouched — then read the overshoot off the console.

Requirements

Hint 1 — the fold, and why

For a real signal the spectrum is a palindrome of conjugates: bin 255 is the mirror of bin 1, bin 236 the mirror of bin 20. Keeping a bin and dropping its mirror keeps half of a cosine and throws the other half away, and what is left is a complex exponential. So:

const k = this.thread.x;
const dist = Math.min(k, this.constants.n - k);
if (dist > this.constants.cut) return 0;
Hint 2 — one return per plane

The zero is the same on both planes, so it can be returned before you ever look at this.thread.y. What survives is a straight pass-through:

if (this.thread.y === 0) return spec[0][k];
return spec[1][k];
Hint 3 — reading the answer

The console prints the peak as a fraction of the wave's 2-unit jump. You are looking for about 9.2%. Gibbs' constant is 8.95%; the difference is only that our samples do not land exactly on the peak of the ripple.

Same idea elsewhere

Every real filter design is a negotiation with this fact. An ideal low-pass has an infinitely long, non-causal impulse response, so hardware and DSP libraries ship windowed approximations instead — scipy.signal.firwin, MATLAB's fir1, the biquad cascades in Web Audio. It is also why a JPEG rings around sharp edges: quantising away a block's high-frequency DCT coefficients is exactly the brick wall you just built.

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.