Task 5 of 5
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:
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.
amplitudeSpectrum(spec, sumW) with output: [256] — a plain number passes into a kernel like any other argument2 * Math.sqrt(re * re + im * im) / sumWrect / hann / blackman, with its raw peak and its recovered amplitude, plus a line carrying Hann's coherent and noise gainsKernel arguments do not have to be arrays. sumW arrives as a
plain number and is used like one:
const re = spec[0][this.thread.x];
const im = spec[1][this.thread.x];
return 2 * Math.sqrt(re * re + im * im) / sumW;Every window kernel here is "sample × taper", so a signal of ones makes it return the taper:
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.
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.
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.
This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.