Task 1 of 5
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.
magnitude with output: [256] — one thread per binspec[0][this.thread.x] and spec[1][this.thread.x]Math.sqrt(re * re + im * im)onBin / offBin, carrying the amplitude 2 * peak / 256 and the busy-bin countWith 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].
const re = spec[0][this.thread.x];
const im = spec[1][this.thread.x];
return Math.sqrt(re * re + im * im);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++;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.
This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.