数字信号处理基础:离散时间信号、线性时不变系统、卷积与相关、z变换入门、差分方程

各位同学,欢迎来到第二章。说实话,这一章是整个音频DSP的“地基”。你后面学滤波器设计、学FFT、学各种音频算法,追根溯源都绕不开今天这几个概念。我当年刚入行时,觉得这些东西太理论,结果做第一个降噪项目就被“线性时不变”这个前提坑了一把——嗯,后面我会细说。

咱们今天的目标很明确:搞懂离散时间信号长什么样,线性时不变系统为什么是“亲儿子”,卷积到底在算什么,z变换怎么用,以及差分方程怎么跟代码对应起来。别怕,咱们用工程师的视角来啃。

本章知识体系速览:离散信号 → 系统特性(线性+时不变) → 卷积运算 → z变换工具 → 差分方程实现。一条线串起来,全是音频算法的基本功。
第2章 数字信号处理基础 · 知识体系 离散时间信号 线性时不变系统 卷积与相关 z变换入门 差分方程 核心逻辑:信号进入系统 → 系统用差分方程描述 → 卷积计算输出 → z变换分析频率特性 所有音频DSP算法(滤波、均衡、混响)都建立在这个框架之上

离散时间信号:从连续世界到数字世界的第一步

咱们做音频DSP,处理的不是麦克风里出来的连续电压,而是经过ADC采样后的离散序列。说白了,就是把连续的时间轴切成一个个均匀的“切片”,每个切片用一个数字表示。

一个离散时间信号可以写成 x[n],n 是整数索引。比如 CD 音质的采样率是 44100 Hz,那就意味着每秒钟有 44100 个 x[n] 样本。我刚开始做嵌入式音频时,总把 n 当成时间,其实 n 只是序号,真正的时间是 n * T,T 是采样间隔。

常见的离散信号有这么几种:

  • 单位脉冲序列 δ[n]:只在 n=0 时为 1,其余全为 0。这是系统的“试金石”。
  • 单位阶跃序列 u[n]:n≥0 时为 1,n<0 时为 0。用来表示信号的“开启”。
  • 正弦序列:x[n] = A·cos(ωn + φ)。注意这里的 ω 是数字角频率,单位是弧度/样本。
  • 指数序列:x[n] = aⁿ。|a|<1 时衰减,|a|>1 时发散。
我的小经验:在嵌入式平台上生成正弦波时,千万别用 math.h 的 sin() 实时计算——太慢了。我一般用查表法或者CORDIC算法,省掉浮点运算的开销。

线性时不变系统:音频算法的“黄金假设”

为什么几乎所有音频DSP教材都先讲LTI系统?因为现实中的音频处理,大部分都建立在“线性”和“时不变”这两个假设上。你想想看,如果同一个算法今天处理一段人声和明天处理结果不一样,那还怎么用?

线性:系统对输入信号的缩放和叠加保持一致性。简单说就是:输入 a·x₁[n] + b·x₂[n],输出就是 a·y₁[n] + b·y₂[n]。混音器就是典型的线性系统——两路信号叠加,输出就是各自处理结果的叠加。

时不变:系统特性不随时间变化。今天输入一个脉冲,明天输入同样的脉冲,输出完全一样。我踩过的坑就在这里——有一次做自适应滤波器,忘了检查系统是否真的时不变,结果在某个频段上出现了奇怪的“呼吸效应”。后来发现是某个系数更新逻辑里混入了时间戳依赖。

注意:LTI 系统是理想模型。实际系统中,ADC/DAC 的时钟抖动、温度漂移、量化噪声都会破坏“时不变”假设。做高精度音频处理时,一定要评估这些非理想因素带来的误差。

卷积与相关:两个看似相似但用途完全不同的运算

卷积是LTI系统的核心运算。系统对任意输入 x[n] 的响应,等于输入与系统单位脉冲响应 h[n] 的卷积:

y[n] = x[n] * h[n] = Σ x[k] · h[n - k]   (k 从 -∞ 到 +∞)

说白了,卷积就是“翻转、平移、相乘、求和”。我当年学的时候觉得这定义太抽象,直到做第一个FIR滤波器项目才真正理解——滤波器的系数就是 h[n],输入信号跟系数做卷积,就是滤波过程。

相关跟卷积长得很像,但少了一个翻转步骤:

