2. 数字音频格式:PCM编码原理、采样率与位深、文件结构解析

做音频处理这么多年,我始终觉得数字音频格式是绕不开的基石。你想想看,无论你后面做降噪、做编解码、还是做语音增强,最终面对的都是那一串冰冷的二进制数。怎么来的?怎么存的?搞明白这些,后面的路才走得踏实。

这一章我们就从最底层的PCM编码讲起,聊聊采样率和位深这些“老生常谈”的概念,再动手拆一拆WAV和AIFF的文件结构。嗯,都是些基本功,但往往基本功越扎实,后面遇到坑的时候越能从容应对。

2.1 PCM编码原理

PCM,全称Pulse Code Modulation,脉冲编码调制。说白了,就是把连续的模拟信号变成离散的数字信号。这个过程分三步:采样、量化、编码。

核心流程:模拟信号 → 采样(时间离散化) → 量化(幅度离散化) → 编码(二进制表示) → 数字信号

我刚开始学的时候,总觉得这玩意儿很抽象。后来自己用示波器看过模拟波形,再用逻辑分析仪抓过数字输出,一下子就通了。模拟信号是连续的,像一条光滑的曲线;数字信号是离散的,像一个个台阶。台阶越密、越细,就越接近原来的曲线。

具体来说:

  • 采样:每隔固定时间取一个样值。这个时间间隔的倒数就是采样率。
  • 量化:把采样得到的电压值归到最近的量化等级上。等级总数由位深决定。
  • 编码:把量化后的等级用二进制数表示出来。比如16bit就是用一个16位的二进制数表示一个样点。

这里有个关键点——量化误差。你想想看,模拟值是连续的,量化等级是有限的,归整的时候必然会有误差。这个误差就是量化噪声。我在做一款低功耗语音唤醒芯片时,就遇到过量化噪声被后续放大器放大导致误唤醒的问题。后来把位深从16bit提到24bit,问题就解决了。说白了,量化噪声是PCM天生的,你只能控制它,不能消灭它。

2.2 常见的采样率与位深

采样率和位深是数字音频的两个核心参数。它们直接决定了音频的质量和数据量。

2.2.1 采样率

采样率决定了频率范围。根据奈奎斯特采样定理,采样率必须大于信号最高频率的两倍,才能无失真地重建信号。

采样率 典型应用场景 最高可表示频率
8 kHz 电话语音、对讲机、早期VoIP 4 kHz
16 kHz 宽带语音、语音识别、语音通信 8 kHz
44.1 kHz CD音质、音乐播放、专业音频 22.05 kHz
48 kHz 影视音频、DVD、专业录音棚 24 kHz

我个人习惯在语音项目里用16kHz,因为人声的主要能量集中在4kHz以下,16kHz采样绰绰有余,还能省带宽和存储。但做音乐处理时,我一般用44.1kHz或48kHz,毕竟人耳理论上能听到20kHz,得留够余量。

避坑指南:我曾经在一个项目里直接用48kHz采样率处理8kHz的电话录音,结果发现高频全是噪声。后来才意识到,录音本身最高只有4kHz,48kHz采样只是把噪声也采进来了。采样率不是越高越好,要和信号带宽匹配。

2.2.2 位深

位深决定了动态范围。每增加1bit,动态范围增加约6dB。16bit的动态范围是96dB,24bit是144dB,32bit是192dB。

位深 动态范围 量化等级数 典型应用
16bit 96 dB 65,536 CD、WAV文件、语音通信
24bit 144 dB 16,777,216 录音棚、专业音频处理
32bit 192 dB 4,294,967,296 高精度音频、科学测量

你可能会问,32bit的192dB动态范围,现实中根本用不到啊?没错,但32bit浮点格式在DSP处理中非常有用。因为浮点运算不会像定点数那样容易溢出,做滤波、混音的时候省心很多。我在做多路混音算法时,就吃过16bit定点溢出的亏,后来改用32bit浮点,世界清净了。

2.3 WAV与AIFF文件结构解析

WAV和AIFF是两种最常见的未压缩音频文件格式。它们都基于RIFF(Resource Interchange File Format)结构,说白了就是“块”的嵌套。

2.3.1 WAV文件结构

WAV文件由若干个Chunk(块)组成。最基本的三个块是:

  • RIFF Chunk:文件头,标识文件类型为WAVE。
  • fmt Chunk:格式块,描述音频格式、声道数、采样率、位深等参数。
  • data Chunk:数据块,存放实际的PCM采样数据。

下面是一个典型的WAV文件结构示意图:

