第八篇 WAV文件格式

WAVE PCM soundfile format

WAV即WAVE,WAVE文件是计算机领域最常用的数字化声音文件格式之一,它是微软专门为Windows系统定义的波形文件格式(Waveform Audio),其扩展名为"*.wav"。

最基本的WAVE文件是PCM(脉冲编码调制)格式的,这种文件直接存储采样的声音数据没有经过任何的压缩,是声卡直接支持的数据格式,要让声卡正确播放其它被压缩的声音数据,就应该先把压缩的数据解压缩成PCM格式,然后再让声卡来播放。

WAVE文件还有很多种有损压缩格式,比如IMA/DVI ADPCM,Microsoft ADPCM,AAC编码等。被压缩的声音数据,需要先解压成PCM格式,才能用声卡来播放

如果是PCM,则为无损格式,文件会比较大,并且大小相对固定,可以使用以下公式计算文件大小。

cpp 复制代码
FileSize = HeadSize + TimeInSecond * SampleRate * Channels * BitsPerSample / 8

其中:

  • HeadSize为WAV文件头部长度;
  • SampleRate,即采样率,可选8000、16000、32000、44100或48000;
  • Channels表示声道数量,通常为1或2;
  • BitsPerSample代表单个Sample的位深,可选8、16以及32,其中32位时可以是float类型。

关于RIFF

RIFF,全称Resource Interchange File Format,是一种按照标记区块存储数据的通用文件存储格式,多用于存储音频、视频等多媒体数据。Microsoft在Windows下的WAV、AVI等都是基于RIFF实现的。一个标准的RIFF规范规范文件,最小存储单位为"块"(Chunk),每个块(Chunk)包含以下三个信息:

只有ID为"RIFF"或者"LIST"的块允许拥有子块(SubChunk)。RIFF文件的第一个块的ID必须是"RIFF",也就是说ID为"LIST"的块只能是子块(SubChunk),他们和各个子块形成了复杂的RIFF文件结构。

RIFF数据域的的起始位置四个字节为类型码(Form Type),用于说明数据域的格式,比如WAV文件的类型码为"WAVE"。

"LIST"块的数据域的起始位置也有一个四字节类型码(List Type),用于说明LIST数据域的数据内容。比如,类型码为"INFO"时,其数据域可能包括"ICOP"、"ICRD"块,用于记录文件版权和创建时间信息。

WAV文件格式

WAV格式遵循RIFF规范(Resource Interchange File Format 资源交互文件格式) 。RIFF文件结构可以看作是树状结构,其基本构成是称为"块"(Chunk)的单元,最顶端是一个"RIFF"块,下面的每个块有"类型块标识(可选)","标志符","数据大小"及"数据"等项所组成,块的结构如表1所示:

上面说到的 "类型块标识"只在部分bhunk中用到,如"WAVE"chunk中,这时表示下面嵌套有别的chunk,当使用了 "类型块标识"时,该chunk就没有别的项(如"标志符","数据大小"等),它只作为文件读取时的一个标识,先找到这个 "类型块标识",再以它为起读取它下面嵌套的其他chunk。

非PCM格式的文件会至少多加入一个"fact"块,它用来记录数据解压缩后的大小。(注意是数据而不是文件)这个"fact"块一般加在"data"块的前面。

每个WAV文件由文件头和数据体两大部分组成,数据体的记录方式是小端(little-endian), 以最简单的无损WAV格式文件为例,此时文件的音频数据体为PCM,比较简单,重点在于WAV的文件头。

1 WAV文件头结构

WAV文件是非常简单的一种RIFF文件,它的文件头包含三部分,RIFF,fmt,fact(fact是非必需有的)

cpp 复制代码
typedef __packed struct
{
        ChunkRIFF riff;        //riff块
        ChunkFMT fmt;          //fmt块
        ChunkFACT fact;        //fact块 在线性PCM,没有这个结构体         
        ChunkDATA data;        //data块                 
}__WaveHeader;
1.1. "RIFF"Chunk的内部组织

RIFF块的格式类型(Format)为"WAVE "。RIFF块包含两个子块(Subchunk),这两个子块的ID分别是"fmt "和"data " 。其中"fmt "子块由结构PCMWAVEFORMAT所组成,其子块的大小就是sizeof(PCMWAVEFORMAT),数据组成就是PCMWAVEFORMAT结构中的数据。

