# Taper the Edges

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

You cannot make an arbitrary signal fit the window: you rarely know its
frequency in advance, and a real signal has many at once. What you *can* do is
make it end where it starts. Multiply every sample by a taper that falls to zero at
both edges, and the tiled copies join at zero with no cliff left for the transform to
explain.

The **Hann window** is the workhorse:
`w[i] = 0.5 - 0.5·cos(2πi / n)`, a single raised cosine. One multiply per
sample, no neighbours, no accumulation — this is the most trivially parallel kernel in
the course, and on a GPU it is free next to the transform that follows it.

One honest footnote, because you will meet both spellings. The *symmetric*
window divides by `n - 1` and is genuinely zero at both ends; it is the
right choice when the window is a filter's impulse response. The *periodic*
(DFT-even) window divides by `n`, so its `n` samples are exactly
one period of the cosine — which is what makes a windowed on-bin tone land on three
bins and nothing else. The two differ by under one part in a hundred and the argument
is decades old, but for spectral analysis the answer is not in doubt: divide by
`n`.

And nothing is free. The taper throws most of the signal away near the edges, so
the measured peak comes back *smaller* — worse than before, until task 5 gives
it back — and it **widens the main lobe**: one sharp bin becomes three. A
window buys dynamic range and pays for it in resolution. The next task puts numbers on
that trade.

## Goal

**Goal:** write the Hann window kernel, then measure the worst leak
with no window and with Hann on the same 8.5-cycle tone.

## Requirements

- Create `hann` with `output: [256]`, returning `signal[i] * (0.5 - 0.5 * Math.cos(2 * Math.PI * i / 256))`
- Window the signal *before* the transform — a taper applied to a finished spectrum is a different, and useless, operation
- Leak = `20 * Math.log10(worst / peak)`, where `peak` is the largest magnitude in bins 0…128 and `worst` the largest outside bins 5…12
- Log one line labelled `rect` and one labelled `hann`, each with its leak in dB, and put the Hann peak amplitude on the Hann line

## Hint 1 — the kernel body

`Math.PI` and `Math.cos` both work inside kernels,
and `this.constants.n` is already wired up:

```js
const i = this.thread.x;
const w = 0.5 - 0.5 * Math.cos(2 * Math.PI * i / this.constants.n);
return signal[i] * w;
```

## Hint 2 — and then use it

The taper goes on the samples, so it has to run *before* the
transform sees them:

```js
const tapered = measure(magnitude(spectrum(hann(offBin))));
```

If the number you get back is the rectangular one again, the windowed signal
never reached `spectrum`.

## Hint 3 — what you should see

The leak drops from about `-18 dB` to about
`-47 dB`: the worst stray bin goes from an eighth of the peak to a
four-hundredth of it. And the Hann peak amplitude comes back around
`0.42` — *further* from the true 1.00 than the unwindowed 0.65
was. That is not a mistake; it is the window's coherent gain, and task 5 divides
it out.

## Same idea elsewhere

Every real analyser windows: an `AnalyserNode` in the Web Audio API
applies a Blackman window before its FFT and does not offer you the choice, numpy ships
`hanning`/`hamming`/`blackman` as one-liners, and
MATLAB's `pwelch` defaults to Hamming. On a GPU the taper is a per-element
multiply you fuse into whatever pass already touches the samples — a
`thrust::transform` in CUDA, two lines at the top of the load in WGSL.

## Starter code

```js
// A taper the signal can end on. Then transform it.
const gpu = new GPU({ mode });

// GIVEN — the same two-plane DFT and magnitude pass as task 1.
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) {
  const re = spec[0][this.thread.x];
  const im = spec[1][this.thread.x];
  return Math.sqrt(re * re + im * im);
}, { output: [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;
  // TODO: multiply this sample by the Hann taper
  //   w = 0.5 - 0.5 * cos(2π i / n)      ← divisor n, not n - 1
  return signal[i];
}, { output: [256], constants: { n: 256 } });

function measure(mag) {
  let peak = 0;
  for (let k = 0; k <= 128; k++) if (mag[k] > peak) peak = mag[k];
  let worst = 0;
  for (let k = 0; k <= 128; k++) {
    if (k >= 5 && k <= 12) continue;
    if (mag[k] > worst) worst = mag[k];
  }
  return { db: 20 * Math.log10(worst / peak), amplitude: 2 * peak / 256 };
}

const plain = measure(magnitude(spectrum(rect(offBin))));
console.log('rect  leak:', plain.db, 'dB');

// TODO: taper offBin with hann FIRST, then transform the windowed samples.
const tapered = measure(magnitude(spectrum(rect(offBin))));
console.log('hann  leak:', tapered.db, 'dB   amplitude:', tapered.amplitude);
```

---

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

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