第 02 篇:时域的语言 —— 卷积、LTI 系统与冲激响应

2 阅读5分钟

第 02 篇:时域的语言 —— 卷积、LTI 系统与冲激响应

系列:《从驱动到算法:音频工程师的信号处理进阶之路》· 基础篇(模块一) 本篇四板块:引言场景 → 理论推导 → 代码实战 → 驱动工程师视角 预计阅读:30–35 min(含推导与代码) 前置:第 01 篇(采样与量化)。我们已经有了离散序列 x[n]。


一、引言场景:信号"流过"一个东西,会发生什么?

第 01 篇我们把声音"采"成了一串数 x[n]。但那时我们只是盯着这串数本身在看:它有多少位、多高频率。

现在换一个更贴近你日常的问题。你在驱动/系统链路里,信号从来不是"静止"的,它总在流过某个东西:

  • 麦克风采到的声音,会流过一段房间——于是有了混响和回声。
  • 一段 PCM,会流过你配置的那个 EQ / 高通滤波器——于是低频的直流偏置被去掉了。
  • 扬声器放出的声音,会流过喇叭的振膜和腔体——于是频响被染色了。

这些"东西"——房间、滤波器、喇叭——在信号处理里有一个统一的名字:系统 (system)。

那么核心问题来了:

给定输入 x[n],信号流过一个系统后,输出 y[n] 到底是什么?能不能用一个公式精确算出来?

答案是能。而且只要这个系统满足两个非常温和的条件(线性和时不变,合称 LTI),那么无论这个系统内部多复杂——不管它是一间音乐厅还是一个 100 阶的滤波器——它对信号做的事情,都可以用同一个运算来描述。

这个运算,就是卷积 (convolution)。

很多人第一次见到卷积公式 y[n]=∑kx[k]h[n−k]y[n]=\sum_k x[k]h[n-k] 就被劝退了:那个求和、那个 n−k、那个"翻转",到底在干嘛?

这一篇我保证做到一件事:不让你去"背"这个公式,而是让你看着一个你早就熟悉的物理场景,亲手把这个公式一块一块搭出来。搭完你会发现——卷积描述的东西,你在硬件里其实见过无数次。


二、理论推导

推导路线(每一步都为下一步铺垫,不跳):

  1. 先把"系统"这个词说清楚,并定义两个温和的条件:线性、时不变。
  2. 引入一个"探针"信号——单位冲激 δ[n],以及系统对它的响应 h[n](冲激响应)。
  3. 关键一步:把任意输入信号 x[n] 拆成一堆冲激的叠加。
  4. 利用线性和时不变,把输出拼出来——卷积公式自然浮现。
  5. 回头解释那个最让人困惑的"翻转 (n−k)"到底是什么意思。

2.1 什么是"系统"?两个温和的条件

系统就是一个"输入信号进去、输出信号出来"的黑盒子。我们记作:

y[n]=T{x[n]}y[n] = T\{x[n]\}

T{·} 表示"这个系统对输入做的变换 (Transform)"。现在这个黑盒子可以是任何东西。但如果它满足下面两个性质,我们就叫它 LTI 系统 (Linear Time-Invariant,线性时不变系统)。

性质一:线性 (Linearity) —— 包含两条:

  • 可加性 (additivity):如果输入 x₁ 单独进去得到 y₁,输入 x₂ 单独进去得到 y₂,那么把 x₁+x₂ 一起送进去,输出就是 y₁+y₂。
  • 齐次性 (homogeneity / scaling):如果 x 进去得到 y,那么 a·x(放大 a 倍)进去,输出就是 a·y。

合起来写成一条:

T{a⋅x1[n]+b⋅x2[n]}=a⋅T{x1[n]}+b⋅T{x2[n]}T\{a\cdot x_1[n] + b\cdot x_2[n]\} = a\cdot T\{x_1[n]\} + b\cdot T\{x_2[n]\}

💡 大白话:线性 = "不串味、不打折"。几个信号一起通过系统,互相不干扰,各走各的,最后在输出端简单相加;信号放大多少倍,输出就跟着放大多少倍。这是"叠加"能成立的前提——待会我们把输入拆成一堆小块分别处理、再加起来,靠的就是它。

