卷 II · 只看几眼CH 08深度 8/20

一个纯音是怎么糊成一片的

这一章处理第 2 章埋的那个伏笔:拿 3.5 圈/秒去缠一段根本没有 3.5 Hz 的信号,机器报了 0.2315。这个数不是误差,也不是噪声——它是你「只看一段」这个动作本身的产物。而它能有多大,是这一卷最难看的一笔账。

★★ 1 根 → 41 根泄漏不是噪声−13.26 dB

▷ 先猜一下

1000 个采样点,采样率 1000 Hz(也就是记录长度正好 1 秒)。信号是一个绝对纯净的余弦,没有噪声,没有其他成分,双精度浮点。

做一次变换,然后问:装下 99% 的能量,需要多少根谱线?

两种情况:
① 余弦的频率是 100 Hz(在这一秒里正好转 100 整圈,正好落在谱线上)
② 余弦的频率是 100.5 Hz(正好落在两根谱线的正中间)

A 都是 1 根。信号是纯的,谱当然也是纯的 B 1 根 / 3 根。挪半格会让能量分给旁边两根,仅此而已 C 1 根 / 41 根 D 1 根 / 500 根。挪半格之后能量会均匀撒满整个频谱

先看两张单子

情况 ①  f = 100 Hz(正好整数圈)

  最高的那根谱线        500.0
  其余 499 根里最大的    8.9 × 10⁻¹²        ← 浮点意义上的零
  装下 99% 能量需要      1 根

情况 ②  f = 100.5 Hz(挪了半格)

  最高的那根谱线        319.0               ← 峰值本身矮了 ⟨−3.90 dB⟩
  装下 99% 能量需要     ⟨41 根⟩
  峰两侧各一格之外还剩   ⟨9.9%⟩ 的能量

频率只挪了 0.5 Hz(相对量 0.5%),99% 能量的落点从 1 根谱线摊到了 41 根,峰值本身还矮了 36%。

这里最要命的一点是:信号本身一点都没变脏。它还是那个绝对纯净的余弦。多出来的四十根谱线,全部是分析过程造出来的。

上面那张是情况 ①:一根线,旁边什么都没有(那些 10⁻¹² 量级的东西根本画不出来)。下面这张图是情况 ② 的绕圈视角:

为什么:你不是在分析信号,你在分析「被剪断的信号」

上一章末尾已经点出了机制。你手里那一秒钟的数据,等于

你分析的东西  =  真信号  ×  一个矩形窗

              ┌────────────┐
   矩形窗:   │            │        窗内是 1,窗外是 0
   ───────────┘            └───────────────
              0            1 秒

时域相乘,在频域里是卷积(第 14 章的定理,这里先用)。所以:

你看到的谱  =  真谱  ✻  矩形窗的谱

真谱是一根无限细的线(一个纯音)。
一根线和任何东西卷积,得到的就是**那个东西本身**,只不过挪到了线的位置上。

于是:**你看到的,就是矩形窗的频谱,被搬到 100.5 Hz 那儿去了。**

那么矩形窗的频谱长什么样?它是一个 sinc 形状(准确说是狄利克雷核):中间一个胖主瓣,两侧一串越来越矮但衰减很慢的旁瓣。本机把它精细采样出来量了一下:

矩形窗的频谱(64 点窗,64 倍过采样)

  主瓣全宽              2.0 个格子
  最高旁瓣             ⟨−13.26 dB⟩          ← 只比主峰低 4.6 倍
  旁瓣衰减速度          每倍频程 6 dB        ← 非常慢

−13.26 dB 是个很糟糕的数:紧挨着主峰的那个假峰,高度是真峰的 22%。而且旁瓣衰减得极慢,一路拖到频谱另一头去。

那为什么情况 ① 一点事都没有?因为那些旁瓣正好落在谱线的缝隙里。狄利克雷核的零点间隔恰好是一个格子,而 100 Hz 正好在格子上——于是每一根其他谱线都精确地采在了一个零点上。

情况 ① (频率落在格子上):

     谱线位置:  |     |     |     |     |     |
     窗的形状:  ─────────╱▔▔▔╲─────────────────
                          ↑ 只有这一根采到了主峰
                 其余每一根都采在零点上 → 8.9e−12

情况 ② (频率挪半格):

     谱线位置:  |     |     |     |     |     |
     窗的形状:  ────╱▔▔▔▔▔╲──────────────────
                       ↑ 峰落在两根中间,两边各采到一半
                 而所有零点都错开了 → 每一根都采到一点东西

