# Magnitude, Phase and the Mirror

*Task 4 of 5 · [The DFT, Honestly](https://gpu.rocks/learn/the-dft-7b1e3f9b.md) · GPU.js Learn*

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:

```js
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
```

## Figures

- **half of every real spectrum is news you already have**

## Goal

**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

- Both kernels keep **two** accumulators, `re` and `im`, in one loop
- `magnitude` returns `Math.sqrt(re * re + im * im)`; `phase` returns `Math.atan2(im, re)`
- Check the mirror in JavaScript: `mag[k]` against `mag[256 - k]`
- Log the count of independent bins — 0…128 inclusive, so `129`

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

```js
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:

```js
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.

## Starter code

```js
// One thread per bin, two accumulators inside it.
const gpu = new GPU({ mode });

const magnitude = gpu.createKernel(function (signal) {
  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);
    // TODO: accumulate the imaginary part as well — it SUBTRACTS
    // signal[i] * Math.sin(angle).
  }
  // TODO: return the magnitude of the finished pair, not just re
  return re;
}, {
  output: [256],
  constants: { n: 256 },
});

const phase = gpu.createKernel(function (signal) {
  // TODO: the same two accumulators, but return Math.atan2(im, re)
  return 0;
}, {
  output: [256],
  constants: { n: 256 },
});

const mag = magnitude(signal);
const ph = phase(signal);
console.log('peak bin 12:', mag[12], 'phase', ph[12]);

// TODO: compare mag[k] with mag[256 - k] across the bottom half, and log how
// many bins are genuinely independent (bins 0 … 128 inclusive).
```

---

Interactive version: https://gpu.rocks/learn/the-dft-7b1e3f9b/4

[Previous task](https://gpu.rocks/learn/the-dft-7b1e3f9b/3.md) · [Next task](https://gpu.rocks/learn/the-dft-7b1e3f9b/5.md)
