WAV 就是「一段 PCM,前面加 44 个字节的说明」。 这 44 个字节把一个谁都不认识的文件变成能双击播放的。
上一节写出来的 .pcm 打不开,就是因为缺这 44 个字节。 这一节手工把它补上,再用标准库读回来验证——WAV 规范就这么简单。
一、44 个字节换了什么
同一个文件,加头前后:
项 值
头长度 44 字节 PCM 数据 356352 字节 WAV 文件 356396 字节
第三行等于前两行相加,一个字节不多。 所以 WAV 从来没有「压缩」这回事:它就是个带说明书的裸 PCM。
这也解释了一个常见疑问:为什么 WAV 那么大。 因为它压根没打算小——它的用途是「无损、可直接寻址、格式简单」。
二、这 44 个字节分别是什么
拆开看,只有三段:
段 字节数 装什么
RIFF 头 12 RIFF + 文件长度 + WAVE
fmt 块 24 fmt + 格式、声道、采样率、位深
data 头 8 data + 数据长度
第二段里的顺序是死的:格式号、声道数、采样率、字节率、块对齐、位深。 少一个字段或者顺序错了,播放器就当成坏文件。
注意每个块开头那四个字节是标识符:RIFF、fmt(后面有个空格)、data。
fmt 那个空格经常被漏掉,漏了就解不出来——这是手写 WAV 头最常见的错。
三、读回来才算验证
写完不算完,得让标准库读一遍:
读回项 值
声道数 2 采样率 44100 每样本字节 2 采样帧数 89088 时长 2.02 秒
五行全对得上,说明手写的头是合法的。 如果这几行读出来是 0 或者负数,基本就是长度字段写错了。
最后提醒:文件长度字段是「文件总长减 8」,不是「总长」。
这个 8 是 RIFF 与长度字段自己的字节数。写错这一处,很多播放器会直接拒开。
四、动手 Demo:手工拼 44 字节的 WAV 头,再用标准库读回来
读上一节生成的 .pcm,用 struct 拼一个 44 字节的头,写在数据前面。 然后用标准库 wave 读回来,五项参数逐个对照——读得出来就说明头是合法的。
import struct, wave
with open(u"o.pcm", u"rb") as f:
data = f.read()
sr, nch, bits = 44100, 2, 16
hdr = (b"RIFF" + struct.pack(u"<I", 36 + len(data)) + b"WAVE"
+ b"fmt " + struct.pack(u"<IHHIIHH", 16, 1, nch, sr,
sr * nch * bits // 8, nch * bits // 8, bits)
+ b"data" + struct.pack(u"<I", len(data)))
with open(u"o.wav", u"wb") as f:
f.write(hdr)
f.write(data)
print(u"头 %d 字节 数据 %d 字节" % (len(hdr), len(data)))
with wave.open(u"o.wav", u"rb") as wf:
print(u"声道 %d 采样率 %d 每样本 %d 字节 采样帧 %d"
% (wf.getnchannels(), wf.getframerate(),
wf.getsampwidth(), wf.getnframes()))
print(u"时长 %.2f 秒" % (wf.getnframes() / float(wf.getframerate())))
实跑输出:
--------------------------------------------------------------------
5.2.6 PCM 与 WAV——44 字节换一个能双击播放的文件
--------------------------------------------------------------------
**① 手工给 PCM 加一个 WAV 头**
项 值
-------------------------
头长度 44 字节
PCM 数据 356352 字节
WAV 文件 356396 字节
**头只有 44 个字节,但就是这 44 个字节,把一个「谁都不认识」的文件变成能双击播放的。**
**WAV 的本质因此可以一句话说完:一段 PCM,前面加一段说明。**
**② 用标准库读回来验证**
读回项 值
-----------------------
声道数 2
采样率 44100
每样本字节 2
采样帧数 89088
时长 2.02 秒
**五行全对得上,说明手写的头是合法的——WAV 规范就这么简单。**
**「RIFF 头 + fmt 块 + data 块」三样凑齐就能播,一个字节都不用多写。**
struct.pack(u"<IHHIIHH", ...) 这个格式串要与 WAV 规范的字段顺序逐字对齐:
4 字节块长度、2 字节格式号、2 字节声道、4 字节采样率、4 字节字节率、
2 字节块对齐、2 字节位深,一共 16 个字节。
那个 36 + len(data) 就是「文件总长减 8」。
36 是 44 减 8,也就是除了 RIFF 与长度字段之外的所有头部字节。
wave 能把文件读开,就说明结构完全合法。
它读的时候会一路校验块标识符,写错一个字母都会抛异常。
30 秒实验
- 命令行对照:
ffmpeg -f s16le -ar 44100 -ac 2 -i 出.pcm 出.wav,一行搞定。 - 看十六进制:用工具打开 wav 前 44 个字节,
52 49 46 46就是 RIFF。 - 故意写错:把
fmt的空格去掉,看wave报什么错。
常见误区
- 长度字段写成文件总长。规范要求是「总长减 8」,写错会被拒开。
- 漏掉
fmt后面的空格。四个字节的标识符含空格,少一个就认不出来。 - 以为 WAV 是压缩格式。它一点没压,大是必然的,换成 FLAC 才能省一半。
自测三问(点开对答案)
1. WAV 的头一共多少字节?由哪三段组成? —— 44 字节。RIFF 头 12 字节、fmt 块 24 字节、data 头 8 字节。 2. 为什么 `RIFF` 后面的长度是「总长减 8」? —— 因为那 8 个字节是 `RIFF` 标识符与长度字段自身,不计入内容长度。 3. WAV 为什么文件这么大? —— 它存的是未压缩的 PCM,不省任何字节,换来的是无损与可直接寻址。- ☐ (本节完,下一节 5.3.1 PCM 编码为 AAC)