JOVANA
Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

加窗與頻譜洩漏:把 FFT 看清楚

你把一個乾淨的正弦波餵進 [[ee-fast-fourier-transform|FFT]],期待看到一根孤零零的高峰,結果卻得到一片橫跨數十個頻格、糊成一團的山脈。沒有東西壞掉——你只是遇到了**頻譜洩漏**,這是透過有限時間窗觀察訊號時無可避免的副作用。本篇解釋它為何發生,以及窗函數如何馴服它。

第一次跑真實 FFT 的失望

在第 4 階之後,你已經會算 FFT 並讀它的幅度頻譜。於是你做一個*應該*是最簡單的實驗:一個純正弦波,比方說 1000 Hz,乾淨取樣,餵進 1024 點轉換。課本理論承諾你會看到一根單一脈衝——只有一個頻格亮起,其餘全部寂靜。你按下執行,得到的卻不是一根針,而是一座凹凸不平的小山:峰值確實在 1000 Hz 附近,但兩側可以清楚看到向下延伸 30、40、50 dB 的裙襬,溢進相鄰的頻格。它看起來像雜訊,但它不是。

線索藏在你*選了哪個*頻率。選一個剛好落在頻格中心的頻率,那根尖峰就會回來,乾淨俐落。把它從中心挪開幾分之一個頻格,裙襬立刻重新爆開。轉換並沒有對你的訊號說謊——它誠實地回報了一個你沒意識到自己創造出來的訊號:那是你截取一段有限樣本的瞬間所製造出來的。

為什麼有限區塊有看不見的邊緣

這裡有一個能解決一切的心智模型。當你取 N 個樣本去跑 DFT,數學上隱含地假設這 N 個樣本是一個無限重複訊號的一個週期。轉換把你的區塊頭尾相接,鋪成一個無限迴圈。如果你的正弦波在區塊內剛好完成整數個週期,最後一個樣本就會平滑地接到下一個複本的第一個樣本——迴圈天衣無縫,你就得到一條乾淨的線。這叫做對齊頻格(bin-aligned)或相干頻率。

但真實世界的單音幾乎永遠不會落在頻格中心。假設你的 1024 點區塊在 48 kHz 下涵蓋約 21.3 ms,頻格間距為 48000/1024 ≈ 46.9 Hz。一個 1000 Hz 的單音落在第 21.33 格——*夾在*兩格之間。此時區塊內含有 21.33 個週期。最後一個樣本停在波形的半途,而下一個複本卻從零開始,於是接縫處出現一個突然的跳變,一道每個區塊重複一次的垂直懸崖。轉換看到這道懸崖,並忠實地把它分解。一個尖銳的不連續富含諧波,於是你那個單一單音的能量就被噴灑到一大片頻格上。那片噴灑*就是*洩漏。

Bin-aligned (5.0 cycles in block)      Off-bin (5.3 cycles in block)

  copy A  |  copy B                       copy A  |  copy B
 /\  /\  /| /\  /\  /\                    /\  /\  /|
/  \/  \/ |/  \/  \/  \                  /  \/  \/ |\
----------+----------                   ---------+ +--------
          ^                                      ^ ^
     seam is SMOOTH                        seam has a JUMP
     -> one clean bin                      -> energy leaks everywhere
左:整數個週期可以無縫循環。右:非整數的週期數在每個區塊邊界留下一道懸崖——FFT 會把這道懸崖分解成寬頻的洩漏。

矩形窗與它醜陋的旁瓣

不管你有沒有意識到,每一次普通的 FFT 其實都已經套了一個窗:矩形窗(boxcar),它把區塊內每個樣本乘以 1,區塊外全部乘以 0。問題出在方波在頻域長什麼樣。它的轉換是一個 sinc 形狀的圖案:一個高聳的主瓣,兩側排著一列衰減緩慢的旁瓣——矩形窗的第一個旁瓣只比峰值低約 −13 dB,而旁瓣以懶洋洋的每八度 6 dB 速率下降。那些旁瓣,正是你看到的裙襬。

為什麼 −13 dB 這麼要命?想像你的訊號裡有兩個單音:一個 1000 Hz 的大聲單音,和一個 1200 Hz、弱了 25 dB 的小聲單音。那個大聲單音的旁瓣,停在 −13 dB 又緩慢衰減,*比整個小聲單音還高*。小聲單音被它大聲鄰居的洩漏裙襬埋住,完全看不見。在音訊、射頻、振動與雷達工作中,這是每天都會碰到的危險:來自強載波的洩漏,蓋掉了你真正在乎的弱訊號。

把邊緣收尾:Hann 與 Hamming 窗