WAV 文件结构示意图 RIFF Chunk(文件头) 标识:'RIFF' | 文件大小 | 格式类型:'WAVE' fmt Chunk(格式块) 标识:'fmt ' | 块大小 | 音频格式 | 声道数 | 采样率 | 字节率 | 块对齐 | 位深 data Chunk(数据块) 标识:'data' | 数据大小 | PCM采样数据(二进制) 可选块:fact / cue / LIST 等

写代码解析WAV文件时,我建议你按以下步骤来:

  1. 读取前4字节,确认是'RIFF'。
  2. 读取4字节文件大小(注意小端序)。
  3. 读取4字节格式类型,确认是'WAVE'。
  4. 循环读取子块,根据块标识('fmt '或'data')分别处理。
  5. 解析fmt块,提取采样率、位深、声道数等参数。
  6. 读取data块,得到PCM数据。

注意:WAV文件使用小端字节序(Little-Endian)。如果你在ARM或x86平台上解析,直接读就行;但如果在某些大端序的DSP上,记得做字节序转换。我曾经在一个PowerPC架构的嵌入式平台上栽过这个跟头,读出来的采样率是65535,吓我一跳。

2.3.2 AIFF文件结构

AIFF是Apple公司推出的格式,和WAV类似,也是基于块结构。但有几个关键区别:

  • 字节序:AIFF使用大端序(Big-Endian)。
  • 数据格式:AIFF默认使用有符号整数,而WAV使用有符号整数(16bit及以上)或无符号整数(8bit)。
  • 块标识:AIFF的格式块叫'COMM',数据块叫'SSND'。
特性 WAV AIFF
字节序 小端(Little-Endian) 大端(Big-Endian)
格式块标识 'fmt ' 'COMM'
数据块标识 'data' 'SSND'
采样数据格式 有符号/无符号整数 有符号整数
扩展名 .wav .aiff / .aif

AIFF的COMM块结构如下:

typedef struct {
    uint32_t  chunkID;       // 'COMM'
    uint32_t  chunkSize;     // 块大小(不含ID和Size字段)
    uint16_t  numChannels;   // 声道数
    uint32_t  numSampleFrames; // 采样帧数
    uint16_t  sampleSize;    // 位深(bit)
    uint16_t  sampleRate[5]; // 采样率(80-bit浮点,IEEE 754 extended)
} COMMChunk;

这里有个坑——采样率字段是80-bit浮点数。我第一次解析AIFF时,看到这个字段直接懵了。后来查了文档才知道,Apple用了一种古老的浮点格式。实际开发中,我建议你直接忽略这个字段,从文件的其他地方获取采样率信息,或者用现成的库来解析。

实用建议:如果你在嵌入式平台上做音频开发,我建议优先使用WAV格式。原因有三:一是小端序和大多数CPU一致,省去字节序转换;二是WAV的解析逻辑更简单,代码量少;三是WAV的兼容性更好,几乎所有平台都支持。AIFF在Apple生态里用得比较多,跨平台时尽量避开。

2.4 数据量与带宽计算

搞清楚了采样率和位深,我们就能算出一段音频的数据量了。公式很简单:

数据量(字节/秒) = 采样率 × 位深 / 8 × 声道数

举个例子:

  • CD音质(44.1kHz, 16bit, 立体声):44,100 × 16 / 8 × 2 = 176,400 字节/秒 ≈ 172 KB/s
  • 电话语音(8kHz, 16bit, 单声道):8,000 × 16 / 8 × 1 = 16,000 字节/秒 ≈ 15.6 KB/s
  • 录音棚(48kHz, 24bit, 立体声):48,000 × 24 / 8 × 2 = 288,000 字节/秒 ≈ 281 KB/s

这个计算在嵌入式系统里特别重要。我曾经做一个蓝牙音频项目,需要实时传输PCM数据。算了一下带宽,发现蓝牙的A2DP链路根本扛不住48kHz/24bit的立体声。后来降到了44.1kHz/16bit,再配合适当的压缩,才勉强跑通。你想想看,如果不提前算好带宽,等硬件做出来才发现跑不动,那可就麻烦了。

关键公式:数据量(MB/分钟) = 采样率(Hz) × 位深(bit) / 8 × 声道数 × 60 / 1024 / 1024

嗯,这一章的内容就这些。PCM编码是数字音频的起点,采样率和位深决定了音频的质量天花板,WAV和AIFF则是我们和音频数据打交道的日常工具。把这些基础搞扎实了,后面讲编解码、讲音频处理算法的时候,你就能更专注于算法本身,而不是被文件格式这种琐事绊住手脚。