这就是全部机制。情况 ① 的干净是一个巧合,情况 ② 才是常态——你不可能事先知道那个音是 100.0 还是 100.5 Hz,然后据此选一个整除的窗口长度。

◆ 主线

频谱泄漏不是噪声,是你「只取一段」这个动作的频域投影。它有三个后果,每一个都会独立地坑人:

  • 能量摊开:本该在一根线上的东西,摊到几十根上(1 → 41)。
  • 峰值变矮(扇贝损失):读出来的振幅偏小,最坏时 −3.90 dB,也就是只有真值的 64%
  • 掩盖弱信号:一个强音的旁瓣可以整个盖住一个真实存在的弱音。这是三条里最贵的一条,下一章会算出它有多贵。

而这三条都是同一个东西造成的:矩形窗的频谱形状。那么解决办法自然也只有一个:换一个形状更好的窗。这就是下一章。

顺手把第 2 章那个 0.2315 结清

第 2 章里,我们拿 3.5 圈/秒去缠一段只有 3 Hz 和 5 Hz 的信号,得到质心 0.2315。现在可以说清楚了:那不是「3 Hz 蹭过来的」,也不是「5 Hz 蹭过来的」,而是

0.2315 = 3 Hz 那根线的泄漏 + 5 Hz 那根线的泄漏
       = 狄利克雷核在偏离 0.5 格处的值 × 1.0
       + 狄利克雷核在偏离 1.5 格处的值 × 0.5
       (两者还带符号,会部分抵消)

换句话说,第 2 章那台机器在非整数速度上给出的那些「不该有的数」,全部是这一章的泄漏。它不是机器的缺陷,是「一秒钟」这个边界的代价。

✎ 术语正名

扇贝损失(scalloping loss)这个奇怪的名字,来自频率响应曲线的形状:把峰值随「频率偏离格子多少」画出来,得到一串扇贝壳一样的弧。矩形窗最深处是 −3.92 dB(本机在离散情形下量到 −3.90 dB)。

它的实际含义很吓人:如果你用 FFT 直接读峰值来测振幅,误差可以达到 36%,而且这个误差完全取决于被测频率碰巧落在哪儿。同一台设备,测 100.0 Hz 读出 500,测 100.5 Hz 读出 319——很多「设备不稳定」的报告,根子在这里。

下一章的平顶窗(flat-top)就是专门为这件事发明的:它的扇贝损失只有 −0.01 dB,代价是主瓣宽到 10 个格子。校准用平顶窗,分辨用汉宁窗,是频谱分析仪上那个下拉框存在的全部理由。

⌨ 自己跑一遍

把两张单子跑出来,十几行:

import numpy as np

