WAV是一种以RIFF为基础的无压缩音频编码格式,该格式以Header、Format Chunk及Data Chunk三部分构成。
本文简要解析了各部分的构成要素,概述了如何使用C++对文件头进行解析以及提取音频数据。

上图展示了WAV文件格式,包括每一field的大小与端序
Header
- ChunkID: 4字节大端序。文件从此处开始,对于WAV或AVI文件,其值总为“RIFF”。
- ChunkSize: 4字节小端序。表示文件总字节数减8,减去的8字节表示ChunkID与ChunkSize本身所占字节数。
- Format: 4字节大端序。对于WAV文件,其值总为“WAVE”
Format Chunk
- Subchunk1ID: 4字节大端序。其值总为“fmt ”,表示Format Chunk从此处开始。
- Subchunk1Size: 4字节小端序。表示Format Chunk的总字节数减8。
- AudioFormat: 2字节小端序。对于WAV文件,其值总为1。
- NumChannels: 2字节小端序。表示总声道个数。
- SampleRate: 4字节小端序。表示在每个通道上每秒包含多少帧。
- ByteRate: 4字节小端序。大小等于SampleRate * BlockAlign,表示每秒共包含多少字节。
- BlockAlign: 2字节小端序。大小等于NumChannels * BitsPerSample / 8, 表示每帧的多通道总字节数。
- BitsPerSample: 2字节小端序。表示每帧包含多少比特。
Data Chunk
- Subchunk2ID: 4字节大端序。其值总为“data”,表示Data Chunk从此处开始。
- Subchunk2Size: 4字节小端序。表示data的总字节数。
- data: 小端序。表示音频波形的帧数据,各声道按帧交叉排列。
使用C++解析WAV文件
文件头结构
定义结构体WaveHeader来保存WAV文件头,即Header、Format Chunk及Data Chunk的非data部分,此外在该结构体中添加了num_frame字段,用来保存文件总帧数,由于Header、Format Chunk与Data Chunk之间可能有其他说明信息,所以还添加了start_pos字段用来保存真正的data开始的位置。
typedef struct WaveHeader { char chunk_id[4] = { 0 }; unsigned int chunk_size = 0; char format[4] = { 0 }; char fmt_chunk_id[4] = { 0 }; unsigned int fmt_chunk_size = 0; unsigned short audio_fomat = 0; unsigned short num_channels = 0; unsigned int sample_rate = 0; unsigned int byte_rate = 0; unsigned short block_align = 0; unsigned short bits_per_sample = 0; char data_chunk_id[4] = { 0 }; unsigned int data_chunk_size = 0; int num_frame = 0; int start_pos = 0; };
提取文件头
/* * fname: 文件路径 * wh: 用来保存文件头的结构体实例 */ void getHead(string fname, WaveHeader &wh) { /* *由于事先并不知道文件大小,故定义足量大小的char数组覆盖文件头 *之后可根据提取到的ChunkSize来定义提取音频数据用的数组 */ const int HEAD_LENGTH = 256 * 1024;//256kb char buf[HEAD_LENGTH]; FILE *stream; freopen_s(&stream, fname.c_str(), "rb", stderr); fread(buf, 1, HEAD_LENGTH, stream); //记录文件读取位置 int pos = 0; //寻找“RIFF”标记 while (pos < HEAD_LENGTH) { if (buf[pos] == 'R'&&buf[pos + 1] == 'I'&&buf[pos + 2] == 'F'&buf[pos + 3] == 'F') { wh.chunk_id[0] = 'R'; wh.chunk_id[1] = 'I'; wh.chunk_id[2] = 'F'; wh.chunk_id[3] = 'F'; pos += 4; break; } ++pos; } //读取Header部分 wh.chunk_size = *(int *)&buf[pos]; pos += 4; wh.format[0] = buf[pos]; wh.format[1] = buf[pos + 1]; wh.format[2] = buf[pos + 2]; wh.format[3] = buf[pos + 3]; pos += 4; //寻找“fmt”标记 while (pos < HEAD_LENGTH) { if (buf[pos] == 'f'&&buf[pos + 1] == 'm'&&buf[pos + 2] == 't') { wh.fmt_chunk_id[0] = 'f'; wh.fmt_chunk_id[1] = 'm'; wh.fmt_chunk_id[2] = 't'; pos += 4; break; } ++pos; } //读取Format Chunk部分 wh.fmt_chunk_size = *(int *)&buf[pos]; pos += 4; wh.audio_fomat = *(short *)&buf[pos]; pos += 2; wh.num_channels = *(short *)&buf[pos]; pos += 2; wh.sample_rate = *(int *)&buf[pos]; pos += 4; wh.byte_rate = *(int *)&buf[pos]; pos += 4; wh.block_align = *(short *)&buf[pos]; pos += 2; wh.bits_per_sample = *(short *)&buf[pos]; pos += 2; //寻找“data”标记 while (pos < HEAD_LENGTH) { if (buf[pos] == 'd'&&buf[pos + 1] == 'a'&&buf[pos + 2] == 't'&buf[pos + 3] == 'a') { wh.data_chunk_id[0] = 'd'; wh.data_chunk_id[1] = 'a'; wh.data_chunk_id[2] = 't'; wh.data_chunk_id[3] = 'a'; pos += 4; break; } ++pos; } //读取Data Chunk的非data部分 wh.data_chunk_size = *(int *)&buf[pos]; pos += 4; //记录真正音频数据的开始位置 wh.start_pos = pos; //计算文件总帧数 wh.num_frame = wh.data_chunk_size / (wh.num_channels*(wh.bits_per_sample / 8)); }
提取波形数据(data)
/* * fname: 文件路径 * wh: 对应的文件头结构体实例 */ void getData(string fname, WaveHeader &wh){ //记录文件读取位置 int pos = wh.start_pos; //为加快处理速度,根据ChunkSize将文件一次读入内存 FILE *stream; freopen_s(&stream, fname.c_str(), "rb", stderr); char* file_data = new char[wh.chunk_size + 8]; fread(file_data, 1, wh.chunk_size + 8, stream); //以每帧2字节为例 short left_data; short right_data; while(pos < wh.start_pos + wh.data_chunk_size){ left_data = *(short*)&file_data[pos]; //TODO: 处理左声道数据 pos += 2; right_data = *(short*)&file_data[pos]; //TODO: 处理右声道数据 pos += 2; } }