r_{xy}[n] = Σ x[k] · y[k + n]   (k 从 -∞ 到 +∞)

相关用来衡量两个信号的相似度。音频对齐、回声检测、模式识别都靠它。我做过一个语音唤醒项目,就是用互相关来检测关键词模板在音频流中的位置。

运算 公式特点 主要用途 计算量(N点)
卷积 翻转 + 平移 + 乘加 滤波、系统响应 O(N²)
互相关 平移 + 乘加(无翻转) 信号匹配、时延估计 O(N²)
自相关 信号与自身的互相关 周期检测、基频提取 O(N²)
性能提示:在嵌入式平台上,时域卷积的 O(N²) 复杂度太奢侈。实际工程中,我通常用 FFT 把卷积转到频域做——O(N log N) 的复杂度,长滤波器时优势巨大。这就是所谓的“快速卷积”。

z变换入门:从时域到频域的“桥梁”

z变换把离散时间信号从时域映射到复频域。定义很简单:

X(z) = Σ x[n] · z^{-n}   (n 从 -∞ 到 +∞)

其中 z 是复变量。你可能会问:为什么要引入这么个抽象的东西?

原因有两个:

  • 卷积变乘法:时域的卷积在z域变成乘法。y[n] = x[n] * h[n] → Y(z) = X(z) · H(z)。这太方便了!
  • 系统分析:通过 H(z) 的零极点位置,可以直接判断系统的稳定性、频率响应特性。

我记得第一次用z变换分析一个IIR滤波器时,看到极点落在单位圆外,就知道这滤波器会发散。当时在仿真里跑确实如此——输出越来越大,最后溢出。z变换让我在写代码之前就能发现问题。

常用的z变换对:

时域序列 z变换 收敛域
δ[n] 1 整个z平面
u[n] 1 / (1 - z⁻¹) |z| > 1
aⁿ · u[n] 1 / (1 - a·z⁻¹) |z| > |a|
cos(ω₀n) · u[n] (1 - cos(ω₀)·z⁻¹) / (1 - 2cos(ω₀)·z⁻¹ + z⁻²) |z| > 1

差分方程:从数学公式到C代码

差分方程是LTI系统的时域描述。一个N阶差分方程长这样:

y[n] = b₀·x[n] + b₁·x[n-1] + ... + b_M·x[n-M]
       - a₁·y[n-1] - a₂·y[n-2] - ... - a_N·y[n-N]

这其实就是滤波器的“真身”。b系数是前馈部分,a系数是反馈部分。如果所有 a 系数都是0,那就是FIR滤波器;否则就是IIR滤波器。

把差分方程写成C代码,我一般这样做:

// 一阶IIR低通滤波器实现
float process_iir_lpf(float x, float *state, float b0, float a1) {
    float y = b0 * x + a1 * state[0];
    state[0] = y;  // 更新状态
    return y;
}

你看,就这么几行。但实际工程中要注意:

  • 状态变量管理:多个通道时,每个通道要有独立的状态数组。
  • 定点化:如果MCU没有FPU,所有系数和状态都要转成Q格式定点数。
  • 溢出保护:IIR的反馈环路容易累积误差,需要做饱和处理。
我曾经踩过的坑:在STM32上实现一个8阶IIR滤波器,直接用float算,结果发现采样率一高就卡死。后来改成Q15定点数,速度提升了4倍。所以,嵌入式音频DSP,定点化是必修课。

差分方程和z变换是天生一对。对差分方程两边做z变换,就能得到系统函数 H(z):

H(z) = (b₀ + b₁·z⁻¹ + ... + b_M·z⁻ᴹ) / (1 + a₁·z⁻¹ + ... + a_N·z⁻ᴺ)

有了 H(z),零极点、频率响应、稳定性,全都能算出来。这就是为什么我说z变换是“桥梁”——它把差分方程这种时域实现,和频域分析连接起来了。

本章核心要点:
  • 离散信号 x[n] 是连续世界的“快照”,采样率决定了信息量
  • LTI 系统是音频算法的基石,线性保证叠加,时不变保证可重复
  • 卷积是LTI系统的运算核心,相关是信号匹配的利器
  • z变换把卷积变乘法,零极点分析让系统设计有据可依
  • 差分方程就是滤波器的C代码原型,实现时注意定点化和状态管理