PCM 音频采样数据参考手册
PCM 音频采样数据参考手册
PCM(Pulse Code Modulation,脉冲编码调制)是未经压缩的数字音频采样数据表示方式。它不是网络协议,也不是带文件头的容器;采样率、位深、声道数和字节序必须由上下文额外约定。
本文范围依据数字音频采样的通用定义、RIFF/WAVE 对 PCM 的描述方式以及常见音频设备接口约定整理。PCM 本身没有一个唯一的文件级标准头,因此本文把“采样值如何表示”和“如何在容器/接口中描述这些采样值”分开说明。
第一章 PCM 的定义与采样模型
PCM 在音频链路中的位置
模拟声音经过 ADC 后得到 PCM;编码器将 PCM 压缩为 AAC、Opus 等码流;播放器解码后通常再次得到 PCM,再交给声卡或 DAC。
1 | 麦克风 → ADC → PCM → 编码器 → AAC/Opus |
PCM 只描述“每个采样点的数值”,不描述采样率、声道布局、时间戳和文件长度。因此裸 PCM 文件不能仅凭扩展名可靠识别。
四个必须明确的参数
| 参数 | 含义 | 常见值 |
|---|---|---|
| sample rate | 每秒采样次数 | 8000、16000、44100、48000 Hz |
| sample format | 每个样本的数值表示 | S16、S24、S32、F32 |
| channels | 声道数 | 1、2、6、8 |
| layout | 多声道样本排列方式 | interleaved、planar |
采样率为 R、声道数为 C、每样本字节数为 B 时:
1 | 每秒字节数 = R × C × B |
这里的“音频帧”指同一采样时刻所有声道的样本集合,不等同于 AAC 帧或视频帧。
有符号整数与浮点样本
S16
S16 使用 16 位有符号整数,范围为 -32768 到 32767。常见 PCM/WAV 使用 little-endian S16LE。
归一化时可以近似写为:
1 | x_float = x_int16 / 32768.0 |
反向转换时必须进行限幅,避免浮点运算后的值溢出整数范围。
S24 与 S32
24 位样本可能以 3 字节紧凑形式存储,也可能放入 32 位容器的高 24 位或低 24 位。解析前必须确认约定,不能仅凭“24 bit”推断内存布局。
S32 常用于硬件接口或中间处理,实际有效位数可能只有 24 位。
F32
F32 通常使用 IEEE-754 单精度浮点,约定范围为 [-1.0, 1.0)。浮点 PCM 不等于压缩格式,仍然是原始采样数据。
第二章 样本表示与内存布局
交错与平面布局
立体声交错(interleaved)数据顺序如下:
1 | L0, R0, L1, R1, L2, R2, ... |
平面(planar)数据则是:
1 | L0, L1, L2, ... | R0, R1, R2, ... |
把 planar 当成 interleaved 播放会造成严重串音;把 interleaved 当成 planar 会使一个声道读到另一声道的一半数据。
音频帧、时间和缓冲
音频帧、周期和缓冲区
声卡常以 period 为单位搬运数据。period_size 表示每个声道的采样数,缓冲区字节数为:
1 | period_bytes = period_size × channels × bytes_per_sample |
例如 16 kHz、单声道、S16、20 ms:
1 | samples = 16000 × 0.020 = 320 |
实时语音系统常以 10 ms、20 ms 或 codec 推荐的帧长作为采集和编码边界。
字节序和对齐
S16LE 的样本 0x1234 在内存中的字节顺序是 34 12;S16BE 则是 12 34。网络传输通常明确使用 network byte order,但裸音频和硬件 DMA 可能使用本机或设备约定的字节序。
解析多字节样本时不要直接把网络缓冲区强制转换成 int16_t *,还要考虑未对齐访问、端序和严格别名规则。
第三章 文件和接口中的 PCM
PCM 与 WAV 的区别
PCM 是数据编码方式;WAV 通常是 RIFF 容器,包含 RIFF、fmt 和 data 等块,data 块中才是 PCM 样本。WAV 也可以承载非 PCM 编码,因此“WAV 等于 PCM”并不严格。
混音、增益和限幅
多个 S16 音频相加可能溢出。常用做法是先提升到 S32 或 float 计算,再进行限幅:
1 | int32_t mixed = (int32_t)a + (int32_t)b; |
直接在 S16 上相加会产生回绕失真。
解析、转换与验证
解析和验证清单
- 确认采样率、位深、声道数、layout、端序。
- 用字节数计算理论时长,与实际播放时长比较。
- 检查每个 DMA/网络包是否是完整音频帧的整数倍。
- 检查丢包后是否发生声道错位或半样本拼接。
- 用波形、频谱和静音段确认符号位与端序正确。
常见错误
- 只传裸 PCM,不传参数,接收端无法可靠播放。
- 将 16 kHz 当成 8 kHz,导致播放速度和音调错误。
- 忽略声道交错,造成串音。
- 将字节数当成采样数,导致时长计算错误。
- 对 S16 直接求和,产生削波或整数回绕。
- 网络分片切断一个样本,重组时没有按样本边界恢复。
规范总结
PCM 的核心不是一个固定字节头,而是参数集合:sample_rate + sample_format + channels + layout + endianness。任何跨线程、跨设备或跨网络传输都应把这些参数写入会话配置、容器头或自定义协议头中。
字节级示例:S16LE 双声道
假设参数为 48 kHz、S16LE、双声道、交错布局,连续两个音频帧可能是:
1 | 样本值:L0=0x0100,R0=0xFE00,L1=0x0200,R1=0xFD00 |
每 4 个字节表示一个时间点的立体声音频帧。若从偏移 2 开始读取,虽然仍能读出两个字节,但已经破坏了声道/采样边界。
S24 的三种常见布局
“24 bit PCM”至少可能表示以下三种形式:
| 布局 | 每样本占用 | 说明 |
|---|---|---|
| packed S24LE | 3 字节 | 低字节在前,连续紧凑 |
| S24 in S32LE | 4 字节 | 有效位放低 24 位 |
| S24 in S32BE | 4 字节 | 有效位和端序由设备定义 |
如果输入是 01 02 03,packed S24LE 的数值为 0x030201;读取为 S32 时不能直接假设最高字节是符号扩展,必须根据设备协议扩展符号位。
静音、削波和符号检查
S16 静音通常接近 00 00,但有偏置的 ADC 可能以中点值表示静音。连续大量 00 80(S16LE 的 -32768)通常表示端序、符号扩展或 DMA 初始化错误。波形长期贴住 32767/-32768 则表示削波。
裸 PCM 文件的描述协议
如果必须传输裸 PCM,建议在会话控制或自定义头中显式写入:
1 | magic[4]、version、sample_rate、sample_format、channels、layout、endianness |
数据包应携带 sample_index 或采集时间戳。不要把“文件名是 xxx_16k_mono.pcm”作为唯一协议约定。
可验证的解析伪代码
1 | for (size_t off = 0; off + frame_bytes <= bytes; off += frame_bytes) { |
实际实现还必须检查 frame_bytes 乘法溢出、输入长度、对齐和通道范围。
PCM 数据构成图
以 S16LE 双声道交错数据为例:
1 | Audio Frame 0 Audio Frame 1 |
| 偏移 | 字节 | 字段 | 作用 |
|---|---|---|---|
| 0 | 00 01 |
L0 | 左声道第 0 个采样,S16LE |
| 2 | 00 FE |
R0 | 右声道第 0 个采样 |
| 4 | 00 02 |
L1 | 左声道第 1 个采样 |
| 6 | 00 FD |
R1 | 右声道第 1 个采样 |
参数到字节的推导
48 kHz、S16LE、双声道、20 ms 的数据构成为:
1 | 采样帧数 = 48000 × 20 / 1000 = 960 |
因此一个网络包如果宣称“20 ms PCM”,但 payload 不是 3840 字节,就必须说明它是压缩数据、分片数据或使用了其他格式。
从采样参数还原 PCM 字节
读取裸 PCM 的第一步不是寻找魔数,而是取得外部格式描述。假设收到一段 16 位小端、单声道、16 kHz 的数据,字节 00 00 00 10 00 20 00 30 应解释为四个连续采样:0、4096、8192、12288。每两个字节组成一个样本,不能把四个字节解释为一个 32 位整数。若改成双声道,同样的八个字节则代表两个时间点:第一个时间点的左、右样本分别是 0 和 4096,第二个时间点的左、右样本分别是 8192 和 12288。声道数改变后,字节总数不变并不意味着时长不变,因为时长计算还要除以声道数。
解析器应先建立一个格式描述结构,固定保存采样率、声道数、每样本位数、有效位数、布局和端序。随后按照 bytes_per_sample × channels 计算音频帧步长,再以帧为单位推进游标。任何不足一个完整帧的尾部都应报告为截断数据,而不是悄悄当作一个短帧播放。对于实时流,半帧可以暂存在缓冲区中等待下一包补齐;对于文件,则应记录损坏偏移。
数值转换与动态范围
S16 使用二进制补码,范围为 -32768 到 32767,负方向比正方向多一个值。将整数归一化到 float 时常用除数为 32768.0;将 float 转回 S16 时,应先限制在 [-1, 1) 范围,再进行舍入。直接强制转换会截断小数并产生量化误差。多路混音时,应先用至少 32 位整数或 float 保存中间结果,再统一做增益和限幅;直接在 S16 上相加会发生回绕。所谓 0 dBFS 表示数字表示的最大幅度,不等于声卡输出端的物理电压。
网络传输中的 PCM 边界
PCM 没有自描述边界,网络层必须补充序号和采样位置。推荐的应用层描述至少包含 format_version、sample_rate、sample_format、channels、layout、first_sample_index 和 payload_bytes。first_sample_index 适合判断丢失了多少采样,时间戳适合和视频时钟做同步。收到一个包后,先校验 payload 长度是否为帧步长的整数倍,再根据序号填入缓存。丢失的 PCM 不应使用后续数据顶替,否则会造成永久声道错位;语音场景可以插入静音或重复短帧,但必须记录这是丢包恢复数据。
第四章 采样、量化与编码表示
采样定理在 PCM 中的含义
连续时间信号经过等间隔采样后,得到离散时间序列。对于带宽上限为 B 的理想基带信号,采样率应大于 2B,否则频谱会发生混叠。PCM 文件本身不保存原始模拟信号的带宽字段,因此采样率只能说明采样点之间的时间间隔,不能保证信号一定没有混叠。实际 ADC 通常在采样前使用模拟低通滤波器,并在数字域使用抗混叠和重采样滤波器。
采样点的时间位置可以写成:
1 | t(n) = t0 + n / sample_rate |
其中 n 是从流开始计算的采样帧序号,t0 是流的起始时间。双声道交错数据中,左右声道共享同一个 n;不能把左声道样本和右声道样本分别当成两个连续时间点。
量化步长与有效位数
把连续幅度映射到有限整数集合的过程称为量化。理想 N 位有符号 PCM 的码字数量为 2^N,但实际设备可能只使用其中一部分有效位。S16 的每个存储值是一个码字,不应误称为 16 位浮点精度。解析器应区分 container_bits 和 valid_bits,这样才能正确计算噪声、对齐和归一化。
有效位、容器位和填充位
许多音频接口用 32 位内存单元承载 20 位或 24 位有效样本。字段描述可以明确写成:
| 属性 | 示例 | 含义 |
|---|---|---|
valid_bits |
24 | 实际表示音频幅度的位数 |
container_bits |
32 | 内存或总线中占用的位数 |
alignment |
MSB/LSB | 有效位靠高位还是低位对齐 |
sign_extension |
yes/no | 填充位是否复制符号位 |
例如一个左对齐的 24 位样本可能存放为 0x12345600,右对齐样本为 0x00123456。两者的数字含义相同,但字节序和移位规则完全不同。
裸 PCM 的格式描述与记录方式
为什么裸 PCM 需要外部描述
PCM payload 没有固定魔数、版本字段或自带长度。00 00 既可能是一个 S16 静音样本,也可能是 S24 的低半部分、F32 的一部分,甚至可能是压缩码流中的普通字节。因而可移植的 PCM 文件必须把格式描述放在容器、旁车文件或会话信令中。
1 | struct PcmFormat { |
该结构是应用协议中的元数据示例,不是 PCM 自带的固定文件头。格式版本应在字段改变时递增,不能让接收端根据 payload 长度猜版本。
帧长度、包长度和尾部
frame_bytes = channels × container_bits / 8 只适用于每个声道样本占用整字节的情况。对于紧凑 20 位或 12 位打包格式,必须使用规范规定的位流计算方式。文件末尾若少于一个完整音频帧,应报告截断、丢弃尾部或在明确容错模式下补零,默认解析模式不应静默掩盖损坏。
字节级解析规则
小端有符号样本的符号扩展
读取 S24LE packed 时,先把三个无符号字节拼接,再检查最高有效位:
1 | u = b0 | (b1 << 8) | (b2 << 16) |
结果应保存到至少 32 位有符号整数。对 S16 也应使用 0、1、-1、最大值和最小值五组测试样本核对端序和符号。
浮点 PCM 的验证
F32 数据除了检查文件长度是否为 4 × channels 的整数倍,还应检查 NaN 和无穷大。超出 [-1, 1) 的有限值可能是制作链路允许的 headroom,不应与格式非法混为一谈。
采样率转换与通道处理
重采样的边界
采样率转换不是简单删除或复制采样点。48000 Hz 转 16000 Hz 时需要抗混叠低通和抽取;16000 Hz 转 48000 Hz 时需要插值和重建滤波。重采样器应保存滤波器状态和分数相位,不能在每个网络包边界重新初始化,否则会产生点击声和频谱突变。
声道映射与混音矩阵
声道数变化应显式使用映射矩阵。立体声转单声道可写成 mono[n] = 0.5 * left[n] + 0.5 * right[n];5.1 转立体声还要规定中央、环绕和低频声道的衰减系数。布局标签描述声道含义,交错顺序描述内存排列,两者必须分别保存。
实现接口与安全校验
解析器的输入检查
计算 sample_rate × channels × bytes_per_sample 时应使用宽整数,并检查零值、极大值和乘法溢出。对于来自不可信网络的长度字段,必须同时检查加法溢出和缓冲区剩余长度。播放位置应以采样帧而不是字节推进:
1 | next_sample_frame = current_sample_frame + decoded_frames |
如果把每个声道的样本都计入时间,双声道音频会被错误地播放为原来的一半速度。
完整性校验
裸 PCM 没有内置 CRC。需要可靠传输时,可在应用包中加入序号、payload 长度、采样帧起点和校验值。校验值只证明字节未被改变,不能证明采样率和声道配置正确;格式元数据应单独校验并绑定到会话版本。
PCM 与标准容器的关系
RIFF/WAVE 中的 PCM 描述
WAVE 的 fmt 块把 PCM 的采样率、通道数、块对齐和平均字节率写入文件头,data 块保存原始样本。block_align 是一个采样帧的字节数,而不是单个声道样本的字节数;byte_rate 通常等于 sample_rate × block_align。解析 WAVE 时还应检查奇数长度块的 RIFF 对齐填充。
参考验证用例
1 | 输入参数:48000 Hz / 2 ch / S16LE / interleaved |
若字节一致但播放时长不正确,检查采样率和声道数;若波形上下翻转,检查符号位;若立体声串音,检查交错/平面解释;若只有 S24 出错,检查紧凑布局、符号扩展和有效位对齐。
线性 PCM 的码值定义
二进制补码整数
有符号整数 PCM 通常使用二进制补码。对于 N 位有效位,最小值为 -2^(N-1),最大值为 2^(N-1)-1。零对应所有有效位为零,最高有效位是符号位。负数不能通过“最高位清零再取负”解码,而应按照补码规则进行符号扩展。
| 有效位数 | 最小码值 | 最大码值 | 常用名称 |
|---|---|---|---|
| 8 | -128 | 127 | S8 |
| 16 | -32768 | 32767 | S16 |
| 24 | -8388608 | 8388607 | S24 |
| 32 | -2147483648 | 2147483647 | S32 |
某些文件和硬件接口使用无符号 8 位 PCM,此时静音中点通常是 128,而不是 0。WAVE 的传统 8-bit PCM 常见这种约定;16 位及以上传统 PCM 通常为有符号值。实现必须由格式描述决定符号性,不能把“PCM”统一强制转换为 int16_t。
归一化规则的不对称性
二进制补码负侧比正侧多一个码值。整数转浮点常用 x / 2^(N-1),使最小负值准确映射为 -1.0,最大正值略小于 1.0。也有系统为了对称峰值使用最大正值作除数,这会让最小负值超出 -1.0。接口应明确采用哪种规则,尤其是在跨库比较波形或生成测试向量时。
1 | S16 → float: f = s / 32768.0 |
浮点转整数时需要定义舍入模式。向零截断会增加小信号偏差,nearest-even 或带抖动量化更适合高质量转换。手册中的字节验证用例应固定舍入规则,否则临界值可能因平台不同得到相邻两个码字。
定点 PCM 与 Q 格式
嵌入式 DSP 有时把 PCM 样本解释为 Q1.15、Q1.23 或 Q1.31 定点数。Q 格式是算法数值约定,不改变底层字节;同一个 0x4000 在 S16 整数域表示 16384,在 Q1.15 中表示约 0.5。传输协议应写“有效位和缩放位置”,不能只写 int16,否则接收端无法知道是否需要归一化。
整数 PCM 码字的完整解释规则
格式描述必须具备的字段
“这是 PCM 数据”不足以解释任何一段字节。至少要知道采样率、声道数、样本数值类型、有效位数、容器位数、字节序、有效位在容器中的对齐方式、声道排列和帧布局。对整数 PCM,建议使用如下不可变格式对象:
| 字段 | 示例 | 作用 |
|---|---|---|
sample_rate |
48000 | 每声道每秒采样帧数 |
channels |
2 | 每帧包含的声道样本数 |
number_type |
signed integer | 有符号补码、无符号偏置或浮点 |
valid_bits |
24 | 真正表达量化码值的位数 |
container_bits |
32 | 每个样本槽在内存/文件占用位数 |
byte_order |
little-endian | 多字节容器的字节顺序 |
valid_alignment |
most-significant | 有效位位于容器高端还是低端 |
frame_layout |
interleaved | 交错帧还是按声道平面 |
channel_order |
FL,FR | 每帧的声道顺序 |
只有当 container_bits 是八的倍数时,单样本才能简单表示为 container_bits/8 字节。某些串行总线或压缩打包格式允许 20 位样本连续紧密排列,这时样本可能跨字节,必须额外说明 bit order、帧边界和填充规则。裸文件扩展名 .pcm 不提供这些信息。
有符号补码的通用解码
设有效位数为 N,先把 N 位无符号码字读入足够宽的无符号整数 u。补码转有符号值的数学定义为:
1 | if u < 2^(N-1): |
该定义不依赖宿主机如何表示负数,也避免对窄有符号类型执行实现相关右移。以 24 位为例:
| 三字节码字 | 无符号值 | 有符号结果 |
|---|---|---|
00 00 00 |
0x000000 |
0 |
01 00 00 |
0x000001 |
1 |
FF FF 7F |
0x7FFFFF |
8388607 |
00 00 80 |
0x800000 |
-8388608 |
FF FF FF |
0xFFFFFF |
-1 |
表中字节按 S24LE 文件顺序书写。先组合为 u=b0 | b1<<8 | b2<<16,再做 24 位补码解释。不能先把 b2 转成有符号 8 位再左移,因为负有符号值左移在 C/C++ 中可能产生未定义行为。
通用符号扩展可在无符号域完成:
1 | sign = 1 << (N - 1) |
当 N 等于宿主整数宽度时,1 << N 会溢出,因此实际实现要使用更宽类型或为 32/64 位建立专门分支。读取外部输入时还应先验证 1 <= N <= supported_width。
无符号偏置 PCM
U8 的零点和幅度
传统 RIFF/WAVE 的 8-bit PCM 通常采用无符号偏置编码:数字静音为 0x80,负峰附近为 0x00,正峰为 0xFF。把 U8 码字转换到以零为中心的整数域:
1 | signed_centered = unsigned_code - 128 |
| U8 字节 | 中心化整数 | 以 128 为除数归一化 |
|---|---|---|
00 |
-128 | -1.00000000 |
01 |
-127 | -0.99218750 |
7F |
-1 | -0.00781250 |
80 |
0 | 0.00000000 |
81 |
1 | 0.00781250 |
FE |
126 | 0.98437500 |
FF |
127 | 0.99218750 |
因此一个全零字节的 U8 WAVE 不是静音,而是持续最小负码,通常表现为很大的直流偏置和开始/结束爆音。反之,S8 裸 PCM 的静音才是 0x00。解析 API 必须分别表达 U8 和 S8,不能仅依赖“bits_per_sample=8”。
U8 转 S16 的精确扩位常用:
1 | s16 = (u8 - 128) << 8 |
这样 0x00 -> -32768,0x80 -> 0,0xFF -> 32512。低八位补零是无损扩位,反向若要求精确恢复原 U8,可先确认 S16 低八位全零,再算 (s16 >> 8)+128。若 S16 来自任意信号,则反向转换必须定义舍入和饱和。
通用无符号偏置公式
对 N 位 offset-binary PCM,中点为 2^(N-1):
1 | center = 2^(N-1) |
但“N 位 PCM 必然是这种无符号格式”并不成立。WAVE 的常见约定、AIFF、硬件 ADC 寄存器和厂商网络格式可能不同,必须由承载格式或设备接口规范确定。
有效位与容器位
两个宽度解决不同问题
有效位数决定码值范围和量化分辨率,容器位数决定内存步长、文件长度和端序交换宽度。例如 24 位有效样本可放在三字节 packed 容器,也可放在四字节槽中。两者每个样本的数值范围相同,但字节率不同:
1 | 48 kHz, stereo, packed 24-bit: |
如果读取器用有效位数计算步长,会把第二种格式每帧少算两字节,声道边界从第一帧之后就开始漂移。Block Align 必须由容器字节数乘声道数得到;归一化比例则由有效位数决定。
高位对齐的 24-in-32
WAVE_FORMAT_EXTENSIBLE 用 wValidBitsPerSample 表达有效位少于容器位的情况,常见规则是有效位在容器的最高有效部分,低位为填充。24-in-32 的逻辑关系:
1 | container = signed_24_value << 8 |
取 24 位值 0x123456,左对齐容器位模式为 0x12345600,小端字节:
1 | 00 56 34 12 |
取值 -1,24 位补码为 0xFFFFFF,左对齐后为 0xFFFFFF00,小端字节:
1 | 00 FF FF FF |
取最小值 -8388608,左对齐为 0x80000000,小端字节:
1 | 00 00 00 80 |
严格文件验证可检查低八个填充位是否全零。若非零,可能是写入器实际存了 32 位有效数据却错误声明 24 位,也可能是未清理的噪声位。解码时可按声明忽略低位,但应报告非规范或不一致状态。
低位对齐与接口差异
许多 C API 使用 int32_t 承载 S24 时采用低位对齐并做符号扩展,例如 +0x123456 在寄存器中是 0x00123456,-1 是 0xFFFFFFFF。这与 WAVE Extensible 高位对齐样本字节不同:
1 | S24 value = 0x123456 |
二者转换需要移位,不能只复制四字节。格式对象应明确 MSB-aligned 或 LSB-aligned/sign-extended。ALSA、音频 DSP、DMA slot 和文件容器的具体格式名各有定义,应用层不得仅凭“24 in 32”猜测。
字节序的逐样本规则
多字节整数
端序描述的是一个样本容器内部的字节顺序,不改变交错声道顺序。S16 值 0x1234:
1 | S16LE: 34 12 |
S24 位模式 0x123456:
1 | S24LE: 56 34 12 |
S32 位模式 0x12345678:
1 | S32LE: 78 56 34 12 |
双声道交错的一帧若 L=0x1234、R=-2=0xFFFE:
1 | S16LE interleaved: 34 12 FE FF |
端序转换应对每个样本分别反转字节,不能反转整个帧。把四字节帧整体反转会同时交换左右声道,得到 FF FE 12 34,既改变端序又改变声道顺序。
位级打包的 bit order
20-bit packed PCM 没有唯一通用字节布局。两个 20 位样本可各占三字节并留四个填充位,也可能两个样本紧密占五字节,还可能装入 32 位 slot。对连续 bitstream,除了 byte order 还必须说明每个字节内先发送 MSB 还是 LSB、样本是否跨字节、帧是否字节对齐以及负数如何扩展。若规范缺少任一项,不能可靠解析。
IEEE 754 浮点 PCM
F32 的字段
常见浮点 PCM 使用 IEEE 754 binary32。一个 32 位码字由符号位、八位指数和 23 位 fraction 构成:
1 | 31 30 23 22 0 |
普通数值为:
1 | (-1)^sign * (1.fraction) * 2^(exponent - 127) |
典型码字和 little-endian 文件字节:
| 数值 | binary32 位模式 | F32LE 字节 |
|---|---|---|
| +0.0 | 00000000 |
00 00 00 00 |
| -0.0 | 80000000 |
00 00 00 80 |
| +0.5 | 3F000000 |
00 00 00 3F |
| -0.5 | BF000000 |
00 00 00 BF |
| +1.0 | 3F800000 |
00 00 80 3F |
| -1.0 | BF800000 |
00 00 80 BF |
| +2.0 | 40000000 |
00 00 00 40 |
WAVE 的 IEEE Float 传统 format tag 为 3;WAVE_FORMAT_EXTENSIBLE 则通过 SubFormat GUID 指示 IEEE Float。wBitsPerSample=32 本身不能区分 S32 与 F32。
F64 与宿主解释
binary64 使用一位符号、11 位指数和 52 位 fraction。+1.0 的位模式是 0x3FF0000000000000,F64LE 字节为:
1 | 00 00 00 00 00 00 F0 3F |
解析时先按声明端序重建整数位模式,再用安全的 bit cast 解释成 float/double。C/C++ 中通过不兼容指针强制别名可能违反 aliasing 和对齐规则;应使用 memcpy、std::bit_cast 或明确的字节加载函数。
非有限值和超范围幅度
浮点 PCM 可以表示 NaN、正负 Infinity、subnormal、负零,也能表示绝对值大于 1.0 的有限样本。标准容器允许存放浮点位模式,并不自动保证“音频样本一定在 [-1,1]”。处理策略应由 API 明确:
| 输入 | 分析路径 | 播放/转整数建议 |
|---|---|---|
| finite 且范围内 | 原样统计 | 按规则舍入转换 |
finite 但 abs(x)>1 |
记录超范围峰值 | 饱和、限幅或由调用方处理 |
| +Infinity | 非有限计数 | 通常映射正最大值或拒绝 |
| -Infinity | 非有限计数 | 通常映射负最小值或拒绝 |
| NaN | NaN 计数并保留原位置信息 | 通常映射静音或拒绝,不能参与普通峰值比较 |
| subnormal | 可原样保留 | 实时 DSP 可按性能策略 flush-to-zero,但要声明 |
NaN 与任何数比较都为 false。若峰值代码只写 if (abs(x)>peak),NaN 会静默绕过检测;应先调用 isfinite。把 NaN 直接强制转换为整数在不同语言或硬件上结果可能不一致,因此必须显式处理。
静音码、满幅和极性
各格式的静音字节
数字静音是数值零,不必然是所有文件字节为零:
| 格式 | 一个样本的静音字节 | 说明 |
|---|---|---|
| U8 | 80 |
偏置中点 |
| S8 | 00 |
补码零 |
| S16LE | 00 00 |
补码零 |
| S24LE | 00 00 00 |
补码零 |
| S32LE | 00 00 00 00 |
补码零 |
| 24-in-32 LE | 00 00 00 00 |
高位有效、低位填充 |
| F32LE +0 | 00 00 00 00 |
正零 |
| F32LE -0 | 00 00 00 80 |
数值也等于零,但位模式不同 |
生成静音缓冲时,只有零码等于全零的格式才可直接 memset(0)。U8 必须填 0x80;含打包元数据、非音频 slot 或设备要求特殊 idle pattern 的硬件缓冲还要遵循接口规范。
正负满幅并不对称
N 位补码的负最小值绝对值比正最大值大一。S16:
1 | negative full scale = -32768 -> 00 80 (LE) |
对 -32768 执行 16 位取负会溢出,仍可能得到 -32768。求绝对峰值时应先扩宽到 32/64 位,或专门处理最小负值。相位反转也需要定义饱和规则:
1 | invert(-32768) = +32767 # 饱和策略 |
若使用回绕算术,最小负值反相不变,会在极端波形处产生不对称失真。
整数与浮点的规范化映射
整数转浮点
推荐的二次幂归一化对 N 位补码为:
1 | scale = 2^(N-1) |
它精确满足最小负值映射为 -1.0,正最大值映射为 1 - 1/scale。典型值:
| 格式 | -FS | 零 | +Max |
|---|---|---|---|
| S8 | -1.0 | 0.0 | 0.9921875 |
| S16 | -1.0 | 0.0 | 0.999969482421875 |
| S24 | -1.0 | 0.0 | 0.9999998807907104 |
| S32 | -1.0 | 0.0 | 0.9999999995343387 |
使用最大正值作除数会得到正端恰好 1.0,但最小负值小于 -1.0。两种约定都可能存在于软件生态中;关键是接口和测试必须固定一种,不能在编码与解码两侧混用。
浮点转整数
对二次幂映射,先处理非有限值,再将输入限制到可表示区间:
1 | lower = -1.0 |
如果先乘再 clamp,超大浮点数可能溢出中间整数转换。若简单限制到 x<=1.0 并计算 round(1.0*32768),会产生 32768,超出 S16 最大值;必须用正端上限或在舍入后饱和。
对 U8,可先映射到中心化 S8 域,再加偏置:
1 | q = round(clamp(x, -1.0, 127/128) * 128) |
得到 -1 -> 0x00、0 -> 0x80、正最大输入上限 -> 0xFF。
可逆范围和浮点精度
binary32 有 24 位有效二进制精度(包含隐含最高位),因此所有 24 位有符号整数都能精确转成 F32 并在适当缩放下恢复;32 位整数并非都能由 F32 精确表示,较大幅值处相邻 F32 间隔超过一个整数码。S32 需要 binary64 或整数路径才能保证逐码字无损往返。
回归测试应分别验证“数值近似”等价和“位精确可逆”。S16/S24 经 F32 的规范化往返在固定舍入下可做到全范围精确;S32 经 F32 不应承诺 bit-exact。即使使用 F64,处理链中的增益、混音或重采样也会改变样本,不能再用原始哈希比较。
原始码字解析的参考算法
整数样本读取
1 | read_integer_sample(bytes, format): |
输出使用足够宽的宿主有符号类型。算法先按容器端序加载,再处理有效位对齐,最后解释符号;顺序不可交换。对低位对齐且容器高位已符号扩展的输入,mask 会取得有效位码字,然后数学符号扩展,结果不依赖高填充位是否已扩展。
写出整数样本
1 | write_integer_sample(value, format): |
“clamp_or_reject”必须由 API 策略决定。音频播放转换常采用饱和,文件无损重打包或协议验证则应在超范围时拒绝,避免悄悄改变数据。写入前应清理所有规定为零的填充位,防止未初始化内存泄漏到文件或总线。
边界测试表
每种整数格式至少测试:零、±1、最小值、最大值、最小值相邻、最大值相邻、交替位模式和超范围输入。对 S24LE:
| 数值 | 预期字节 |
|---|---|
| 0 | 00 00 00 |
| 1 | 01 00 00 |
| -1 | FF FF FF |
| 8388607 | FF FF 7F |
| -8388608 | 00 00 80 |
1193046 (0x123456) |
56 34 12 |
| -1193046 | AA CB ED |
最后一项可用模运算验证:2^24-0x123456=0xEDCBAA,小端即 AA CB ED。同一向量再以大端、24-in-32 高位对齐和低位符号扩展形式运行,可以发现端序、移位和符号扩展顺序错误。
第五章 WAVE 中的 PCM 格式字段
fmt 基础字段
传统 WAVE PCM 的 fmt 块至少有 16 字节。所有多字节整数使用 little-endian:
| 偏移 | 长度 | 字段 | PCM 语义 |
|---|---|---|---|
| 0 | 2 | wFormatTag |
PCM 常为 1,IEEE float 常为 3 |
| 2 | 2 | nChannels |
声道数 |
| 4 | 4 | nSamplesPerSec |
采样率 |
| 8 | 4 | nAvgBytesPerSec |
平均每秒字节数 |
| 12 | 2 | nBlockAlign |
一个采样帧占用字节数 |
| 14 | 2 | wBitsPerSample |
容器或样本位数 |
对于整字节线性 PCM,通常应满足:
1 | nBlockAlign = nChannels × ceil(wBitsPerSample / 8) |
如果声明值不一致,解析器不应任选一个继续计算。可采用严格模式拒绝文件,也可以在容错模式下以 nBlockAlign 作为实际字节步长、以采样率计算时间,同时报告头部矛盾。
data 块与帧数
data 块大小除以 nBlockAlign 得到完整采样帧数量,余数表示截断或非标准尾部。WAVE 允许 fmt 与 data 之间出现 LIST、JUNK、fact 等块,读取器不能假定 data 固定从偏移 44 开始。每个 RIFF 子块仍遵守奇数长度补一个 padding 字节的规则。
RF64 与大文件边界
传统 RIFF 使用 32 位 size,长时间多声道高采样率 PCM 可能超过上限。RF64 使用特定标识和 ds64 块保存 64 位 RIFF/data 大小及样本数量。看到传统 size 字段的占位值时,应先解析 ds64,不能把它当成真实 4 GiB 长度。RF64 是 WAVE 容器扩展,不改变 PCM 样本编码本身。
WAVE_FORMAT_EXTENSIBLE
扩展格式的必要性
当声道数较多、有效位数与容器位数不同或需要明确子格式 GUID 时,WAVE 常使用 WAVE_FORMAT_EXTENSIBLE。基础 wFormatTag 表示扩展格式,cbSize 指示扩展数据长度,后续字段给出有效位数、声道掩码和子格式。
| 字段 | 长度 | 作用 |
|---|---|---|
wValidBitsPerSample |
2 bytes | 容器中真正有效的位数 |
dwChannelMask |
4 bytes | 扬声器位置位掩码 |
SubFormat |
16 bytes | PCM、IEEE float 等子格式 GUID |
wBitsPerSample 表示容器宽度,wValidBitsPerSample 表示实际幅度精度。例如 24 位有效数据可放在 32 位容器中。读取器需要按照扩展格式的对齐约定解释有效位,不能仅把每四字节读成满精度 S32。
声道掩码
声道掩码用位表示 front left、front right、front center、low frequency、back left、back right 等扬声器位置。交错数据中的声道顺序通常按照已置位扬声器位置的规范顺序排列,而不是根据用户界面显示名称任意排列。nChannels 应与掩码中置位数量一致;若掩码为零,则布局未指定,应用只能保留原顺序而不能假定 5.1 的默认排列。
GUID 字节序
WAVE GUID 在文件中的前几个整数成员按 little-endian 存储,尾部字节数组按原顺序存储。直接把 16 字节逐字节与文本 GUID 比较,容易产生端序错误。实现应按 GUID 结构解析或使用经过验证的常量字节序列。
WAVE 文件的完整字节语法
RIFF/WAVE 文件头
WAVE 使用 RIFF 的块结构。文件最外层并不是一个固定 44 字节的“WAV 头”,而是一个 RIFF 容器;常见的 44 字节只代表最简单的 RIFF + fmt + data 组合。只要增加扩展格式、广播元数据、对齐块或未知私有块,data 的起始位置就会改变。因此读取器必须遍历块,不能把偏移 40 当作永久有效的 data 长度位置。
最外层的 12 字节结构如下。字符字段按文件中出现的 ASCII 字节读取,整数按无符号 little-endian 读取。
| 文件偏移 | 长度 | 字段 | 取值与作用 |
|---|---|---|---|
| 0 | 4 | ChunkID |
必须为 RIFF,RF64/BW64 扩展分别使用其他标识 |
| 4 | 4 | ChunkSize |
从偏移 8 开始的 RIFF payload 长度,即经典文件总长度减 8 |
| 8 | 4 | FormType |
WAVE 文件必须为 WAVE |
经典 RIFF 的根块结束位置按下式计算:
1 | riff_payload_begin = 8 |
ChunkSize 不包含前面的 ChunkID 和 ChunkSize 自身,却包含 WAVE 四字节以及其后的全部子块。解析器应使用至少 64 位无符号整数完成加法并先检查溢出,再与实际文件长度比较。若声明结束位置大于文件长度,文件已截断;若小于文件长度,尾部可能是拼接数据、下载残留或容器外附加内容,严格模式应报告而不是默默吞并。
RIFF 子块通用布局
每个 WAVE 子块都使用八字节块头:
1 | +0 char[4] chunk_id |
chunk_size 只计算有效 payload,不计算偶数字节对齐所需的 pad。下一个块头的位置为:
1 | next = payload_offset + chunk_size + (chunk_size & 1) |
pad 的值通常为零,但其数值不属于音频数据,读取器不能把它计入 data 长度。解析循环每次至少需要验证剩余八字节能够容纳块头,再验证 chunk_size + pad 不越过 RIFF 根边界。未知块必须按照同一公式跳过;“不认识就停止”会导致带 JUNK、LIST、bext 或厂商块的合法文件无法读取。
fmt 块的三种常见长度
fmt 的块标识最后一个字符是空格,即字节 66 6D 74 20。线性 PCM 最常见的 payload 长度是 16 字节,对应 WAVEFORMAT 的核心字段。采用 WAVEFORMATEX 时,核心字段后还有两字节 cbSize;采用 WAVEFORMATEXTENSIBLE 时,wFormatTag 为 0xFFFE,cbSize 通常为 22,扩展区总计 22 字节,所以 fmt payload 通常为 40 字节。
1 | PCM 基础格式: |
解析 cbSize 时必须同时满足 fmt chunk_size >= 18 和 18 + cbSize <= fmt chunk_size。某些文件会在格式结构后保留额外零字节;它们仍在 fmt 块边界内,但不能被误认为下一个 RIFF 块。对于 WAVE_FORMAT_PCM,常见文件可能没有 cbSize,也可能带值为零的 cbSize,读取器应依据块长度而不是依据想象中的 C 结构体大小取字段。
PCM 与 IEEE Float 子格式标识
传统 wFormatTag 中,0x0001 表示整数 PCM,0x0003 表示 IEEE 浮点 PCM。扩展格式则通过 SubFormat GUID 区分。PCM 子格式的文本 GUID 为 00000001-0000-0010-8000-00AA00389B71,在 WAVE 文件里的 16 字节序列为:
1 | 01 00 00 00 00 00 10 00 80 00 00 AA 00 38 9B 71 |
IEEE Float 子格式只把首个 32 位值改为 3:
1 | 03 00 00 00 00 00 10 00 80 00 00 AA 00 38 9B 71 |
不能只检查 GUID 的第一个字节。完整比较可以避免把恰好以 01 或 03 开头的厂商 GUID误判为标准子格式。GUID 的 Data1、Data2、Data3 成员在文件中为 little-endian,而最后八字节保持数组顺序,这正是文本形式与原始字节看起来不完全同序的原因。
有效位在容器中的位置
在 WAVEFORMATEXTENSIBLE 中,wBitsPerSample 描述每个声道样本占用的容器位数,wValidBitsPerSample 描述其中有效精度。有效位小于容器位时,有效码字按最高有效位一侧对齐,低位作为填充。例如 20 位有符号样本放在 24 位容器中,文件里的最低四位应为零;取值时先把 24 位容器符号扩展,再算术右移四位,或保持 Q 格式缩放但必须在接口中明确。
1 | 24-bit container, 20 valid bits |
若编码器把 20 位值放在最低 20 位而头部仍声明扩展格式有效位为 20,就会产生约 24 dB 的幅度差或错误符号。验证器可以检查所有样本的低四位是否持续为零;这不是单独的充分证据,但能够快速发现左右对齐与声明不一致。
data、fact 与采样帧计数
data 块不是必然唯一
最普通的 WAVE 只有一个 data 块,但稳健的容器模型不应仅凭“找到第一个 data”就忽略其余结构。某些 RIFF/WAVE 变体会使用 LIST/wavl 组织多个数据或静音片段;流式修复工具也可能留下多个候选 data 块。基础 PCM 播放器可以明确只支持单 data,但必须报告限制。若选择合并多个数据块,应保持文件顺序、在块边界按完整采样帧校验,并定义时间线是否连续。
对于单个 data 块,完整帧数、尾部余数和名义时长为:
1 | complete_frames = data_size / nBlockAlign |
这里的除法对帧数取整数下界。trailing_bytes != 0 表示最后一个交错采样帧不完整;读取器不能把缺失声道补零后假装文件无损,也不能用向上取整增加时长。容错播放可以忽略残余字节,但验证报告必须给出残余数量和绝对文件偏移。
fact 块的语义边界
fact 块最初用于保存非 PCM 格式解码后的样本长度,其至少包含一个 little-endian dwSampleLength。该值通常表示每声道的采样数量,而不是全部声道样本值之和。对传统整数 PCM,fact 通常省略,因为帧数可以从 data_size / nBlockAlign 精确推导;IEEE Float 或其他格式中更常见。
读取器发现 fact 后不应让它无条件覆盖可以从 PCM 数据精确计算的帧数。更可靠的做法是同时保留“由数据推导的帧数”和“头部声明的帧数”,比较二者并报告差异。录制中断、头部未回填或错误地把声道样本总数写入 dwSampleLength,都会造成数值不一致。
Byte Rate、Block Align 与长度的交叉校验
格式字段之间存在一组可机械验证的不变量。对按整字节容器存放、所有声道容器宽度相同的线性 PCM:
1 | bytes_per_sample_container = ceil(wBitsPerSample / 8) |
所有乘法都要在 64 位或更宽的域中检查溢出。nChannels == 0、nSamplesPerSec == 0、nBlockAlign == 0 都不能进入除法。若 wBitsPerSample 不是 8 的倍数,还需依据具体格式决定是紧密跨字节打包还是向整字节容器取整;传统 WAVE PCM 实务中通常使用整字节容器,不能仅凭位数猜测跨样本 bit packing。
经典 44 字节 WAVE 的完整实例
实例参数与文件布局
下面构造一个双声道、48 kHz、16 位有符号 little-endian PCM 文件,包含四个采样帧。每帧四字节,data payload 共 16 字节,文件总长度 60 字节,RIFF ChunkSize 为 52。四帧的左右声道值依次为 (0, 0)、(32767, -32768)、(1, -1)、(0x1234, -0x1234)。
1 | 00000000 52 49 46 46 34 00 00 00 57 41 56 45 66 6D 74 20 |
按绝对偏移拆解头部:
| 偏移 | 字节 | 解释 |
|---|---|---|
0x00 |
52 49 46 46 |
ASCII RIFF |
0x04 |
34 00 00 00 |
little-endian 52,文件长度应为 60 |
0x08 |
57 41 56 45 |
ASCII WAVE |
0x0C |
66 6D 74 20 |
fmt 块标识 |
0x10 |
10 00 00 00 |
fmt payload 长 16 |
0x14 |
01 00 |
WAVE_FORMAT_PCM |
0x16 |
02 00 |
两声道 |
0x18 |
80 BB 00 00 |
48000 Hz |
0x1C |
00 EE 02 00 |
192000 byte/s |
0x20 |
04 00 |
每采样帧 4 字节 |
0x22 |
10 00 |
每声道样本 16 位 |
0x24 |
64 61 74 61 |
data 块标识 |
0x28 |
10 00 00 00 |
音频 payload 长 16 字节 |
实例样本逐帧还原
从 0x2C 起按四字节一帧读取,每帧先左声道 S16LE,再右声道 S16LE:
| 帧索引 | 原始四字节 | 左声道 | 右声道 |
|---|---|---|---|
| 0 | 00 00 00 00 |
0 | 0 |
| 1 | FF 7F 00 80 |
32767 | -32768 |
| 2 | 01 00 FF FF |
1 | -1 |
| 3 | 34 12 CC ED |
4660 | -4660 |
最后一个负值的补码验证为 0xEDCC。按 S16LE 组合后其无符号值是 60876,因为最高位为一,需要减去 65536,结果为 -4660。这个实例同时覆盖零、正负满量程、正负一个 LSB 和普通对称数值,可用于验证端序、符号扩展、交错顺序以及极值归一化。
名义时长只有:
1 | 4 / 48000 second = 1 / 12000 second ≈ 83.333 microseconds |
不能使用 file_size / byte_rate 计算有效时长,因为文件大小包含 RIFF 和块头。正确分子是有效 PCM 数据对应的完整采样帧数量。
RF64 与 BW64 的 64 位长度结构
RF64 根块与占位值
当 WAVE 文件可能超过经典 RIFF 的 32 位长度上限时,RF64 将根标识改为 RF64,根 ChunkSize 写成 0xFFFFFFFF,并使用 ds64 块携带真实 64 位大小。主 data 块的 32 位长度也通常写成 0xFFFFFFFF。占位值不是“恰好有 4294967295 字节”,而是要求读取器转向 ds64 查询。
RF64 的 ds64 应位于根类型 WAVE 后的前部,使流式读取器在遇到巨大 data 前已经获得边界。其基础 payload 为:
| 相对偏移 | 长度 | 字段 | 含义 |
|---|---|---|---|
| 0 | 8 | riffSize |
整个 RF64 根块的真实 64 位 size,语义对应经典 ChunkSize |
| 8 | 8 | dataSize |
主 data payload 的真实 64 位长度 |
| 16 | 8 | sampleCount |
每声道采样帧数量;未知时可为零,具体写入策略应记录 |
| 24 | 4 | tableLength |
后续尺寸表的条目数量 |
| 28 | 12 × N | table entry | 四字节 chunk ID 与八字节真实 chunk size |
每个附加表项由 chunkId[4] 和 chunkSize64 组成,用于记录其他使用 0xFFFFFFFF 占位的超大块。解析器先验证 ds64 chunk_size >= 28,再验证 28 + tableLength × 12 不超过 ds64 payload。若多个同 ID 块都需要替换长度,必须遵循格式对条目匹配顺序的约定,不能把 ID 简单存成只能保留一个值的字典。
BW64 与 PCM 数据的不变部分
BW64 延续相同的 64 位 RIFF 思路,根标识使用 BW64,并面向广播和基于场景的音频工作流规定相关元数据块。无论根容器是 RIFF、RF64 还是 BW64,data 中线性 PCM 的端序、交错方式和每帧步长仍由 fmt 决定。容器升级解决的是寻址范围和元数据表达能力,不会自动把 S16LE 变成其他采样编码。
64 位边界解析顺序
安全读取 RF64/BW64 时,可以采用以下状态顺序:
1 | read root id and 32-bit placeholder |
不能先按 0xFFFFFFFF 跳过 data 再寻找 ds64,因为这可能越过文件结尾;也不能把全部普通 32 位 chunk size 无条件替换为表中数值。只有使用占位值且存在相应 64 位来源的块才需要替换。
5.1 声道 S24 WAVE_FORMAT_EXTENSIBLE 实例
测试参数
下面是一份真实可读取的 WAVE 文件,参数为 48000 Hz、六声道 5.1、每声道 24 bit packed PCM,共四个采样帧。每帧包含六个三字节样本,所以 nBlockAlign=18;每秒字节数为 48000×18=864000。文件总长 174 字节。
六个声道按掩码规范顺序写入不同的固定归一化值,便于发现声道错位:
| 声道 | 归一化值 | S24 整数 | 三字节 little-endian |
|---|---|---|---|
| Front Left | +0.125 | 0x100000 |
00 00 10 |
| Front Right | +0.25 | 0x200000 |
00 00 20 |
| Front Center | +0.375 | 0x300000 |
00 00 30 |
| Low Frequency | +0.5 | 0x400000 |
00 00 40 |
| Back Left | -0.125 | 0xF00000 |
00 00 F0 |
| Back Right | -0.25 | 0xE00000 |
00 00 E0 |
负值一栏的十六进制按 24 位二进制补码显示。0xF00000 符号扩展到 32 位为 0xFFF00000=-1048576,除以 2^23 得到 -0.125。
完整 174 字节
1 | 0000 52 49 46 46 A6 00 00 00 57 41 56 45 66 6D 74 20 |
根 RIFF size 为 0xA6=166,加八字节头等于 174。文件中还存在一个 LIST/INFO 元数据块,因此 data 并不位于常见偏移 36/44;它的 chunk header 从 0x5E 开始,payload 从 0x66 开始。
fmt 基础字段解析
fmt chunk header 位于 0x0C,payload size 是 0x28=40。从 0x14 开始逐字段解释:
| 绝对偏移 | 字节 | 字段 | 值 |
|---|---|---|---|
0x14 |
FE FF |
wFormatTag |
0xFFFE,Extensible |
0x16 |
06 00 |
nChannels |
6 |
0x18 |
80 BB 00 00 |
nSamplesPerSec |
48000 |
0x1C |
00 2F 0D 00 |
nAvgBytesPerSec |
864000 |
0x20 |
12 00 |
nBlockAlign |
18 |
0x22 |
18 00 |
wBitsPerSample |
24 |
0x24 |
16 00 |
cbSize |
22 |
公式全部闭合:
1 | container_bytes_per_sample = ceil(24 / 8) = 3 |
Extensible 扩展字段解析
扩展区从 0x26 开始:
| 绝对偏移 | 字节 | 字段 | 解释 |
|---|---|---|---|
0x26 |
18 00 |
wValidBitsPerSample |
24 个有效位 |
0x28 |
3F 00 00 00 |
dwChannelMask |
bits 0~5 置位 |
0x2C |
16-byte GUID | SubFormat |
PCM |
SubFormat 原始字节为:
1 | 01 00 00 00 00 00 10 00 80 00 00 AA 00 38 9B 71 |
完整 GUID 比较得到 KSDATAFORMAT_SUBTYPE_PCM。不能只因为 wFormatTag=0xFFFE 就认定是整数 PCM;真正的子格式由这个 GUID 决定。
0x3F 声道掩码
0x3F 的低六位分别对应:
| Mask Bit | 数值 | 扬声器位置 | 数据中的声道索引 |
|---|---|---|---|
| 0 | 0x0001 |
Front Left | 0 |
| 1 | 0x0002 |
Front Right | 1 |
| 2 | 0x0004 |
Front Center | 2 |
| 3 | 0x0008 |
Low Frequency | 3 |
| 4 | 0x0010 |
Back Left | 4 |
| 5 | 0x0020 |
Back Right | 5 |
置位数 popcount(0x3F)=6,与 nChannels=6 一致。声道顺序按已置位扬声器位的规范顺序解释,因此测试帧中的六个常量恰好按 FL、FR、FC、LFE、BL、BR 排列。
data 长度与帧数
data header 字节为:
1 | offset 0x5E: 64 61 74 61 48 00 00 00 |
payload size 0x48=72。每帧 18 字节:
1 | frame_count = 72 / 18 = 4 |
从 0x66 开始的前 18 字节为:
1 | 00 00 10 | 00 00 20 | 00 00 30 | 00 00 40 | 00 00 F0 | 00 00 E0 |
后面三帧完全重复。这个向量同时验证三字节符号扩展、18 字节 frame stride、5.1 声道顺序和 data 非固定偏移。
强制 RF64 S16 实例
文件目的
RF64 通常用于超过 4 GiB 的文件,但写入器也可以在小文件上强制使用 RF64,以测试读取器的 64 位逻辑。下面的文件是 48000 Hz、双声道、S16LE、四个采样帧,总长 130 字节。左右声道固定为 +0.25 和 -0.25,即 S16 值 8192 与 -8192。
1 | 0000 52 46 36 34 FF FF FF FF 57 41 56 45 64 73 36 34 |
RF64 根部
根 ID 是 RF64,32 位 size 为 0xFFFFFFFF,form type 为 WAVE。解析器看到占位值后不能计算 8+0xFFFFFFFF 作为根结束,而应立即读取后面的 ds64。
ds64 chunk 位于 0x0C:
1 | 64 73 36 34 1C 00 00 00 |
payload size 是 28,恰好等于没有额外 table entry 时的基础结构长度。
ds64 逐字段实算
| 绝对偏移 | 原始字节 | 字段 | 值 |
|---|---|---|---|
0x14 |
7A 00 00 00 00 00 00 00 |
riffSize |
122 |
0x1C |
10 00 00 00 00 00 00 00 |
dataSize |
16 |
0x24 |
04 00 00 00 00 00 00 00 |
sampleCount |
4 |
0x2C |
00 00 00 00 |
tableLength |
0 |
riffSize=122 按 RIFF size 语义加八字节得到 130,与实际文件长度完全一致。dataSize=16 对应四帧、每帧四字节;sampleCount=4 与 16/4 相同。三种证据闭合:
1 | actual_file_size = riffSize64 + 8 = 122 + 8 = 130 |
data 占位长度
data chunk header 位于 0x6A:
1 | 64 61 74 61 FF FF FF FF |
32 位 size 仍是占位值,真实 payload 长度来自 ds64.dataSize=16。payload 从 0x72 开始,到 0x82 结束。读取器必须用真实 16 字节推进;若用 0xFFFFFFFF,会立即越过文件。
S16 样本还原
四帧 payload 为:
1 | 00 20 00 E0 | 00 20 00 E0 | 00 20 00 E0 | 00 20 00 E0 |
00 20 是 little-endian 0x2000=8192;00 E0 是 0xE000,按 S16 减去 65536 得 -8192。归一化除以 32768,得到 +0.25 与 -0.25。
RF64 ds64 一致性审计
根 Size
若实际文件是 seekable 且已完整写入,riffSize64+8 应与 RF64 form 的真实结束位置一致。文件允许存在多个顶层 form 或外部尾随数据时,比较对象是当前 RF64 form 边界,不应无条件等于整个物理文件大小。
Data Size
dataSize64 必须不超过当前 form 可用范围,并与主 data payload 的物理字节数相容。主 data 的 32 位字段不是占位值时,应按所采用 RF64/BW64 profile 判断是否允许,并把两个长度并列报告,不能静默选择较小者。
RF64 Sample Count 交叉校验
线性 PCM 可由 dataSize64/blockAlign 精确推导完整采样帧。sampleCount64 非零时应相等;余数说明 data 截断或格式字段错误。压缩格式的 sample count 可能需要 fact 和编码语法共同验证,不能一律用字节除 block align。
Table Entry
tableLength 乘 12 必须在 ds64 payload 剩余范围内。表项按顺序保留,因为同一 FourCC 可能多次出现。解析器只将表项用于规范允许且 32 位长度为占位值的目标块;普通小块不能因为 ID 相同就被替换成巨大长度。
WAVE 读取器的边界检查算法
解析上下文
读取器至少保存实际文件长度、RIFF 声明边界、当前块头偏移、已解析格式、数据块列表和诊断集合。块偏移和长度统一用无符号 64 位或更宽类型,避免在 32 位平台上打开大文件时截断。所有“偏移加长度”操作使用 checked addition:先判断 length <= limit - offset,再执行加法。
1 | checked_range(offset, length, limit): |
这种减法式检查比先计算 offset + length 再比较更安全,因为后者可能已经发生整数回绕。
两阶段解析
第一阶段只建立 RIFF 块目录并验证边界,第二阶段解释 fmt 、data、fact、ds64 等已知块。两阶段设计让工具能够报告“块结构合法但格式字段矛盾”与“块本身越界”之间的区别,也便于保留未知块。流式播放无法先扫描完整文件时,仍应使用相同的不变量,只是目录逐步增长。
1 | parse_wave(file): |
若 RIFF 根边界本身为奇数,不应擅自把根外一个字节计入根 payload。子块 pad 的归属由子块步进公式处理。实际文件可能允许根块之后存在其他 RIFF form,但单个 WAVE 解析结果必须清楚标出自己的边界。
诊断等级
验证器可把问题分为结构错误、格式矛盾和可疑但可读取三类。块头不足、payload 越界、除零条件和整数溢出属于结构错误;nAvgBytesPerSec 与计算值不一致、声道掩码置位数不同于 nChannels 属于格式矛盾;未知块、非零 pad、根块后的尾随字节则通常属于需要报告的兼容性现象。等级划分应由明确规则决定,不能因某个播放器“能播”就把所有错误降级。
第六章 声道布局和多声道样本
交错帧的精确定义
一个交错采样帧包含同一采样时刻的所有声道样本。若有六声道、每样本 24 位 packed,则 frame stride 为 18 字节;第 n 帧第 c 声道的地址为:
1 | offset(n, c) = n × frame_stride + c × sample_stride |
这个公式只适用于均匀容器宽度。若协议将不同声道用不同格式编码,就不再是常规 PCM interleaved layout,应定义更高层的复合格式。
Planar 缓冲区
Planar 接口常以多个指针表示各声道平面,而不是一个文件中的连续大块。若所有平面连续存储,还必须提供每个平面的 stride 或 frame count。把 planar 数据写入传统 WAVE 前通常要重新交错,因为标准 PCM WAVE 的数据块一般按帧交错。
声道重排
从一种 API 转到另一种 API 时,声道标签可能相同但索引顺序不同。正确转换过程是先把源索引映射到扬声器标签,再把标签映射到目标索引。仅按数组位置复制可能把中心声道与低频声道互换,听感上会表现为人声减弱或低频异常。
第七章 PCM 时钟与时间戳
采样时钟和系统时钟
标称 48 kHz 设备不一定在物理上精确每秒产生 48000 个采样;晶振误差会造成 ppm 级漂移。音视频同步系统需要区分采样计数器和系统时间戳。采样计数器保证 PCM 内部连续,系统时间戳用于与摄像头、网络或容器时间轴对齐。
时间戳对应点
接口必须规定 timestamp 对应 payload 的第一个采样帧、最后一个采样帧还是采集完成时刻。推荐把它定义为第一个采样帧的呈现时间,并另带 frame count:
1 | end_time = first_pts + frame_count / sample_rate |
如果时间戳来自 DMA 中断时刻,还要扣除硬件 FIFO、DMA period 和驱动缓冲产生的固定延迟。否则虽然每包间隔正确,音频与视频仍会存在恒定偏移。
漂移修正
长时间播放中,若采样时钟和主时钟速率略有不同,缓冲区会逐渐积累或耗尽。修正方法包括异步重采样、偶尔插入/删除极少采样或调整设备时钟。直接周期性丢弃整个 20 ms PCM 包会产生明显点击声,也会造成时间戳不连续。
缓冲区、DMA 与环形队列
Period、buffer 和 latency
音频设备通常使用多个 period 组成环形 buffer。单个 period 的理论时长为 period_frames / sample_rate,总硬件缓冲延迟约为可用缓冲帧数除以采样率,再叠加转换和调度延迟。period 越小,交互延迟通常越低,但中断和线程唤醒频率更高。
环形缓冲写入
环形缓冲必须以采样帧为最小提交单位。写指针跨越尾部时,可以分两次复制,但逻辑 frame 不能被另一个生产者插入。生产者和消费者应交换 frame count 或单调游标,不能只交换裸指针;多线程实现还要规定内存可见性和溢出策略。
欠载和溢出
播放欠载时常见策略是输出静音并保持时间轴;采集溢出时必须记录丢失的采样帧数。覆盖最旧数据会保持最新实时性,但会造成时间不连续;拒绝新数据会增加延迟。策略属于应用设计,但无论选择哪种,都应以采样帧为单位并向同步层报告 discontinuity。
第八章 格式转换算法
端序转换
S16 的端序转换是交换每个样本的两个字节,而不是反转整个缓冲区。多声道数据仍保持帧和声道顺序。S24 packed 需要在每三个字节内部反转;S24 in S32 则按四字节容器处理,同时保留有效位对齐。
位深转换
高位深转低位深时简单右移会产生量化失真和相关噪声。基础实现可以舍入后截断,高质量实现可加入 TPDF dither;低位深转高位深只能扩大数值容器,无法恢复丢失精度。位深转换后要更新格式元数据和所有 stride/byte rate,不能只改变 payload。
S24 Packed 的整数解包
Little-endian 三字节组合
S24LE 每个样本正好三字节,不能直接把未对齐地址强制转换为 int32_t*。可先组合为 24 位无符号值,再显式符号扩展:
1 | int32_t read_s24le(const uint8_t *p) { |
最高有效位是 bit 23,对应第三字节的 bit 7。扩展掩码必须是 0xFF000000;误用 0xFFF00000 会破坏部分合法正负值。
Big-endian 三字节组合
S24BE 的字节权重相反:
1 | int32_t read_s24be(const uint8_t *p) { |
端序只改变三个存储字节的排列,不改变二进制补码位宽、归一化除数或声道交错顺序。
边界测试向量
| 数值 | 24 位码字 | S24LE | S24BE |
|---|---|---|---|
| 0 | 0x000000 |
00 00 00 |
00 00 00 |
| 1 | 0x000001 |
01 00 00 |
00 00 01 |
| -1 | 0xFFFFFF |
FF FF FF |
FF FF FF |
| 最大正值 8388607 | 0x7FFFFF |
FF FF 7F |
7F FF FF |
| 最小负值 -8388608 | 0x800000 |
00 00 80 |
80 00 00 |
| 1193046 | 0x123456 |
56 34 12 |
12 34 56 |
| -1193046 | 0xEDCBAA |
AA CB ED |
ED CB AA |
最后一个负数可由 0x1000000-0x123456=0xEDCBAA 验证。测试应同时检查数值和重新写回的三个字节,避免某条路径能读对却在写出时丢高位。
写出 S24
写入前把 int32_t 限幅到 [-8388608, 8388607],再取低 24 位:
1 | void write_s24le(uint8_t *p, int32_t x) { |
从负的 int32_t 转为 uint32_t 后取低位,可以明确得到补码字节。不要对负数使用实现相关的有符号右移来决定协议字节。
24 位有效数据放入 32 位容器
左对齐规则
WAVE_FORMAT_EXTENSIBLE 声明 wBitsPerSample=32、wValidBitsPerSample=24 时,有效数据位于容器高 24 位,低八位为填充零。把有符号 24 位数值 v24 写入容器的数学关系为:
1 | container_s32 = v24 * 2^8 |
解析时先按 S32LE 读取,再算术右移八位得到 24 位数值:
1 | v24 = container_s32 >> 8 |
C/C++ 中左移负有符号值可能触发未定义行为,生产代码可在更宽的有符号域做乘法,或转换为无符号码字后移动,并用测试向量验证。
32 位容器字节向量
| 24 位有效值 | 左对齐 S32 码字 | 32-bit little-endian |
|---|---|---|
| 0 | 0x00000000 |
00 00 00 00 |
| 1 | 0x00000100 |
00 01 00 00 |
| -1 | 0xFFFFFF00 |
00 FF FF FF |
| 8388607 | 0x7FFFFF00 |
00 FF FF 7F |
| -8388608 | 0x80000000 |
00 00 00 80 |
0x123456 |
0x12345600 |
00 56 34 12 |
-0x123456 |
0xEDCBAA00 |
00 AA CB ED |
所有合法样本的首个 little-endian 字节,即容器最低八位,都应为零。验证器发现大量非零低位时,应报告“有效位对齐与声明不一致”的强烈证据,但仍需考虑输入是否实际为满 32 位 PCM 而 Header 被误写。
与 Packed S24 的转换
从 24-in-32 转为 packed S24,不能简单复制前三个 little-endian 字节。容器 00 56 34 12 表示有效码字 12 34 56,packed S24LE 应为 56 34 12,需要丢弃最低填充字节:
1 | 32-bit container LE: [00] [56] [34] [12] |
反向转换则在 packed 三字节前插入一个低位零字节,而不是在末尾补零:
1 | dst32[0] = 0; |
这个复制方式自然保留负数补码的最高字节,例如 AA CB ED 变成 00 AA CB ED。
归一化等价性
若把左对齐容器直接按 S32 归一化,除数为 2^31;若先右移得到 S24,除数为 2^23。两种方法数学上相等:
1 | (v24 * 2^8) / 2^31 = v24 / 2^23 |
因此不必为了浮点转换先物理移动数据,但必须知道低八位不是额外精度。峰值统计时将它误称为 32 位有效 PCM,会把理论动态范围和 ENOB 描述夸大 48 dB。
有效位转换的舍入规则
右移与舍入
从 N 位有效精度降到 M 位,需要丢弃 k=N-M 个低位。单纯算术右移相当于向负无穷方向量化,正负误差不对称。最接近舍入需要结合符号、被丢弃部分和 tie 规则处理。
对于非负整数,可先加 2^(k-1) 再右移;对于负数若直接使用同一加法,tie 和方向可能与目标定义不同。参考实现应明确采用 nearest-away、nearest-even 或截断,并对正负半 LSB 建立向量,而不是只测试零和满幅。
Nearest-even 概念算法
1 | q = arithmetic_floor_div(x, 2^k) |
这里先定义 floor division,使负数 remainder 仍非负。不同语言的整数除法可能向零截断,实现时要调整。最后对 q 做目标位宽饱和。
Dither 单位
目标 M 位 PCM 的一个 LSB 对应源 N 位域的 2^(N-M)。TPDF dither 通常在量化前以目标 LSB 为尺度生成两个独立均匀变量之差。若误用源 LSB,抖动小到无法去相关;误用满幅比例则噪声过大。
可逆扩位
从 S16 扩为 24 有效位时,常见数值保持策略是乘 2^8,使 S16 满量程占据 S24 的高 16 位;随后再降回 S16 且未做其他处理时可精确恢复。若只是把整数 16-bit 数值不缩放地放入 S24,则电平会下降约 48.16 dB。API 应区分“保持归一化幅度”和“保持整数数值”两种转换语义。
整数与浮点转换伪代码
1 | float s16_to_f32(int16_t s) { |
实际 SIMD 实现应保证与标量路径使用相同的舍入、NaN 和限幅规则,否则同一文件在不同 CPU 上可能产生不一致结果。
第九章 PCM 分析和验证方法
波形统计
验证器可以按声道统计最小值、最大值、均值、均方根、削波计数、NaN/Inf 计数和直流偏置。连续静音不代表格式正确:把 S16LE 当作 S16BE 读取低幅静音时也可能看似接近零,因此还需要已知频率测试信号或字节级向量。
正弦测试向量
采样率为 R、频率为 f、幅度为 A 时,第 n 个浮点样本可生成:
1 | x[n] = A × sin(2πfn/R) |
选择能在缓冲区内形成整数周期的频率,可以检查首尾连续性。双声道可令左右使用不同频率,以检测声道互换和交错错误。生成后再按明确的舍入规则量化,保存预期字节哈希作为回归测试。
文件级一致性检查
对于裸 PCM,检查输入长度是 frame stride 的整数倍,并由外部参数计算时长。对于 WAVE/RF64,额外检查 RIFF 层级、fmt 长度、格式字段公式、data/ds64 长度、声道掩码和事实样本数。工具能成功播放只能证明它采取了某种容错策略,不能替代这些结构校验。
PCM 实现错误诊断
播放速度和音调同时变化
这通常是采样率解释错误。16 kHz 数据按 8 kHz 播放,时长会加倍且音调下降;只做重采样而没有改变播放时钟则是另一类问题。先用字节数和格式公式计算理论时长,再比较设备实际消耗 frame 的速度。
噪声、爆音和周期性点击
持续强噪声常来自端序、格式或对齐错误;固定包周期的点击声常来自每包重置滤波器、丢弃尾部半帧或时间戳修正过猛;偶发爆音可能来自环形缓冲竞争或整数溢出。诊断时应保留错误点前后的原始字节、frame index 和缓冲游标。
单侧静音或声道串音
单侧静音可能是声道掩码、索引或平面指针错误;左右交替失真常见于把双声道当单声道;多声道的人声消失可能是中心声道映射错误或下混相位抵消。不要仅通过改变 channels 数值尝试修复,应回到源布局和字节步长逐项核对。
量化误差与信噪比
理想量化噪声模型
在输入信号充分跨越量化级且误差近似不相关时,均匀量化误差可近似分布在半个量化步长范围内。常见理想满幅正弦量化信噪比近似为 6.02N + 1.76 dB,其中 N 是有效位数。该公式不是所有音频内容和设备的实测动态范围保证;模拟前端噪声、时钟抖动、非线性和有效位数都会降低结果。
ENOB
ADC 数据手册常用 Effective Number of Bits 描述实际信噪和失真对应的有效位。一个接口虽然输出 24 位 PCM,真实 ENOB 可能明显低于 24。文件仍应按 24 位容器解析,不能因低位噪声大就擅自截成 16 位;是否降位深属于后续处理决策。
直流偏置
理想以零为中心的 PCM 静音均值接近零,但 ADC 偏置、模拟耦合和数字处理可能产生 DC。验证器应分别统计每声道均值和 RMS。端序错误也可能产生巨大均值,因此诊断要同时查看字节分布、峰值和频谱零频分量。
Dither 与噪声整形
为什么需要 Dither
高位深转低位深时,直接截断会让量化误差与信号相关,在低幅周期信号中表现为谐波失真。加入适当随机抖动可把误差去相关,代价是提高稳定噪声底。TPDF dither 常由两个独立均匀随机变量之差构成,并按目标 LSB 缩放。
Dither 的位置
Dither 应在最终量化到目标整数位深前加入;如果加完后又进行大幅增益、重采样或再次量化,噪声统计会改变。多阶段处理通常保留高精度 float/S32,到最后输出设备或文件时统一 dither。
Noise Shaping
噪声整形利用误差反馈把量化噪声能量移到听感不敏感频段。滤波器有状态,不能在每个 buffer 边界重置。若目标采样率、位深或滤波稳定性发生变化,应重新设计/初始化,并处理状态过渡以避免爆音。
可重复测试
随机 dither 会让逐字节 golden file 不稳定。测试实现可使用固定种子或注入确定性随机源,分别验证统计分布和字节输出。生产环境若使用伪随机源,也不应将安全随机要求与音频 dither 混为一谈。
PCM 数值运算规则
增益
线性增益 g 作用为 y = g × x。dB 到线性幅度换算为 g = 10^(dB/20)。例如 -6 dB 约为 0.501,不是减去六个整数码值。整数实现应扩大中间位宽,并规定舍入和饱和。
饱和与回绕
饱和将超过范围的值夹到最大/最小码值,回绕则按整数模运算跳到相反极性。音频输出通常需要饱和;无符号 C 整数运算天然模回绕,有符号溢出还可能是未定义行为。先提升到 int32_t/int64_t 或 float 再限幅。
混音余量
两路满幅相关信号相加可增加约 6 dB,N 路同相信号峰值可达单路 N 倍。固定衰减 1/N 可避免理论峰值溢出,但会降低普通内容响度。实际混音器可能使用 headroom、动态限制器或响度策略,这些都应在输出量化前完成。
反相与最小负值
二补码最小负值没有对应的正整数。例如 S16 的 -(-32768) 无法在 int16 表示,直接取负可能溢出。反相应提升位宽,并把结果饱和到 32767 或按接口规定处理。
Packed PCM 位布局
20 位紧凑格式
20 位样本可以把两个样本装入 5 字节,也可以每样本占 3 或 4 字节。双样本五字节布局的 nibble 顺序需要协议明确,无法只凭“20-bit packed”推导。通用解析器应以格式 ID 选择专用 unpack 函数,而不是使用 ceil(bits/8)。
24 位小端符号扩展示例
1 | bytes = 00 00 80 |
若把 00 00 80 零扩展为 0x00800000 后直接解释为正数,就会把满幅负值变为正值。符号扩展依据第 23 位,而不是目标容器第 31 位的原始字节。
左对齐 24-in-32
左对齐样本 0x80000000 可表示 24 位最小负值,低 8 位为 padding。转 packed S24 时通常右移 8 后写三字节;转 float 时按有效位尺度缩放。若把低 24 位截出,会得到零。
非字节对齐流
某些通信和硬件位流无字节对齐,每个样本紧邻下一个样本。解析需要 bit reader 和明确 MSB/LSB first 规则。文件截断检查也应按总 bit 数,而不是只检查字节数;最后一个字节未使用 bit 的规定值需由格式定义。
RIFF/WAVE 扩展块
fact
fact 块常用于非 PCM WAVE,保存解码后 sample length 等信息。经典线性 PCM 通常可由 data size/block align 推导而不需要 fact。若存在 fact,验证器可与推导帧数比较,但不能在格式 tag 明确为压缩音频时仍用 PCM 公式覆盖。
JUNK 与 PAD
这些块用于占位或对齐,允许写入器以后替换元数据而不移动 data。它们有正常 RIFF chunk header 和 size,不能当作文件随机垃圾。移动或删除时需更新父 RIFF size 和后续 offset 相关元数据。
LIST INFO
INFO 列表可保存名称、作者、注释、软件等文本。文本编码和终止符可能因来源不同,解析器按 chunk 长度读取并限制日志。INFO 不改变 PCM 样本参数。
cue 与 LIST adtl
cue 可定义采样位置标记,关联数据列表可给标记添加标签或注释。位置字段应结合目标 data chunk 和 sample offset 解释。编辑 PCM 长度或重采样后,cue 点需要重新映射;只修改 data 不更新 cue 会让标记漂移。
smpl
Sampler 块可保存 MIDI unity note、pitch fraction、SMPTE 信息和循环区间。循环 start/end 通常按采样位置解释,边界语义应依据格式。重采样或裁剪同样需要更新,否则循环会落到错误内容。
多个 data 块
常见 WAVE 只有一个 data,但 RIFF 解析器不应假定永远如此。生态对多个 data 块的支持不一致。严格应用可拒绝,兼容读取器可按文件顺序逻辑拼接,同时保证每块长度按 blockAlign 对齐并在报告中说明策略。
Broadcast Wave Format
BWF 与 PCM
Broadcast Wave Format 在 WAVE 基础上增加广播制作元数据,PCM data 编码不因此改变。核心扩展之一是 bext,可包含描述、来源、日期时间、时间参考、版本和 UMID 等。它适合专业录音和跨系统同步。
Time Reference
bext 时间参考通常以从某个时间基准到文件首采样的 sample count 表示,字段可跨低/高 32 位。换算秒需要使用 WAVE 采样率。它与 RIFF 文件创建日期不是同一概念,也不能替代每个实时数据包的时间戳。
Loudness 字段
较新 BWF 版本可保存响度值、响度范围、最大真峰等元数据。字段通常使用定点/缩放约定和特殊未知值。它们描述测量结果,不改变 PCM 码值;编辑增益后应重新测量或标记元数据失效。
iXML
制作流程可使用 iXML 保存场景、take、轨道名称等 XML 元数据。XML 解析必须设置资源限制并禁止不必要的外部实体访问。iXML 中轨道描述可辅助声道命名,但底层声道数和 blockAlign 仍以 fmt 为准。
Broadcast Audio Extension 的字节结构
bext 是 RIFF 子块
Broadcast Wave Format 的核心元数据放在 FourCC 为 bext 的 RIFF 子块中。它遵循普通 RIFF 子块布局:四字节 ID、四字节小端 ckSize、恰好 ckSize 字节 payload;payload 长度为奇数时,再跟一个不计入 ckSize 的 pad byte。读取器必须仍按通用 chunk 扫描,不能假定 bext 永远出现在固定偏移。
1 | 62 65 78 74 # "bext" |
现代 BWF 的固定部分为 602 字节,后面可追加可变长度 CodingHistory。因而:
1 | ckSize = 602 + coding_history_byte_count |
如果 ckSize<602,对声明使用完整 BWF 扩展的文件应报告截断,不能用零填充伪造缺失字段。兼容工具可能遇到早期或不完整实现,应把兼容策略与标准验证结果分开记录。
固定 602 字节字段表
偏移以 bext payload 起点为零:
| 偏移 | 长度 | 字段 | 编码与作用 |
|---|---|---|---|
| 0 | 256 | Description | 固定宽度 ASCII 描述 |
| 256 | 32 | Originator | 音频来源/机构标识 |
| 288 | 32 | OriginatorReference | 来源系统生成的唯一参考文本 |
| 320 | 10 | OriginationDate | 十字符日期,常用 YYYY-MM-DD |
| 330 | 8 | OriginationTime | 八字符时间,常用 HH:MM:SS |
| 338 | 4 | TimeReferenceLow | 64 位采样时间参考的低 32 位,小端 |
| 342 | 4 | TimeReferenceHigh | 64 位采样时间参考的高 32 位,小端 |
| 346 | 2 | Version | BWF 扩展版本,小端无符号整数 |
| 348 | 64 | UMID | SMPTE UMID 字节区域;未使用时清零 |
| 412 | 2 | LoudnessValue | 综合响度,二字节有符号定点 |
| 414 | 2 | LoudnessRange | 响度范围,二字节有符号定点 |
| 416 | 2 | MaxTruePeakLevel | 最大真峰值,二字节有符号定点 |
| 418 | 2 | MaxMomentaryLoudness | 最大瞬时响度,二字节有符号定点 |
| 420 | 2 | MaxShortTermLoudness | 最大短时响度,二字节有符号定点 |
| 422 | 180 | Reserved | 保留区域,写入时清零 |
| 602 | 可变 | CodingHistory | 可选 ASCII 编码历史,直到 chunk payload 末尾 |
固定长度校验:
1 | 256 + 32 + 32 + 10 + 8 |
这些偏移按 payload 计算,不包括前面的 bext 和 ckSize 八字节。若从 chunk 起点计算,所有偏移再加 8。工具输出偏移时应注明坐标系,避免把文件偏移、chunk 相对偏移和 payload 相对偏移混用。
固定宽度文本字段
字段不保证 NUL 终止
Description、Originator、OriginatorReference、日期和时间都是固定字节区域。写入器通常用 ASCII 内容后补 0x00,但字段占满时不一定还有 NUL。读取器应在字段宽度内寻找首个 0x00,若没有就使用整个字段;不得把字段地址直接传给无长度限制的 C 字符串函数。
1 | decode_fixed_ascii(bytes, width): |
解析后去除 NUL padding 不等于可以随意去掉所有空格。字段末尾空格可能是上游系统的固定宽度填充,也可能属于用户文本;规范化策略应与原始字节分开保存。重新写文件若要求 bit-exact 元数据,应保留原始固定区域,而不是把解码字符串重新编码。
日期和时间是描述性 UTC/本地制作时间字段,具体时区不能仅从八个时间字符推导。读取器可验证基本形态和范围:月份 01~12、小时 00~23等;不能把无效文本当成二进制时间戳使用。TimeReference 是另一个字段,表示样本位置而不是日历日期。
OriginatorReference
OriginatorReference 用于在制作链路中标识素材。它是固定 32 字节文本,不是 RIFF chunk ID,也不是密码学哈希。不同系统可能按自身约定组合国家、机构、序列号和日期。验证器能检查长度与可打印性,但若没有业务规则,不能声称它全局唯一。
64 位 TimeReference
低高 32 位组合
TimeReference 由两个独立的小端 uint32 保存。数学组合为:
1 | time_reference = TimeReferenceLow |
它表示从参考时间轴原点到当前 WAVE 文件第一个采样帧的样本计数。单位是“采样帧”,不是字节、单声道样本总数、毫秒或 100 ns tick。换算秒:
1 | seconds = time_reference / sample_rate |
双声道不会再除以 2,因为采样率和 TimeReference 都以帧时钟计数。一帧同时包含两个声道样本。
一小时实例
48 kHz 时间轴上一小时的帧计数:
1 | 48000 * 60 * 60 = 172800000 |
两个字段的文件字节:
1 | TimeReferenceLow : 00 B8 4C 0A |
读取后得到 172800000/48000=3600 秒。若错误地把字段按大端读取,低部会变成 0x00B84C0A,时间位置完全错误。
必须使用高 32 位的实例
48 kHz 时间轴上 48 小时:
1 | 48000 * 48 * 60 * 60 = 8294400000 |
若旧代码只读 low,会得到 3999432704 帧,对应约 23.14 小时,而不是 48 小时。合并前应先把 high 转为 64 位再左移;对 32 位值执行 high << 32 在 C/C++ 中无效或未定义。
与 data 帧数的关系
文件覆盖的时间轴半开区间为:
1 | [time_reference, |
decoded_frame_count 通常由 dataSize/blockAlign 计算。相邻文件连续录制时,理想关系是:
1 | next.time_reference == current.time_reference + current.frame_count |
大于表示时间轴有间隙,小于表示重叠。但只有在采样率、时间参考原点和录音时钟语义一致时才能这样比较。跨设备文件即使采样率同为 48 kHz,也可能有时钟漂移或不同原点。
加法要做 64 位溢出检查。恶意文件可把 TimeReference 设为接近 UINT64_MAX,再加帧数导致回绕;验证器应报告范围错误而不是产生一个很小的结束位置。
Version、UMID 和保留区
版本决定字段语义
Version 是小端 16 位值。版本演进通常可概括为:早期版本提供基本描述和时间参考;后续版本启用 64 字节 UMID;更高版本定义五个响度字段。固定区域仍保留相同偏移,使读取器可以按 ckSize 定位 CodingHistory。
读取策略:
- 总是按固定偏移安全读取已存在的 602 字节;
- 只有 Version 声明支持时,才把 UMID 或响度区域解释成有效元数据;
- 低版本中这些区域应按规范写零,非零时报告不一致但保留原始字节;
- 遇到未知更高版本时,解析已知前缀并保留未知/保留区域,不要假定布局一定终止于已知字段。
Version 不是 ASCII 字符 '0'、'1'、'2',也不是大端。十六进制字节 02 00 才表示版本 2。
UMID 字节区域
UMID 区域固定 64 字节,可容纳扩展 UMID;较短的 Basic UMID 使用前部并把剩余字节清零。它是结构化二进制标识,不是普通 NUL 终止文本。日志应输出有界十六进制或按 SMPTE UMID 语法解析,不能把任意字节直接当 UTF-8。
判断“未设置”可检查 64 字节是否全零。不能只检查第一个字节,因为损坏或私有写入可能在后部非零。复制和转封装时若无法理解 UMID,也应原样保留整个区域。
Reserved 必须可跳过
180 字节 Reserved 为将来扩展保留。写新文件应全部置零,避免内存残留泄漏;读取器不应要求未来版本永远为零,而应根据 Version 决定。当前版本中发现非零保留字节可作为警告,不宜据此错误计算 CodingHistory 起点——起点始终是 payload 偏移 602。
五个响度字段
定点缩放
版本支持时,五个字段是小端有符号 16 位定点量,通常以百分之一单位保存:
| 字段 | 物理量 | 存储到显示的换算 |
|---|---|---|
| LoudnessValue | 节目综合响度 | raw / 100.0 LUFS |
| LoudnessRange | 响度范围 | raw / 100.0 LU |
| MaxTruePeakLevel | 最大真峰 | raw / 100.0 dBTP |
| MaxMomentaryLoudness | 最大瞬时响度 | raw / 100.0 LUFS |
| MaxShortTermLoudness | 最大短时响度 | raw / 100.0 LUFS |
例如综合响度 -23.00 LUFS:
1 | raw = -2300 |
最大真峰 -1.25 dBTP:
1 | raw = -125 |
规范约定的未知/未测量哨兵值应与合法测量区别处理,不能显示成一个真实的巨大正响度。解析器同时保存 raw 和“known/unknown”状态;写入器没有测量结果时写哨兵,而不是写零,因为 0.00 LUFS/dBTP 是一个具体数值。
元数据一致性
响度字段描述对音频内容的测量,不改变 PCM 样本。执行增益、裁剪、拼接、声道混音或重采样后,旧测量可能失效。工具应选择重新测量、清空为未知或明确保留并标记可能过期;悄悄保留会误导广播响度合规流程。
验证器可检查值是否在业务合理范围,但不应仅凭范围断言标准语法错误。真正证明元数据正确需要对 PCM 按相应响度测量标准重新计算,并考虑门控、声道权重和真峰过采样;简单 sample peak 不能验证 MaxTruePeakLevel。
CodingHistory
可变区域边界
CodingHistory 从 payload 偏移 602 开始,长度严格等于:
1 | history_size = ckSize - 602 |
它不依赖 NUL 终止,也不能用扫描到下一个看似 FourCC 的字节决定结束。若 chunk size 为奇数,payload 后的 RIFF pad byte 不属于 CodingHistory。读取器必须先按 ckSize 取历史,再跳 padding。
历史通常由 CRLF 结束的 ASCII 行组成,可记录模拟/数字来源、采样率、字长、声道模式、编码链和自由文本。常见字段形式包括:
1 | A=PCM,F=48000,W=24,M=stereo,T=original recording\r\n |
这是制作历史,不是当前 fmt 的权威替代。若历史写着 44.1 kHz 而当前 fmt 为 48 kHz,可能表示曾经重采样,也可能是错误元数据;解码仍以 fmt 为准,并把差异呈现在审计报告中。
追加与保留
每次处理可在原历史后追加一行,但必须重算 bext.ckSize、pad 和 RIFF/RF64 外层长度。若原历史末尾没有 CRLF,追加器应先按策略补分隔,不能把新记录粘到旧行。文件超过 RIFF 32 位限制时,元数据增长也可能触发转 RF64/BW64,不能只更新局部 32 位 size。
解析 CodingHistory 时限制最大长度和行数,转义控制字符,不执行其中任何文本。它不是命令脚本,也不能因为出现 A=, F= 就假定每行字段完整可靠。
bext 解析与验证算法
有界读取流程
1 | parse_bext(chunk): |
所有 slice 终点采用半开区间。bounded_view 保证即使外层文件后面还有数据,bext 解析器也不能越过自己的 ckSize。成功后由 RIFF 层使用 8+ckSize+(ckSize&1) 前进。
交叉验证报告
参考验证器至少输出:
1 | bext file offset |
再与 fmt 、data、RF64 ds64.sampleCount 和相邻文件时间参考交叉验证。BWF 元数据解析成功不代表 PCM 数据完整;同样,音频能播放也不证明 TimeReference、响度或 chunk size 正确。
AIFF 中的 PCM
FORM 与大端字段
AIFF 使用 IFF FORM 结构,常见 form type 为 AIFF。多字节整数和线性 PCM 样本传统上采用 big-endian。主要 chunk 包括 COMM 和 SSND,chunk 同样有长度与偶数对齐规则,但字段语义不同于 RIFF/WAVE。
COMM
COMM 给出声道数、sample frame 数、sample size 和采样率。采样率使用 80 位 extended floating-point 表示,解析器不能按普通 64 位 double 原始字节直接读取。声明 frame 数可与 SSND 实际长度交叉检查。
SSND
SSND 包含 offset、blockSize 和声音数据。offset 表示从相关位置到首声音数据的偏移,不能把 SSND payload 开头立即当样本。blockSize 用于存储对齐提示,不等于 PCM 音频帧 stride。
AIFC
AIFC 扩展允许不同压缩类型和端序,例如特定标识可表示 little-endian PCM。读取器应由 compression type 决定样本编码,不能看到 FORM 结构就一律使用大端整数。
AIFF 文件的完整 Chunk 语法
FORM 根容器
AIFF 基于 IFF。文件从 FORM 开始,随后是四字节大端 ckSize 和四字节 form type。经典 AIFF 的 form type 为 AIFF,AIFF-C 为 AIFC:
1 | offset size field |
FORM.ckSize 从 form type 的第一个字节计算到 FORM 内容末尾。在文件尾没有额外数据时:
1 | file_size = 8 + FORM.ckSize |
每个子块使用:
1 | ckID[4] |
pad 使下一 chunk 从偶数字节边界开始,不计入 ckSize。下一块位置为 data_offset+ckSize+(ckSize&1),所有加法都要检查越界。不能搜索 ASCII COMM 或 SSND 猜边界,因为声音数据中可出现相同四字节。未知 chunk 应按长度跳过并保留。
大端字段与样本
AIFF Header 和经典线性 PCM 样本使用大端序。Chunk size、声道数、帧数、样本位数、SSND offset/blockSize 都按大端读取:
1 | read_u16_be(p) = (u16(p[0]) << 8) | p[1] |
移位前提升为无符号类型,避免有符号 char 的符号扩展。宿主机端序不会改变文件规则。
Common Chunk COMM
AIFF 的 18 字节结构
AIFF 的 COMM payload 恰好 18 字节:
| payload 偏移 | 长度 | 字段 | 类型与作用 |
|---|---|---|---|
| 0 | 2 | numChannels |
大端无符号声道数 |
| 2 | 4 | numSampleFrames |
大端无符号每声道帧数 |
| 6 | 2 | sampleSize |
样本有效位数 |
| 8 | 10 | sampleRate |
IEEE 754 80-bit extended |
numSampleFrames 是帧数,不是所有声道样本总数。双声道 1000 帧包含 2000 个样本。未压缩字节对齐 PCM 的预期大小:
1 | container_bytes = ceil(sampleSize / 8) |
不能用 sampleSize/8 的向下整数除法,否则 20 位会被错算为两字节。声道数、帧数及乘法都要限制和检查溢出。COMM 是解释 SSND 的必要配置;重复 COMM 会产生歧义,严格模式应拒绝。
80-bit Extended 采样率
位字段和数值公式
AIFF 用十字节 extended 浮点保存采样率:
1 | byte 0..1: sign:1 + exponent:15 |
指数偏置为 16383。设 se 为前两字节、mantissa 为后八字节大端无符号整数,对普通有限规格化值:
1 | sign = se >> 15 |
与 binary32/binary64 不同,extended 的整数最高位显式存储。全零指数和 mantissa 表示零;全一指数用于 Infinity/NaN 等特殊值。采样率为零、负数或非有限值都不能用于时长计算。
44100 Hz 和 48000 Hz
44100 Hz 的常见十字节编码:
1 | 40 0E AC 44 00 00 00 00 00 00 |
1 | sign = 0 |
48000 Hz 常见编码:
1 | 40 0E BB 80 00 00 00 00 00 00 |
不能把十字节截成八字节后直接解释为 double,也不能把 40 0E 当采样率。解析器可以转为 binary64 供应用计算,但无损重写应保存原始十字节。
安全转换
1 | read_extended80_rate(bytes): |
ldexp 避免手工构造巨大幂时的中间溢出。非整数采样率的时长计算应保留精度并声明舍入规则。
Sound Data Chunk SSND
Header 与声音起点
SSND payload 至少八字节:
| payload 偏移 | 长度 | 字段 | 作用 |
|---|---|---|---|
| 0 | 4 | offset |
blockSize 后到首声音字节的偏移 |
| 4 | 4 | blockSize |
存储块对齐提示,零表示不指定 |
| 8 | offset |
offset bytes | 不属于声音样本 |
8+offset |
其余 | sound data | 按 COMM 解释 |
1 | require ssnd_ckSize >= 8 |
offset 区域不保证全零,按长度跳过。blockSize 不是 PCM 帧 block_align,不能替代 channels*ceil(sampleSize/8)。现代普通文件常将两者都写零。
帧、端序与尾部
经典 AIFF 样本按帧交错,多字节样本大端:
1 | stereo S16BE: |
把 COMM 推导的 expected bytes 与 SSND sound bytes 比较:小于表示截断,大于表示存在未解释尾部,不是 block_align 整数倍表示不完整帧。Chunk pad 位于 SSND payload 后,不计入 ckSize 和声音数据。
56 字节单声道 AIFF 实例
完整字节
构造单声道、44100 Hz、S16BE、一帧,样本 +16384=0x4000:
1 | 46 4F 52 4D 00 00 00 30 41 49 46 46 |
| 文件偏移 | 长度 | 结构 | 解释 |
|---|---|---|---|
0x00 |
4 | FORM |
根 ID |
0x04 |
4 | 00 00 00 30 |
FORM.ckSize=48 |
0x08 |
4 | AIFF |
form type |
0x0C |
8 | COMM Header | ckSize=18 |
0x14 |
18 | COMM payload | 1 ch、1 frame、16 bit、44100 Hz |
0x26 |
8 | SSND Header | ckSize=10 |
0x2E |
4 | offset | 0 |
0x32 |
4 | blockSize | 0 |
0x36 |
2 | 40 00 |
+16384 |
长度闭合
1 | COMM physical = 4 + 4 + 18 = 26 |
COMM 预期声音量:
1 | 1 frame * 1 channel * 2 bytes = 2 bytes |
SSND 提供 10-8-0=2 字节,完全闭合。40 00 按 S16BE 为 16384,归一化为 0.5。若误按小端会得到 64;若忘记 SSND 八字节 Header,会把 offset 的零字节当静音样本。
AIFF-C 扩展
FVER
AIFF-C form type 为 AIFC,常含 FVER chunk。Version 1 的典型完整块:
1 | 46 56 45 52 00 00 00 04 A2 80 51 40 |
即 ckID="FVER"、ckSize=4、version=0xA2805140。该版本是大端二进制,不是 ASCII 日期。
扩展 COMM 与 Pascal String
AIFF-C COMM 前 18 字节与 AIFF 相同,后面增加:
| payload 偏移 | 长度 | 字段 |
|---|---|---|
| 18 | 4 | compressionType FourCC |
| 22 | 可变 | compressionName Pascal string |
Pascal string 首字节为字符数 N,随后 N 字节文本,并按格式规则补齐。解析前验证:
1 | require comm_ckSize >= 23 |
显示名称不决定解码,真正权威字段是 compressionType。NONE 通常表示经典大端整数 PCM,sowt 常表示小端补码 PCM;浮点和其他类型必须按明确 FourCC 映射。遇到未知 type 时可报告声道、帧数和采样率,但不能猜测 SSND 是大端整数。
AIFF 与 AIFC form type 决定 COMM 语法。若对 AIFC 只读前 18 字节,会丢失 compressionType,并可能把小端、浮点或压缩数据错误解释为经典 PCM。
AIFF/AIFF-C 解析参考流程
两阶段 Chunk 索引
1 | parse_form(file): |
先索引允许 SSND 出现在 COMM 前,也可保留 MARK、INST、NAME、AUTH、ANNO 等元数据。索引保存绝对偏移和长度,不按不可信 ckSize 直接分配大缓冲。
验证清单
验证 FORM size 闭合、所有 child chunk 位于 FORM 内、奇数 payload 有 pad、COMM 唯一且长度匹配 form type、采样率正且有限、声道/sampleSize 在支持范围、SSND offset 不越界、声音字节覆盖声明帧数、所有乘加不溢出。
数值验证抽取首尾帧,按 compressionType 和端序解析最小值、最大值、零、DC offset及浮点非有限值。播放器能打开文件不等于字段一致;参考验证器应同时报告 COMM 声明帧数和由 SSND 长度反推的帧数。
CAF 中的 PCM
64 位长度
Core Audio Format 使用大端字段和 64 位 chunk size,适合超大文件和多种音频描述。desc chunk 保存采样率、format ID、format flags、bytes/packet、frames/packet、channels/frame 和 bits/channel。
LPCM Flags
Linear PCM 的 format flags 可表示 float、little-endian、signed integer、packed、aligned high、non-interleaved 等。相同 bits/channel 在不同 flags 下可能有不同容器布局。CAF 是展示“PCM 必须由参数集合描述”的典型例子。
Data Chunk
CAF data chunk 通常先有 edit count,再是音频数据。chunk size 与 bytes/packet、frames/packet 应一致;可变 packet 格式还需 packet table。线性固定 PCM 可以直接由格式描述定位 frame。
CAF 文件头与 Chunk 通用结构
八字节文件头
Core Audio Format 文件以八字节 Header 开始,所有 Header 数值使用大端:
| 文件偏移 | 长度 | 字段 | 合法值/作用 |
|---|---|---|---|
| 0 | 4 | mFileType |
ASCII caff |
| 4 | 2 | mFileVersion |
当前常用版本 1 |
| 6 | 2 | mFileFlags |
当前版本通常为 0 |
字节表示:
1 | 63 61 66 66 00 01 00 00 |
CAF 不使用 RIFF 的根 size,因此文件可自然超过 4 GiB。Header 后连续排列 chunk,读取器以文件实际长度作为外层硬边界。
十二字节 Chunk Header
每个 CAF chunk:
1 | mChunkType[4] |
Chunk size 是 64 位有符号值,不包括十二字节 Header。普通 chunk 要求非负并且不越过文件末尾。特定场景下,最后的 data chunk 可使用 -1 表示长度延伸到文件末尾;其他负值或非末尾未知长度不能用普通跳过算法解释。
1 | if chunk_size >= 0: |
CAF chunk 不是 RIFF chunk,不能无条件添加 (size&1) pad。解析器应按 CAF 的 64 位 size 精确前进。未知正长度 chunk 可安全索引和跳过;未知负长度 chunk 无法确定后续边界,应拒绝。
Audio Description Chunk desc
固定 32 字节 ASBD
desc 保存 AudioStreamBasicDescription,payload 固定 32 字节:
| payload 偏移 | 长度 | 字段 | 类型 |
|---|---|---|---|
| 0 | 8 | mSampleRate |
big-endian IEEE 754 binary64 |
| 8 | 4 | mFormatID |
FourCC,线性 PCM 为 lpcm |
| 12 | 4 | mFormatFlags |
大端位掩码 |
| 16 | 4 | mBytesPerPacket |
每 packet 字节数,0 可表示可变 |
| 20 | 4 | mFramesPerPacket |
每 packet 采样帧数 |
| 24 | 4 | mChannelsPerFrame |
每帧声道数 |
| 28 | 4 | mBitsPerChannel |
每声道有效位数 |
desc.mChunkSize 应为 32。采样率 binary64 需先按大端重建位模式,再 bit-cast。48000.0 的位模式为 0x40E7700000000000,文件字节:
1 | 40 E7 70 00 00 00 00 00 |
不能按大端 uint64 得到整数后直接转换为 double 数值;那会把位模式的整数值转换成约 4.676e18,而不是解释为 48000.0。
Packet 与 Frame
CAF 使用 packet 作为通用音频封装单位。对固定线性 PCM,常设 mFramesPerPacket=1,则一个 packet 就是一帧交错样本:
1 | mBytesPerPacket = bytes_per_sample_container * channels |
也可以一个 packet 包含多个帧,此时:
1 | bytes_per_frame = mBytesPerPacket / mFramesPerPacket |
前提是二者非零且整除。对 interleaved LPCM:
1 | bytes_per_frame == channels * container_bytes |
对 non-interleaved 标志,音频缓冲/packet 的解释不同,不能继续套用交错帧公式。文件 API 对非交错 CAF 的具体 packet 排列应按标志和容器规范处理。
Linear PCM Format Flags
位定义
mFormatID="lpcm" 时,常用 flag:
| 位值 | 名称 | 含义 |
|---|---|---|
0x00000001 |
IsFloat | 样本为 IEEE 浮点;否则为整数路径 |
0x00000002 |
IsLittleEndian | 多字节样本小端;未置位为大端 |
0x00000004 |
IsSignedInteger | 整数样本为有符号补码 |
0x00000008 |
IsPacked | 有效位紧密占据容器,不留未描述间隙 |
0x00000010 |
IsAlignedHigh | 有效位少于容器位时位于高端 |
0x00000020 |
IsNonInterleaved | 声道数据不按帧交错 |
0x00000040 |
IsNonMixable | 格式不应按普通可混合线性 PCM 处理 |
Flag 组合必须与 bits、bytes 和 packet 字段一起解释。例如 S16LE 交错、每 packet 一帧,常用:
1 | IsLittleEndian | IsSignedInteger | IsPacked |
F32LE 则通常设置 IsFloat、IsLittleEndian、IsPacked,不设置 IsSignedInteger:
1 | 0x01 | 0x02 | 0x08 = 0x0000000B |
同时设置 IsFloat 与 IsSignedInteger 是矛盾配置。单字节样本的 endian 标志对字节排列没有实际影响,但仍应按文件声明保存;不能由此推断 U8/S8,符号性仍看 flags。
Packed 与 AlignedHigh
当 bits_per_channel == container_bits,Packed 表示所有容器位都有效,AlignedHigh 通常没有额外效果。若 24 位有效数据放入四字节容器:
1 | container_bits = (bytes_per_frame / channels) * 8 = 32 |
IsAlignedHigh=1 表示有效位在 bits 31..8;未置位时通常按低端位置解释。IsPacked 与“每样本恰好三字节”不是同义词:应从 bytes/packet、frames/packet、channels 共同得到容器步长,再结合 flags 判断有效位位置。
data Chunk 的精确边界
Edit Count
CAF data payload 的前四字节是大端 mEditCount,声音数据从其后开始:
1 | data payload: |
因此正长度 data chunk 必须 mChunkSize>=4。声音长度:
1 | audio_bytes = data_chunk_size - 4 |
不能把 edit count 当第一帧样本。对于 chunk_size=-1,有效 payload 长度由文件末尾减 payload 起点得到,再扣四字节 edit count。
Edit Count 用于指示内容编辑状态,通常新文件为零。它不是帧数或 packet 数,不能用于计算时长。修改音频内容的工具应按格式语义维护它,而只重写无关元数据是否递增则由具体编辑语义决定。
固定 PCM 的帧数反推
对 mBytesPerPacket>0、mFramesPerPacket>0:
1 | require audio_bytes % mBytesPerPacket == 0 |
乘法和加法使用 64 位受检运算。若尾部不是完整 packet,报告截断,不要静默丢弃。可变 packet 音频需要 pakt 等 Packet Table 提供大小和帧数;但固定 LPCM 不应凭空要求 Packet Table。
76 字节 S16LE Stereo CAF 实例
完整文件
参数:48000 Hz、双声道、有符号 S16LE、交错、每 packet 一帧、两帧。样本:
1 | frame 0: L=0, R=32767 |
完整 76 字节:
1 | 63 61 66 66 00 01 00 00 |
desc 逐字段
| 字节 | 值 |
|---|---|
40 E7 70 00 00 00 00 00 |
binary64 48000.0 |
6C 70 63 6D |
lpcm |
00 00 00 0E |
little-endian + signed + packed |
00 00 00 04 |
4 bytes/packet |
00 00 00 01 |
1 frame/packet |
00 00 00 02 |
2 channels/frame |
00 00 00 10 |
16 bits/channel |
交叉公式:
1 | container bytes/channel = 16 / 8 = 2 |
data 与样本
Data chunk size 为 12,包含四字节 edit count 和八字节声音:
1 | edit count: 00 00 00 00 -> 0 |
audio_bytes=12-4=8,packet_count=8/4=2,frame_count=2*1=2。文件大小闭合:
1 | CAF header = 8 |
若把 CAF 大端 chunk size 当小端,desc 的 00...20 会变成巨大值;若忽略 LPCM little-endian flag,FF 7F 会被解析成 -129;若忘记 edit count,首帧会变成两个零声道样本并使尾部出现不完整帧。
CAF Chunk 索引与验证算法
解析流程
1 | parse_caf(file): |
read_i64_be 必须保留符号,以识别全 FF 的 -1;若一开始读成 uint64,0xFFFFFFFFFFFFFFFF 会被当作 18 EiB 长度。普通大小转为宿主 size 前先与文件剩余和实现上限比较。
一致性报告
LPCM 验证器报告:sample rate 原始位模式与数值、format ID、每个 flag、packet/frame/channel/bits 字段、派生容器位和 block align、data edit count、audio bytes、packet/frame 数、余数、时长、首尾帧字节与解析值。
还要检查互斥 flags、bytes/packet 是否与 frames/channels/container 对应、浮点位宽是否受支持、non-interleaved 布局是否由实现覆盖、未知 chunk 是否闭合、data=-1 是否终止文件。单独通过 desc 或能够播放几秒都不足以证明 CAF 结构正确。
第十章 网络中的线性 PCM
RTP L16
RTP 音频的 L16 编码通常表示 16 位有符号线性 PCM,并使用网络字节序(big-endian)。采样率和声道数由 payload type 静态约定或 SDP rtpmap 动态描述。将本机 S16LE payload 原样放入 L16 会导致每样本字节颠倒。
RTP L24
L24 使用每样本 24 位网络顺序。多声道按同一采样时刻的声道顺序交错,具体声道顺序需要信令。RTP timestamp 以采样时钟推进,每个 packet 增量等于 packet 中每声道 sample frame 数,不乘声道数。
Packet Duration
给定 payload bytes、channel count 和 bytes/sample:
1 | frames_per_packet = payload_bytes / (channels × bytes_per_sample) |
payload 不能整除 frame stride 表示截断或格式配置错误。网络 padding 和 RTP header 不计入 PCM payload bytes。
丢包处理
线性 PCM 每包通常可独立解码,但丢失会造成时间缺口。接收端从 RTP sequence 检测包丢失,从 timestamp 确定缺少的采样帧数。插静音不会恢复信号,只维持时间;下一包仍按其 timestamp 放置,不能前移填补空洞。
RTP 固定头与 PCM Payload 边界
最小十二字节 Header
RTP L16/L24 payload 前是 RTP Header。最小 Header 为 12 字节,存在 CSRC、扩展头或 padding 时更长:
1 | 0 1 2 3 |
| 字段 | 位数 | PCM 会话中的意义 |
|---|---|---|
| Version | 2 | 当前 RTP 版本 2 |
| P | 1 | payload 尾部是否有 RTP padding |
| X | 1 | 是否存在 Header Extension |
| CC | 4 | 固定头后 CSRC 数量,每项四字节 |
| Marker | 1 | 音频 Profile 常用于 talkspurt 等边界,不是每包固定值 |
| PT | 7 | Payload Type,由 Profile/SDP 映射到 L16/L24 |
| Sequence | 16 | 每 RTP 包递增,用于乱序、重复和丢包检测 |
| Timestamp | 32 | payload 第一帧在采样时钟中的位置 |
| SSRC | 32 | 同步源标识 |
解析 payload 起点:
1 | header_size = 12 + 4 * CC |
扩展长度单位是 32-bit word,不是字节。Padding 最后一字节给出包括自身在内的 pad 字节数。PCM 帧整除检查针对去掉 RTP padding 后的 payload;若把 padding 当样本,会产生额外异常帧。
L16 的数值和声道排列
网络字节序的 S16
L16 每个样本是 16 位有符号补码,网络顺序为 big-endian:
| 数值 | L16 两字节 |
|---|---|
| 0 | 00 00 |
| 1 | 00 01 |
| -1 | FF FF |
| 32767 | 7F FF |
| -32768 | 80 00 |
本机 S16LE 缓冲中的 32767 是 FF 7F,发送 L16 前必须写为 7F FF。接收端可逐样本大端读取后存入宿主整数,不必先对整个 payload 批量反转;批量反转必须仍保持每个样本和每帧声道顺序。
交错帧
多声道 payload 按采样时刻交错。双声道两个采样帧:
1 | frame 0: L=0, R=32767 |
L16 payload:
1 | 00 00 7F FF 80 00 FF FF |
拆分:
1 | frame 0 = 00 00 | 7F FF |
payload 长八字节,双声道 L16 frame stride 为四字节,因此包含两帧。不能先写所有左声道再写所有右声道,除非另一个明确协议声明 planar 布局;RTP L16 的普通多声道 payload 是交错样本序列。
声道数量由 RTP payload 映射信令给出。大于双声道时,还需要双方一致的声道顺序约定;只知道 channels=6 不足以区分 FL/FR/FC/LFE/SL/SR 等位置。
L24 的三字节码字
边界向量
L24 每样本为 24 位有符号补码,按网络大端顺序:
| 数值 | 24 位模式 | L24 字节 |
|---|---|---|
| 0 | 0x000000 |
00 00 00 |
| 1 | 0x000001 |
00 00 01 |
| -1 | 0xFFFFFF |
FF FF FF |
| 8388607 | 0x7FFFFF |
7F FF FF |
| -8388608 | 0x800000 |
80 00 00 |
| 1193046 | 0x123456 |
12 34 56 |
读取:
1 | u = (u32(b0) << 16) | (u32(b1) << 8) | b2 |
发送时把有符号值按模 2^24 得到低 24 位,再依次写 bits 23..16、15..8、7..0。不能把低位对齐 int32_t 的四个内存字节直接截前三个:小端机器中 0x00123456 的前三字节是 56 34 12,顺序与 L24 相反。
双声道 L24 frame stride 为六字节。payload 长度必须是六的整数倍;不足三字节表示样本截断,能整除三但不能整除六表示声道帧截断。
SDP Payload 映射
动态 Payload Type
动态 PT 常通过 SDP rtpmap 声明:
1 | m=audio 5004 RTP/AVP 96 |
语义:PT 96 映射为 L16,采样时钟 48000 Hz,两个声道。L24 示例:
1 | m=audio 5006 RTP/AVP 97 |
48000 同时决定 RTP timestamp 的时钟单位,不是网络发送包率。/2 是声道数,不表示 sample size 或每包两帧。SDP 文本和实际 payload 必须一致;若发送端实际为单声道但声明双声道,接收端会把相邻时刻样本错配成左右声道,并把时长缩短一半。
RTP/AVP 还历史性定义了特定静态 L16 PT 和固定采样率/声道组合。使用动态 PT 时不要凭 PT 数字猜格式,必须读取会话信令;同一个 96 在不同会话可以映射完全不同的编码。
信令变更边界
采样率、声道数或 L16/L24 类型不能在同一 SSRC 数据流中无通知地切换。新 SDP/会话协商生效时,接收器应建立新格式版本,处理抖动缓冲和时间轴边界。仅观察 payload 长度无法可靠自动识别:1200 字节既可表示 300 帧 stereo L16,也可表示 200 帧 stereo L24,或其他声道组合。
RTP 包的完整字节实例
Header 参数
设置 V=2、P=0、X=0、CC=0、M=0、PT=96、sequence=0x1234、timestamp=0x01020304、SSRC=0x11223344:
1 | 80 60 12 34 01 02 03 04 11 22 33 44 |
解析:
1 | 0x80 = 10 0 0 0000 -> V2, no padding/extension/CSRC |
附加前述双声道两帧 L16 payload,完整 UDP payload 中的 RTP 包为 20 字节:
1 | 80 60 12 34 01 02 03 04 11 22 33 44 |
下一包若也携带两帧,sequence 为 0x1235,timestamp 应为:
1 | 0x01020304 + 2 = 0x01020306 |
不是加 payload 字节数 8,不是加样本总数 4,也不是加声道数。RTP timestamp 单位是每声道采样帧时钟。
Packet Duration 与 MTU
固定时长计算
采样率 R、每包帧数 F:
1 | duration_seconds = F / R |
48 kHz、20 ms:
1 | F = 48000 * 0.020 = 960 frames |
这些都大于常见以太网 MTU 下的单包 RTP payload。线性 PCM 不应因此依赖 IP 分片,而应选择更短 packet duration,在音频帧边界拆成多个 RTP 包。
MTU 预算
MTU 1500、IPv4 无选项、UDP 8、最小 RTP 12,无加密和隧道:
1 | max_payload = 1500 - 20 - 8 - 12 = 1460 bytes |
Stereo L16 每帧四字节:
1 | max_frames = floor(1460 / 4) = 365 |
若使用 IPv6 基础头 40 字节:
1 | max_payload = 1500 - 40 - 8 - 12 = 1440 |
SRTP 标签、Header Extension、TURN、VPN、PPPoE 等继续占用预算。工程上先确定可用 RTP payload,再向下取 frame stride 整数倍:
1 | payload = floor(max_payload / frame_stride) * frame_stride |
绝不能为了填满 MTU 写半个 PCM 帧。若可用 payload 小于一个 frame stride,会话配置不可发送,应报错。
序号、时间戳与回绕
16 位 Sequence
Sequence 每包加一并按 2^16 回绕:
1 | FFFE, FFFF, 0000, 0001 |
判断相邻应使用模序号算法,而不是普通无符号大于比较。接收器还要区分小范围乱序、重复、真正丢包和源重启。SSRC 改变通常建立新的序号/时间戳状态。
32 位 Timestamp
Timestamp 按帧数增加并按 2^32 回绕。48 kHz 下回绕周期约:
1 | 2^32 / 48000 = 89478.4853 s ≈ 24.855 h |
长会话一定会遇到回绕,因此时间线扩展器根据相邻包和合理跳变窗口构造 64 位 extended timestamp。不能看到新值小于旧值就判定倒退。
Timestamp 描述采样时刻,RTP 包到达时间受网络抖动影响。播放调度需要 RTCP Sender Report 或本地时钟映射、抖动估计和缓冲策略;不能用 UDP 接收系统时间替换源采样时间。
丢包、乱序与时间空洞
由 Timestamp 确定缺口
假设每包 240 帧,已播放起点 T 的包,下一预期起点是 T+240。如果后续包起点是 T+480 且中间序号缺一个,则缺少 240 帧。接收器可以插 240 帧静音、保持空洞或用 PLC 估计,但不能把后续真实包前移到 T+240,否则媒体时间线永久缩短并与其他流失去同步。
Sequence gap 与 timestamp gap 应交叉检查:
- 序号缺一且时间戳正好缺一包帧数:典型丢包;
- 序号连续但 timestamp 跳变:源 discontinuity、格式切换或发送错误;
- 序号缺包但 timestamp 连续:可能丢的是无 payload/冗余包,或发送端时间戳错误;
- 相同 sequence 重复:去重,不重复写入环形缓冲;
- 先收到较大 sequence 后收到较小:在抖动窗口内排序,而非立即判旧包。
L16/L24 每包独立,不存在跨包编码状态,但音频连续性仍依赖所有帧。PLC 不应生成看似真实的原始 PCM 供归档而不标记;实时播放可在 side metadata 中记录 concealed frame 范围。
丢失时长上限
对不可信 timestamp 跳变设置最大可接受空洞。恶意包可声称跳过数十亿帧,若接收器按差值分配静音将耗尽内存。采用流式时间线:记录 gap 区间或分批生成,超过策略上限触发重同步。
RTP L16/L24 有界解析器
参考流程
1 | parse_pcm_rtp(packet, negotiated_format): |
先由会话决定格式,再解析字节。不要根据 payload 首字节、峰值范围或包长自动猜 L16/L24。输出若为 S16LE/S24LE,需要逐样本转换端序;若为 float,则按有效位归一化并保持时间元数据。
回归向量
测试至少覆盖:单/双/多声道,零和正负峰,payload 一个帧、多个帧和非整数帧;RTP CC/X/P 组合;序号与 timestamp 回绕;乱序、重复、丢一/多包;PT 不匹配;SDP 声道数错误;L16/L24 端序;超大 timestamp gap;SSRC 切换。
端到端验证同时保存发送前 PCM 帧 hash 和接收后去掉 RTP Header 的解码帧 hash。无丢包且无数值转换时应 bit-exact;发生 PLC 时只在明确标记的缺失区间允许不同,并保证后续真实帧仍落在原 timestamp 位置。
PCM 分块与文件 I/O
短读和短写
文件或 socket read/write 可能返回少于请求字节。调用方应循环直到完成、EOF 或错误,并保存不足一个 frame 的尾部。一次系统调用成功不保证得到一个完整 period 或一个完整音频帧。
Scatter/Gather
交错/planar 转换或网络 header+payload 可使用 scatter/gather I/O,但每个 iovec 的生命周期和总长度必须有效。若把 planar 多声道直接作为多个 iovec 发送,接收协议也必须定义 planar;系统调用不会自动交错。
Memory Mapping
mmap 读取大 PCM 文件可减少复制,但访问尾部仍需按 frame stride 检查。文件被并发截断时可能产生访问异常;可靠工具应锁定文件或使用普通受控读取。未对齐样本仍不能直接强制转换指针。
重采样实现细节
有理比例
输入率 Rin 到输出率 Rout 可约分为插值因子 L 和抽取因子 M。多相 FIR 实现避免为插值后的每个零样本做无效计算。长期输出 frame 数应由整数相位累计决定,避免浮点索引误差。
滤波器延迟
线性相位 FIR 具有群延迟。离线转换可在首尾 padding 后裁掉延迟,实时系统则需要把延迟计入时间戳或同步预算。若分块处理时没有保留前一块滤波历史,边界波形不连续。
Rate Change
运行中切换输出采样率或漂移补偿比例时,突然重置相位会产生跳变。可平滑改变 ratio,或者在零交叉/静音区切换并交叉淡化。输出 timestamp 仍应来自累计实际 output frames。
PCM 比较和回归测试
Bit-exact 比较
相同格式且算法承诺确定性时,可逐字节比较。先检查元数据完全一致,再比较 frame。端序转换或 float NaN payload 可能让语义相近但字节不同,因此报告要说明比较层级。
数值误差比较
重采样、浮点运算或不同 DCT/滤波实现通常不能 bit-exact。可按声道计算最大绝对误差、均方误差、信噪比和延迟对齐后的相关性。阈值必须由算法精度和目标位深定义,不能任意接受“听起来一样”。
相位与延迟对齐
两个波形可能仅相差固定滤波延迟。比较前可用已知 impulse 或互相关估计 offset,再在重叠区比较。但不能用任意动态时间扭曲掩盖丢帧或采样率错误。
测试向量集合
应包含全零、正负极值、单位 impulse、斜坡、不同频率正弦、左右独立信号、直流偏置、随机噪声、非整 period 尾部、NaN/Inf float 和跨缓冲半帧。每个向量固定格式描述、预期 frame 数、关键字节和统计值。
PCM 元数据验证顺序
容器优先
先验证容器边界和字段,再建立 PCM format;格式未确认前不要解析 data 为样本。WAVE fmt 损坏时,即使 data 能被某组常见参数播放,也只能称为启发式恢复。
格式公式
验证 channels、sample rate、container bits、valid bits、block align 和 byte rate 的关系。Extensible 格式再验证 GUID、channel mask 和有效位。任何矛盾都保留原值并报告采用策略。
数据范围
检查 data 长度、完整 frame 数、尾部、整数极值分布和 float 非有限值。数值范围检查不能证明内容正确,但能快速发现错误格式解释。
时间与标记
最后核对 fact/ds64/bext/cue/smpl 等样本计数或位置元数据。裁剪、重采样或拼接文件时,这些字段需要同步更新,不能只改 RIFF/data size。
第十一章 PCM 与硬件串行总线
内存字节不等于引脚 Bitstream
内存中的 S16LE 用两个字节表示一个样本;串行音频总线则按 bit clock 把样本位逐个发送,并用 frame/word select 标记声道时隙。总线规定 MSB-first、有效位对齐和时钟边沿,CPU 内存端序不能直接套到引脚波形。
Serializer 与 DMA
音频外设通常从 DMA 读取 16/32 位容器,再按配置发送有效位。一个 0x00001234 在 left-aligned 24-in-32 与 right-aligned 24-in-32 中产生不同波形。驱动应同时配置 memory width、slot width、valid bits 和 alignment。
时钟角色
串行总线通常有 bit clock、frame/word select,某些系统另有 master clock。发送端或接收端可作为 clock master。采样率由时钟比值决定,而不是 DMA 每秒被软件调用多少次;错误的时钟分频会让标称 48 kHz PCM 实际以其他速率播放。
I²S 数据格式
信号
经典 I²S 使用 serial clock、word select 和 serial data。Word select 区分左右声道时隙,数据通常 MSB first。I²S 的关键特征是有效字的 MSB 相对 WS 边沿延迟一个 bit clock。
一个 Bit 延迟
左声道/右声道切换后,下一 bit clock 才出现新样本 MSB。若接收器按 left-justified 解释 I²S,所有样本会移位一位,幅度/符号均错误。逻辑分析仪解码配置必须选择正确格式。
Slot Width
16 位有效样本可放入 16、24 或 32 bit slot;24 位也常放 32 bit slot。剩余位填零、符号扩展或忽略由接口约定。每声道每 frame 的 bit clock 数由 slot width,而不是 valid bits 决定。
1 | BCLK = sample_rate × slots_per_frame × bits_per_slot |
48 kHz、双声道、32-bit slot 需要 3.072 MHz BCLK。把它按 16-bit slot 配置会使采样帧边界/速率错误。
WS 极性
常见 I²S 约定 WS 某电平为左声道、另一电平为右声道,但外设可能允许反相。接口配置应明确极性;左右反接不会破坏样本数值,却会交换声像。
Left-Justified 与 Right-Justified
Left-Justified
Left-Justified 在 WS 边沿后立即发送新样本 MSB,没有 I²S 的一 bit 延迟。有效位靠时隙左侧,低位后可填充。若 slot width 大于 valid bits,接收器在固定 MSB位置截取。
Right-Justified
Right-Justified 让样本 LSB 对齐时隙末端,前面是填充/符号位。它要求双方知道 valid bits,才能定位 MSB。24-bit right-justified 被按 32-bit signed 直接读取时,需要正确符号扩展。
格式误配症状
I²S 与 left-justified 差一 bit,常表现为幅度翻倍/减半、符号异常和严重失真;right-justified 位宽误配则表现为固定缩放或噪声。先用已知单 bit 测试 pattern 验证对齐,比播放音乐更容易定位。
DSP/PCM 与 TDM 时隙
Frame Sync
DSP A/B 或 PCM mode 通常使用短 frame sync pulse 标记一帧起点,随后连续发送一个或多个时隙。A/B 变体可能在 sync 当拍或下一拍开始 MSB。具体外设名称不完全统一,必须以控制器手册的时序图为准。
TDM
Time Division Multiplexing 将多个声道放进一个 frame 的多个固定 slot。配置包括 slot count、slot width、active slot mask、valid bits、frame sync width/极性和 data line 数。声道数不一定等于总 slot 数;禁用 slot仍占时钟时间。
Slot Mapping
DMA buffer 的声道顺序与 TDM slot映射可能由硬件寄存器重排。格式描述应保存逻辑 channel label → memory index → wire slot 三层映射。仅写“8 channel PCM”不足以确定麦克风阵列方向。
BCLK 预算
96 kHz、8 slot、32 bit 需要 24.576 MHz BCLK。外设和 PCB必须支持该频率。增加有效位但不改 slot width不改变 BCLK;增加 slot count则成比例增加。
I²S/TDM 采集验证
Pattern 测试
可让发送端按声道输出 0x000001、0x000002、最高正值和最高负值等 pattern,用逻辑分析仪核对 frame sync、slot、MSB/LSB、极性和有效位。随机音频难以区分一 bit 偏移和模拟噪声。
DMA Frame Stride
DMA 每个总线 frame 在内存可能占 slot_count × container_bytes,即使只有部分 active channels。驱动若按 active channel 数推进,会在下一 frame 从错误位置读取。硬件是否压缩 inactive slot需查接口定义。
Cache 一致性
带 cache 的嵌入式系统中,DMA 与 CPU共享 PCM buffer需执行正确 clean/invalidate 和内存屏障。旧 cache line表现为周期性重复音频,与格式错误不同。Buffer地址、长度还需满足 DMA alignment。
I²S 帧的位级时间关系
WS、Slot 和 Sample Word
经典双声道 I²S 的一个音频帧包含左、右两个时隙。Word Select 的标准常见极性为 0 表示左声道、1 表示右声道;新声道 Sample 的 MSB 在 WS 改变后的下一个 bit clock 出现,即“一位延迟”。概念时序:
1 | WS: _________0________________|_________1________________ |
图中真正边沿极性还要结合控制器的 clock polarity/phase 配置;核心不变量是 I²S 的 WS 提前一个 bit clock,而 Left-Justified 在 WS 边界立即对齐 MSB。逻辑分析仪若选择错误协议,波形仍可能解出“有数值”,但全部样本会移一位。
Slot width 是每个声道时隙占用的 bit clock 数,Sample width 是有效音频位数:
1 | frame_bits = slots_per_frame * slot_width |
有效位少于 slot width 时,需要明确它们在时隙中的位置及剩余位。I²S/Left-Justified 常从 MSB 开始连续发送有效位,之后低端时钟可发送零或无关填充;Right-Justified 则让 LSB 靠时隙末端。
16 位样本的线缆位序
S16 样本 0x1234 的有效位按 MSB-first:
1 | 0001 0010 0011 0100 |
无论 CPU 内存中是 34 12 还是 12 34,线上位序都由 serializer 配置决定。对 32-bit slot、16 valid bits 的 MSB-aligned I²S,概念 slot 内容为:
1 | 0001001000110100 0000000000000000 |
若外设要求 DMA word 中有效样本预先左移到 bits 31..16,则内存数值应为 0x12340000;另一些外设接受低位 0x00001234 并在 serializer 内部自动对齐。驱动必须依据寄存器语义决定,不能仅凭 wire slot width 推断 DMA 位位置。
符号位错位的症状
I²S 数据被 Left-Justified 接收器少延迟一拍时,接收值可能相当于左移或右移一位并混入相邻/填充 bit。若符号位丢失,负样本可变成大正值;若多出一个高位零,幅度减半;若最低位来自下一时隙,可能出现与声道切换同步的噪声。
使用以下 Pattern 比音乐更容易判断:
| S16 Pattern | 二进制特征 | 可检测问题 |
|---|---|---|
0x0001 |
只有 LSB 为 1 | LSB 位置和 slot 尾部 |
0x4000 |
次高位为 1 | 一位左右偏移 |
0x7FFF |
正最大 | 符号位与饱和 |
0x8000 |
仅符号位为 1 | MSB、符号扩展和极性 |
0x5555 |
交替 01 | bit 顺序、采样边沿 |
0xAAAA |
交替 10 | 与上一向量互补 |
左右声道分别发送不同 Pattern,例如 L=0x1234、R=0x5678,可同时检查 WS 极性和声道交换。
Clock Edge、Setup 与 Hold
发送变化边沿与接收采样边沿
串行数据应在一个 SCK 边沿附近改变,在相对边沿被接收器采样,以留出 setup/hold 时间。I²S 控制器常提供 clock polarity 或 sampling edge 选项,名称可能是 rising/falling、normal/inverted 或 leading/trailing。双方若选相同变化边沿,接收端会在数据尚未稳定时采样,表现为温度、电压或频率相关的随机 bit 错误。
配置时以器件数据手册时序图为准,记录:
1 | SCK idle polarity |
逻辑分析仪低采样率可能看不出 setup/hold 违规。要验证高速 TDM,示波器或足够带宽的逻辑分析仪应同时观察 SCK、FS/WS 和 SD,并在接收器引脚处测量,而非只看发送器寄存器。
时钟方向
Clock master 产生 BCLK/WS,slave 跟随。若双方都配置 master,会发生电气争用或两个近似频率拍频;双方都配置 slave 则没有时钟。某些 Codec 的 MCLK 必须先稳定,随后通过 PLL 生成内部采样时钟,即使 BCLK/WS 由另一个器件提供。
接口文档应分别声明 MCLK、BCLK、WS 的源,不要只写“Codec 是 slave”。设备可能在 MCLK 上是输入、在 BCLK 上是输出,或者反之。
BCLK、MCLK 与采样率计算
双声道常见组合
| Sample Rate | Slots | Slot Width | BCLK |
|---|---|---|---|
| 44100 | 2 | 16 | 1.4112 MHz |
| 44100 | 2 | 32 | 2.8224 MHz |
| 48000 | 2 | 16 | 1.536 MHz |
| 48000 | 2 | 32 | 3.072 MHz |
| 96000 | 2 | 32 | 6.144 MHz |
| 192000 | 2 | 32 | 12.288 MHz |
48 kHz S24 放在 32-bit slot 时仍是 3.072 MHz,不是 48000*2*24=2.304 MHz。后者只有双方明确采用 24-bit slot 才成立。
MCLK 常见为采样率的 128、256、384、512 等倍,但不是 I²S 数据格式的统一固定要求:
1 | MCLK = Fs * mclk_ratio |
48 kHz、256 Fs 为 12.288 MHz;44.1 kHz、256 Fs 为 11.2896 MHz。若系统只有 12 MHz 晶振,通过整数分频无法精确得到这些频率,需 PLL/分数分频或接受 ppm 误差。
分频与 PPM
给定实际 BCLK:
1 | actual_Fs = actual_BCLK / (slot_count * slot_width) |
标称 48 kHz,实际 48004.8 Hz 为 +100 ppm。录制十分钟会累计:
1 | 600 s * 100 / 1,000,000 = 0.06 s |
与另一个独立时钟域同步时,60 ms 漂移足以造成明显缓冲增减。系统需要异步重采样、样本滑移或反馈控制,不能只扩大有限环形缓冲期待永久吸收。
DMA 容器与 FIFO Packing
三种宽度不能混同
驱动同时面对:
| 宽度 | 例子 | 决定内容 |
|---|---|---|
| valid bits | 24 | PCM 量化和符号位 |
| wire slot bits | 32 | 每声道消耗的 BCLK |
| DMA transfer width | 16/32 | 每次总线/FIFO 内存访问大小 |
DMA width 不一定等于 sample container。某些外设为双声道 S16 使用一个 32-bit FIFO word 打包 [L16,R16];某些每次写一个 16-bit;另一些即使有效位 16 也要求每声道一个 32-bit word。错误配置可能每隔一个样本丢失、左右交换或重复半字。
32 位 FIFO 的半字顺序
小端 CPU 内存的 32 位 word 0x56781234 字节为:
1 | 34 12 78 56 |
这是否表示 L=0x1234, R=0x5678,取决于外设 FIFO 从低半字还是高半字先送、DMA burst 如何映射和 serializer channel select。不能仅凭内存 hexdump断言 wire 顺序。应使用左右不同 Pattern 在引脚验证,并把寄存器/SoC 手册中的 FIFO packing 写进驱动格式描述。
S24 的常见 DMA 表示
24-bit wire sample 常由 32-bit DMA word 承载:
1 | 模式 A: sample in bits 23..0, high byte sign-extension or ignored |
值 0x123456 分别可能是:
1 | A low-aligned: 0x00123456 or sign-extended 0x00123456 |
接收负数时还要确认外设是否自动符号扩展。若模式 A 只把低 24 位写入而高八位为零,CPU 必须显式 24 位符号扩展;把 word 直接转 int32_t 会把所有负样本当大正值。
TDM 帧与活动时隙
总时隙数决定时钟
TDM 可有 4、8、16 等固定 slot,即使只启用部分 slot,禁用 slot 通常仍占 BCLK:
1 | 96 kHz * 8 slots * 32 bits = 24.576 MHz |
若只启用 slot 0、2、4、6 四个麦克风,逻辑声道数为 4,但 wire frame 仍有 8 slots。DMA 是否只输出 active slots 或保留八个容器取决于控制器;这决定内存 frame stride 是 4*4=16 还是 8*4=32 字节。
三层声道映射
一个完整格式对象应分别保存:
1 | logical channel label -> DMA memory index -> wire slot number |
例如:
| 逻辑声道 | DMA Index | Wire Slot |
|---|---|---|
| MIC-FL | 0 | 0 |
| MIC-FR | 1 | 2 |
| MIC-RL | 2 | 4 |
| MIC-RR | 3 | 6 |
如果算法期望 [FL,FR,RL,RR],驱动却把原始八 slot 直接当四连续声道,波束形成方向和相位关系会错误。声道映射错误的单通道波形可能看似完全正常,必须用逐 slot 不同测试信号验证。
Frame Sync 宽度与 DSP A/B
DSP/PCM 模式常以一个或若干 BCLK 宽的 FS pulse 标记 frame,而不是像立体声 I²S 那样 WS 在半帧保持电平。DSP A 与 DSP B 的名称在控制器生态中不总是一致,常见差异是首个 MSB 在 FS 同一 bit cell 还是下一 bit cell。双方必须对齐:
1 | FS polarity |
仅配置“DSP mode”不足以建立互操作。
环形 DMA 的帧边界
Period 长度
DMA period bytes 必须是内存 frame stride 的整数倍:
1 | period_frames = period_bytes / dma_frame_stride |
48 kHz、stereo、每声道 32-bit DMA 容器,frame stride=8。若 period=3840 bytes:
1 | frames = 3840/8 = 480 |
若错误按 packed S24 的六字节帧计算,会得到 640 帧和 13.333 ms,导致时间戳、AEC block 和网络 packetization全错。
Wrap 和 Partial Frame
环形缓冲写指针按字节回绕,但上层提交点必须保持在 frame 边界。若一个 DMA descriptor 在物理末尾分成两段,组合后的总长度仍需整帧;消费者不能把尾部半帧作为完整样本提交。安全索引:
1 | available_frames = available_bytes / frame_stride |
tail 留待下一次,不丢弃也不填零。硬件若保证 descriptor 总是整 frame,驱动仍应在初始化时验证 length/alignment 而不是依赖偶然配置。
Cache Line 所有权
非一致性 cache 平台上,RX DMA 完成后 CPU invalidate 对应 cache line;TX 前 CPU clean。若一个 cache line 同时包含 DMA 所有和 CPU 所有数据,clean/invalidate 可覆盖对方修改,因此 buffer 起点和 period 边界最好按 cache line 对齐,并用所有权状态机防止并发访问。
内存屏障保证 descriptor 状态与数据可见顺序。音频周期重复、每隔固定 cache line 出现旧数据或只在高负载出现噪声,常是 cache/所有权问题,不应先归咎于 PCM 端序。
I²S/TDM 字节到波形验证流程
发送路径
从一个已知内存向量开始,逐层记录:
1 | logical signed sample |
以 S24 0x123456 为例,若 API 为低对齐 int32、DMA 要求高对齐、wire slot 32:
1 | API value = 0x00123456 |
逻辑分析仪应在正确 WS/FS 和 slot 下得到 0x123456。若得到 0x091A2B,很可能整体右移一位;若得到 0x345600,可能截取位位置错误;若左右 Pattern 对调,则检查 WS 极性和 FIFO halfword 顺序。
接收路径
接收验证反向记录 wire 到内存:捕获 SCK/FS/SD,按协议解出每 slot 位模式,比较外设 FIFO read 值、DMA buffer bytes 和 CPU 符号扩展结果。每一步使用同一 frame/slot 序号,不能只比较统计峰值。
对于 24-bit 负向量 0x800000:
1 | wire bits = 10000000 00000000 00000000 |
若 CPU 得到 +8388608,说明符号扩展缺失;若得到 -2147483648,可能把高对齐容器直接当 S32 未右移;两者波形幅度表现不同。
自动化检查表
测试矩阵覆盖 I²S、Left/Right-Justified、DSP A/B,16/24/32 valid bits,16/24/32 slots,主/从时钟,WS/FS 极性,采样边沿,单/双/多 slot,active mask 和 DMA 16/32-bit width。每个组合至少发送零、±1、正负峰、单 bit 和左右/逐 slot 唯一 Pattern。
验证输出包括实际 Fs/BCLK/MCLK、ppm、每 frame clock 数、FS width、MSB delay、slot bit value、DMA stride、period frames 和 timestamp 增量。只有波形、内存和时间三层同时闭合,才能确认硬件链路正确。
双声道 S24-in-32 的闭合测试向量
格式声明
设采样率 48000 Hz,I²S 双声道,32-bit slot,24 valid bits,wire 上有效位 MSB-first 后补八个零;DMA 每声道使用一个小端 32-bit word,要求有效位高对齐。选择两帧:
1 | frame 0: L=+0x123456, R=-1 |
时钟参数:
1 | slots_per_frame = 2 |
两帧 DMA 区域占 2 frames * 2 channels * 4 bytes=16 字节。
数值到高对齐容器
24 位码字左移八位:
| 帧/声道 | S24 码字 | DMA 32-bit 位模式 | 小端内存字节 |
|---|---|---|---|
| F0/L | 0x123456 |
0x12345600 |
00 56 34 12 |
| F0/R | 0xFFFFFF |
0xFFFFFF00 |
00 FF FF FF |
| F1/L | 0x800000 |
0x80000000 |
00 00 00 80 |
| F1/R | 0x7FFFFF |
0x7FFFFF00 |
00 FF FF 7F |
完整 DMA hexdump:
1 | 00 56 34 12 00 FF FF FF |
每四字节是一个小端容器;不能把整段字节按出现顺序直接当 wire bitstream。外设加载 word 后按 serializer 规则先发 bit 31。
Wire Slot
去掉内存端序概念后,四个 slot 的 32 位线上内容:
1 | F0/L: 00010010 00110100 01010110 00000000 = 12 34 56 00 |
右移八位并进行 24 位符号解释,依次恢复 1193046,-1,-8388608,8388607。所有 slot 的低八位应为零;若逻辑分析仪看到有效值在 bits 23..0,则 DMA/serializer 采用了低对齐而非本例声明。
接收端反向校验
1 | for each dma_word: |
在 C 中应先把 DMA word 读为 uint32_t 再逻辑右移。若先转 int32_t 并算术右移,对负数通常能得到预期,但语言/实现细节和后续 mask 容易混淆;无符号提取加明确符号扩展更可审计。
两帧时间长度:
1 | 2 / 48000 = 41.666666... microseconds |
若 DMA 完成时间显示约 83.33 μs,常见原因是实际 WS 24 kHz、slot/frame 配置翻倍或驱动把每声道 word 当成一帧。字节向量正确仍需同时检查时钟。
故障注入
将 F0/L DMA word 误写为低对齐 0x00123456,小端字节变成:
1 | 56 34 12 00 |
若 serializer 仍取 bits 31..8,线上有效 24 位为 0x001234,比期望少最低字节并缩小 256 倍。将 I²S 接收模式误设 Left-Justified,则所有 slot 可能整体错一 bit。交换 FIFO halfword/word 顺序则表现为 R/L 或相邻帧交换。黄金向量应分别注入这三类错误,确认诊断能够区分“内存对齐”“串行协议相位”和“FIFO 顺序”。
AES3 与 S/PDIF 中的 PCM
Subframe
AES3 将每个音频 sample 放在固定 subframe中,并加入前导码、辅助/音频 sample bit、validity、user、channel status 和 parity。两个 subframe通常组成一个 stereo frame。线路编码和前导码不是裸 PCM payload。
Biphase Mark Coding
物理链路使用双相标记编码以嵌入时钟,接收端先恢复 channel bit和subframe,再提取 PCM。抓取同轴/光纤电平后不能直接按 S16/S24读取。硬件接收器通常输出解码后的 I²S/TDM 或内存 PCM。
Preamble
不同前导码标识 block起点、channel A和channel B。一个 channel status block跨多个 frame收集。前导码因 BMC违规模式而可被同步识别,不按普通 data bit解释。
Audio Sample Bits
Subframe可承载最多一定数量的 audio/aux bits,实际有效位由 channel status等约定。16/20/24-bit PCM在 subframe中的对齐需按 AES3定义处理。提取后再转换为内存容器和端序。
V/U/C/P
Validity表示该 sample是否适合转换为模拟音频,user bit和channel status承载辅助信息,parity用于检测 subframe bit错误。Parity正确不代表 PCM配置或声音内容正确;Validity置位时接收端可静音/标记错误。
Channel Status
Channel status可描述专业/消费模式、PCM/非音频、采样率、字长等。某些链路也承载压缩 burst,此时虽然硬件接口像 S/PDIF,payload并不是线性 PCM。必须检查 non-audio 标志和 burst protocol。
USB Audio 中的 PCM
Endpoint Packet
USB Audio通常通过 isochronous endpoint传输 PCM frame。格式 descriptor定义 subslot size、bit resolution、channel数和采样率/clock source。每个 USB packet可包含可变数量 sample frame,以适应 USB frame/microframe与音频采样率的比值。
44.1 kHz Packet Pattern
44.1 kHz不能在每个 1 ms USB frame中固定放整数 44.1个采样帧,通常在44和45之间按反馈/调度分布。接收端必须以 packet length/block align计算实际 frame数,不能假定每包相同。
Asynchronous Feedback
异步 endpoint可用 feedback报告设备实际消费/产生速率,主机据此调整每包 frame数量。Feedback使用规定定点格式,不是 PCM timestamp。忽略 feedback会导致环形缓冲最终欠载或溢出。
Subslot 与 Bit Resolution
24-bit PCM常用3-byte subslot,也可能用4-byte subslot且 bit resolution=24。USB descriptor明确二者。转换到 CPU S32时需要按 USB Audio约定进行符号扩展和有效位对齐。
Isochronous 错误
Isochronous不重传,丢包需由音频层插静音/隐藏并保持采样时间。Packet sequence由USB调度隐式管理,上层仍应记录丢失 frame数。不能把下一 packet前移填补,避免时钟漂移。
PCM 与 G.711 的边界
Companded PCM
G.711 μ-law/A-law通常被称为 PCM telephony,但每个8-bit code不是线性幅度等步长样本,而是压扩映射。它需要查表/公式解码为线性 PCM。把 .ulaw 字节按 unsigned PCM播放会产生强噪声。
μ-law
μ-law code包含符号、段和量化部分,并常有bit取反约定。静音code和线性零的字节不同。解码输出常为S16近似幅度;编码时大幅值使用较粗量化步长。
A-law
A-law使用不同段与bit交替规则,和μ-law code不可互换。RTP payload type/SDP明确 PCMU或PCMA。采样率常为8 kHz单声道,但这来自应用profile,不是所有压扩数学本身强制的唯一场景。
文件标签
WAVE format tag可标识 A-law/μ-law,data块仍是code字节而非线性PCM。wBitsPerSample=8不能据此判断为unsigned linear PCM,必须先看 format tag。
RF64 与 ds64
RF64 标识
RF64文件顶层通常使用 RF64 form ID,并在传统32位 size位置放置占位值,form type仍为WAVE。紧随前部的 ds64提供64位 RIFF size、data size和sample count等。解析器应在遇占位size时查ds64,而不是把占位当真实长度。
ds64 字段
核心字段包括 riffSize、dataSize、sampleCount 和 table length,随后可为其他大 chunk提供 ID/64-bit size映射。所有字段little-endian。Table count×entry size需检查溢出和chunk剩余。
ds64 Sample Count 语义
sampleCount通常指每声道sample frame数,与 dataSize/blockAlign应一致。若为未知/占位,仍可从完整data推导。二者冲突时保留原值并报告,不能修改data来匹配头。
Chunk Size 替换
需要64位size的chunk在自身32位字段使用占位,真实值由ds64 table按chunk ID/顺序解析。多个同ID chunk可能需要严格匹配规则,不能用单个字典覆盖所有重复 ID。
BW64
与 RF64 的关系
BW64是面向广播工作流的大文件WAVE变体,沿用64位尺寸思想并要求/允许特定元数据。PCM样本表示仍由fmt/data决定。读取器识别顶层标识与ds64,并保留未知广播chunk。
chna
Channel Assignment元数据可把音频轨道/声道关联到广播音频对象标识。它描述语义映射,不改变 interleaved PCM字节顺序。条目数与声道/track关系需验证。
axml
ADM XML等元数据可放在axml中。解析设置XML资源限制,外部实体默认禁用。元数据声道引用与fmt/chna不一致时报告语义冲突,不应改变底层blockAlign。
大文件写入
写入器可先放占位ds64,结束时回填实际64位size/sample count。崩溃恢复扫描data后重建ds64,但无法凭PCM字节恢复全部广播元数据。修复输出到新文件并保留原始。
WAVE 十六进制实例
44 字节经典头
一个无额外块的传统PCM WAVE常见布局:
1 | 52 49 46 46 24 00 00 00 57 41 56 45 |
示例为PCM tag=1、2声道、48000 Hz、byte rate=192000、block align=4、16 bit,data size为0。RIFF size 0x24等于当前文件长度44减8。真实带数据文件要更新data和RIFF size。
逐字段验证
80 BB 00 00小端为48000,00 EE 02 00为192000;验证 48000×4=192000。如果误按大端会得到不合理超大值,合理范围检查可以提前发现端序错误。
非固定44字节
加入JUNK、LIST、bext或扩展fmt后,data不再偏移44。Reader必须遍历chunk。所谓“跳过44字节就是PCM”只适用于该最小布局,不能作为通用WAVE算法。
WAVE_FORMAT_EXTENSIBLE 十六进制逻辑
结构长度
fmt chunk通常扩展到40字节:前16字节WAVEFORMATEX基础、2字节cbSize(常见22)、2字节valid bits、4字节channel mask和16字节SubFormat GUID。Chunk size应覆盖全部字段。
5.1 示例检查
六声道、S24-in-S32时,channels=6、bitsPerSample=32、validBits=24、blockAlign=24。48 kHz byte rate=1,152,000。Channel mask应有六个与布局对应的bit,置位数与channels一致。
SubFormat
基础tag是EXTENSIBLE,真正线性PCM/float由SubFormat GUID决定。看到bits=32不能断言S32:它也可能是F32或24有效位容器。GUID解析完成前不要选择sample decoder。
PCM 时钟测量
通过 Sample Count
在真实时间区间 T 内设备产生 N frame,实际率近似 N/T。系统时钟本身也有误差,精确测量需要参考时钟。不要用线程sleep和一次buffer计数估算ppm。
PPM
1 | ppm = (actual_rate - nominal_rate) / nominal_rate × 1,000,000 |
48 kHz设备+100 ppm每秒多约4.8 frame,约208秒累积1000 frame。音视频长录制中足以产生可见漂移。
Timestamp Regression
收集多个 (sample_index, system_time) 点做线性回归,可估计实际率和起始offset,并减小调度抖动影响。检测 discontinuity后分段拟合,不能跨丢帧/设备重启使用一条直线。
Clock Domain Crossing
音频ADC、视频sensor和网络时钟可能不同域。同步器选择master clock,将其他轨道timestamp映射到master并通过重采样/丢帧调整。仅让首帧时间相同不能解决速率漂移。
PCM 延迟分解
采集延迟
包括模拟滤波/ADC群延迟、硬件FIFO、DMA period等待、驱动和线程调度。Timestamp若在period完成中断采集,首sample实际早于中断约一个period加硬件延迟。
处理延迟
重采样器、AEC、降噪、编码器和队列各有frame/滤波延迟。接口应报告算法latency或在timestamp补偿。只测CPU执行时间会漏掉lookahead和buffering。
播放延迟
包括应用队列、设备buffer、DMA、DAC和模拟输出。低延迟调优要测端到端loopback,不仅看period_size。减小buffer过度会增加xrun。
延迟元数据
容器通常保存presentation timeline,不一定保存物理I/O延迟。实时协议可在控制层交换capture time和latency estimate,但这不是裸PCM字段。文档应分清样本PTS与预计扬声器发声时刻。
第十二章 PCM API 设计
Format 对象不可变
活动流的format对象最好不可变并带version。采样率/声道/布局变化时在frame边界发布新对象,下游重建buffer。原地修改共享字段会让正在处理的buffer被新格式解释。
Buffer 描述
Buffer携带data planes、frame count、stride、format引用、first sample index/PTS和flags。不要只传 void* + bytes。Planar每plane的可用bytes和stride分别验证。
Ownership
明确buffer由谁释放、DMA何时归还、异步处理能否持有。Zero-copy需要引用计数或生命周期协议。复用尚在播放的DMA buffer会产生周期性内容撕裂。
Discontinuity
丢包、xrun、seek和format change通过flag/事件显式通知。下游滤波器可重置历史或交叉淡化;仅从timestamp跳变猜测可能太晚。
PCM Converter 验证顺序
端序
先用固定整数pattern验证端序和有效位,再处理交错。若源值读取就错,后续重采样/混音测试没有意义。
Layout
使用每声道不同常量或频率验证channel map与planar/interleaved。检查输出每个frame对应同一时间点。
数值
验证极值、零、±1 LSB、NaN/Inf和饱和。整数/float往返在可逆范围的允许误差提前定义。
Rate
用impulse和正弦验证滤波延迟、幅频响应、输出frame count和跨buffer连续性。长时测试验证分数相位无累计丢/多 frame。
时间
转换后timestamp应包含重采样比例和算法delay策略。比较累计输出frame/率与输入timeline,确保没有逐buffer舍入漂移。