WAVE PCM soundfile format
WAV即WAVE,WAVE文件是计算机领域最常用的数字化声音文件格式之一,它是微软专门为Windows系统定义的波形文件格式(Waveform Audio),其扩展名为"*.wav"。
最基本的WAVE文件是PCM(脉冲编码调制)格式的,这种文件直接存储采样的声音数据没有经过任何的压缩,是声卡直接支持的数据格式,要让声卡正确播放其它被压缩的声音数据,就应该先把压缩的数据解压缩成PCM格式,然后再让声卡来播放。
WAVE文件还有很多种有损压缩格式,比如IMA/DVI ADPCM,Microsoft ADPCM,AAC编码等。被压缩的声音数据,需要先解压成PCM格式,才能用声卡来播放
如果是PCM,则为无损格式,文件会比较大,并且大小相对固定,可以使用以下公式计算文件大小。
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是非必需有的)
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结构中的数据。
typedef __packed struct
{
u32 ChunkID; //chunk id;这里固定为"RIFF",即0X46464952
u32 ChunkSize ; //集合大小;文件总大小-8
u32 Format; //格式;WAVE,即0X45564157
}ChunkRIFF ;
1.2. “FMT”Chunk的内部组织
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的内部组织
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文件也不是必须的
例如:一个典型的WAV文件头部长度是44字节,包含了采样率,通道数,位深等信息。
channel_1.wav的WAV文件头部
实际数据长度:0x43D4
数据开始地址是0x2c
数据结束地址是0x43FF
43FF-2C +1 = 43D4
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,文件头后为波形数据区
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的内部组织
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在单声道下的定义如下:
//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规范,该子块也必然记录着其长度信息,所以我们还是有办法能够动态计算出其位置,下面是计算步骤:
- 判断fmt块长度是否为18。
- 如果fmt长度为18,那么必然从0x26位置开始为附加信息块,0x30-0x33位置记录着该子块长度。
- 根据步骤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的附加数据