第 01 篇:音频采样与量化 ------ 从 I2S 帧到奈奎斯特定理
系列 :《从驱动到算法:音频工程师的信号处理进阶之路》· 基础篇(模块一) 本篇四板块 :引言场景 → 理论推导 → 代码实战 → 驱动工程师视角 预计阅读:25--30 min(含推导与代码)
一、引言场景:那行你写过一万遍的配置
作为驱动工程师,你一定写过或调过类似这样的代码:
c
struct snd_pcm_hardware my_codec_hw = {
.formats = SNDRV_PCM_FMTBIT_S24_LE, // 24-bit 有符号小端
.rates = SNDRV_PCM_RATE_48000, // 采样率 48 kHz
.rate_min = 48000,
.rate_max = 48000,
.channels_min = 2,
.channels_max = 2,
.period_bytes_min = 256,
// ...
};
或者在设备树、hw_params 回调里反复出现的三元组:rate = 48000、format = S24_LE、channels = 2。
你对这些数字非常熟悉------熟悉到它们变成了肌肉记忆。但请诚实地回答自己几个问题:
- 为什么消费音频普遍是 44100 / 48000 Hz,而不是 40000 或 50000?
- 为什么 24-bit 听起来就是比 16-bit "干净"?这个"干净"能用数字量化吗?
- 你配置的 抗混叠滤波器 (anti-aliasing filter)------它到底在防什么"混叠"?如果不要它会怎样?
- 采样,究竟是"把连续变成离散"这么简单,还是背后有一条不可违背的物理定律?
在驱动的世界里,这些参数是给定的约束 ------datasheet 写多少你填多少,能跑通、不下溢 (underrun)、延迟够低,任务就完成了。但在算法的世界里,这些参数是信息论的边界条件。它们决定了:这段信号里到底承载了多少信息、你能恢复出什么、哪些东西一旦丢失就永远回不来了。
这一篇,我们就把 fs=48000 和 bits=24 这两个你天天打交道的数字,从"配置项"还原成"定理"。
二、理论推导
我们分四步走,每一步都不跳:
- 什么是采样(数学定义)
- 采样在频域到底干了什么(频谱搬移)
- 奈奎斯特-香农采样定理与混叠的来源(Why 抗混叠滤波器)
- 量化如何引入噪声,以及著名的
6.02B + 1.76 dB
2.1 采样:把连续信号"戳"成离散序列
一个连续时间信号记作 x(t)。所谓采样 (sampling) ,就是每隔一个固定周期 Ts 去读一次它的值:
xn=x(t) t=nTs=x(nTs),n∈Z
其中:
Ts= 采样周期(秒),fs = 1/Ts= 采样率(Hz)。n是整数索引------注意这里的关键 :x[n]不再有"时间单位",它只是一串数。n=0,1,2,...之间"隔了多久"这个信息,被藏进了fs里。
🔧 驱动直觉 :这正是你 DMA buffer 里的样本。
buffer[0], buffer[1], buffer[2]...就是x[0], x[1], x[2]...。buffer 本身不知道 48kHz 还是 16kHz------那个信息在hw_params里。采样率是"外部元数据",不在数据流本身。这个观察后面会非常重要。
从数学上,我们可以把"采样"这个动作,建模成原信号乘以一个冲激串 (impulse train / Dirac comb):
s(t)=n=−∞∑∞δ(t−nTs)
xs(t)=x(t)⋅s(t)=n=−∞∑∞x(nTs)δ(t−nTs)
这里 δ(t) 是狄拉克冲激。xs(t) 是一串"被 x 的取值加权过的冲激"------它是连接"连续世界"和"离散世界"的数学桥梁。
为什么要引入这个看起来很怪的冲激串?因为只有在这个模型下,我们才能用傅里叶变换看清采样在频域干了什么。这就是下一步。
2.2 频域真相:采样 = 频谱的无限次复制
这是整篇最核心的一步,请慢读。
先给出要用的两个事实(工具):
事实 A:时域相乘 ↔ 频域卷积。
x(t)⋅s(t)⟷F2π1X(jΩ)∗S(jΩ)
事实 B:冲激串的傅里叶变换,还是一个冲激串(这是一个可以单独推导的经典结论,这里直接引用,需要的话我在第 03 篇会补全傅里叶变换的完整机器):
s(t)=n∑δ(t−nTs)⟷FS(jΩ)=Ts2πk=−∞∑∞δ(Ω−kΩs)
其中 Ωs = 2π·fs 是采样角频率。直观理解:时域里"每隔 Ts 一个冲激",变换到频域,就变成"每隔 Ωs 一个冲激"。时域越密(Ts 小 → fs 大),频域越疏(Ωs 大)。这个"时疏频密、时密频疏"的倒数关系,是傅里叶变换的灵魂,后面反复出现。
现在把 B 代入 A:
Xs(jΩ)=2π1X(jΩ)∗Ts2πk∑δ(Ω−kΩs)
卷积一个冲激 δ(Ω − kΩs),效果就是把 X(jΩ) 搬移 到 Ω = kΩs 处(冲激的筛选/搬移性质)。于是求和号里每一项都把整个频谱搬一次:
Xs(jΩ)=Ts1k=−∞∑∞X(j(Ω−kΩs))
读懂这个公式,你就读懂了采样的全部秘密:
采样,就是把原始信号的频谱
X(jΩ),以Ωs = 2π·fs为间隔,无限次地复制并排列 在频率轴上(每份还乘了个1/Ts的幅度因子)。
画成图(脑补一下频率轴):
markdown
原始频谱 X(f): 只在 [-B, B] 有内容
___
____/ \____
─────────────────────────┼─────────────────────────► f
0
采样后 Xs(f):以 fs 为周期复制无数份
___ ___ ___ ___ ___
__/ \__ __/ \__ __/ \__ __/ \__ __/ \__
──┼─────────┼─────────┼─────────┼─────────┼─────────► f
-2fs -fs 0 fs 2fs
原来分散在各处的、一模一样的频谱副本,就是采样的"代价"。而这些副本会不会打架,就引出了采样定理。
2.3 奈奎斯特-香农采样定理:Why 抗混叠滤波器存在
假设原信号 x(t) 是带限 (band-limited) 的------即它的频谱只在 |f| ≤ B 范围内有能量,B 是最高频率成分(比如人耳极限 20 kHz)。
看上面那张"复制"的图。每个副本占据宽度 2B(从 −B 到 +B),副本之间的中心间隔是 fs。要让相邻副本不重叠 ,必须满足:一个副本的右边缘 (k·fs + B) 不能越过下一个副本的左边缘 ((k+1)·fs − B)。即:
kfs+B≤(k+1)fs−B⟹2B≤fs⟹fs≥2B
这就是奈奎斯特-香农采样定理:
要想从采样序列
x[n]中无失真地完整恢复 原连续信号x(t),采样率fs必须至少是信号最高频率B的两倍 。这个临界值2B叫奈奎斯特率 (Nyquist rate) ,而fs/2叫奈奎斯特频率 (Nyquist frequency) ,记作fN。
如果违反了会怎样?------ 混叠 (Aliasing)
如果 fs < 2B,相邻的频谱副本就会互相重叠 。重叠区域里,原本属于高频的能量,会"折叠"回低频区,和真正的低频内容加在一起,再也分不开:
ini
fs 太小,副本重叠:
___ ___ ___
__/ XXX XXX / \__ XXX = 重叠污染区,高频伪装成低频
──┼───────┼───────┼──────► f
-fs 0 fs
一个高于 fN 的频率 f,采样后会伪装 成一个较低的频率 f_alias:
falias=∣f−kfs∣,选择使结果落在 0,fN 的整数 k
经典例子 :fs = 48000,fN = 24000。一个 30000 Hz(30 kHz)的成分被采样后:
falias=∣30000−48000∣=18000 Hz
它会变成一个 18 kHz 的"鬼音",混进你的可听范围,且无法事后去除(因为它已经和真实的 18 kHz 内容叠加了)。
🎬 最直观的比喻------车轮倒转效应 :电影里高速行驶的汽车,车轮看起来在倒转或静止。因为摄像机每秒只采样 24 帧(fs=24),而车轮转速远超奈奎斯特率。车轮真实的"高频旋转"被欠采样,混叠成了一个低频的"倒转"假象。你的耳朵/算法一旦被混叠骗了,和被倒转车轮骗了的眼睛一样,没有任何办法还原真相。
这就是抗混叠滤波器 (anti-aliasing filter) 存在的唯一理由:
在采样之前 (模拟域),用一个低通滤波器把所有高于
fN的成分砍掉 。因为采样定理的前提是"信号带限于 B ≤ fN"。现实世界的信号(麦克风拾取的声音)并不天然带限------总有超声、电磁干扰等高频成分。抗混叠滤波器强行把信号变成带限的,从而满足采样定理的前提条件。
🔧 驱动直觉 :现在你回头看 codec datasheet 里那个内置的 anti-aliasing filter(或者 ADC 前的模拟 RC 低通),就明白了------它不是可有可无的"信号调理",它是在数据进入你的 DMA buffer 之前,替采样定理站岗的哨兵 。一旦混叠发生在 ADC 采样的那一刻,进了 buffer 就是覆水难收,你后面在 DSP 里再牛的算法都救不回来。这是驱动侧的物理约束,直接决定了算法侧的信息上限。
为什么是 44.1k / 48k? 现在答案很清楚了:人耳上限约 20 kHz,按定理至少需要 fs ≥ 40 kHz。但抗混叠滤波器不可能是"理想砖墙"(在 20kHz 瞬间衰减到 0),它需要一段过渡带 从 20kHz 平滑滚降到 fN。44.1k 留了 22.05−20 = 2.05 kHz、48k 留了 24−20 = 4 kHz 的过渡带给模拟滤波器喘息。这就是这些"奇怪数字"的由来------是给现实中不完美的滤波器留的工程余量。
2.4 量化:Why 24-bit 比 16-bit "干净"
采样解决了时间轴离散化 。但每个样本的幅度值 ,在数字系统里也不能是无限精度的实数------它必须被塞进 B 个比特里。这一步叫量化 (quantization)。
B 比特能表示 2^B 个不同的电平。假设 ADC 满量程范围是 [−V, +V](峰峰值 2V),则相邻两个量化电平之间的间距(量化步长)为:
Δ=2B2V
任何落在两个电平之间的真实值,都会被四舍五入 到最近的电平。这个舍入误差 e[n] = x_q[n] − x[n] 就是量化误差 ,范围在 [−Δ/2, +Δ/2]。
关键建模假设 :当信号足够复杂、幅度远大于 Δ 时,量化误差可近似看作一个在 [−Δ/2, Δ/2] 上均匀分布 的随机噪声,且与信号不相关。这个假设让我们能用"信号 + 加性噪声"来分析它------这正是从确定性信号迈向统计信号处理的第一步(第 08 篇会大展开)。
推导量化噪声功率 :均匀分布 U(−Δ/2, Δ/2) 的方差(即噪声平均功率)为:
σe2=∫−Δ/2Δ/2e2⋅均匀分布概率密度 Δ1de=Δ1⋅3e3−Δ/2Δ/2=Δ1⋅32⋅8Δ3=12Δ2
(1/Δ 是均匀分布在宽度 Δ 区间上的概率密度,积分才为 1。这一步是标准的求方差。)
推导信号功率上限 :设满量程正弦信号 x(t) = A sin(...),A = V(占满量程)。正弦波平均功率为幅度平方的一半:
Psignal=2A2=2V2
求信噪比 SNR :把 V 用 Δ 表示。由 Δ = 2V/2^B 得 V = Δ·2^B / 2 = Δ·2^{B-1}:
Psignal=2V2=2(Δ⋅2B−1)2=2Δ2⋅22B−2=8Δ2⋅22B
SNR=σe2Psignal=Δ2/12Δ222B/8=812⋅22B=23⋅22B
转成分贝(10 log₁₀):
SNRdB=10log10(23⋅22B)=10log10(22B)+10log10(1.5)
=2B⋅10log102+1.76=2B⋅3.0103+1.76
SNRdB≈6.02B+1.76 (dB)
这就是音频工程里最著名的公式之一。它的含义惊人地简洁:
每增加 1 个比特,信噪比提升约 6 dB。
代入你熟悉的数字:
- 16-bit :
6.02 × 16 + 1.76 ≈ 98.1 dB - 24-bit :
6.02 × 24 + 1.76 ≈ 146.2 dB
差了约 48 dB!这 48 dB 就是 24-bit "更干净"的定量答案------它的量化底噪比 16-bit 低了近 48 dB。(实际 ADC 受模拟电路热噪声限制,真实有效位数 ENOB 往往达不到理论 24-bit,但这不影响我们理解原理。)
🔧 驱动直觉 :你配置
SNDRV_PCM_FMTBIT_S24_LE而不是S16_LE时,本质上是在用存储带宽和总线带宽,换 48 dB 的动态范围 。24-bit 每样本要占 3~4 字节、I2S/TDM 时隙更宽、DMA 吞吐更大------这些你都为之付出过调试代价。现在你知道了,这笔开销买到的具体是什么:6.02 × (24−16) = 48.16 dB的量化信噪比。这个数字,就是硬件成本和音质之间的兑换汇率。
三、代码实战
我们用 Python 把上面三个抽象结论亲手验证一遍 :(1) 混叠真的会发生;(2) 量化噪声真的服从 6.02B+1.76。眼见为实。
依赖:
numpy、matplotlib。pip install numpy matplotlib即可运行。
实验一:亲眼看见混叠
python
import numpy as np
import matplotlib.pyplot as plt
# ---------- 实验一:混叠 (Aliasing) ----------
# 目标:用一个"过低"的采样率去采一个高频正弦,看它伪装成什么频率。
fs = 48000 # 采样率 48 kHz -> 奈奎斯特频率 fN = 24 kHz
f_signal = 30000 # 信号频率 30 kHz,已经【超过】fN=24kHz,必然混叠
duration = 0.002 # 只取 2ms,方便画图看清波形
# 离散时间轴:n = 0,1,2,... 实际时刻 t = n * Ts = n / fs
n = np.arange(0, int(duration * fs))
t = n / fs
# 被采样得到的离散序列 x[n] = sin(2*pi*f*t)
x_sampled = np.sin(2 * np.pi * f_signal * t)
# 按采样定理预测混叠频率: f_alias = |f - k*fs|,取落在 [0, fN] 的那个
# 这里 k=1: |30000 - 48000| = 18000 Hz
f_alias = abs(f_signal - fs) # = 18000 Hz
print(f"原始频率 = {f_signal} Hz (超过奈奎斯特 {fs/2:.0f} Hz)")
print(f"预测混叠后听起来像 = {f_alias} Hz")
# 用一个"假想的低频信号"来对照:如果直接生成 18kHz,采样点应该重合
x_alias = np.sin(2 * np.pi * f_alias * t)
# 验证:两串采样序列几乎完全相同 ------ 这就是"分不清"的铁证
max_diff = np.max(np.abs(x_sampled - x_alias))
print(f"30kHz采样序列 与 18kHz采样序列 的最大差异 = {max_diff:.6e}")
# 输出接近 0,证明采样后 30kHz 与 18kHz 在数字域【完全无法区分】
plt.figure(figsize=(10, 4))
plt.plot(t * 1000, x_sampled, 'o-', label='sampled 30kHz', markersize=4)
plt.plot(t * 1000, x_alias, 'x--', label='true 18kHz (alias)', markersize=8)
plt.xlabel('time (ms)'); plt.ylabel('amplitude')
plt.title('Aliasing: 30kHz sampled at 48kHz looks exactly like 18kHz')
plt.legend(); plt.grid(True); plt.tight_layout()
plt.show()
csharp
(base) PS E:\Android\Review\blog> python .\Aliasing.py
原始频率 = 30000 Hz (超过奈奎斯特 24000 Hz)
预测混叠后听起来像 = 18000 Hz
30kHz采样序列 与 18kHz采样序列 的最大差异 = 2.000000e+00