性质二:时不变 (Time-Invariance) —— 系统的"脾气"不随时间变。今天给它一个输入得到某个输出;明天(延迟一段时间)给它同样的输入,它给出的还是同样的输出,只是也跟着延迟了同样的时间。数学表达:

若 x[n]→y[n],则 x[n−n0]→y[n−n0]\text{若 } x[n] \to y[n],\quad\text{则 } x[n-n_0] \to y[n-n_0]

💡 大白话:时不变 = "系统不会变心"。这间房间的回声特性,你上午测和下午测是一样的;你把声音延迟 3 秒再放,回声也只是整体延迟 3 秒,形状不变。

🔧 驱动直觉:你的一个固定系数的 FIR 滤波器就是 LTI 的——系数不随时间改(时不变),两路信号混合通过它等于分别通过再相加(线性)。而一个自动增益控制 AGC 就不是时不变的——它的增益会根据信号大小动态变化,"脾气"随时间变。这个区分很重要:我们这一篇建立的所有工具,只对 LTI 系统成立。后面第 09 篇之后的自适应滤波器,恰恰是"故意打破时不变"来追踪变化的信号——但那是后话,得先把 LTI 这块地基打牢。

2.2 探针:单位冲激 δ[n] 和它的回声 h[n]

要研究一个未知系统,最聪明的办法是给它一个最干净、最简单的输入,看它怎么反应。这个"最简单的输入"就是单位冲激 (unit impulse),记作 δ[n]:

