WAV是基于RIFF结构的音频文件,是微软为windows系统开发的一种标准音频文件格式。如PCM中介绍,粗略的可以理解为wav数据就是加了wav头的pcm数据,实际WAV文件由RIFF chunk、Format chunk 和 Data chunk三个主要区块组成。辅以fact、cue与list chunk。
一、WAV格式概要
┌─────────────────────────────────────────────────────────────────┐
│ WAV 文件结构 │
├─────────────────────────────────────────────────────────────────┤
│ RIFF Header │
│ ┌─────────────┬─────────────┬─────────────┐ │
│ │ Chunk ID │ Chunk Size │ Format │ │
│ │ "RIFF" │ 4 bytes │ "WAVE" │ │
│ │ 4 bytes │ (小端序) │ 4 bytes │ │
│ └─────────────┴─────────────┴─────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ Chunk 1 (fmt 、XMA2 等) │
│ ┌─────────────┬─────────────┬─────────────┐ │
│ │ Chunk ID │ Chunk Size │ Chunk Data │ │
│ │ 4 bytes │ 4 bytes │ N bytes │ │
│ └─────────────┴─────────────┴─────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ Chunk 2 (data 等) │
│ ┌─────────────┬─────────────┬─────────────┐ │
│ │ Chunk ID │ Chunk Size │ Chunk Data │ │
│ │ 4 bytes │ 4 bytes │ N bytes │ │
│ └─────────────┴─────────────┴─────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ ... 更多 Chunk ... │
└─────────────────────────────────────────────────────────────────┘
常规主要trunk
RIFF RIFF chunk :主要说明本文件保持的音频格式;
Formt chunk :主要说明音频格式的相关属性,如采样率、采用位数等信息;
Data chunk:主要记录实际的音频数据和长度信息;
大部分的都以以上三个trunk块组成,
辅助chunk
不过wav还有fact、cue与list 等其他辅助trunk:
|----------|---------------------|-------|--------------------------|
| Chunk ID | 名称 | 必选 | 功能描述 |
| fmt | Format | 是 | 音频格式参数(采样率、位深、通道数等) |
| data | Data | 是 | 音频采样数据 |
| fact | Fact | 否 | 采样数信息(压缩格式必需) |
| bext | Broadcast Extension | 否 | 广播元数据(EBU R128 标准) |
| LIST | List | 否 | 容器 chunk,包含 INFO/adtl 子块 |
| ID3 | ID3v2 | 否 | ID3 元数据 |
| cue | Cue Points | 否 | 章节标记点 |
| XMA2 | XMA2 Config | 否 | XMA2 编解码器配置,与fmt互斥 |
| SMV0 | SMV Video | 否 | SMV 视频附加数据 |
注意: 整个chunk除标识字符(RIFF、WAVE、fmt和data)外,都是小端计算。
二、RIFF chunk
2.1 结构介绍

RIFFchunk结构分为3个子块,chunk id、chunk size和chunk format。
2.2
| 子块 | 字节数 | 含义 |
|---|---|---|
| Chunk id | 4 | "RIFF", 所有的wav都固定为这个值。 |
| Chunk size | 4 | 该子块之后文件的总字节数。文件长度 - 8 =Chunk size 。 |
| chunk format | 4 | "WAVE", 所有的wav都固定为这个值。 |
2.2 示例:

如上图为许嵩-有何不可的wav文件,根据以上结构,解析出
| 子块 | 字节数 | 16进制对应值 | 解析值 |
|---|---|---|---|
| Chunk id | 4 | 52 49 46 46 | RIFF |
| Chunk size | 4 | B8 E7 8C 02 | Chunk size = 0x028ce7b8 = 42,788,792字节(小端计算,见上面注意说明) |
| chunk format | 4 | B8 E7 8C 02 | WAVE |
问
文件大小 = 42,788,792 +8 = 42,788,800 字节, 如文件的实际大小一致,见下图的"大小"

三、Formt(fmt) chunk
3.1 结构介绍