cpp 复制代码
typedef __packed struct
{
    u32 ChunkID;                  //chunk id;这里固定为"RIFF",即0X46464952
    u32 ChunkSize ;               //集合大小;文件总大小-8
    u32 Format;                   //格式;WAVE,即0X45564157
}ChunkRIFF ;
1.2. "FMT"Chunk的内部组织
cpp 复制代码
typedef __packed struct
{
    u32 ChunkID;            //chunk id;这里固定为"fmt ",即0X20746D66
    u32 ChunkSize ;         //子集合大小(不包括ID和Size);这里为:20.
    u16 AudioFormat;        //音频格式;0X01,表示线性PCM;0X11表示IMA ADPCM
    u16 NumOfChannels;  //通道数量;1,表示单声道;2,表示双声道;
    u32 SampleRate;     //采样率;0X1F40,表示8Khz
    u32 ByteRate;       //字节速率;
    u16 BlockAlign;     //块对齐(字节);
    u16 BitsPerSample;  //单个采样数据大小;4位ADPCM,设置为4
    u16 ByteExtraData;   //附加的数据字节;2个; 线性PCM,没有这个参数
    u16 sampleperblock;  //一般是一个数据块中的采样数量 如:0x01F9
}ChunkFMT;  

ByteExtraData这个数据重点说一下,在原始线性PCM编码格式,不需要这个参数,只有压缩的PCM编码格式,一般都是按块存储的,需要知道一个块内的采样数量。

在IMA-ADPCM编码格式下的ByteExtraData和sampleperblock, 在data Chunk前面4个字节。

sampleperblock = 0x1F9,表示一个block中有 505个采样点。

1.3. "Fact"Chunk的内部组织
cpp 复制代码
typedef __packed struct
{
    u32 ChunkID;                 //chunk id;这里固定为"fact",即0X74636166;
    u32 ChunkSize ;              //子集合大小(不包括ID和Size);这里为:4.
    u32 NumOfSamples;            //采样的数量;
}ChunkFACT;

"All (compressed) non-PCM formats must have a Fact chunk (Rev. 3documentation). The chunk contains at least one value, the number of samples in the file."

虽然标准协议要求"所有非PCM编码的WAV文件要求有fact块,它用来记录数据解压缩后的大小。(注意是数据而不是文件)"

NumOfSamples是这个chunk中最重要的数据,如果这是某种压缩格式的声音文件,那么长这里可以知道它解压缩后的大小,对于解压时的计算会有很大的好处!

这个"fact"块一般加在"data"块的前面。但实际分析了两个wav文件的文件头,一种PCM编码,一种IMA-ADPCM编码,这两个文件都没有fact块,但是window可以正常播放;说明fact块对这两种类型的wav文件也不是必须的

用GoldWave.exe导出的IMA-ADPCM,"fact"块是放在"data"块的后面

1.4. WAV文件头解析

例如:一个典型的WAV文件头部长度是44字节,包含了采样率,通道数,位深等信息。

例子:channel_1.wav的WAV文件头部

在"data"Chunk中描述音频数据的大小是0x43D4,此信息后的所有字节都是音频数据

第一个音频数据0xFF,起始地址是0x2c

数据结束地址是0x43FF

0x43FF-2C +1 正好是0x43D4, 0x2c是文档头

例子:

As an example, here are the opening 72 bytes of a WAVE file with bytes shown as hexadecimal numbers:

复制代码
52 49 46 46 24 08 00 00 57 41 56 45 66 6d 74 20 10 00 00 00 01 00 02 00 
22 56 00 00 88 58 01 00 04 00 10 00 64 61 74 61 00 08 00 00 00 00 00 00 
24 17 1e f3 3c 13 3c 14 16 f9 18 f9 34 e7 23 a6 3c f2 24 f2 11 ce 1a 0d 

例子:波形文件的文件头,占用44Byte,文件头后为波形数据区

cpp 复制代码
struct FORMAT_WAV
{
    long ChunkID;               //"RIFF"
    long ChunkSize;           //chunk(大块)的数量,wav文件的总大小,单位字节
    long Format;                  //"WAVE"
    long Subchunk1ID;      //"fmt"  第一个chunk的ID
    long Subchunk1Size;   //第一个chunk的Size
    short AudioFormat;       //音频格式
    short NumChannels;   //声道的数量
    long SampleRate;         //采样率
    long ByteRate;               //比特率
    short BlockAlign;           //块对齐
    short BitsPerSample;  //每个采样点的位宽
    long Subchunk2ID;      //"data" 第二个chunk的ID
    long Subchunk2Size;   //第二个chunk的Size,波形数据的大小,单位为字节
}
2. WAV音频数据体(data chuck)结构
2.1. "data"Chunk的内部组织
cpp 复制代码
typedef __packed struct
{
    u32 ChunkID;                   //chunk id;这里固定为"data",即0X64617461
    u32 ChunkSize ;                // 音频数据块大小。(除去WAV头的所有数据)
}ChunkDATA;

"data"chunk的前8个字节存储的是标志符"data"和后接数据大小size(DWORD)

从"data"chunk的第9个字节开始,存储的就是声音信息的数据了,这些数据可能是压缩的,也可能是没有压缩的。