δ[n]={1,n=00,n≠0\delta[n] = \begin{cases} 1, & n = 0 \\ 0, & n \neq 0 \end{cases}

它就是"在 n=0 时刻,一个孤零零的 1,其余时刻全是 0"。

💡 大白话:δ[n] 就是"啪"地拍一下手——一个瞬间的、单位强度的脉冲。在离散世界里它一点都不玄乎,就是数组 [..., 0, 0, 1, 0, 0, ...]。

⚠️ 和第 01 篇那个 δ(t) 区分一下:第 01 篇采样时用的是连续狄拉克冲激 δ(t),它是个"无限高、无限窄、面积为 1"的数学理想物,比较抽象。而这里的离散单位冲激 δ[n] 老实多了,就是"某一格取值为 1"。别被同一个符号吓到,离散版本非常朴素。

现在,把这个冲激送进我们的 LTI 系统,它吐出来的东西,我们给它起个专门的名字——冲激响应 (impulse response),记作 h[n]:

h[n]=T{δ[n]}h[n] = T\{\delta[n]\}

💡 大白话 + 比喻:你对着一间空房间"啪"地拍一下手(输入一个冲激 δ[n]),然后录下来的那一整段带回声、逐渐衰减的声音,就是这间房间的冲激响应 h[n]。它就像系统的"指纹"或"回声签名"——一次拍手,就把这个系统的全部脾气都问出来了。

为什么说"全部脾气"?这正是下面要证明的惊人结论:只要知道了 h[n],你就能算出这个系统对任何输入的响应。一次拍手,定生死。我们来看这是怎么做到的。

2.3 关键一步:任何信号都是"一堆冲激的叠加"

这是整个推导的枢纽,请慢读。

看你手里的离散信号 x[n],比如 x = [x[0], x[1], x[2], ...] = [0.5, -0.3, 0.8, ...]。

我换一个角度看它:x[0]=0.5 是什么?它是"在 n=0 这一格,有一个高度为 0.5 的值"。这不就是一个被放大了 0.5 倍、待在 n=0 位置的冲激吗?即 0.5·δ[n]。

同理,x[1]=−0.3 是"在 n=1 这一格有个 −0.3",也就是一个放大 −0.3 倍、平移到 n=1 位置的冲激,即 −0.3·δ[n−1]。

(这里用到冲激的平移:δ[n−1] 就是把那个"孤零零的 1"从 n=0 挪到 n=1。所以 δ[n−k] 就是"在 n=k 位置有个 1"。)

把每一格都这样看,再全部加起来,就得到一个恒等式:

x[n]=∑k=−∞∞x[k] δ[n−k]x[n] = \sum_{k=-\infty}^{\infty} x[k]\,\delta[n-k]

逐块拆解这个式子(这是本篇第一个必须彻底看懂的公式):

  • 求和变量是 k,它遍历每一个时刻(每一格)。
  • 对某个固定的 k,x[k] 是一个具体的数(第 k 格的高度,比如 0.5)。
  • δ[n−k] 是"位置在 n=k 的单位冲激"。
  • x[k]·δ[n−k] 合起来 = "在第 k 格竖起一根高度为 x[k] 的杆子"。
  • 外面的 ∑ 把所有格子的杆子加起来 —— 拼出来的正是原信号 x[n]。

💡 大白话 + 图像:这就像用乐高积木复刻一根高低起伏的柱状图。x[n] 是你要复刻的目标形状,而每一块乐高就是一个"移动到某位置、拉伸到某高度的冲激"。你把这些积木一块块摆到对应位置,拼出来就是完整的 x[n]。

x[n] 的柱状图:              拆成一根根独立的杆子(每根都是一个缩放+平移的冲激):

  0.8 ┤     █                 n=0: 0.5·δ[n]      →  0.5 高,在第0格
  0.5 ┤ █   █          =      n=1: -0.3·δ[n-1]   → -0.3 高,在第1格
    0 ┤─█─┬─█─            +   n=2: 0.8·δ[n-2]    →  0.8 高,在第2格
 -0.3 ┤   █                   ...(每一格一根)
      └─┬─┬─┬─
        0 1 2  n

这一步为什么关键?因为我们把一个复杂的、任意形状的输入,拆成了一堆我们完全了解其去向的简单积木(冲激)。而系统对单个冲激的反应我们已经知道了(就是 h[n])。接下来只要把"每块积木的反应"加起来就行——而"能这样加起来",正是靠 2.1 的线性。

2.4 卷积公式的诞生:把输出一块块拼出来

现在做一件事:把上面拆好的 x[n] 送进系统 T{·},看输出 y[n]。

y[n]=T{x[n]}=T{∑k=−∞∞x[k] δ[n−k]}y[n] = T\{x[n]\} = T\left\{ \sum_{k=-\infty}^{\infty} x[k]\,\delta[n-k] \right\}

第 1 步——用线性中的"可加性",把 T 搬进求和号里(系统对"一堆东西之和"的响应 = 对每个东西响应之和):

y[n]=∑k=−∞∞T{ x[k] δ[n−k] }y[n] = \sum_{k=-\infty}^{\infty} T\{\, x[k]\,\delta[n-k] \,\}

第 2 步——用线性中的"齐次性",把常数 x[k] 提到 T 外面(注意:对系统 T 来说,求和变量 k 是"位置索引",x[k] 是一个固定的数、一个缩放系数,可以拎出来):

y[n]=∑k=−∞∞x[k]  T{ δ[n−k] }y[n] = \sum_{k=-\infty}^{\infty} x[k]\; T\{\, \delta[n-k] \,\}

第 3 步——用时不变。我们知道系统对"位于 0 的冲激"δ[n] 的响应是 h[n]。那对"平移到 k 的冲激"δ[n−k] 的响应是什么?根据时不变(输入平移多少,输出就平移多少,形状不变),响应就是平移后的 h[n−k]:

T{δ[n−k]}=h[n−k]T\{\delta[n-k]\} = h[n-k]

代入:

 y[n]=∑k=−∞∞x[k] h[n−k]  ≜  x[n]∗h[n] \boxed{\,y[n] = \sum_{k=-\infty}^{\infty} x[k]\,h[n-k] \;\triangleq\; x[n] * h[n]\,}

这就是卷积。 符号 * 就代表这个"相乘再求和"的运算。我们没有去背它,而是从"拆积木 → 线性搬运 → 时不变平移 → 再拼起来"一步步推出来的。

回头看它到底在说什么(大白话总复述):

输出在 n 时刻的值 y[n],等于把过去每一个输入冲激 x[k] 各自激起的"回声" h[n−k],在 n 这个时刻的贡献全部叠加起来。

这跟你对回声的物理直觉完全一致:此刻你听到的声音,是刚才每一个瞬间发出的声音、经过房间反射衰减后、在此刻残留部分的总和。卷积就是"回声叠加"的数学写法。

2.5 破解"翻转"之谜:n−k 到底在干嘛

很多教材会说"卷积要把 h 翻转再平移滑动",这是最劝退的一句话。我们来把它彻底说清,其实一点都不神秘。

固定一个我们关心的输出时刻,比如求 y[5]。代入公式:

y[5]=∑kx[k] h[5−k]y[5] = \sum_{k} x[k]\,h[5-k]

把 k 从小到大列出来,看 h 的下标 5−k 怎么变:

kx[k]h 的下标 5−k
0x[0]h[5]
1x[1]h[4]
2x[2]h[3]
3x[3]h[2]
4x[4]h[1]
5x[5]h[0]

看出来了吗?当 x 的下标从小往大走(0→5),h 的下标却从大往小走(5→0)——h 是倒着和 x 对齐相乘的。这就是所谓的"翻转 (flip)"。

为什么会倒过来?物理意义其实特别顺:

求 y[5](此刻 n=5 听到的声音)时,考虑最近才发生的输入 x[5]——它刚发生,只来得及产生冲激响应的第 0 个样本 h[0](回声刚起头)。而较早的输入 x[0](5 步之前发生的)——它的回声已经传播了 5 步,此刻贡献的是冲激响应的第 5 个样本 h[5](回声的尾巴)。

换句话说:越早的输入,其回声已经走得越远(用 h 越靠后的部分);越晚的输入,回声才刚开始(用 h 越靠前的部分)。这个"时间上的先后对应 h 上的远近",数学上就体现为 h[n−k] 那个减号带来的翻转。翻转不是数学家故意刁难,而是因果时序的自然结果。

💡 一句话记住:n−k 里的减号 = "回声传播需要时间"。它保证了较早的输入用到冲激响应较靠后(较晚)的部分。

至此,卷积的公式、意义、翻转全部讲透了。下面用代码把它"看见"。


三、代码实战(重在看懂结果)

我们做三个递进的实验,每个都紧扣前面的理论,重点在读懂输出说明了什么。

依赖:numpy、matplotlib。

实验一:手写卷积 vs 库函数——先确认我们"真的懂了"

先按公式 y[n]=∑kx[k]h[n−k]y[n]=\sum_k x[k]h[n-k] 一个字一个字地手写实现,再和 numpy 官方 np.convolve 对比。如果两者结果一致,就证明我们对公式的理解分毫不差。

import numpy as np

def my_convolve(x, h):
    """
    严格按定义 y[n] = sum_k x[k] * h[n-k] 手写卷积。
    目的不是高效,而是让公式"跑起来",验证我们的理解。
    """
    N, M = len(x), len(h)
    y = np.zeros(N + M - 1)          # 卷积输出长度 = N+M-1(下面会解释为什么)
    for n in range(N + M - 1):       # 遍历每一个输出时刻 n
        acc = 0.0
        for k in range(N):           # 对每一个输入样本 x[k] 累加它的贡献
            j = n - k                # h 的下标 = n-k(就是那个"翻转")
            if 0 <= j < M:           # 只在 h[j] 存在时才累加(边界外视为0)
                acc += x[k] * h[j]   # x[k] 激起的回声在 n 时刻的贡献
        y[n] = acc
    return y

x = np.array([1.0, 2.0, 3.0])        # 输入信号
h = np.array([1.0, 1.0, 1.0])        # 冲激响应:一个简单的"3点求和"系统

y_mine = my_convolve(x, h)
y_numpy = np.convolve(x, h)

print("我的手写卷积:", y_mine)
print("numpy 卷积  :", y_numpy)
print("两者最大差异:", np.max(np.abs(y_mine - y_numpy)))

运行结果:

我的手写卷积: [1. 3. 6. 5. 3.]
numpy 卷积  : [1. 3. 6. 5. 3.]
两者最大差异: 0.0

结果解读(这里是重点):

  1. "最大差异 0.0" 说明什么:我们逐字翻译公式写出的代码,和工业级库算出的结果一模一样。这证明 2.4 推出的卷积公式和 2.5 的"翻转"理解是完全正确的——你现在是真的掌握了卷积,而不是记了个符号。

  2. 输出 [1, 3, 6, 5, 3] 是怎么来的? 我们的 h=[1,1,1] 是一个"把相邻 3 个输入加起来"的系统。手动验算几个点,你会对卷积形成肌肉记忆:

    • y[0] = x[0]·h[0] = 1·1 = 1(信号刚进来,只有第一个样本起作用)
    • y[1] = x[0]·h[1] + x[1]·h[0] = 1·1 + 2·1 = 3
    • y[2] = x[0]·h[2] + x[1]·h[1] + x[2]·h[0] = 1+2+3 = 6(系统"填满"了,正好是三个数之和)
    • y[3] = x[1]·h[2] + x[2]·h[1] = 2+3 = 5(输入没了,开始"排空")
    • y[4] = x[2]·h[2] = 3(最后的尾巴)
  3. 为什么输出长度是 N+M−1 = 3+3−1 = 5,比输入还长? 这是卷积一个极重要、又极易被忽略的性质:信号流过一个有"记忆"(长度 M)的系统后,会被"拖长"。输入 3 个样本,输出却有 5 个。多出来的 M−1=2 个样本,就是输入结束后、系统里残留的回声在慢慢"排空"(上面的 y[3], y[4] 就是排空阶段)。

🔧 驱动直觉(记住这个数字 M−1):这个"拖长"在你的世界里是有名有姓的东西——滤波器的群延迟 / 尾部延迟。一个 M 阶 FIR 滤波器,会给数据流引入约 M−1 个样本的延迟和拖尾。这正是低延迟音频里大家对"滤波器阶数"斤斤计较的根本原因:阶数越高(h 越长),滤波效果可能越好,但拖尾越长、延迟越大。你在驱动侧优化 end-to-end latency 时砍的那几个样本,一部分就来自这里。

实验二:冲激响应就是系统的"指纹"——一次拍手问出全部脾气

2.2 里我们说"只要知道 h[n],就能算出系统对任何输入的响应"。这个实验直接验证它,同时把 h[n] 当成一段真实的房间回声来体会。

import numpy as np
import matplotlib.pyplot as plt

fs = 16000

# --- 造一个"房间"的冲激响应 h[n]:直达声 + 两个衰减回声 ---
# 想象你在房间里"啪"拍一下手,录到:先是原声,然后墙壁反射回来两次,一次比一次弱
h = np.zeros(int(0.3 * fs))     # 0.3秒长的回声
h[0] = 1.0                      # n=0: 直达声(拍手声本身)
h[int(0.08 * fs)] = 0.5         # 80ms 后:第一次反射,衰减到 0.5
h[int(0.18 * fs)] = 0.25        # 180ms 后:第二次反射,衰减到 0.25

# --- 输入一个真实信号:一小段 1kHz 的"滴"声 ---
dur = 0.05
t = np.arange(int(dur * fs)) / fs
x = np.sin(2 * np.pi * 1000 * t)   # 50ms 的 1kHz 纯音

# --- 让信号"流过"这个房间:就是卷积 ---
y = np.convolve(x, h)

print(f"输入长度 = {len(x)} 样本 ({len(x)/fs*1000:.0f} ms)")
print(f"冲激响应长度 = {len(h)} 样本 ({len(h)/fs*1000:.0f} ms)")
print(f"输出长度 = {len(y)} 样本 ({len(y)/fs*1000:.0f} ms)")

fig, ax = plt.subplots(3, 1, figsize=(10, 7))
ax[0].plot(np.arange(len(x))/fs*1000, x);  ax[0].set_title('input x[n]: a 50ms 1kHz beep')
ax[1].stem(np.arange(len(h))/fs*1000, h);  ax[1].set_title('impulse response h[n]: room "fingerprint"')
ax[2].plot(np.arange(len(y))/fs*1000, y);  ax[2].set_title('output y[n] = x * h: original + 2 echoes')
for a in ax: a.set_xlabel('time (ms)'); a.grid(True)
plt.tight_layout(); plt.show()

运行结果解读:

  1. 长度打印:输入 800 样本、h 4800 样本,输出 800+4800−1 = 5599 样本。又一次印证了实验一的结论——信号被系统"拖长"了,多出来的部分就是回声的余韵。

  2. 三张图的故事(最关键):

    • 第一张:干净的 50ms "滴"声,一个孤立的音。
    • 第二张:h[n] 是三根杆子——一根主的(直达声)+ 两根越来越矮的(两次反射)。这就是这个房间的"指纹"。
    • 第三张:输出里,你会看到那个"滴"声出现了三次,一次比一次弱,间隔正好对应 h 里三根杆子的位置(0ms、80ms、180ms)。

    这张输出图就是"回声"两个字的可视化。它完美对应 2.4 的大白话:"输出 = 每个输入激起的回声的叠加"。输入信号被 h 的每一根杆子各"复制"了一份、按杆子高度缩放、按杆子位置延迟——这正是卷积在做的事。

  3. 深层结论:我们只测了一次"拍手"(得到 h),就能算出这个房间对任意声音(这里是"滴"声,也可以是音乐、人声)的效果。这就是 2.2 说的"一次拍手,定生死"。这也正是卷积混响 (convolution reverb) 音频插件的原理——录下一座著名音乐厅的冲激响应,就能把任何干声"放进"那座音乐厅。

🔧 驱动直觉:这个 h[n](回声路径)在你后面要学的回声消除 AEC(第 13 篇)里就是核心——扬声器到麦克风的声学回声路径,本质就是一个冲激响应 h[n]。AEC 要做的,就是估计出这个 h[n],然后把 参考信号 * h 算出来的预测回声从麦克风信号里减掉。你现在理解的这个"输入卷积房间=回声",倒过来就是 AEC 要解的问题。

实验三:卷积做低通——理论与"听感/波形"对上

最后一个实验,把卷积和你最熟的"滤波"挂钩:一个简单的滑动平均其实就是卷积,且它是个低通滤波器。

import numpy as np
import matplotlib.pyplot as plt

fs = 16000
t = np.arange(int(0.02 * fs)) / fs
# 输入 = 低频(200Hz) + 高频噪声(3kHz),模拟"有用信号+高频干扰"
clean = np.sin(2 * np.pi * 200 * t)
noise = 0.5 * np.sin(2 * np.pi * 3000 * t)
x = clean + noise

# 一个 8 点滑动平均滤波器:h = [1/8, 1/8, ..., 1/8]
# 它的含义:每个输出 = 最近 8 个输入的平均
M = 8
h = np.ones(M) / M

y = np.convolve(x, h)[:len(x)]   # 取前 len(x) 个,对齐时间轴便于观察

plt.figure(figsize=(10, 4))
plt.plot(t*1000, x, alpha=0.5, label='input: 200Hz + 3kHz noise')
plt.plot(t*1000, y, linewidth=2, label='output: after 8-pt moving average')
plt.plot(t*1000, clean, '--', label='original 200Hz (reference)')
plt.xlabel('time (ms)'); plt.ylabel('amplitude'); plt.legend(); plt.grid(True)
plt.title('Moving average (a convolution) acts as a low-pass filter')
plt.tight_layout(); plt.show()

运行结果解读:

  1. 你会看到:蓝色输入信号毛毛躁躁(高频噪声让它抖动剧烈);橙色输出信号变平滑了,高频抖动被明显压下去,且形状贴近虚线的纯 200Hz 参考信号。

  2. 为什么"求平均"能滤掉高频? 直觉是这样的:高频信号变化快,相邻几个样本有正有负,一平均就相互抵消;低频信号变化慢,相邻几个样本值都差不多,平均后基本不变。所以"滑动平均"天然对高频不友好、对低频友好——它就是个低通滤波器。

  3. 和理论的呼应:h = [1/8,...,1/8] 就是这个滤波器的冲激响应。整个滤波过程 y = x * h 就是一次卷积。"设计一个滤波器"这件事,本质上就是"设计一个合适的 h[n]"——这句话是第 04 篇(FIR/IIR)的总纲,这里先埋下种子。

  4. 注意输出开头有点"不对劲":最前面几个样本值偏小/爬升。这是因为滤波器刚开始工作时,"最近 8 个样本"里有一部分是补的 0(信号还没填满),这就是边界瞬态 (transient)。它对应实验一里 y[0]=1 那种"系统刚启动、还没填满"的阶段。

🔧 驱动直觉:滑动平均是嵌入式里最省算力的低通——只要加法和一次移位(除以 8 = 右移 3 位)。你在 MCU 上给 ADC 读数做平滑,大概率写过它。现在你知道了:你写的那个"读 8 次求平均",在信号处理语言里是一次和 [1/8,...] 的卷积,是一个如假包换的 FIR 低通滤波器。你早就在用卷积了,只是没叫它这个名字。


四、驱动工程师视角:你其实一直在做卷积

这一篇的核心认知桥梁是:卷积不是一个陌生的新概念,而是你在硬件里见过无数次的物理过程的数学名字。 逐条对照:

概念驱动工程师视角(你熟悉的)算法工程师视角(数学语言)
LTI 系统一个固定系数的滤波器、一段传输线、一个喇叭满足线性+时不变的变换 T{·}
冲激 δ[n]一个单样本脉冲,[0,0,1,0,0]拆解任意信号的"基本积木"
冲激响应 h[n]拍一下手录下的房间回声;滤波器系数表系统的完整"指纹",T{δ[n]}
卷积 y=x*h"读最近 N 个样本加权求和"、回声叠加、FIR 累加∑kx[k]h[n−k]\sum_k x[k]h[n-k]
输出被拖长 M−1滤波器群延迟/尾部延迟,你优化 latency 时在砍它卷积输出长度 N+M−1
滤波器阶数系数表长度,越长越耗 MAC、延迟越大h[n] 的长度 M
边界瞬态滤波器刚启动时输出"不对",需要预热卷积起始段 h 尚未填满

三个"原来如此",帮你把已有经验一次性接上算法:

  1. 你调过的 FIR 滤波器,就是在做卷积。 FIR 的那张系数表 b[0..M-1],就是它的冲激响应 h[n]。每来一个新样本,你把最近 M 个输入和系数表对应相乘再求和 —— 这行你写过的 MAC (乘累加) 循环,一字不差就是卷积公式 ∑kx[k]h[n−k]\sum_k x[k]h[n-k]。DSP 里的 MAC 指令,硬件加速的就是卷积。

  2. 回声/混响,就是信号卷积房间的冲激响应。 你在做 AEC/去混响调试时面对的物理现象,数学本质就是一次卷积。这让第 13 篇 AEC 不再是天书。

  3. "系统的延迟"有了精确来源。 你一直凭经验知道"滤波器会引入延迟",现在你知道它精确等于和冲激响应长度相关的 M−1 量级,且这个延迟是卷积"拖尾"的必然结果,不是实现的 bug。

最关键的一句话总结:

驱动侧,你把 h[n](滤波器系数)当作一张要正确加载、正确做 MAC 运算的配置表; 算法侧,h[n] 是一个系统的完整身份——它编码了这个系统对世界的全部反应方式。 而卷积,就是把"输入信号"和"系统身份"结合、算出"世界会听到什么"的唯一运算。理解了卷积,你就掌握了描述一切 LTI 系统的通用语言。


五、与下一篇的衔接

这一篇我们建立了时域里描述系统的语言:冲激响应 h[n] + 卷积。这套语言完整、精确,但有一个现实的麻烦——

卷积算起来太累了。 你在实验二里也看到了,h 有 4800 个点,每一个输出样本都要做几千次乘加。信号一长、h 一长,卷积的计算量会大到吓人(正比于两者长度之积)。

更要命的是,在时域里,很多问题看不清。比如"这个滤波器到底放过了哪些频率、压制了哪些频率?"——盯着一堆 h[n] 系数你根本看不出来。实验三里我们只能靠"高频抵消、低频保留"的直觉去猜它是低通,无法定量。

**下一篇(第 03 篇《频域的钥匙:从傅里叶级数到 FFT》)**要交给你另一副眼镜——频域。我们会看到一件近乎魔法的事:

时域里那个又慢又难懂的卷积 x * h,到了频域,会变成一个简单的乘法 X · H。

这就是大名鼎鼎的卷积定理。它不仅能让计算量暴降(FFT 快速卷积的基础),更能让"这个系统对各个频率做了什么"变得一目了然——H 就直接告诉你每个频率被放大还是被压制。第 03 篇会带你从傅里叶级数一路搭到 FFT,彻底理解为什么"频率"是看待信号的第二个、也是威力更大的视角。

到那时,第 01 篇的"采样"、第 02 篇的"卷积"、第 03 篇的"频域"会合成一张完整的地图——这三篇是整个系列真正的地基。


第 02 篇完。


附:本篇符号小结(并入全系列《符号与约定表》)

符号含义首次出现
T{·}系统变换算子2.1
δ[n]离散单位冲激(n=0 时为 1,其余为 0)2.2
h[n]冲激响应,h[n]=T{δ[n]}2.2
*卷积运算2.4
M冲激响应/滤波器长度(阶数)2.4
LTI线性时不变系统2.1