N = fs = 1000
def report(f):
    n = np.arange(N)
    m = np.abs(np.fft.fft(np.cos(2*np.pi*f*n/fs)))[:N//2]
    e = m**2
    srt = np.sort(e)[::-1]
    need = int(np.searchsorted(np.cumsum(srt), 0.99*e.sum()) + 1)
    print(f'f={f}  峰值 {m.max():.1f}  99% 能量需要 {need} 根谱线')

report(100)      # f=100    峰值 500.0  99% 能量需要 1 根谱线
report(100.5)    # f=100.5  峰值 319.0  99% 能量需要 41 根谱线

值得多试几个:100.1(99% 只要 5 根)、100.25(21 根)、100.5(41 根,最坏)。泄漏的严重程度是「离格子有多远」的连续函数,半格处最坏。

再看看扇贝损失那条曲线:

for d in (0, 0.1, 0.2, 0.3, 0.4, 0.5):
    n = np.arange(N)
    m = np.abs(np.fft.fft(np.cos(2*np.pi*(100+d)*n/fs)))[:N//2].max()
    print(f'偏离 {d:.1f} 格   峰值 {m:6.1f}   {20*np.log10(m/500):6.2f} dB')

python3 -c "import numpy as np;N=1000;n=np.arange(N);print(np.abs(np.fft.fft(np.cos(2*np.pi*100.5*n/N))).max())"

在线跑:Google Colab。想直观看:任何一款频谱分析软件(Audacity 的「绘制频谱」)里切换窗函数,同一段音频的谱底会整个抬起或压下——那就是这一章。

▸ 在现实里
  • 振动监测里的轴承故障。一台电机的振动谱上,转频那根线极高,而轴承早期故障的特征频率是一根很小的线,可能低 40 dB。用矩形窗的话,转频的旁瓣(−13 dB 起步)会把它整个埋掉——设备已经在报警,而你的分析工具把警报盖住了。这是工业上换窗函数最主要的动机。
  • 音频里的「假谐波」。拿一段人声做频谱,会看到基频周围一堆小尖尖。新手会以为那是嗓音的细节,其实大部分是泄漏。换成汉宁窗,它们会塌下去一大截。
  • 射电天文的谱线搜索。要在宽带噪声里找一条极窄的谱线(比如 21 cm 氢线),最怕的就是某个强源的泄漏在那个位置上堆出一个假峰。这个领域对窗函数的讲究程度,远超过一般工程。
  • 你的性能火焰图。这不是频谱,但同一个道理:采样式性能分析器每隔 10 ms 采一次栈,如果你的程序里有一个周期恰好接近 10 ms 的循环,采样结果会严重偏斜——那是第 6 章的混叠;而如果你只采了 100 个样本就下结论,那是第 7 章的分辨率。这两章加上这一章,是「用有限的观测推断一个系统」这件事的三张账单。
✗ 这个直觉是错的

「频谱上主峰旁边那些小尖尖是噪声。信号采得干净一点、平均几次,它们就会降下去。」

平均不会让它们降。噪声是随机的,多次平均会按 √n 变小;泄漏是确定性的,你平均一万次它一模一样。这是区分二者最可靠的判据:再采一段,看那些小尖尖会不会出现在完全相同的位置、相同的高度。会的话,那是泄漏。

上面那张单子是最干净的证据:那个信号没有任何噪声,是双精度浮点算出来的纯余弦,而 99% 能量摊到了 41 根谱线上。这些能量一个比特都不是噪声,全部来自「只取了一秒」这个动作。

还有一个连带的错误值得一起纠正:「补零之后频谱上冒出更多小峰,说明分辨出了更多成分」。上一章已经说过补零不增加信息——那些新冒出来的小峰,正是本来藏在谱线缝隙里的泄漏旁瓣,补零只是把它们画了出来。判据是:真实成分的位置不随窗口长度改变,泄漏旁瓣的位置会随窗口长度成比例移动。

◇ 揭晓

正确答案是 C1 根 / 41 根。峰值还从 500.0 掉到了 319.0(−3.90 dB)。

A 「信号是纯的,谱当然也是纯的」——这个直觉的漏洞在于「信号」到底指什么。你分析的不是那个纯余弦,而是「纯余弦 × 矩形窗」。后者一点也不纯:它在 0 秒和 1 秒处各有一个突然的跳变,而跳变是宽频的东西。你看到的所有多余频率,都是那两个跳变的谱。 B 「只会分给旁边两根」——这是最接近的一个错,也是很多人的默认模型。它错在低估了旁瓣衰减有多慢:矩形窗的旁瓣按 1/k 衰减(每倍频程只降 6 dB),所以离峰 20 根谱线的地方还有可观的能量。要想真的「只分给旁边两三根」,需要一个旁瓣衰减快得多的窗——那正是下一章要买的东西,而它有明码标价。 D 「均匀撒满整个频谱」——撒得很开,但绝不均匀。狄利克雷核是有结构的:主瓣、第一旁瓣、第二旁瓣,一路衰减。如果它真是均匀的,反倒好办了(那就是白噪声底,可以减掉);难办的正是它有形状——那些旁瓣看起来非常像真实的谱峰。
⟳ 换个域看
「我把信号从第 0 秒到第 1 秒剪了下来」——一个再自然不过、根本不觉得是在做操作的动作。 同一个动作,是拿真实频谱去和一个狄利克雷核做卷积:主瓣 2 格宽、最高旁瓣 −13.26 dB、按 1/k 慢慢拖出去。你的每一根真实谱线都被这个东西涂抹了一遍。

这个换域是这本书的一个转折点。在此之前,我们说「时域和频域是同一份数据的两套坐标」;从这里开始,你会一次次看到时域里的一个「无操作」,在频域里是一个很有分量的操作——剪断、补零、重采样、量化、丢帧,全都如此。

凡是你在时域里做过的手脚,频域都记着账。下一章就是去看这笔账能不能少付一点。答案是能,但要拿别的东西去换。

这一章的一句话

频谱上那些多余的东西,多数不是噪声,是你「只取了一段」这个动作在频域里的样子;它会摊开能量、压低峰值、并且用旁瓣盖住真实存在的弱信号。

卷 II 到此结束:一眼一眼地看,代价是混叠;只看一段,代价是分辨率和泄漏。下一卷讲怎么讨价还价——而这一卷的三笔账里,只有泄漏是可以谈的。下一章会把五种窗的价目表全部列出来,包括那个把最高旁瓣从 −13.3 dB 压到 −93.0 dB 的选项,以及它要你付出的东西。