PCM的音频数据是原始数据没有被压缩,因此PCM格式的音频数据是以原始的音频流数据顺序存储下去的。如图所示:(它的基本组织单位是BYTE(8bit)或WORD(16bit)

2.2. 数据块BLOCK结构

在IMA-ADPCM中,"data"chuck中的数据是以block形式来组织的,我把它叫做"段",也就是说在进行压缩时,并不是依次把所有的数据进行压缩保存,而是分段进行的,这样有一个十分重要的好处:那就是在只需要文件中的某一段信息时,可以在解压缩时可以只解所需数据所在的段就行了,没有必要再从文件开始起一个一个地解压缩。这对于处理大文件将有相当的优势。同时,这样也可以保证声音效果。

Block一般是由block header (block头) 和 data 两者组成的。Block在单声道下的定义如下:

cpp 复制代码
//ADPCM压缩的数据块结构
typedef __packed struct
{
    u16 presample;               //block中第一个采样值(未压缩)
    u8 index ;                   //上一个数据块的最后一个 index,第一个block的index=0
    u8 rsv;                      //保留
    u8 dat[sampleperblock-1];    //数据
}DATA_BLOCK;

为了数据存储对齐,方便处理,一般一个音频BLOCK的大小是16的整数倍;如果设置BLOCK大小为256Byte,减去数据块头长度4字节,还剩252字节,4bit表示一个采样的话,可存储共252x2+1=505个采样点(加上数据头里的一个采样值)。

对于PCM编码的WAV文件,只需要按照顺序存储原始采样值即可,不需要分块。

WAV扩展

有一些WAV的头部并不仅仅只有44个字节,比如通过FFmpge编码而来的WAV文件头部信息通常大于44个字节。这是因为根据WAV规范,其头部还支持携带附加信息,所以只按照44个字节的长度去解析WAV头部信息是不一定正确的,还需要考虑附加信息。那么如何知道一个WAV文件头部是否包含附加信息呢?

根据"fmt "子块长度来判断即可。

如果fmt SubChunk Size等于0x10(16),表示头部不包含附加信息,即WAV头部信息长度为44;如果等于0x12(18),则包含附加信息,此时头部信息长度大于44。

当WAV头部包含附加信息时,fmt SubChunk Size长度为18,并且紧随是另一个子块,这个包含了一些自定义的附加信息,接着往下才是"data"子块,格式如下:

如果一个无损WAV文件头部包含了附加信息,那么PCM音频所在的位置就不确定了,但由于附加信息也是一个子块(SubChunk),根据RIFF规范,该子块也必然记录着其长度信息,所以我们还是有办法能够动态计算出其位置,下面是计算步骤:

  1. 判断fmt块长度是否为18。
  2. 如果fmt长度为18,那么必然从0x26位置开始为附加信息块,0x30-0x33位置记录着该子块长度。
  3. 根据步骤2获取的子块长度,假定为N(16进制),那么PCM音频信息开始位置为:0x34 + N + 8。

读取WAV文件的方法

在知道了WAV文件的内部数据组织后,可以直接通过FILE或HFILE来实现文件的读取,但由于WAV文件是以RIFF格式来组织的,所以用多媒体输入输出流来操作将更加方便,可以直接在文件中查找chunk并定位数据。

PCM和IMA-ADPCM编码的WAV实际例子

用GoldWave.exe 从pcm格式导出ima adpcm格式的wav头比较

wav文件头的大小:

PCM格式的wav,它文件头是44byte

ADPCM格式的wav,它的文件头是48byte, 多了4byte的附加数据

相关推荐
Qingniu0119 分钟前
【青牛科技】应用方案 | RTC实时时钟芯片D8563和D1302
科技·单片机·嵌入式硬件·实时音视频·安防·工控·储能
Mortal_hhh2 小时前
VScode的C/C++点击转到定义,不是跳转定义而是跳转声明怎么办?(内附详细做法)
ide·vscode·stm32·编辑器
深圳市青牛科技实业有限公司2 小时前
【青牛科技】应用方案|D2587A高压大电流DC-DC
人工智能·科技·单片机·嵌入式硬件·机器人·安防监控
Mr.谢尔比3 小时前
电赛入门之软件stm32keil+cubemx
stm32·单片机·嵌入式硬件·mcu·信息与通信·信号处理
LightningJie3 小时前
STM32中ARR(自动重装寄存器)为什么要减1
stm32·单片机·嵌入式硬件
鹿屿二向箔3 小时前
STM32外设之SPI的介绍
stm32
西瓜籽@3 小时前
STM32——毕设基于单片机的多功能节能窗控制系统
stm32·单片机·课程设计
远翔调光芯片^138287988726 小时前
远翔升压恒流芯片FP7209X与FP7209M什么区别?做以下应用市场摄影补光灯、便携灯、智能家居(调光)市场、太阳能、车灯、洗墙灯、舞台灯必看!
科技·单片机·智能家居·能源
极客小张7 小时前
基于STM32的智能充电桩:集成RTOS、MQTT与SQLite的先进管理系统设计思路
stm32·单片机·嵌入式硬件·mqtt·sqlite·毕业设计·智能充电桩
m0_739312879 小时前
【STM32】项目实战——OV7725/OV2604摄像头颜色识别检测(开源)
stm32·单片机·嵌入式硬件