Task 2 of 5
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.
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.output: [256, 2]; the incoming spectrum is read as spec[0][k] and spec[1][k]Math.min(k, this.constants.n - k) — never the raw index0 on both planes when that distance exceeds this.constants.cutFor 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;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];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.
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.
This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.