第八篇 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的附加数据

相关推荐
redcocal8 小时前
地平线秋招
python·嵌入式硬件·算法·fpga开发·求职招聘
辰哥单片机设计11 小时前
门磁模块详解(防盗感应开关 STM32)
stm32·单片机·嵌入式硬件·传感器
夜间去看海11 小时前
基于51单片机的自动清洗系统(自动洗衣机)
嵌入式硬件·51单片机·proteus·洗衣机
yrx02030712 小时前
stm32 IIC总线busy解决方法
stm32·单片机·嵌入式硬件
YHPsophie13 小时前
ATGM331C-5T杭州中科微BDS/GNSS全星座定位授时模块应用领域
经验分享·笔记·单片机·信息与通信·交通物流
Archie_IT14 小时前
【STM32系统】基于STM32设计的SD卡数据读取与上位机显示系统(SDIO接口驱动、雷龙SD卡)——文末资料下载
arm开发·stm32·单片机·嵌入式硬件
辰哥单片机设计14 小时前
1×4矩阵键盘详解(STM32)
stm32·单片机·嵌入式硬件·矩阵·传感器
wmkswd14 小时前
CAN总线-STM32上CAN外设
stm32·单片机·嵌入式硬件
Ruohongxu14 小时前
LAN8720A-CP-TR-ABC QFN-24 以太网收发器芯片
单片机