# A Tone the Window Does Not Fit

*Task 1 of 5 · [Windowing & Spectral Leakage](https://gpu.rocks/learn/windowing-f563138d.md) · GPU.js Learn*

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.

## Goal

**Goal:** write the magnitude kernel, then log for each signal its
peak amplitude and how many of bins 0…128 carry more than 1% of the peak.

## Requirements

- Create `magnitude` with `output: [256]` — one thread per bin
- Read both planes of your own bin: `spec[0][this.thread.x]` and `spec[1][this.thread.x]`
- Return `Math.sqrt(re * re + im * im)`
- Log one line per signal, labelled `onBin` / `offBin`, carrying the amplitude `2 * peak / 256` and the busy-bin count

## Hint 1 — which bin is mine?

With `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]`.

## Hint 2 — the kernel body

```js
const re = spec[0][this.thread.x];
const im = spec[1][this.thread.x];
return Math.sqrt(re * re + im * im);
```

## Hint 3 — the measuring, in plain JavaScript

Only bins 0…128 matter: above Nyquist the spectrum of a real signal is
just the mirror image.

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

## Same idea elsewhere

Split real/imaginary planes are how every serious FFT ships: cuFFT and rocFFT
expose both interleaved (`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.

## Starter code

```js
// Two tones, one window. Only one of them fits.
const gpu = new GPU({ mode });

// GIVEN — the naive DFT, in this track's two-plane form:
//   output: [256, 2]  →  result[0][k] = real part of bin k
//                        result[1][k] = imaginary part of bin k
// (k * i) % n keeps every angle inside one turn: a float32 cos() has lost
// four digits by the time its argument reaches a thousand radians.
const spectrum = gpu.createKernel(function (x) {
  const k = this.thread.x;
  let re = 0;
  let im = 0;
  for (let i = 0; i < this.constants.n; i++) {
    const angle = (-2 * Math.PI * ((k * i) % this.constants.n)) / this.constants.n;
    re += x[i] * Math.cos(angle);
    im += x[i] * Math.sin(angle);
  }
  if (this.thread.y === 0) return re;
  return im;
}, { output: [256, 2], constants: { n: 256 } });

const magnitude = gpu.createKernel(function (spec) {
  // TODO: |X[k]| = sqrt(re² + im²), with re = spec[0][k] and im = spec[1][k].
  return 0;
}, { output: [256] });

function report(label, signal) {
  const mag = magnitude(spectrum(signal));
  // TODO: peak      = the largest magnitude in bins 0…128
  //       amplitude = 2 * peak / 256
  //       busy      = how many of bins 0…128 exceed 0.01 * peak
  console.log(label, 'amplitude:', 0, 'busy bins:', 0);
}

report('onBin ', onBin);
report('offBin', offBin);
```

---

Interactive version: https://gpu.rocks/learn/windowing-f563138d/1

[Next task](https://gpu.rocks/learn/windowing-f563138d/2.md)