| 子块 | 字节数 | 含义 | |
|---|---|---|---|
| ID | 4 | "fmt ",固定值。 | |
| Size | 4 | fmt chunk 的大小,一般有 16/18/20/22/40 字节 (也有超过 40 字节的情况),超过 16 字节部分为扩展块 | |
| Info | Format | 2 | 编码格式代码,其值见常见编码格式表,如果上述取值为 16,则此值通常为 1,代表该音频的编码方式是 PCM 编码 |
| Info | Channels | 2 | 声道数目,1 代表单声道,2 代表双声道 |
| Info | SampleRate | 4 | 采样频率,8/11.025/12/16/22.05/24/32/44.1/48/64/88.2/96/176.4/192 kHZ4 bytesByteRate 传输速率,每秒的字节数,计算公式为:SampleRate * FmtChannels * BitsPerSample/8 |
| Info | ByteRate | 4 | 比特率,每秒的数据字节数 b/s |
| Info | BlockAilgn | 2 | 块对齐,告知播放软件一次性需处理多少字节,公式为: BitsPerSample*FmtChannels/8 |
| Info | BitsPerSample | 2 | 采样位数,一般有8/16/24/32/64,值越大,对声音的还原度越高 |
| other-data | size-16 | 扩展数据,用户可以根据实际情况进行扩展 |
上表中的format和size 对应下表的format和size
| Format | 格式名称 | Size | fact 块 |
|---|---|---|---|
| 0x01 | PCM / 非压缩格式 | 16 | |
| 0x02 | Microsoft ADPCM | 18 | √ |
| 0x03 | IEEE float | 18 | √ |
| 0x06 | ITU G.711 μ-law | 18√ | |
| 0x07 | ITU G.711 a-law | 18√ | |
| 0x031 | GSM 6.10 | 20 | √ |
| 0x040 | ITU G.721 ADPCM | √ | |
| 0xFFFE | 见子格式块中的编码格式 | 40 |
3.2 示例

| 子块 | 字节数 | 16进制值 | 解析 | |
|---|---|---|---|---|
| ID | 4 | 66 6D 74 20 | "fmt " | |
| Size | 4 | 10 00 00 00 | 0x10 = 16 | |
| Info | Format | 2 | 01 00 | 0x01 = 1 |
| Info | Channels | 2 | 02 00 | 0x02 =2 |
| Info | SampleRate | 4 | 44 AC 00 00 | 0xAC44 =44,100 |
| Info | ByteRate | 4 | 10 B1 02 00 | 0x02B110 = 176,400 b/s |
| Info | BlockAilgn | 2 | 04 00 | 0x04 = 4 |
| Info | BitsPerSample | 2 | 10 00 | 0x10 = 16 |
| Extended-data | 16-16 =0 | / | 无扩展 |
由以上可以看到,当前wav格式的
编码格式为:PCM
声道数为:双声道
采样率为:44100 HZ
对齐位位:4字节
采用位数为:16 bit
比特率= 176400 b/s *8 = 176.4 KB/s * 8 = 1411.2 kbps ≈ 1411 kbps
以上信息与播放器解析的一致,如下图

四、Data chunk
4.1 结构介绍

| 子块 | 字节数 | 含义 |
|---|---|---|
| ID | 4 | "data", 所有的wav都固定为这个值。 |
| size | 4 | 原始音频数据的大小,就是这之后字节数。 |
| Audio | 不定 | 实际的音频数据 |
4.2 示例

