Task 4 of 5
Two planes of numbers are rarely what you want to look at. The pair
(re, im) is a point, and its two natural readings are the magnitude
√(re² + im²) — how much of that frequency is present, immune to any shift —
and the phase atan2(im, re) — where in its cycle it started.
Task 2's delay moved the phase by a quarter turn and left the magnitude untouched; that is
the same fact, seen from the other end.
Then the part worth knowing: for a real input the spectrum is
conjugate-symmetric. Bin n − k always holds the mirror of bin k —
same magnitude, opposite phase — so the top half of every spectrum in this module has been
redundant all along. Bins 0…n/2, and only those, decide everything. That redundancy is
exactly what a real-input FFT sells when it claims to be twice as fast.
A magnitude spectrum is also what a live analyser hands you. In a page (not here — a
Web Worker has no AudioContext and no microphone, so every signal in this
module is built in the content file instead) the wiring is:
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const audio = new AudioContext();
const analyser = audio.createAnalyser();
audio.createMediaStreamSource(stream).connect(analyser);
const samples = new Float32Array(analyser.fftSize);
analyser.getFloatTimeDomainData(samples);
const magnitudes = magnitude(Array.from(samples)); // ← this task's kernel
signal, each
output: [256] — one returning the magnitude of every bin, one returning the
phase — then confirm the mirror in JavaScript and log how many bins are genuinely
independent.re and im, in one loopmagnitude returns Math.sqrt(re * re + im * im); phase returns Math.atan2(im, re)mag[k] against mag[256 - k]129Nothing needs a second loop: the cosine and the sine share an angle, so read the sample once and feed both totals.
let re = 0;
let im = 0;
for (let i = 0; i < this.constants.n; i++) {
const angle = 2 * Math.PI * this.thread.x * i / this.constants.n;
re += signal[i] * Math.cos(angle);
im -= signal[i] * Math.sin(angle);
}Take the square root after the loop, on the finished pair. Doing it
inside — accumulating √(…) per term — throws the phase away before the sum
can use it, and every bin ends up holding the same number.
Math.hypot is not on gpu.js's whitelist, so write
Math.sqrt(re * re + im * im).
A plain loop over the bottom half, comparing each bin with its partner:
let worst = 0;
for (let k = 1; k < 128; k++) {
worst = Math.max(worst, Math.abs(mag[k] - mag[256 - k]));
}
console.log('largest mirror difference:', worst);
Bins 0 and 128 are their own partners, which is why the independent count is
128 + 1 and not 128.
r2c plans, cuFFT's R2C transform and rocFFT's real-to-complex
mode all return n/2 + 1 bins and refuse to compute the rest, halving both the work and the
memory. Reading a spectrum as magnitude and phase instead of real and imaginary is the same
change of coordinates Colour Spaces makes when it trades RGB for hue and value.
This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.