Task 1 of 5
The discrete Fourier transform asks one question per bin: how much of this signal looks like a wave that fits exactly k whole cycles into the buffer? Answering it is a sum over every sample, and every bin's sum is independent of every other one — so it is one thread per bin, each pulling the whole signal. The answer is a complex number: how much, and at what phase.
gpu.js has no complex type, so this track carries complex data as two planes of
floats. output: [n, 2] is read result[p][i] — plane
p = 0 is the real part, plane 1 the imaginary part, i the bin. (A
2D output [w, h] is indexed [y][x], so [n, 2] gives
exactly [plane][bin].) What you usually plot is the magnitude,
√(re² + im²): how much of that frequency is present, phase discarded.
Two signals here. orderAB plays a 256 Hz tone and then a quieter 640 Hz
one. orderBA plays the same two tones the other way round — it is
orderAB read backwards. Transform both. The two spectra will not merely look
alike; they agree to the last bit, and that is a theorem rather than a coincidence:
reversing a signal conjugates its spectrum, and conjugating does not change a magnitude.
A spectrum knows what is in a signal. It has no idea when.
this.constants.n.output: [256, 2] — 256 bins across, two planes; the result is read spectrum[plane][bin]this.constants.n samples with angle = -2 * Math.PI * k * i / this.constants.nre += signal[i] * Math.cos(angle) and im += signal[i] * Math.sin(angle)this.constants.n, and return re on plane 0, im on plane 1this.thread.x is the bin (0…255) and this.thread.y is
the plane (0 or 1). Both threads of a bin do the same sum and each keeps half
of it — wasteful, and at 512 samples not worth caring about. An FFT is the version that
stops paying for it.
for (let i = 0; i < this.constants.n; i++) {
const angle = (-2 * Math.PI * k * i) / this.constants.n;
re += signal[i] * Math.cos(angle);
im += signal[i] * Math.sin(angle);
}
re = re / this.constants.n;
im = im / this.constants.n;The forward transform winds clockwise: the angle is negative. Drop the minus and every bin comes back conjugated — the imaginary plane flips sign while the magnitudes stay exactly right, which is what makes this one so good at hiding.
.real/.imag views all lay complex data
out, and it is what you just wrote. Nobody ships a naive DFT, though: it is O(n²), and the
FFT rebuilds it as a log₂ n ladder of butterflies — the same halving-ladder shape the
Reductions module climbs, driven by a JS loop over a stride-taking kernel. At n = 512 the
naive version is 262,144 multiply-adds, which a GPU eats without noticing.
This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.