数字音频基础:采样定理、量化、编码、PCM格式

说实话,很多做声学特征提取的朋友,一上来就调库、算MFCC,结果数据稍微变个采样率就崩了。为什么?因为底层的数字音频原理没吃透。这一章,我们就来把数字音频的四个核心概念——采样、量化、编码、PCM——彻底讲明白。

我个人习惯把数字音频比作「用尺子量声音」。你想想看,声音是连续的波浪,但计算机只能存离散的数字。怎么把波浪变成数字?这就是本章要解决的问题。

2.1 采样定理:多快才算够?

采样,说白了就是每隔一小段时间,记录一次声音的瞬时值。问题来了:隔多久采一次,才能把原始声音完整还原回来?

这里有个黄金法则——奈奎斯特-香农采样定理:

采样频率必须大于信号最高频率的两倍。

公式:fs > 2 × fmax

举个例子。人耳能听到的最高频率大约是20kHz。所以CD音频的采样率定在44.1kHz,刚好大于40kHz。为什么不是40kHz整?因为实际滤波器做不到一刀切,得留点余量。

我在项目中遇到过一件事:有个同事录鸟叫声,采样率设了22kHz。结果高频鸟鸣全变成了「嘶嘶」的混叠噪声。嗯,这就是典型的欠采样——高频信号伪装成了低频信号,混进来了。

避坑指南:我曾经在嵌入式项目里为了省存储,把采样率降到8kHz。结果语音识别准确率直接腰斩。后来才发现,8kHz只能覆盖电话语音(300Hz-3.4kHz),高频信息全丢了。所以,采样率的选择取决于你的目标频段,不是越低越好。

常见的采样率有这些:

采样率 可还原最高频率 典型应用
8 kHz 4 kHz 电话、对讲机
16 kHz 8 kHz 语音识别、语音合成
44.1 kHz 22.05 kHz CD、音乐
48 kHz 24 kHz 电影、专业音频
96 kHz 48 kHz 高解析度音频

2.2 量化:把连续值变成离散值

采样解决了「什么时候记」的问题。量化解决的是「记成什么数」的问题。

你想想看,采样得到的电压值是连续的,比如3.14159V。但计算机只能用有限位数的二进制数来表示。量化就是把这个连续值,就近取整到最近的量化电平上。

这里有个关键参数——量化位数(bit depth):

  • 8 bit:256个量化等级。动态范围约48dB。听起来像老式游戏机。
  • 16 bit:65536个等级。动态范围96dB。CD标准。
  • 24 bit:16777216个等级。动态范围144dB。录音棚常用。
  • 32 bit float:浮点表示,动态范围接近无限。后期处理首选。

我的经验:做声学特征提取时,如果原始音频是16bit,我一般不会降成8bit。因为量化噪声会抬高底噪,影响信噪比。但反过来,16bit升到24bit不会提升音质——就像把一张480p的图片放大成4K,细节不会凭空出现。

量化会产生一个不可避免的问题——量化误差。每次取整都会引入一点小偏差,这些偏差听起来就是背景「嘶嘶」声,我们叫它量化噪声。

量化噪声的功率计算公式:

量化噪声功率 = Δ² / 12
其中 Δ = 满量程电压 / 2^(bit_depth)

看出来了吗?每增加1 bit,量化噪声功率降低6dB。这就是为什么16bit比8bit安静那么多的原因。

2.3 编码:把数字变成字节流

采样和量化做完,我们得到了一串数字。但计算机怎么存这些数字?这就轮到编码出场了。

最常见的编码方式就是PCM(脉冲编码调制)。说白了,就是把量化后的数值,按顺序一个接一个地写进文件里。

PCM编码有几个关键参数:

  • 采样率:每秒采多少个点
  • 量化位数:每个点用多少bit
  • 声道数:单声道、立体声、5.1环绕等
  • 字节序:大端(Big Endian)还是小端(Little Endian)
  • 编码格式:有符号整型、无符号整型、浮点型

举个例子,一段CD音质的立体声音频:

采样率:44100 Hz
量化位数:16 bit
声道数:2(立体声)
字节序:小端
编码格式:有符号整型

每秒数据量 = 44100 × 2 × 2 = 176400 字节 ≈ 172 KB/s

我刚开始做音频处理时,经常搞混字节序。有一次在ARM芯片上读WAV文件,出来的声音全是噪声。查了半天才发现,PC是小端,ARM默认大端,读出来的字节顺序反了。嗯,从那以后我写代码都会显式指定字节序。

2.4 PCM格式:最朴素的音频格式

PCM是音频的「原始格式」。WAV、AIFF这些格式,本质上就是在PCM数据外面包了一层文件头。

一个标准的WAV文件结构是这样的:

