In Lab 21 a steady whistle sometimes made one crisp spike and sometimes splattered across
half the display. That wasn't the microphone misbehaving — it's a real property of the
DFT, and it has a genuinely elegant fix. Let's tune in.
What You'll Build
A side-by-side demonstration of spectral leakage, then four window functions measured against
each other so you can see exactly what each one costs and buys.
Learning Objectives
Explain why the DFT assumes your window repeats forever
Identify the discontinuity that causes leakage
Apply a Hanning window and measure the improvement
Compare rectangular, Hanning, Hamming and Blackman windows
Describe the sidelobe/resolution tradeoff
Account for the amplitude a window costs you
Concepts Introduced
ID
Concept
411
Spectral Leakage Effect
412
Rectangular Window
413
Hanning Window
414
Hamming Window
415
Blackman Window
416
Main Lobe Width
417
Side Lobe Level
418
Window Tradeoff
419
Coherent Gain
420
Edge Discontinuity
421
Window Table
Background
The DFT thinks your signal loops
A DFT sees N samples and assumes they repeat forever, end joined to beginning. If the wave fits
a whole number of cycles in the window, the loop is seamless. If it doesn't, there's a jump
at the seam — and a jump is a sharp edge, full of frequencies that were never in the sound.
That's spectral leakage: energy from one true frequency spilling into neighbouring bins.
Same pure tone, same amplitude. Only the frequency moved — by half a bin. One is a clean spike;
the other contaminates ten bins.
Real sounds are never bin-exact
A bin is 50 Hz wide in Lab 21. Your whistle doesn't politely land on a multiple of 50.
So leakage isn't an edge case you occasionally hit — it's the normal situation, and
the clean spike is the rare accident.
The fix: fade the edges
A window function multiplies your samples by a curve that starts at zero, rises to one in the
middle, and falls back to zero. Now the ends do meet, and there's no seam.
# Lab 22: Windowing and Spectral Leakage## In Lab 21 a steady whistle sometimes made one clean spike and sometimes# smeared across several bars. That was not the microphone being flaky. It# is a real and unavoidable property of the DFT, and it has a fix.## The cause: the DFT assumes your window of samples repeats forever. If the# wave does not complete a whole number of cycles inside the window, the# repeat has a JUMP in it -- and a jump contains lots of frequencies that# were never in the original sound.## The fix: fade the window in and out at the edges so there is no jump.# That is a WINDOW FUNCTION.importconfigimportmathfromfftlabimportFFTN=128RATE=config.SAMPLE_RATEBIN_HZ=RATE/Nfft=FFT(N)deftone(freq,n=N):return[math.sin(2*math.pi*freq*(i/RATE))foriinrange(n)]defspectrum(signal,window=None):re,im=fft.buffers()foriinrange(N):re[i]=signal[i]*(window[i]ifwindowelse1.0)im[i]=0.0fft.run(re,im)returnfft.magnitudes(re,im)defshow(mags,label,lo=0,hi=20):peak=max(mags)or1.0print()print("--- %s ---"%label)forkinrange(lo,hi):bar="#"*int(mags[k]/peak*46)print("%4d%7.0f Hz %s"%(k,k*BIN_HZ,bar))# =========================================================================# PART 1 -- a tone that fits, and one that does not# =========================================================================print("=== PART 1: the problem ===")print("bin width = %.0f Hz"%BIN_HZ)on_bin=8*BIN_HZ# exactly bin 8off_bin=8.5*BIN_HZ# right between bins 8 and 9show(spectrum(tone(on_bin)),"%.0f Hz -- lands exactly on bin 8"%on_bin,4,14)show(spectrum(tone(off_bin)),"%.0f Hz -- falls BETWEEN bins"%off_bin,4,14)print()print("The first is one clean spike. The second smears across many bins.")print("Same purity of tone, same amplitude -- only the frequency changed.")# =========================================================================# PART 2 -- why: look at the edges# =========================================================================print()print("=== PART 2: why it happens ===")print()print("The DFT assumes your window repeats forever. Check the seam:")forlabel,fin(("on-bin ",on_bin),("off-bin ",off_bin)):s=tone(f)jump=abs(s[0]-s[N-1])print(" %s first=%+.3f last=%+.3f jump at the seam = %.3f"%(label,s[0],s[N-1],jump))print()print("The on-bin tone joins up smoothly. The off-bin one has a step in it,")print("and a step is a sharp edge -- full of frequencies that were never")print("in the sound. That is SPECTRAL LEAKAGE.")# =========================================================================# PART 3 -- window functions# =========================================================================defrectangular(n):return[1.0]*ndefhanning(n):return[0.5-0.5*math.cos(2*math.pi*i/(n-1))foriinrange(n)]defhamming(n):return[0.54-0.46*math.cos(2*math.pi*i/(n-1))foriinrange(n)]defblackman(n):return[0.42-0.5*math.cos(2*math.pi*i/(n-1))+0.08*math.cos(4*math.pi*i/(n-1))foriinrange(n)]print()print("=== PART 3: the fix ===")print()print("A window fades the samples in and out, so the ends meet at zero and")print("there is no seam. Here is the Hanning window's shape:")w=hanning(32)foriinrange(0,32,2):print(" %2d%s"%(i,"*"*int(w[i]*40)))show(spectrum(tone(off_bin),hanning(N)),"%.0f Hz WITH a Hanning window"%off_bin,4,14)print()print("Still wider than a bin-exact peak -- windows cannot work miracles --")print("but the smear is dramatically reduced.")# =========================================================================# PART 4 -- comparing windows# =========================================================================print()print("=== PART 4: which window? ===")print()print("%-14s%10s%14s%12s"%("window","peak","spread","worst sidelobe"))forname,fnin(("rectangular",rectangular),("hanning",hanning),("hamming",hamming),("blackman",blackman)):mags=spectrum(tone(off_bin),fn(N))peak=max(mags)# SPREAD: how many bins this one pure tone contaminates above 1% of its# own peak. This is the practically useful number -- it says how much of# your spectrum a single tone ruins.## (Textbooks usually quote "main lobe width" measured to the first null.# That is a cleaner theoretical quantity but it is fragile to measure on# real data, where the tail has no crisp null. We measure the thing we# actually care about instead, and label it honestly.)wide=sum(1forminmags[:N//2]ifm>0.01*peak)# worst sidelobe: the largest value well away from the peakpk=mags.index(peak)side=0.0forkinrange(N//2):ifabs(k-pk)>4andmags[k]>side:side=mags[k]db=20*math.log10(side/peak)ifside>0else-99print("%-14s%10.1f%10d bins %10.1f dB"%(name,peak,wide,db))print()print("Read that table as a TRADEOFF, not a ranking:")print(" rectangular : biggest peak, but it contaminates the whole spectrum")print(" hanning : good all-round compromise")print(" blackman : cleanest spectrum, smallest peak")print()print("Low sidelobes = spot a quiet tone sitting next to a loud one.")print("Narrow lobe = tell two CLOSE tones apart.")print("Windows buy the first by giving up a little of the second, and they")print("all cost you peak height. Choose based on what you are hunting.")# =========================================================================# PART 5 -- what a window costs you# =========================================================================print()print("=== PART 5: windows lose amplitude ===")clean=max(spectrum(tone(on_bin)))forname,fnin(("rectangular",rectangular),("hanning",hanning),("blackman",blackman)):p=max(spectrum(tone(on_bin),fn(N)))print(" %-12s peak %8.1f (%.2f of unwindowed)"%(name,p,p/clean))print()print("A window multiplies most samples by less than 1, so the total energy")print("drops. That factor is called COHERENT GAIN -- divide by it if you")print("need true amplitudes rather than just a nice-looking picture.")
Part 1 shows the on-bin and off-bin spectra back to back.
Step 2 — Find the seam
12
on-bin first=+0.000 last=-0.383 jump at the seam = 0.383
off-bin first=+0.000 last=+0.924 jump at the seam = 0.924
The off-bin tone's discontinuity is more than twice as large. That step is the leakage.
Step 3 — Apply a window
Part 3 windows the same off-bin tone. The smear collapses dramatically — though not to a single
bin. Windows reduce leakage; they don't abolish it.
Step 4 — Compare four windows
12345
window peak spread worst sidelobe
rectangular 41.9 34 bins -17.8 dB
hanning 27.0 6 bins -46.8 dB
hamming 28.2 10 bins -37.7 dB
blackman 23.5 6 bins -57.9 dB
"Spread" is how many bins one pure tone contaminates above 1% of its own peak. Rectangular
(i.e. no window) ruins 34 bins; Hanning ruins 6.
The sidelobe column is the headline: from −17.8 dB to −57.9 dB is a 40 dB improvement — a
factor of 100 in amplitude.
There is no 'best' window
Low sidelobes let you spot a quiet tone beside a loud one. A narrow main lobe lets you
separate two close tones. Windows buy the first by giving up a little of the second,
and they all cost peak height. Hanning is the sensible default; pick another when you
know which problem you have.
Step 5 — Pay the bill
123
rectangular peak 64.0 (1.00 of unwindowed)
hanning peak 31.7 (0.50 of unwindowed)
blackman peak 26.7 (0.42 of unwindowed)
A window multiplies most samples by less than one, so total energy drops. Hanning keeps about
half. That factor is coherent gain — divide by it if you need true amplitudes rather
than a nice-looking picture.
Step 6 — Predict, then measure
Prediction: add a Hanning window to your Lab 21 spectrum analyzer. What happens to the
bars while you whistle?
Try it. Precompute the window table once at startup — recomputing a cosine per sample per frame
is exactly the mistake Lab 18 taught you to avoid.
Expected Output
The leakage comparison in Part 1, the window shape in Part 3, and the two tables above.
Troubleshooting
Symptom
Likely cause
Fix
No difference from windowing
Test tone is bin-exact
Use a frequency between bins
Everything got quieter
Working as designed
That's coherent gain — see Part 5
Peak moved bins
Shouldn't happen for a strong tone
Check the window is applied elementwise
Leakage worse with a window
Window applied twice
Reset the buffer each frame
Challenges
Window your analyzer. Add Hanning to Lab 21 with a precomputed table. Does the whistle
peak look tighter?
Two close tones. Generate 800 Hz and 900 Hz together. Which window separates them best?
Now try 800 Hz loud and 1,500 Hz very quiet — does the answer change?
Correct the amplitude. Divide by the coherent gain and confirm the windowed peak matches
the unwindowed one for a bin-exact tone.
Check Your Understanding
Why does the DFT behave as if your samples repeat forever?
What exactly causes leakage for an off-bin tone?
Reading the table: which window would you pick to find a quiet tone next to a loud one?
What is coherent gain, and when must you correct for it?
Why is leakage the normal case rather than the exception with real sounds?
Your spectra just got honest
You know why peaks smear and how to tame them. Next lab we squeeze real precision out
of those peaks — enough to build a working instrument tuner.