Task 4 of 5

Magnitude, Phase and the Mirror

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
half of every real spectrum is news you already have
Goal: write two kernels over 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.

Requirements

Hint 1 — two accumulators, one pass

Nothing 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);
}
Hint 2 — the magnitude goes last

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).

Hint 3 — checking the mirror

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.

Same idea elsewhere

Every serious FFT library sells this symmetry as an API: FFTW's 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.

All tasks in The DFT, Honestly

  1. One Bin, One Thread
  2. One Number Is Not Enough
  3. Every Bin, One Thread Each
  4. Magnitude, Phase and the Mirror
  5. There and Back Again

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