偏移地址 长度 内容
0x00 4 bytes "RIFF" 标识
0x04 4 bytes 文件总长度
0x08 4 bytes "WAVE" 标识
0x0C 4 bytes "fmt " 块标识
0x10 4 bytes fmt块长度(16)
0x14 2 bytes 音频格式(1=PCM)
0x16 2 bytes 声道数
0x18 4 bytes 采样率
0x1C 4 bytes 字节率
0x20 2 bytes 块对齐
0x22 2 bytes 量化位数
0x24 4 bytes "data" 块标识
0x28 4 bytes 数据长度
0x2C ... PCM数据

用Python读取PCM数据其实很简单:

import numpy as np

def read_pcm_from_wav(filename):
    """从WAV文件中读取PCM数据"""
    import wave
    with wave.open(filename, 'rb') as wf:
        # 获取参数
        n_channels = wf.getnchannels()
        sampwidth = wf.getsampwidth()  # 字节数
        framerate = wf.getframerate()
        n_frames = wf.getnframes()
        
        # 读取原始数据
        raw_data = wf.readframes(n_frames)
        
        # 根据量化位数选择dtype
        if sampwidth == 1:
            dtype = np.uint8  # 8-bit 无符号
        elif sampwidth == 2:
            dtype = np.int16  # 16-bit 有符号
        elif sampwidth == 4:
            dtype = np.int32  # 32-bit 有符号
        else:
            raise ValueError(f"不支持的量化位数: {sampwidth}")
        
        # 转为numpy数组
        audio = np.frombuffer(raw_data, dtype=dtype)
        
        # 如果是多声道,重塑为 (帧数, 声道数)
        if n_channels > 1:
            audio = audio.reshape(-1, n_channels)
        
        return audio, framerate

实用技巧:我写音频处理代码时,习惯先把PCM数据归一化到[-1, 1]范围。这样不管原始是16bit还是24bit,后续处理逻辑都一样。归一化公式很简单:audio_float = audio_int / (2^(bit_depth-1))。

2.5 知识体系总览

说了这么多,我们来画一张图,把数字音频的整个流程串起来:

数字音频处理流程 数字音频处理流程 模拟音频信号 采样 (Sampling) 量化 (Quantization) 编码 (Encoding) PCM 数字音频 关键参数 采样率:fs > 2fmax 量化位数:8/16/24/32 bit 声道数:1/2/5.1/7.1 字节序:大端/小端 编码格式:PCM/浮点 常见格式 WAV:PCM + RIFF头 AIFF:PCM + IFF头 RAW:纯PCM无头 避坑提醒 • 采样率不够 → 混叠 • 量化位数低 → 噪声大 • 字节序搞错 → 全是噪声

这张图把整个流程串起来了。从模拟信号开始,经过采样、量化、编码,最终得到PCM数字音频。每一步都有对应的参数需要关注,任何一个环节出问题,最终的声音都会「变味」。

2.6 动手实践:用Python查看音频参数

光说不练假把式。我们来写段代码,看看真实音频文件的参数:

import wave
import numpy as np

def inspect_audio(filename):
    """查看音频文件的基本信息"""
    with wave.open(filename, 'rb') as wf:
        print(f"声道数: {wf.getnchannels()}")
        print(f"量化位数: {wf.getsampwidth() * 8} bit")
        print(f"采样率: {wf.getframerate()} Hz")
        print(f"总帧数: {wf.getnframes()}")
        print(f"时长: {wf.getnframes() / wf.getframerate():.2f} 秒")
        
        # 读取前1秒的数据做分析
        frames_to_read = wf.getframerate()
        raw = wf.readframes(frames_to_read)
        audio = np.frombuffer(raw, dtype=np.int16)
        
        print(f"\n前1秒数据统计:")
        print(f"  最大值: {audio.max()}")
        print(f"  最小值: {audio.min()}")
        print(f"  平均值: {audio.mean():.2f}")
        print(f"  标准差: {audio.std():.2f}")

# 使用示例
# inspect_audio("example.wav")

这段代码会告诉你一个音频文件的所有底层参数。我每次拿到新的音频数据集,第一件事就是跑这个脚本,看看采样率、量化位数对不对得上。曾经有一次,标注团队说「所有音频都是16kHz」,结果我一跑脚本,发现有一半是8kHz——嗯,数据预处理阶段就把问题揪出来了,省了后面大量返工的时间。

核心要点回顾:

  • 采样定理:fs > 2fmax,欠采样会导致混叠
  • 量化:bit数越高,动态范围越大,量化噪声越小
  • PCM:最原始的音频编码方式,WAV/AIFF都是它的「马甲」
  • 字节序:跨平台时一定要显式指定,否则全是噪声