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

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

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


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

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

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

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

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

那么核心问题来了:

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

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

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

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

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


二、理论推导

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

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

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

系统就是一个"输入信号进去、输出信号出来"的黑盒子。我们记作:
yn=T{xn}yn = T\{xn\} 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} T\{a\cdot x_1n + b\cdot x_2n\} = a\cdot T\{x_1n\} + b\cdot T\{x_2n\} T{a⋅x1n+b⋅x2n}=a⋅T{x1n}+b⋅T{x2n}

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

性质二:时不变 (Time-Invariance) ------ 系统的"脾气"不随时间变。今天给它一个输入得到某个输出;明天(延迟一段时间)给它同样的输入,它给出的还是同样的输出,只是也跟着延迟了同样的时间。数学表达:
若 xn→yn,则 xn−n0→yn−n0\text{若 } xn \to yn,\quad\text{则 } xn-n_0 \to yn-n_0 若 xn→yn,则 xn−n0→yn−n0

💡 大白话:时不变 = "系统不会变心"。这间房间的回声特性,你上午测和下午测是一样的;你把声音延迟 3 秒再放,回声也只是整体延迟 3 秒,形状不变。
🔧 驱动直觉 :你的一个固定系数的 FIR 滤波器就是 LTI 的------系数不随时间改(时不变),两路信号混合通过它等于分别通过再相加(线性)。而一个自动增益控制 AGC不是 时不变的------它的增益会根据信号大小动态变化,"脾气"随时间变。这个区分很重要:我们这一篇建立的所有工具,只对 LTI 系统成立。后面第 09 篇之后的自适应滤波器,恰恰是"故意打破时不变"来追踪变化的信号------但那是后话,得先把 LTI 这块地基打牢。

2.2 探针:单位冲激 δn 和它的回声 hn

要研究一个未知系统,最聪明的办法是给它一个最干净、最简单的输入 ,看它怎么反应。这个"最简单的输入"就是单位冲激 (unit impulse) ,记作 δ[n]
δn= { 1, n=0 0, n≠0 \deltan = \begin{cases} 1, & n = 0 \\ 0, & n \neq 0 \end{cases} δ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}hn = T\{\deltan\} 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−kxn = \sum_{k=-\infty}^{\infty} xk\,\deltan-k xn=k=−∞∑∞xkδn−k

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

  • 求和变量是 k,它遍历每一个时刻(每一格)。
  • 对某个固定的 kx[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} yn = T\{xn\} = T\left\{ \sum_{k=-\infty}^{\infty} xk\,\deltan-k \right\} yn=T{xn}=T{k=−∞∑∞xkδn−k}

第 1 步------用线性中的"可加性",把 T 搬进求和号里(系统对"一堆东西之和"的响应 = 对每个东西响应之和):
yn= ∑k=−∞∞ T{ xk δn−k }yn = \sum_{k=-\infty}^{\infty} T\{\, xk\,\deltan-k \,\} yn=k=−∞∑∞T{xkδn−k}

第 2 步------用线性中的"齐次性",把常数 x[k] 提到 T 外面 (注意:对系统 T 来说,求和变量 k 是"位置索引",x[k] 是一个固定的数、一个缩放系数,可以拎出来):
yn= ∑k=−∞∞ xk  T{ δn−k }yn = \sum_{k=-\infty}^{\infty} xk\; T\{\, \deltan-k \,\} yn=k=−∞∑∞xkT{δn−k}

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

代入:
 yn= ∑k=−∞∞ xk hn−k  ≜  xn∗hn  \boxed{\,yn = \sum_{k=-\infty}^{\infty} xk\,hn-k \;\triangleq\; xn * hn\,} yn=k=−∞∑∞xkhn−k≜xn∗hn

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

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

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

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

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

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

固定一个我们关心的输出时刻,比如求 y[5]。代入公式:
y5=∑kxk h5−ky5 = \sum_{k} xk\,h5-k 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 里的减号 = "回声传播需要时间"。它保证了较早的输入用到冲激响应较靠后(较晚)的部分。

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


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

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

依赖:numpymatplotlib

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

先按公式 yn=∑kxkhn−kyn=\sum_k xkhn-k 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

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

  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] 当成一段真实的房间回声来体会。

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()

运行结果解读:

  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 要解的问题。

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

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

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()

运行结果解读:

  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] 拆解任意信号的"基本积木"
冲激响应 hn 拍一下手录下的房间回声;滤波器系数表 系统的完整"指纹",T{δ[n]}
卷积 y=x*h "读最近 N 个样本加权求和"、回声叠加、FIR 累加 ∑kxkhn−k \sum_k xkhn-k ∑kxkhn−k
输出被拖长 M−1 滤波器群延迟/尾部延迟,你优化 latency 时在砍它 卷积输出长度 N+M−1
滤波器阶数 系数表长度,越长越耗 MAC、延迟越大 h[n] 的长度 M
边界瞬态 滤波器刚启动时输出"不对",需要预热 卷积起始段 h 尚未填满

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

  1. 你调过的 FIR 滤波器,就是在做卷积。 FIR 的那张系数表 b[0..M-1],就是它的冲激响应 h[n]。每来一个新样本,你把最近 M 个输入和系数表对应相乘再求和 ------ 这行你写过的 MAC (乘累加) 循环,一字不差就是卷积公式 ∑kxkhn−k \sum_k xkhn-k ∑kxkhn−kDSP 里的 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
相关推荐
大辉狼_音频架构1 小时前
第 01 篇:音频采样与量化 —— 从 I2S 帧到奈奎斯特定理
嵌入式·音视频开发
星轨初途2 小时前
LeetCode 热题 100——day7 接雨水
数据结构·c++·笔记·算法·leetcode·职场和发展
qeen872 小时前
【数据结构】搜索二叉树的介绍与算法原理解析及实现
数据结构·c++·学习·算法·模板
阳明山水2 小时前
销量预测的“隐形杀手”:概念漂移与自适应学习实战
人工智能·深度学习·算法·机器学习·架构
szarron3 小时前
HT666 0.6-6GHz 定向高增益手持天线:EMC 测试与频谱定位实战指南
网络·算法·5g·信号处理·射频工程·频谱仪
liulilittle3 小时前
反量化:反量化Q8零(q8_0)
人工智能·算法·机器学习·ai·llm
青 春 记 忆9 小时前
LeetCode 21. 合并两个有序链表|Python 解法详解
python·算法·leetcode·链表
Sagittarius_A*11 小时前
后量子密码|通识认知 03|量子威胁辟谣:Grover 算法对对称密码的影响与误区
算法·信息安全·密码学·量子计算·pqc·后量子密码
QAQo7T13 小时前
Python组蓝桥杯备赛超详细知识点总结笔记_排序算法篇
笔记·python·算法·蓝桥杯·排序算法