第 02 篇:时域的语言 ------ 卷积、LTI 系统与冲激响应
系列 :《从驱动到算法:音频工程师的信号处理进阶之路》· 基础篇(模块一) 本篇四板块 :引言场景 → 理论推导 → 代码实战 → 驱动工程师视角 预计阅读 :30--35 min(含推导与代码) 前置 :第 01 篇(采样与量化)。我们已经有了离散序列
x[n]。
一、引言场景:信号"流过"一个东西,会发生什么?
第 01 篇我们把声音"采"成了一串数 x[n]。但那时我们只是盯着这串数本身在看:它有多少位、多高频率。
现在换一个更贴近你日常的问题。你在驱动/系统链路里,信号从来不是"静止"的,它总在流过某个东西:
- 麦克风采到的声音,会流过一段房间------于是有了混响和回声。
- 一段 PCM,会流过你配置的那个 EQ / 高通滤波器------于是低频的直流偏置被去掉了。
- 扬声器放出的声音,会流过喇叭的振膜和腔体------于是频响被染色了。
这些"东西"------房间、滤波器、喇叭------在信号处理里有一个统一的名字:系统 (system)。
那么核心问题来了:
给定输入
x[n],信号流过一个系统后,输出y[n]到底是什么?能不能用一个公式精确算出来?
答案是能。而且只要这个系统满足两个非常温和的条件(线性 和时不变 ,合称 LTI),那么无论这个系统内部多复杂------不管它是一间音乐厅还是一个 100 阶的滤波器------它对信号做的事情,都可以用同一个运算来描述。
这个运算,就是卷积 (convolution)。
很多人第一次见到卷积公式 yn=∑kxkhn−k 就被劝退了:那个求和、那个 n−k、那个"翻转",到底在干嘛?
这一篇我保证做到一件事 :不让你去"背"这个公式,而是让你看着一个你早就熟悉的物理场景,亲手把这个公式一块一块搭出来。搭完你会发现------卷积描述的东西,你在硬件里其实见过无数次。
二、理论推导
推导路线(每一步都为下一步铺垫,不跳):
- 先把"系统"这个词说清楚,并定义两个温和的条件:线性、时不变。
- 引入一个"探针"信号------单位冲激
δ[n],以及系统对它的响应h[n](冲激响应)。 - 关键一步:把任意 输入信号
x[n]拆成一堆冲激的叠加。 - 利用线性和时不变,把输出拼出来------卷积公式自然浮现。
- 回头解释那个最让人困惑的"翻转 (n−k)"到底是什么意思。
2.1 什么是"系统"?两个温和的条件
系统就是一个"输入信号进去、输出信号出来"的黑盒子。我们记作:
yn=T{xn}
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⋅x1n+b⋅x2n}=a⋅T{x1n}+b⋅T{x2n}
💡 大白话 :线性 = "不串味、不打折"。几个信号一起通过系统,互相不干扰,各走各的,最后在输出端简单相加;信号放大多少倍,输出就跟着放大多少倍。这是"叠加"能成立的前提------待会我们把输入拆成一堆小块分别处理、再加起来,靠的就是它。
性质二:时不变 (Time-Invariance) ------ 系统的"脾气"不随时间变。今天给它一个输入得到某个输出;明天(延迟一段时间)给它同样的输入,它给出的还是同样的输出,只是也跟着延迟了同样的时间。数学表达:
若 xn→yn,则 xn−n0→yn−n0
💡 大白话:时不变 = "系统不会变心"。这间房间的回声特性,你上午测和下午测是一样的;你把声音延迟 3 秒再放,回声也只是整体延迟 3 秒,形状不变。
🔧 驱动直觉 :你的一个固定系数的 FIR 滤波器就是 LTI 的------系数不随时间改(时不变),两路信号混合通过它等于分别通过再相加(线性)。而一个自动增益控制 AGC 就不是 时不变的------它的增益会根据信号大小动态变化,"脾气"随时间变。这个区分很重要:我们这一篇建立的所有工具,只对 LTI 系统成立。后面第 09 篇之后的自适应滤波器,恰恰是"故意打破时不变"来追踪变化的信号------但那是后话,得先把 LTI 这块地基打牢。
2.2 探针:单位冲激 δn 和它的回声 hn
要研究一个未知系统,最聪明的办法是给它一个最干净、最简单的输入 ,看它怎么反应。这个"最简单的输入"就是单位冲激 (unit impulse) ,记作 δ[n]:
δn={1,0,n=0n=0
它就是"在 n=0 时刻,一个孤零零的 1,其余时刻全是 0"。
💡 大白话 :
δ[n]就是"啪"地拍一下手------一个瞬间的、单位强度的脉冲。在离散世界里它一点都不玄乎,就是数组[..., 0, 0, 1, 0, 0, ...]。
⚠️ 和第 01 篇那个 δ(t) 区分一下 :第 01 篇采样时用的是连续 狄拉克冲激δ(t),它是个"无限高、无限窄、面积为 1"的数学理想物,比较抽象。而这里的离散 单位冲激δ[n]老实多了,就是"某一格取值为 1"。别被同一个符号吓到,离散版本非常朴素。
现在,把这个冲激送进我们的 LTI 系统,它吐出来的东西,我们给它起个专门的名字------冲激响应 (impulse response) ,记作 h[n]:
hn=T{δ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"。)
把每一格都这样看,再全部加起来,就得到一个恒等式:
xn=k=−∞∑∞xkδn−k
逐块拆解这个式子(这是本篇第一个必须彻底看懂的公式):
- 求和变量是
k,它遍历每一个时刻(每一格)。 - 对某个固定的
k,x[k]是一个具体的数(第 k 格的高度,比如 0.5)。 δ[n−k]是"位置在n=k的单位冲激"。x[k]·δ[n−k]合起来 = "在第 k 格竖起一根高度为 xk 的杆子"。- 外面的
∑把所有格子的杆子加起来 ------ 拼出来的正是原信号x[n]。
💡 大白话 + 图像 :这就像用乐高积木复刻一根高低起伏的柱状图 。
x[n]是你要复刻的目标形状,而每一块乐高就是一个"移动到某位置、拉伸到某高度的冲激"。你把这些积木一块块摆到对应位置,拼出来就是完整的x[n]。
ini
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]。
yn=T{xn}=T{k=−∞∑∞xkδn−k}
第 1 步------用线性中的"可加性",把 T 搬进求和号里(系统对"一堆东西之和"的响应 = 对每个东西响应之和):
yn=k=−∞∑∞T{xkδn−k}
第 2 步------用线性中的"齐次性",把常数 x[k] 提到 T 外面 (注意:对系统 T 来说,求和变量 k 是"位置索引",x[k] 是一个固定的数、一个缩放系数,可以拎出来):
yn=k=−∞∑∞xkT{δn−k}
第 3 步------用时不变 。我们知道系统对"位于 0 的冲激"δ[n] 的响应是 h[n]。那对"平移到 k 的冲激"δ[n−k] 的响应是什么?根据时不变(输入平移多少,输出就平移多少,形状不变),响应就是平移后的 h[n−k]:
T{δn−k}=hn−k
代入:
yn=k=−∞∑∞xkhn−k≜xn∗hn
这就是卷积。 符号 * 就代表这个"相乘再求和"的运算。我们没有去背它,而是从"拆积木 → 线性搬运 → 时不变平移 → 再拼起来"一步步推出来的。
回头看它到底在说什么(大白话总复述):
输出在
n时刻的值y[n],等于把过去每一个输入冲激x[k]各自激起的"回声"h[n−k],在n这个时刻的贡献全部叠加起来。
这跟你对回声 的物理直觉完全一致:此刻你听到的声音,是刚才每一个瞬间发出的声音、经过房间反射衰减后、在此刻残留部分的总和。卷积就是"回声叠加"的数学写法。
2.5 破解"翻转"之谜:n−k 到底在干嘛
很多教材会说"卷积要把 h 翻转再平移滑动",这是最劝退的一句话。我们来把它彻底说清,其实一点都不神秘。
固定一个我们关心的输出时刻,比如求 y[5]。代入公式:
y5=k∑xkh5−k
把 k 从小到大列出来,看 h 的下标 5−k 怎么变:
| k | xk | h 的下标 5−k |
|---|---|---|
| 0 | x0 | h5 |
| 1 | x1 | h4 |
| 2 | x2 | h3 |
| 3 | x3 | h2 |
| 4 | x4 | h1 |
| 5 | x5 | h0 |
看出来了吗?当 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 库函数------先确认我们"真的懂了"
先按公式 yn=∑kxkhn−k 一个字一个字地手写实现,再和 numpy 官方 np.convolve 对比。如果两者结果一致,就证明我们对公式的理解分毫不差。
python
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)))
运行结果:
ini
我的手写卷积: [1. 3. 6. 5. 3.]
numpy 卷积 : [1. 3. 6. 5. 3.]
两者最大差异: 0.0
结果解读(这里是重点):
-
"最大差异 0.0" 说明什么 :我们逐字翻译公式写出的代码,和工业级库算出的结果一模一样。这证明 2.4 推出的卷积公式和 2.5 的"翻转"理解是完全正确的------你现在是真的掌握了卷积,而不是记了个符号。
-
输出
[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 = 3y[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(最后的尾巴)
-
为什么输出长度是
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] 当成一段真实的房间回声来体会。
python
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()
运行结果解读:
-
长度打印 :输入 800 样本、
h4800 样本,输出800+4800−1 = 5599样本。又一次印证了实验一的结论------信号被系统"拖长"了,多出来的部分就是回声的余韵。 -
三张图的故事(最关键):
- 第一张:干净的 50ms "滴"声,一个孤立的音。
- 第二张:
h[n]是三根杆子------一根主的(直达声)+ 两根越来越矮的(两次反射)。这就是这个房间的"指纹"。 - 第三张:输出里,你会看到那个"滴"声出现了三次 ,一次比一次弱,间隔正好对应
h里三根杆子的位置(0ms、80ms、180ms)。
这张输出图就是"回声"两个字的可视化。它完美对应 2.4 的大白话:"输出 = 每个输入激起的回声的叠加"。输入信号被
h的每一根杆子各"复制"了一份、按杆子高度缩放、按杆子位置延迟------这正是卷积在做的事。 -
深层结论 :我们只测了一次"拍手"(得到
h),就能算出这个房间对任意 声音(这里是"滴"声,也可以是音乐、人声)的效果。这就是 2.2 说的"一次拍手,定生死"。这也正是卷积混响 (convolution reverb) 音频插件的原理------录下一座著名音乐厅的冲激响应,就能把任何干声"放进"那座音乐厅。
🔧 驱动直觉 :这个
h[n](回声路径)在你后面要学的回声消除 AEC (第 13 篇)里就是核心------扬声器到麦克风的声学回声路径,本质就是一个冲激响应h[n]。AEC 要做的,就是估计出这个h[n],然后把参考信号 * h算出来的预测回声从麦克风信号里减掉。你现在理解的这个"输入卷积房间=回声",倒过来就是 AEC 要解的问题。
实验三:卷积做低通------理论与"听感/波形"对上
最后一个实验,把卷积和你最熟的"滤波"挂钩:一个简单的滑动平均其实就是卷积,且它是个低通滤波器。
python
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()
运行结果解读:
-
你会看到 :蓝色输入信号毛毛躁躁(高频噪声让它抖动剧烈);橙色输出信号变平滑了,高频抖动被明显压下去,且形状贴近虚线的纯 200Hz 参考信号。
-
为什么"求平均"能滤掉高频? 直觉是这样的:高频信号变化快,相邻几个样本有正有负,一平均就相互抵消 ;低频信号变化慢,相邻几个样本值都差不多,平均后基本不变。所以"滑动平均"天然对高频不友好、对低频友好------它就是个低通滤波器。
-
和理论的呼应 :
h = [1/8,...,1/8]就是这个滤波器的冲激响应。整个滤波过程y = x * h就是一次卷积。"设计一个滤波器"这件事,本质上就是"设计一个合适的h[n]"------这句话是第 04 篇(FIR/IIR)的总纲,这里先埋下种子。 -
注意输出开头有点"不对劲" :最前面几个样本值偏小/爬升。这是因为滤波器刚开始工作时,"最近 8 个样本"里有一部分是补的 0(信号还没填满),这就是边界瞬态 (transient) 。它对应实验一里
y[0]=1那种"系统刚启动、还没填满"的阶段。
🔧 驱动直觉 :滑动平均是嵌入式里最省算力的低通------只要加法和一次移位(除以 8 = 右移 3 位)。你在 MCU 上给 ADC 读数做平滑,大概率写过它。现在你知道了:你写的那个"读 8 次求平均",在信号处理语言里是一次和
[1/8,...]的卷积,是一个如假包换的 FIR 低通滤波器。你早就在用卷积了,只是没叫它这个名字。
四、驱动工程师视角:你其实一直在做卷积
这一篇的核心认知桥梁是:卷积不是一个陌生的新概念,而是你在硬件里见过无数次的物理过程的数学名字。 逐条对照:
| 概念 | 驱动工程师视角(你熟悉的) | 算法工程师视角(数学语言) |
|---|---|---|
| LTI 系统 | 一个固定系数的滤波器、一段传输线、一个喇叭 | 满足线性+时不变的变换 T{·} |
| 冲激 δn | 一个单样本脉冲,[0,0,1,0,0] |
拆解任意信号的"基本积木" |
| 冲激响应 hn | 拍一下手录下的房间回声;滤波器系数表 | 系统的完整"指纹",T{δ[n]} |
| 卷积 y=x*h | "读最近 N 个样本加权求和"、回声叠加、FIR 累加 | ∑kxkhn−k |
| 输出被拖长 M−1 | 滤波器群延迟/尾部延迟,你优化 latency 时在砍它 | 卷积输出长度 N+M−1 |
| 滤波器阶数 | 系数表长度,越长越耗 MAC、延迟越大 | h[n] 的长度 M |
| 边界瞬态 | 滤波器刚启动时输出"不对",需要预热 | 卷积起始段 h 尚未填满 |
三个"原来如此",帮你把已有经验一次性接上算法:
-
你调过的 FIR 滤波器,就是在做卷积。 FIR 的那张系数表
b[0..M-1],就是它的冲激响应h[n]。每来一个新样本,你把最近 M 个输入和系数表对应相乘再求和 ------ 这行你写过的 MAC (乘累加) 循环,一字不差就是卷积公式 ∑kxkhn−k。DSP 里的 MAC 指令,硬件加速的就是卷积。 -
回声/混响,就是信号卷积房间的冲激响应。 你在做 AEC/去混响调试时面对的物理现象,数学本质就是一次卷积。这让第 13 篇 AEC 不再是天书。
-
"系统的延迟"有了精确来源。 你一直凭经验知道"滤波器会引入延迟",现在你知道它精确等于和冲激响应长度相关的
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 |