音频格式之WAV

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 的作用

  1. 广播专业元数据:存储符合 EBU R128 标准的专业信息

  2. 时间同步time_reference 用于多轨录音同步

  3. 物料追踪:UMID 提供全球唯一标识

  4. 处理历史coding_history 记录音频处理链

  5. 版本兼容:支持不同版本的 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;
        }
    }
相关推荐
AI创界者1 小时前
PilotTTS 一键整合包(Win/Mac):8G 显存畅跑,实测解锁情绪与副语言的精准控制
人工智能·macos·aigc·音视频
u152109648492 小时前
S.S.Audio PRO A2音频隔离器
嵌入式硬件·音视频·实时音视频·视频编解码·视频
VidDown4 小时前
显卡处理视频技术详解:从硬解码到 NVENC,GPU 如何让视频处理起飞?
javascript·编辑器·音视频·视频编解码·视频
EasyDSS5 小时前
全能音视频平台/私有化音视频系统EasyDSS!直播/点播/会议/集群对讲一站式落地
音视频
Damon_X5 小时前
车载音频复习
音视频
3DVisionary6 小时前
告别数据中断:XTDIC-VG视频引伸计在金属疲劳测试中3个真实案例
人工智能·音视频·应用案例·xtdic-vg·视频引伸计·疲劳测试·实战复盘
VidDown7 小时前
视频帧率技术详解:从 24fps 到 120fps,帧率如何影响你的观看体验?
网络·网络协议·编辑器·音视频·视频编解码·视频
byte轻骑兵9 小时前
【AVRCP】规范精讲[30]:新播放器上线全流程,蓝牙音频如何发现并接管新应用
音视频·avrcp·蓝牙耳机·音频控制·蓝牙车机
HyperAI超神经9 小时前
支持真人/动漫/动物驱动,美团开源多风格音频驱动视频生成框架LongCat 1.5;百万级图表理解数据集ChartNet提升VLM图表重建与表格提取能力
音视频