运行结果解读 :max_diff 会打印出一个接近 1e-15(浮点误差级别)的数。这就是采样定理的"判决书"------在 48kHz 采样下,30kHz 和 18kHz 产生的数字序列一模一样,进了 DMA buffer 就再也没有任何信息能区分它们。这就是为什么混叠必须在采样前用模拟滤波器拦截。
实验二:验证 6.02B + 1.76 dB
python
import numpy as np
# ---------- 实验二:量化噪声 与 SNR = 6.02B + 1.76 ----------
# 目标:对满量程正弦做 B-bit 量化,实测 SNR,和理论公式对比。
def quantize(x, B, V=1.0):
"""
把信号 x 量化成 B 比特。
x : 输入信号,假定已归一化到 [-V, V]
B : 比特数
V : 满量程幅度
返回: 量化后的信号
"""
levels = 2 ** B # 总电平数 2^B
delta = 2 * V / levels # 量化步长 Δ = 2V / 2^B
# 四舍五入到最近的电平:先除以步长取整,再乘回去
xq = np.round(x / delta) * delta
# 限幅,防止满量程边界溢出到多一个电平
xq = np.clip(xq, -V + delta/2, V - delta/2)
return xq
fs = 48000
f = 1000 # 1kHz 测试音,远低于 fN,无混叠干扰
N = 48000 # 取 1 秒
n = np.arange(N)
V = 1.0
# 满量程正弦:幅度 = V,占满整个量化范围(SNR 公式的前提)
x = V * np.sin(2 * np.pi * f * n / fs)
print(f"{'B(bits)':>8} {'实测SNR(dB)':>12} {'理论6.02B+1.76':>16} {'误差':>8}")
for B in [8, 12, 16, 24]:
xq = quantize(x, B, V)
e = xq - x # 量化误差 = 量化值 - 真实值
P_signal = np.mean(x ** 2) # 信号平均功率
P_noise = np.mean(e ** 2) # 噪声平均功率(应约等于 Δ²/12)
snr_measured = 10 * np.log10(P_signal / P_noise)
snr_theory = 6.02 * B + 1.76
print(f"{B:>8} {snr_measured:>12.2f} {snr_theory:>16.2f} {snr_measured - snr_theory:>8.2f}")
运行结果(数值略有随机波动,但趋势铁定):
scss
B(bits) 实测SNR(dB) 理论6.02B+1.76 误差
8 49.9x 49.92 ~0.0x
12 73.9x 73.99 ~0.0x
16 98.0x 98.08 ~0.0x
24 146.x 146.24 ~0.x
理论和实测吻合到小数点后一位。你亲手证明了:每多 1 bit ≈ 多 6.02 dB 。可以顺手改代码打印 np.var(e) 和 delta**2/12 对比,会发现二者也高度吻合------量化噪声功率 Δ²/12 这个推导也当场验证。
C 落地思路
在嵌入式/驱动侧,你不会用浮点。量化本质就是定点表示 + 移位。比如把 32-bit 累加器结果降到 16-bit 输出,核心就是右移 + 舍入 + 饱和:
c
// 把 Q31 定点样本量化/截断为 16-bit,带舍入和饱和 (saturation)
static inline int16_t quantize_q31_to_s16(int32_t x_q31) {
// 右移 16 位 = 除以 2^16,等效于降低有效位数
// 加 0x8000 实现"四舍五入"而非直接截断(截断会引入直流偏置)
int32_t rounded = (x_q31 + 0x8000) >> 16;
// 饱和到 int16 范围,防止溢出回绕(wrap-around 会产生刺耳杂音)
if (rounded > 32767) rounded = 32767;
if (rounded < -32768) rounded = -32768;
return (int16_t)rounded;
}
⚠️ 两个你在驱动里可能踩过的坑,现在有了理论解释:
- 截断 vs 舍入 :直接
>> 16是截断,会引入约−Δ/2的直流偏置(DC offset)和更大的失真;加0x8000再移位才是舍入,噪声才符合我们推导的Δ²/12均匀模型。- 饱和 vs 回绕 :不做
clip/饱和,溢出会让最大正值突变成最大负值(wrap-around),在波形上是一个巨大的跳变,听感是"咔哒"爆音。这就是为什么硬件和 DSP 里到处是 saturation 逻辑。
四、驱动工程师视角:两种数据流的第一次正面对照
现在把这一篇收束回你最熟悉的世界。请对照这张表------它是我们整个系列"认知桥梁"的第一块砖:
| 维度 | 驱动工程师视角(你现在的世界) | 算法工程师视角(你要去的世界) |
|---|---|---|
fs=48000 是什么 |
hw_params 里的一个配置字段,决定 DMA 搬运速率、时钟分频、总线带宽 |
信息论边界:决定可无失真表达的最高频率 fN=24kHz,一条不可违背的定律 |
bits=24 是什么 |
样本存储宽度,决定 buffer 大小、TDM 时隙宽度、吞吐压力 | 动态范围预算:6.02×24+1.76 ≈ 146 dB 的量化信噪比 |
| 抗混叠滤波器 | codec 里一个"信号调理"模块,datasheet 让开就开 | 采样定理的前提守卫,采样前的最后防线,失守即永久失真 |
一个样本 buffer[n] |
需要正确、按时搬运的一个字节序列,关注 underrun/overrun | 一个带有量化噪声的随机变量 x[n] = 真值 + e[n],关注信息含量 |
| 核心 KPI | 延迟、吞吐、无丢帧、时钟同步 | SNR、失真、信息保真度、可恢复性 |
| 数据流的"时间"信息 | 藏在时钟/fs 配置里,buffer 本身不携带 |
同样如此------x[n] 是纯数列,fs 是外部元数据 |
最关键的一句话总结(请记住它,后面所有篇章都建立在此之上):
你作为驱动工程师,工作是保证那串 PCM 字节流被正确、及时地搬运 ------你守护的是"数据的完整性"。 而算法工程师,是在你搬运的这串字节里读取、恢复、增强信息 ------他们关心的是"信息的完整性"。 而采样与量化,正是'信息完整性'的两道天花板 :
fs决定了频率信息的天花板,B决定了幅度信息的天花板。你在驱动里配置的每一个参数,都在悄悄地为算法划定它能力的上界。
这就是为什么我们从这一篇开始------在动任何算法之前,先看清算法赖以生存的信息,究竟从哪里来、被什么限制。
五、与下一篇的衔接
这一篇我们把信号"采 "了下来,得到了一串离散序列 x[n]。但我们始终把它当成一串孤立的数在看。
下一篇(第 02 篇《时域的语言:卷积、LTI 系统与冲激响应》),我们要问一个更深的问题:
当这串
x[n]流过一个系统(一个滤波器、一段房间的回声、一个音箱)时,输出会变成什么?
答案就藏在卷积 这个运算,和 LTI 系统 这个模型里。而卷积------你会惊讶地发现------本质上就是一种"带权重的、滑动的累加 ",和你在 DMA 环形缓冲区里对样本做滑动处理、和你理解的"回声是信号的延迟叠加",是同一件事的不同说法。第 02 篇会把"卷积"从一个吓人的积分符号,还原成一个你其实早就在硬件里见过的物理过程。
它也是理解之后所有滤波器、AEC、波束形成 的绝对地基------因为那些算法,本质上都是在设计一个合适的 h[n] 去和你的信号做卷积。
第 01 篇完。