解方直接源自診斷。接縫處的懸崖來自訊號被突兀地切斷。所以不要乘上一個硬邦邦的方波,而是把你的 N 個樣本乘上一個在兩端平滑淡出為零的窗函數。現在第一個與最後一個樣本被輕柔地拉到零,重複迴圈裡的接縫變得連續,懸崖消失了——大部分洩漏也隨之消失。這就是加窗:一次逐樣本的相乘,便宜到不行,在 FFT 之前施加。

兩個升餘弦窗主宰著日常實務。Hann 窗(常被誤稱為 Hanning)是一個單一的餘弦隆起,w[n] = 0.5·(1 − cos(2πn/(N−1))),在兩端剛好碰到零。它的第一個旁瓣降到約 −31 dB,旁瓣以 18 dB/八度的速度快速滾降——非常適合獵捕弱音。Hamming 窗,w[n] = 0.54 − 0.46·cos(2πn/(N−1)),形狀幾乎相同,但兩端不完全降到零。那一點點的基座抵消了最近的旁瓣到約 −43 dB,是兩者中最好的第一旁瓣,代價是遠處的滾降較慢。

# Apply a Hann window before the FFT (Python / NumPy)
import numpy as np

N      = 1024
sig    = np.sin(2*np.pi*1000*np.arange(N)/48000)   # 1 kHz, off-bin

win    = np.hanning(N)             # raised-cosine taper, 0 -> 1 -> 0
spec_r = np.abs(np.fft.rfft(sig))        # rectangular: ugly skirts
spec_w = np.abs(np.fft.rfft(sig*win))    # windowed: skirts collapse

# Coherent-gain fix: a Hann window throws away ~half the energy.
# Scale the magnitude back up so amplitudes read correctly:
spec_w *= 2.0 / np.sum(win)        # 1/mean(win) = 1/0.5 = 2.0
整個技巧只是多一行——乘上窗——再加一個增益修正,讓峰值高度仍然有意義。

你永遠逃不掉的取捨

加窗不是免費的魔法——它是一筆交易。把邊緣收尾會壓制旁瓣,但同時也會加寬主瓣。矩形窗的主瓣約 1 個頻格寬(量到第一個零點是 2 個頻格)。Hann 窗的主瓣大約*兩倍*寬;Hamming 也差不多。所以殺死洩漏的這個動作本身會把峰值糊開,兩個矩形窗*剛好*能分辨的單音,在 Hann 窗下可能合併成一個胖隆起。你用頻率解析度,換來了動態範圍。

Window         Main-lobe width   Highest side-lobe   Side-lobe roll-off
-----------    ---------------   -----------------   ------------------
Rectangular    1.0  bins (best)       -13 dB (worst)        6 dB/oct
Hamming        ~1.4 bins             -43 dB                6 dB/oct
Hann           ~1.5 bins             -31 dB               18 dB/oct (best)
Blackman       ~1.7 bins (worst)     -58 dB                18 dB/oct

           narrow main lobe <----------------> low side-lobes
          (sharp resolution)                  (wide dynamic range)
順著表格往下走,主瓣變寬而旁瓣下降。沒有任何一個窗能同時贏得兩欄——這就是解析度對洩漏取捨的一圖總結。
  1. 只是想在乾淨訊號裡找單音?Hann 開始——快速衰減旁瓣的全能好手。
  2. 要在強音旁邊獵捕藏起來的弱音?選用低旁瓣的窗,例如 Blackman 或 Kaiser,並接受較寬的主瓣。
  3. 要分開兩個頻率極度接近的單音?保持主瓣狹窄——維持矩形(或接近矩形)並忍受裙襬。
  4. 要量一個單音的絕對功率?先加窗以阻止洩漏把能量偷到鄰居,再套用該窗的相干增益修正。

綜合起來:像專家一樣讀頻譜

退一步看,整條鏈就講得通了。連續波透過取樣變成 離散時間訊號;取樣早就要求你遵守 Nyquist(第 3 階)。接著你截取一段有限區塊——而這個截取動作悄悄地乘上了一個窗。FFT 回報的是*訊號 × 窗*的頻譜,在頻域中就是你的真實頻譜與窗的響應做迴旋。你看到的每一個峰,都是窗的形狀,停在一個真實單音上面。一旦你把這點內化,「亂糟糟」的頻譜就不再是個謎,而是一份你能讀懂、能信任的量測。

這份直覺也是下一階的基礎。設計一個加窗-sinc 低通濾波器,*正是同一個操作*反過來跑:你拿一個理想的磚牆響應,反轉換成無限長的脈衝響應,然後把它加窗到有限長度——而你在這裡對抗的旁瓣,會以濾波器阻帶漣波的形式重新出現。現在把窗學透,FIR 濾波器設計就會像老朋友一樣親切。