# Give the Amplitude Back

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

Task 3 left a loose end. The Hann window collapsed the leak by 29 dB and also
dropped the measured amplitude from 0.65 to 0.42 — *further* from the truth than
before. That is neither a bug nor a mystery. The window multiplies the signal by
something whose average is exactly 0.5, so the peak comes back exactly half size.
Divide it back out and you are finished.

The number to divide by is the window's **coherent gain**,
CG = Σw / n — the taper's mean value, 1 for rectangular, 0.5 for Hann, 0.42 for
Blackman. Folding it into the amplitude formula from task 1 leaves something pleasantly
compact:

```js
amplitude = 2 · |X[k]| / Σw
```

Miss it and every amplitude your analyser reports is low by a constant factor — 50%
for Hann, 58% for Blackman. That is exactly the kind of bug that survives for years,
because a clean scale error never looks broken.

Its sister number is the **noise gain**, Σw² / n, which is 0.375 for
Hann. That one is for *power*: the mean square of a broadband noise floor scales
with the sum of the squares, not with the square of the sum. Amplitude of a tone → Σw.
Power of a noise floor → Σw². Reach for the wrong one and you are wrong by a factor
nothing will ever flag, so it is worth carrying both.

And a tidy way to get either: hand your window kernel a signal of 256 ones, and it
hands you back the window.

## Goal

**Goal:** build a calibrated amplitude spectrum and recover the true
amplitude of one tone through all three windows.

## Requirements

- Create `amplitudeSpectrum(spec, sumW)` with `output: [256]` — a plain number passes into a kernel like any other argument
- Return `2 * Math.sqrt(re * re + im * im) / sumW`
- Get each window's samples by running its kernel on a signal of 256 ones, then sum them and their squares
- Log one line per window, labelled `rect` / `hann` / `blackman`, with its raw peak and its recovered amplitude, plus a line carrying Hann's coherent and noise gains

## Hint 1 — a scalar argument

Kernel arguments do not have to be arrays. `sumW` arrives as a
plain number and is used like one:

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

## Hint 2 — the window, from the window kernel

Every window kernel here is "sample × taper", so a signal of ones makes it
return the taper:

```js
const w = hann(flat);            // flat = new Array(256).fill(1)
let sumW = 0;
let sumW2 = 0;
for (let i = 0; i < w.length; i++) {
  sumW += w[i];
  sumW2 += w[i] * w[i];
}
```

Which also gives the rectangular case for free: `rect(flat)` is 256
ones, so its `sumW` is 256.

## Hint 3 — what you should see

Three raw peaks — `76.8`, `38.4`,
`32.256` — and one amplitude, `0.600`, three times over.
Hann's coherent gain is `0.5` exactly and its noise gain
`0.375` exactly: a periodic Hann window sums to n/2 and its squares to
3n/8, which doubles as a check that your window is the periodic one.

## Same idea elsewhere

Coherent gain is why a home-made analyser and a real one disagree by a
constant: scipy's `welch` takes `scaling='spectrum'` versus
`'density'` precisely to pick Σw versus Σw² normalisation, and every vendor's
spectrum-analyser manual carries a window table with both columns. On the GPU it is one
scalar uniform folded into a pass you already run — the cheapest correctness fix in this
module, and the most commonly skipped.

## Starter code

```js
// Three windows, three different peaks, one true amplitude.
const gpu = new GPU({ mode });

// GIVEN — the transform and the windows you have already built.
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 } });

// The rectangular window: multiply by 1 and stop dead at the edges. It is
// still a window — just the one that does nothing but chop.
const rect = gpu.createKernel(function (signal) {
  return signal[this.thread.x];
}, { output: [256] });

const hann = gpu.createKernel(function (signal) {
  const i = this.thread.x;
  const w = 0.5 - 0.5 * Math.cos(2 * Math.PI * i / this.constants.n);
  return signal[i] * w;
}, { output: [256], constants: { n: 256 } });

const blackman = gpu.createKernel(function (signal) {
  const i = this.thread.x;
  const a = 2 * Math.PI * i / this.constants.n;
  const w = 0.42 - 0.5 * Math.cos(a) + 0.08 * Math.cos(2 * a);
  return signal[i] * w;
}, { output: [256], constants: { n: 256 } });

const amplitudeSpectrum = gpu.createKernel(function (spec, sumW) {
  // TODO: 2 * |X[k]| / sumW — task 1's magnitude pass, calibrated.
  return 0;
}, { output: [256] });

const flat = new Array(256).fill(1);

function report(label, window, windowed) {
  // TODO: sumW = the sum of the window, sumW2 = the sum of its squares
  const sumW = 256;
  const sumW2 = 256;

  const amp = amplitudeSpectrum(spectrum(windowed), sumW);
  let peak = 0;
  for (let k = 0; k <= 128; k++) if (amp[k] > peak) peak = amp[k];

  console.log(label, 'raw peak:', peak * sumW / 2, 'amplitude:', peak);
  return { sumW, sumW2 };
}

report('rect    ', rect(flat), rect(signal));
const h = report('hann    ', hann(flat), hann(signal));
report('blackman', blackman(flat), blackman(signal));
console.log('hann gains — coherent:', h.sumW / 256, 'noise:', h.sumW2 / 256);
```

---

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

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