| 子块 | 字节数 | 16进制对应值 | 解析值 |
| ID | 4 | 64 61 74 61 | data |
| size | 4 | B8 E7 8C 02 | size = 0x028ce7b8 = 42,788,792字节(小端计算,见上面注意说明) |
|---|
、
五、 list chunk
LIST chunk ┌─────────────┬─────────────┬─────────────┬─────────────┐ │ "LIST" │ Size │ Type │ Sub-chunks │ │ 4 bytes │ 4 bytes │ 4 bytes │ N bytes │ └─────────────┴─────────────┴─────────────┴─────────────┘
5.1 info 子chunk
LIST chunk
┌─────────────┬─────────────┬─────────────┬─────────────────────────────┐
│ "LIST" │ Size │ "INFO" │ 子块序列 │
│ 4 bytes │ 4 bytes │ 4 bytes │ │
└─────────────┴─────────────┴─────────────┴─────────────────────────────┘
│
▼
┌───────────────────────────────────────┐
│ 子块1 │ 子块2 │ ... │ 子块N │
│ (IART) │ (INAM) │ │ (ICOP) │
└───────────────────────────────────────┘
每个子块结构:
┌─────────────┬─────────────┬─────────────┐
│ Chunk Code │ Chunk Size │ Chunk Data │
│ 4 bytes │ 4 bytes │ N bytes │
└─────────────┴─────────────┴─────────────┘
|------|---------------|-----------------|
| 子块代码 | 含义 | 示例 |
| IART | Artist(艺术家) | "John Doe" |
| INAM | Name(标题) | "My Song" |
| ICOP | Copyright(版权) | "(C) 2024" |
| IACD | Album(专辑) | "Greatest Hits" |
| ITRK | Track(曲目) | "03/12" |
| IGNR | Genre(流派) | "Rock" |
| IYER | Year(年份) | "2024" |
5.2 adt1 子chunk
adtl(Audio Data List) 是 RIFF LIST chunk 的子类型,用于存储章节标签名称 。它与 cue chunk 配合使用:
| Chunk | 功能 |
|---|---|
cue |
定义章节位置(时间戳) |
adtl |
定义章节名称(人类可读标签) |
WAV 文件中的章节信息组织
┌─────────────────────────────────────────────────────────┐
│ cue chunk │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Chapter 1: id=1, offset=10000 │ │
│ │ Chapter 2: id=2, offset=20000 │ │
│ └───────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ LIST chunk (adtl) │
│ ┌───────────────────────────────────────────────────┐ │
│ │ labl: id=1, label="Chapter 1: Introduction" │ │
│ │ labl: id=2, label="Chapter 2: Main Content" │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────┐
│ 最终章节元数据 │
│ Chapter 1: │
│ id=1 │
│ offset=10000 │
│ title="Chapter 1..."│
└───────────────────────┘
六、其它chunk
6.1 fact chunk
文件总采样率,可用用于计算播放整个音频文件所需的时间。
┌─────────────────────┬─────────────────┬─────────────────────┐
│ 字段名 │ 大小 (字节) │ 说明 │
├─────────────────────┼─────────────────┼─────────────────────┤
│ ID │ 4 │ 固定"fact"
│ size │ 4 │ trunk大小
│ SampleCount │ 4 │ 总采样数 │
└─────────────────────┴─────────────────┴─────────────────────┘
6.2 bext chunk
BEXT Chunk 的作用:
-
广播专业元数据:存储符合 EBU R128 标准的专业信息
-
时间同步 :
time_reference用于多轨录音同步 -
物料追踪:UMID 提供全球唯一标识
-
处理历史 :
coding_history记录音频处理链 -
版本兼容:支持不同版本的 BWF 格式
bext chunk 结构(BWF Version 1)
┌─────────────────────────────────────────────────────────┐
│ Chunk ID │ "bext" │ 4 bytes │
│ Chunk Size │ 可变长度 │ 4 bytes │
├─────────────────────────────────────────────────────────┤
│ description │ 文件描述 │ 256 bytes │
│ originator │ 创建者 │ 32 bytes │
│ originator_reference │ 创建者参考编号 │ 32 bytes │
│ origination_date │ 创建日期 │ 10 bytes (YYYY-MM-DD) │
│ origination_time │ 创建时间 │ 8 bytes (HH:MM:SS) │
│ time_reference │ 时间参考 │ 8 bytes (采样数) │
│ version │ BWF 版本 │ 2 bytes │
│ UMID │ 唯一物料标识 │ 64 bytes (可选) │
│ reserved │ 保留字段 │ 190 bytes │
│ coding_history │ 编码历史 │ 可变长度 (可选) │
└─────────────────────────────────────────────────────────┘
6.3 ID3 chunk
ID3 chunk 结构
┌─────────────┬─────────────┬─────────────────────────────┐
│ Chunk ID │ Chunk Size │ ID3v2 Tag Data │
│ "ID3 " │ 4 bytes │ 可变长度 │
│ 4 bytes │ (小端序) │ │
└─────────────┴─────────────┴─────────────────────────────┘
ID3的格式可以具体参考相关ID3介绍,此处不做赘述。
6.4 cue chunk
Cue chunk 结构
┌─────────────┬─────────────┬─────────────────────────────┐
│ Chunk ID │ Chunk Size │ Number of Cues │
│ "cue " │ 4 bytes │ 4 bytes │
│ 4 bytes │ (小端序) │ │
├───────────────────────────────────────────────────────────┤
│ Cue Point 1 │
│ ┌─────────────┬─────────────┬─────────────────────────┐ │
│ │ Cue ID │ Position │ Chunk ID │ │
│ │ 4 bytes │ 4 bytes │ 4 bytes │ │
│ ├─────────────┼─────────────┼─────────────────────────┤ │
│ │ Chunk Start │ Block Start │ Sample Offset │ │
│ │ 4 bytes │ 4 bytes │ 4 bytes │ │
│ └─────────────┴─────────────┴─────────────────────────┘ │
├───────────────────────────────────────────────────────────┤
│ Cue Point 2 ... │
└───────────────────────────────────────────────────────────┘
| 字段 | 大小 | 说明 |
|---|---|---|
Cue ID |
4 bytes | 章节唯一标识 |
Position |
4 bytes | 播放位置(采样数) |
Chunk ID |
4 bytes | 目标 chunk(通常为 data) |
Chunk Start |
4 bytes | chunk 起始偏移 |
Block Start |
4 bytes | 块起始偏移 |
Sample Offset |
4 bytes | 相对于块的采样偏移 |
6.5 XMA2 chunk
XMA2 chunk 结构
┌─────────────┬─────────────┬─────────────────────────────┐
│ Chunk ID │ Chunk Size │ XMA2 Configuration Data │
│ "XMA2" │ 4 bytes │ 可变长度 │
│ 4 bytes │ (小端序) │ │
└─────────────┴─────────────┴─────────────────────────────┘
编解码器特性
| 特性 | 说明 |
|---|---|
| 压缩类型 | 有损压缩 |
| 采样率 | 最高 48kHz |
| 通道数 | 最多 8 通道 |
| 比特率 | 可变比特率(VBR) |
| 平台 | Xbox 360、Windows |
6.6 SMV0 chunk
SMV0 chunk 结构
┌─────────────┬─────────────┬─────────────────────────────┐
│ Chunk ID │ Chunk Size │ SMV Configuration │
│ "SMV0" │ 4 bytes │ 版本号 + 视频参数 │
│ 4 bytes │ (小端序) │ │
└─────────────┴─────────────┴─────────────────────────────┘
SMV 格式特点
| 特性 | 说明 |
|---|---|
| 复合格式 | WAV 音频 + JPEG 视频 |
| 版本号 | 固定为 0200 |
| 视频编码 | SMVJPEG(特殊 JPEG 变体) |
| 帧存储 | 多个视频帧共用一个 JPEG |
七、ffmpeg解码源码
截取ffmpeg解码源码
cpp
for (;;) {
AVStream *vst;
size = next_tag(pb, &tag, wav->rifx);
next_tag_ofs = avio_tell(pb) + size;
if (avio_feof(pb))
break;
switch (tag) {
case MKTAG('f', 'm', 't', ' '):
/* only parse the first 'fmt ' tag found */
if (!got_xma2 && !got_fmt && (ret = wav_parse_fmt_tag(s, size, st)) < 0) {
return ret;
} else if (got_fmt)
av_log(s, AV_LOG_WARNING, "found more than one 'fmt ' tag\n");
got_fmt = 1;
break;
case MKTAG('X', 'M', 'A', '2'):
/* only parse the first 'XMA2' tag found */
if (!got_fmt && !got_xma2 && (ret = wav_parse_xma2_tag(s, size, st)) < 0) {
return ret;
} else if (got_xma2)
av_log(s, AV_LOG_WARNING, "found more than one 'XMA2' tag\n");
got_xma2 = 1;
break;
case MKTAG('d', 'a', 't', 'a'):
if (!(pb->seekable & AVIO_SEEKABLE_NORMAL) && !got_fmt && !got_xma2) {
av_log(s, AV_LOG_ERROR,
"found no 'fmt ' tag before the 'data' tag\n");
return AVERROR_INVALIDDATA;
}
if (rf64 || bw64) {
next_tag_ofs = wav->data_end = av_sat_add64(avio_tell(pb), data_size);
} else if (size != 0xFFFFFFFF) {
data_size = size;
next_tag_ofs = wav->data_end = size ? next_tag_ofs : INT64_MAX;
} else {
av_log(s, AV_LOG_WARNING, "Ignoring maximum wav data size, "
"file may be invalid\n");
data_size = 0;
next_tag_ofs = wav->data_end = INT64_MAX;
}
data_ofs = avio_tell(pb);
/* don't look for footer metadata if we can't seek or if we don't
* know where the data tag ends
*/
if (!(pb->seekable & AVIO_SEEKABLE_NORMAL) || (!(rf64 && !bw64) && !size))
goto break_loop;
break;
case MKTAG('f', 'a', 'c', 't'):
if (!sample_count)
sample_count = (!wav->rifx ? avio_rl32(pb) : avio_rb32(pb));
break;
case MKTAG('b', 'e', 'x', 't'):
if ((ret = wav_parse_bext_tag(s, size)) < 0)
return ret;
break;
case MKTAG('S','M','V','0'):
if (!got_fmt) {
av_log(s, AV_LOG_ERROR, "found no 'fmt ' tag before the 'SMV0' tag\n");
return AVERROR_INVALIDDATA;
}
// SMV file, a wav file with video appended.
if (size != MKTAG('0','2','0','0')) {
av_log(s, AV_LOG_ERROR, "Unknown SMV version found\n");
goto break_loop;
}
av_log(s, AV_LOG_DEBUG, "Found SMV data\n");
wav->smv_given_first = 0;
vst = avformat_new_stream(s, NULL);
if (!vst)
return AVERROR(ENOMEM);
wav->vst = vst;
avio_r8(pb);
vst->id = 1;
vst->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
vst->codecpar->codec_id = AV_CODEC_ID_SMVJPEG;
vst->codecpar->width = avio_rl24(pb);
vst->codecpar->height = avio_rl24(pb);
if ((ret = ff_alloc_extradata(vst->codecpar, 4)) < 0) {
av_log(s, AV_LOG_ERROR, "Could not allocate extradata.\n");
return ret;
}
size = avio_rl24(pb);
wav->smv_data_ofs = avio_tell(pb) + (size - 5) * 3;
avio_rl24(pb);
wav->smv_block_size = avio_rl24(pb);
if (!wav->smv_block_size)
return AVERROR_INVALIDDATA;
avpriv_set_pts_info(vst, 32, 1, avio_rl24(pb));
vst->duration = avio_rl24(pb);
avio_rl24(pb);
avio_rl24(pb);
wav->smv_frames_per_jpeg = avio_rl24(pb);
if (wav->smv_frames_per_jpeg > 65536) {
av_log(s, AV_LOG_ERROR, "too many frames per jpeg\n");
return AVERROR_INVALIDDATA;
}
AV_WL32(vst->codecpar->extradata, wav->smv_frames_per_jpeg);
goto break_loop;
case MKTAG('L', 'I', 'S', 'T'):
case MKTAG('l', 'i', 's', 't'):
if (size < 4) {
av_log(s, AV_LOG_ERROR, "too short LIST tag\n");
return AVERROR_INVALIDDATA;
}
switch (avio_rl32(pb)) {
case MKTAG('I', 'N', 'F', 'O'):
ff_read_riff_info(s, size - 4);
break;
case MKTAG('a', 'd', 't', 'l'):
if (s->nb_chapters > 0) {
while (avio_tell(pb) < next_tag_ofs &&
!avio_feof(pb)) {
char cue_label[512];
unsigned id, sub_size;
if (avio_rl32(pb) != MKTAG('l', 'a', 'b', 'l'))
break;
sub_size = avio_rl32(pb);
if (sub_size < 5)
break;
id = avio_rl32(pb);
avio_get_str(pb, sub_size - 4, cue_label, sizeof(cue_label));
avio_skip(pb, avio_tell(pb) & 1);
for (int i = 0; i < s->nb_chapters; i++) {
if (s->chapters[i]->id == id) {
av_dict_set(&s->chapters[i]->metadata, "title", cue_label, 0);
break;
}
}
}
}
break;
}
break;
case MKTAG('I', 'D', '3', ' '):
case MKTAG('i', 'd', '3', ' '): {
ID3v2ExtraMeta *id3v2_extra_meta;
ff_id3v2_read(s, ID3v2_DEFAULT_MAGIC, &id3v2_extra_meta, 0);
if (id3v2_extra_meta) {
ff_id3v2_parse_apic(s, id3v2_extra_meta);
ff_id3v2_parse_chapters(s, id3v2_extra_meta);
ff_id3v2_parse_priv(s, id3v2_extra_meta);
}
ff_id3v2_free_extra_meta(&id3v2_extra_meta);
}
break;
case MKTAG('c', 'u', 'e', ' '):
if (size >= 4 && got_fmt && st->codecpar->sample_rate > 0) {
AVRational tb = {1, st->codecpar->sample_rate};
unsigned nb_cues = avio_rl32(pb);
if (size >= nb_cues * 24LL + 4LL) {
for (int i = 0; i < nb_cues; i++) {
unsigned offset, id = avio_rl32(pb);
if (avio_feof(pb))
return AVERROR_INVALIDDATA;
avio_skip(pb, 16);
offset = avio_rl32(pb);
if (!avpriv_new_chapter(s, id, tb, offset, AV_NOPTS_VALUE, NULL))
return AVERROR(ENOMEM);
}
}
}
break;
}
/* seek to next tag unless we know that we'll run into EOF */
if ((avio_size(pb) > 0 && next_tag_ofs >= avio_size(pb)) ||
wav_seek_tag(wav, pb, next_tag_ofs, SEEK_SET) < 0) {
break;
}
}