# Measure the Trade

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

"Use a Hann window" is folklore until you can say what it costs. Two numbers
settle it, and both are in the picture above.

**Main-lobe width** is how many bins a single pure tone occupies — the
frequency resolution you have left. **Peak side-lobe level** is how far
down, in dB, the worst ripple outside that lobe sits — the dynamic range you have
bought. The rectangular window (which is all "no window" ever meant: multiply by 1 and
stop dead at the edges) wins the first and loses the second, badly. Every other window
trades one for the other, and choosing between them is the whole skill.

To see either you have to look *between* the bins, because a windowed on-bin
tone lands on integer bins where these curves are exactly zero. The standard trick is
**zero padding**: run a 1,024-point transform over a 256-sample signal, so
768 of the terms are zero. Nothing is added and nothing is lost, but the spectrum is now
sampled four times per bin. Be clear about what that buys — interpolation, never
resolution. Zero padding draws the same curve with more dots; only a longer window makes
the curve narrower.

## Figures

- **the whole trade in one picture — nobody gets both**

## Goal

**Goal:** write the Blackman window, then tabulate main-lobe width
and peak side-lobe level for rectangular, Hann and Blackman on the same on-bin tone.

## Requirements

- Create `blackman` with `output: [256]`: the sample times `0.42 - 0.5·cos(2πi / n) + 0.08·cos(4πi / n)`
- Main lobe: from the peak at grid index 128, step outward while the magnitude keeps falling — that stops on the first null. Width in bins is `2 * steps / 4`
- Peak side lobe: `20 * Math.log10(worst / peak)`, with `worst` the largest magnitude from that null out to grid index 512
- Log one line per window, labelled `rect` / `hann` / `blackman`, carrying its width in bins and its side-lobe level in dB

## Hint 1 — the Blackman kernel

Three cosine terms, and the third runs at double the frequency — so name
the angle once and the whole window is one line:

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

One gpu.js trap while you are in here: do not name a kernel local after one of
your own constants. `const n = this.constants.n;` transpiles to
`const constants_n = constants_n;` and the kernel dies with "cannot
access before initialization".

## Hint 2 — walking out to the first null

A main lobe falls monotonically to its first zero, and only then do the
side lobes start climbing again. So keep stepping while the next sample is smaller
than this one:

```js
let i = peakAt;
while (mag[i + 1] < mag[i]) i++;
const bins = 2 * (i - peakAt) / 4;   // 4 grid points per bin
```

## Hint 3 — what the table should say

Widths of `2`, `4` and `6` bins, and side
lobes at roughly `-13`, `-32` and `-58 dB`. Read
it as a menu: three extra bins of width buy 45 dB of dynamic range. Hamming, not
asked for here, sits between Hann and Blackman at 4 bins and about
`-41 dB`.

## Same idea elsewhere

Those two numbers are the datasheet entry for every window in every DSP
library — Harris's 1978 survey tabulates a few dozen of them and is still the paper
people cite. Choosing one is a real engineering decision: a spectrum analyser hunting a
-60 dB spur beside a strong carrier needs Blackman's side lobes and can afford the
width, while a pitch tracker separating two close partials wants Hann, or no window at
all.

## Starter code

```js
// Two numbers decide which window you want. Measure both.
const gpu = new GPU({ mode });

// GIVEN — a 1024-point transform over the same 256 samples: 768 implied zeros,
// so the spectrum comes out sampled 4x per bin. Interpolation, not resolution.
const paddedSpectrum = 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.nfft)) / this.constants.nfft;
    re += x[i] * Math.cos(angle);
    im += x[i] * Math.sin(angle);
  }
  if (this.thread.y === 0) return re;
  return im;
}, { output: [1024, 2], constants: { n: 256, nfft: 1024 } });

const paddedMagnitude = gpu.createKernel(function (spec) {
  const re = spec[0][this.thread.x];
  const im = spec[1][this.thread.x];
  return Math.sqrt(re * re + im * im);
}, { output: [1024] });

// 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] });

// GIVEN — the Hann window from the last task.
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;
  // TODO: w = 0.42 - 0.5·cos(2πi/n) + 0.08·cos(4πi/n), times the sample
  return signal[i];
}, { output: [256], constants: { n: 256 } });

function analyse(windowed) {
  const mag = paddedMagnitude(paddedSpectrum(windowed));
  const peakAt = 32 * 4;        // the tone sits on bin 32, 4 grid points per bin
  const peak = mag[peakAt];

  // TODO 1: step out from peakAt while the next magnitude is smaller than this
  //         one. Where you stop is the first null.
  let i = peakAt;

  // TODO 2: main-lobe width in bins = 2 * (i - peakAt) / 4
  const bins = 0;

  // TODO 3: worst = the largest magnitude from i out to grid index 512
  let worst = 0;

  return { bins, db: 20 * Math.log10(worst / peak) };
}

for (const [label, windowed] of [
  ['rect    ', rect(signal)],
  ['hann    ', hann(signal)],
  ['blackman', blackman(signal)],
]) {
  const r = analyse(windowed);
  console.log(label, 'main lobe:', r.bins, 'bins   peak side lobe:', r.db, 'dB');
}
```

---

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

[Previous task](https://gpu.rocks/learn/windowing-f563138d/3.md) · [Next task](https://gpu.rocks/learn/windowing-f563138d/5.md)
