Task 1 of 5

A Tone the Window Does Not Fit

Two tones, same amplitude, same length, the same 256-sample window. One difference: onBin completes exactly 8 cycles inside the window and offBin completes 8.5. Transform both and one comes back as a single clean spike, while the other smears across the whole spectrum with a peak lower than the amplitude actually in the signal. That smear is spectral leakage, and this module exists so that it never fools you.

The transform is written for you below — it is the naive DFT, and it follows this track's convention for complex numbers. gpu.js has no complex type, so a spectrum is two planes of floats: output: [n, 2], indexed result[p][k], where plane p = 0 holds the real part of bin k and plane p = 1 the imaginary part. (A 2D output [w, h] is indexed [y][x], so [n, 2] is exactly [plane][bin].) Your job is the pass after it: one thread per bin, turning two planes into one magnitude.

Then measure both tones rather than taking my word for it. Two numbers each: the peak amplitude — a real cosine of amplitude A splits its energy between bin k and its mirror above Nyquist, so A = 2·|X| / n — and how many of the 129 bins up to Nyquist hold more than 1% of that peak.

Goal: write the magnitude kernel, then log for each signal its peak amplitude and how many of bins 0…128 carry more than 1% of the peak.

Requirements

Hint 1 — which bin is mine?

With output: [256] there are 256 threads and this.thread.x is the bin number. The spectrum handed in is two rows: row 0 is every bin's real part, row 1 is every bin's imaginary part. So your own bin's two halves are spec[0][this.thread.x] and spec[1][this.thread.x].

Hint 2 — the kernel body
const re = spec[0][this.thread.x];
const im = spec[1][this.thread.x];
return Math.sqrt(re * re + im * im);
Hint 3 — the measuring, in plain JavaScript

Only bins 0…128 matter: above Nyquist the spectrum of a real signal is just the mirror image.

let peak = 0;
for (let k = 0; k <= 128; k++) if (mag[k] > peak) peak = mag[k];
let busy = 0;
for (let k = 0; k <= 128; k++) if (mag[k] > 0.01 * peak) busy++;

Same idea elsewhere

Split real/imaginary planes are how every serious FFT ships: cuFFT and rocFFT expose both interleaved (cufftComplex) and planar layouts, WebGPU compute pipelines almost always pick planar because a storage buffer of f32 is what the hardware wants, and the magnitude pass you just wrote is one length() call in WGSL or HLSL.

All tasks in Windowing & Spectral Leakage

  1. A Tone the Window Does Not Fit
  2. What the Transform Actually Sees
  3. Taper the Edges
  4. Measure the Trade
  5. Give the Amplitude Back

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