窗:拿主瓣换旁瓣
上一章的诊断已经很清楚了:所有的泄漏都来自「矩形窗」这个形状——你在第 0 秒和第 1 秒处各切了一刀,而那两刀是宽频的。既然病因是形状,药方就只有一个:换个形状。这一章列出药单和价目表,包括那个能把旁瓣从 −13.3 dB 压到 −93.0 dB 的选项。
一段 1024 点的录音,采样率 1000 Hz。里面有两个东西:
- 一个强音,100.5 Hz(故意挑在两根谱线中间,泄漏最严重的位置)
- 一个弱音,120 Hz,振幅只有强音的 千分之一(也就是低 60 dB)
问:直接做变换(也就是用矩形窗),120 Hz 那个位置上,弱音会比「强音泄漏过来的本底」高多少 dB?
药方:把两头压到零
上一章说,泄漏来自窗口边缘那两个突然的跳变。那就别让它突然:
拿一条中间高、两头缓缓落到零的曲线,去乘你的数据。这样接到窗口边缘的时候,信号已经淡出了,那一刀切在零上——没有跳变,也就没有宽频的东西被造出来。
最常见的那条曲线叫汉宁窗(Hann window),它就是一个抬高了的余弦:
w[n] = 0.5 − 0.5 · cos(2πn / (N−1)) n = 0 … N−1 n=0 时 w = 0 ← 左端严格为零 n=(N−1)/2 w = 1 ← 中间为一 n=N−1 w = 0 ← 右端严格为零 用法:把数据逐点乘上 w,再做变换。就这一步。
本机把这个式子和 numpy.hanning(512) 逐点比过,最大差 4.4 × 10⁻¹⁶;汉明窗和布莱克曼窗同样逐点相同。这一章后面所有的数字,都是用这套与标准实现完全一致的窗算出来的。
价目表
换窗不是免费的。把五种常用窗的四项指标全量出来(512 点窗,64 倍过采样,指标定义见下):
| 窗 | 主瓣全宽 | 最高旁瓣 | 扇贝损失 | 相干增益 |
|---|---|---|---|---|
| 矩形(什么都不做) | 2.0 格 | −13.3 dB | −3.92 dB | 1.0000 |
| 汉宁 Hann | 4.0 格 | −31.5 dB | −1.42 dB | 0.4990 |
| 汉明 Hamming | 4.0 格 | −42.7 dB | −1.75 dB | 0.5391 |
| 布莱克曼 Blackman | 6.0 格 | −58.1 dB | −1.09 dB | 0.4192 |
| 平顶 Flat-top | 10.0 格 | −93.0 dB | −0.01 dB | 0.2152 |
从上往下读这张表,一条清清楚楚的交易线:
主瓣越宽 ←────────────────────→ 旁瓣越低 (分辨率越差) (泄漏越少) 矩形 2 格 / −13.3 dB 分得最开,糊得最厉害 汉宁 4 格 / −31.5 dB 主瓣翻倍,换来旁瓣低 18 dB(8 倍) 布莱克曼 6 格 / −58.1 dB 主瓣三倍,换来旁瓣低 45 dB(180 倍) 平顶 10 格 / −93.0 dB 主瓣五倍,换来旁瓣低 80 dB(一万倍)
窗函数只有一个作用:在「分辨率」和「泄漏」之间选一个位置。没有免费的选项,也没有一个万能的窗。
为什么必然是跷跷板,可以在直觉上说清:窗两头压得越狠,边界越平滑,旁瓣越低;但压狠了之后,真正参与计算的有效数据长度就变短了——汉宁窗的相干增益只有 0.4990,意思是它实际上只用上了半个窗的能量。而第 7 章说过,有效时长变短,分辨率就变差。低旁瓣是拿有效观测时长换来的。
把开头那个问题量出来
回到那个「强音旁边藏了一个弱 60 dB 的音」的场景。三种窗各跑一遍:
强音 100.5 Hz,弱音 120 Hz(低 60 dB),1024 点 窗 120 Hz 处读到 强音泄漏的本底 弱音高出本底 ──────────────────────────────────────────────────────────── 矩形 −45.6 dB ⟨−47.4 dB⟩ ⟨ 1.8 dB⟩ ✗ 基本被埋 汉宁 −60.1 dB −101.5 dB 41.4 dB ✓ 布莱克曼 −60.1 dB −108.9 dB 48.9 dB ✓
三件事值得看清楚。
第一,矩形窗那一行是灾难。弱音只比本底高 1.8 dB,在任何有噪声的真实数据里,这等于不存在。而且更坏的是:读到的 −45.6 dB 比真值 −60 dB 高了 14 dB——你不但看不见它,就算凑巧注意到了,量出来的大小也是错的。
第二,加窗之后读数变准了。汉宁窗读到 −60.1 dB,真值是 −60 dB。差的那 0.1 dB 是弱音自己的扇贝损失。
第三,布莱克曼比汉宁只多买到 7.5 dB。它的旁瓣低了 27 dB,但在这个场景里没换来同等的收益——因为 120 Hz 离 100.5 Hz 有 19.5 个格子远,汉宁窗的旁瓣在那么远的地方已经衰减得很低了。越贵的窗不是越好,要看你的弱信号离强信号有多近。
表里四个指标,各管一件事,选窗的时候要看的是不同的列:
- 主瓣全宽:两个等强的音要分开,至少得差这么多格。「我要分辨两个频率」→ 看这一列,选小的。
- 最高旁瓣:一个强音能在多远处伪造出多高的假峰。「我要在强音旁边找弱音」→ 看这一列,选低的。
- 扇贝损失:频率碰巧落在两格中间时,峰值读数会偏低多少。「我要量准振幅」→ 看这一列,选接近 0 的(平顶窗 −0.01 dB)。
- 相干增益:加窗把整体幅度缩小了多少倍。读振幅之前必须除掉它,否则汉宁窗会让你的读数系统性偏小一半。这是加窗最常见的一个实现 bug。
三句话的选窗指南:分辨两个频率用汉宁;在强信号旁边挖弱信号用布莱克曼或更狠的;测振幅用平顶。什么都不知道的时候,汉宁是最安全的默认值——这也是几乎所有软件把它设为默认的原因。
把价目表和掩蔽实验都跑出来:
import numpy as np
def metrics(w, over=64):
N = len(w); M = N*over
sp = np.abs(np.fft.fft(w, M)); peak = sp[0]
top = 0
while top+1 < M//2 and sp[top+1] >= sp[top]: top += 1 # 平顶窗的顶是平的
i = top+1
while i < M//2 and sp[i] < sp[i-1]: i += 1 # 走到第一个零点
return (2*(i-1)/over,
20*np.log10(sp[i:M//2].max()/peak),
20*np.log10(sp[over//2]/peak))
for name, w in [('矩形', np.ones(512)), ('汉宁', np.hanning(512)),
('汉明', np.hamming(512)), ('布莱克曼', np.blackman(512))]:
m, s, sc = metrics(w)
print(f'{name:<5} 主瓣 {m:4.1f} 格 旁瓣 {s:6.1f} dB 扇贝 {sc:5.2f} dB')
掩蔽实验(这个更有说服力,因为你会亲眼看到那个弱音「浮出来」):
N, fs = 1024, 1000
n = np.arange(N)
x = np.cos(2*np.pi*100.5*n/fs) + 0.001*np.cos(2*np.pi*120*n/fs)
for name, w in [('矩形', np.ones(N)), ('汉宁', np.hanning(N))]:
m = np.abs(np.fft.fft(x*w))
k = round(120*N/fs)
print(name, '120 Hz 处 %.1f dB' % (20*np.log10(m[k]/m.max())))
python3 -c "import numpy as np;print(np.hanning(8))"
在线跑:Google Colab。想不写代码就看到效果:Audacity 里「分析 → 绘制频谱」,右下角有个窗函数下拉框,切换它,同一段音频的谱底会整个升降。Harris 1978 那篇经典综述把三十多种窗的指标全列了,上面这张表的数字和它对得上。
- 频谱分析仪上那个下拉框。任何一台仪器、任何一个音频分析软件,都有这个选项,而且默认多半是汉宁。现在你知道那个框在卖什么了。
- 音频编码器里的窗。MP3 和 AAC 用的 MDCT 有一个额外的硬要求:相邻两个窗必须能无缝拼回原信号(叫完美重构条件),所以它们用的是专门设计的正弦窗或 KBD 窗,而不能随便挑一个旁瓣低的。这是这一章的交易之外的第三个约束。
- 雷达的旁瓣抑制。一架大飞机的回波旁瓣可以完全盖住旁边一架小飞机。军用雷达对旁瓣的要求经常到 −60 dB 以下,用的是这一章表里更狠的窗(Kaiser、Dolph–Chebyshev),代价是分辨率变差、检测距离变短。
- OFDM 的循环前缀。这是同一个问题的另一种解法:与其把两头压到零,不如把信号的尾巴复制到头上,让这一段在窗口内看起来是严格周期的——那样就回到了第 8 章「情况 ①」那个巧合,泄漏严格为零。代价是浪费了一部分传输时间(典型 7%–25%)。加窗和加循环前缀,是同一个账单的两种付法。
「加窗是为了让频谱看起来干净一点,属于美化。数据分析要严谨,就应该用原始数据,不加窗。」
「不加窗」这个选项不存在。只要你的数据是有限长的,你就已经加了一个窗——矩形窗。而矩形窗恰好是这张表里旁瓣最高、扇贝损失最大的那一个。
所以真正的选择不是「加不加窗」,而是「用哪个窗」。默认不动,等于主动选了最差的那个。上面那个 1.8 dB 就是这个默认值的价钱。
反过来说,也确实有该用矩形窗的场合,而且理由很硬:当你的信号在窗口里严格周期(比如你自己控制了激励频率,让它整除窗长),矩形窗是最优的——泄漏严格为零(第 8 章那个 8.9 × 10⁻¹² ),分辨率还最好。仪器测试里的「相干采样」就是专门去凑这个条件的。
判据是:问一句「我这段数据的两端接得上吗」。接得上(首尾连成一个周期),用矩形;接不上(绝大多数真实数据),加窗。
正确答案是 C:只高 1.8 dB,基本被埋。换成汉宁窗是 41.4 dB。
A 「它就在那儿,一根干净的线」——这个直觉假设了频谱是线性叠加的,而且各管各的。前半句对(傅里叶确实线性),后半句错:强音在这份数据里不是一根线,它是一整片摊开的东西(第 8 章:99% 能量摊在 41 根谱线上),而 120 Hz 正落在这片东西上面。 B 「有点泄漏但还是明显冒头」——量级估错了。矩形窗的旁瓣从 −13.3 dB 起步、按 1/k 慢慢衰减,在 19.5 格之外还剩下 −47.4 dB。而弱音本身只有 −60 dB。60 dB 是个很大的动态范围(一千倍),而矩形窗的旁瓣能吃掉其中的四十多 dB。 D 「完全看不见」——差一点就对了,但还是差了 1.8 dB。这 1.8 dB 恰恰是最坑人的地方:它确实冒了一点头,所以你会在图上看到 120 Hz 处有个小凸起,然后你会认为自己「看见了」这个弱音——但你量出来的高度是 −45.6 dB,比真值高了 14 dB。看得见但量不准,比完全看不见更容易做出错误结论。这一章是这本书里第一次出现明码标价的交易,而它的结构会在后面重复出现:你想在频域里得到一个更好的形状,就必须在时域里付出一个动作,而那个动作总是「让信号变得不那么理想」。
第 15 章设计滤波器时会看到一模一样的表(那里叫「过渡带 vs 阻带抑制」,用的还是同一批窗);第 11 章切段做频谱图时会看到它的时间版本。整个卷 III 讲的都是这一件事:频域里没有免费的形状。
这一章的一句话
窗函数只做一件事:在分辨率和泄漏之间挑一个位置——主瓣宽一倍,换旁瓣低一个数量级;而「不加窗」这个选项不存在,你只是默认选了最差的那个。
这一章的交易还是可以谈的:多花点钱(更宽的主瓣)就能买到更低的旁瓣。下一章会遇到一个谈不了的——不管你换什么窗、用什么算法、花多少算力,有一个乘积你永远压不下去。本机拿四个宽度差了十倍的高斯脉冲量了一遍,它们的 σt × σf 全都等于 0.079577,一位不差。那个数是 1/4π,而它有一个更出名的名字。