Wave文件的格式非常混乱。如果把wave文件的格式比作盆汤,毫无疑问有太多的厨师在完全未经协调的情况下,向这道汤里添加了太多的佐料。Wave文 件的格式规范中,有太多相互独立而且缺乏协调的组织向其中增加内容。结果是wave文件中有很多chunk是在重复别的chunk中的数据,而且通常是用 一种完全不同的方式。下面的讲解中我们尽量把注意力集中于wave文件中那些最经常出现的chunk上。
____________________________
| RIFF WAVE Chunk |
| groupID = 'RIFF' |
| riffType = 'WAVE' |
| __________________ |
| | Format Chunk | |
| | ckID = 'fmt ' | |
| |__________________| |
| __________________ |
| | Sound Data Chunk | |
| | ckID = 'data' | |
| |__________________| |
|__________________________|
采样点和采样帧
解释一个Wave文件内容的过程很大部分围绕在采样点和采样帧这两个概念上。一个采样点是代表了给定声音在给定的时间点上的一个样本值。如果我们假设才用 的是PCM格式的wave文件,并且样点精度大于8bit,每一个采样点被保存为一个占用9-32bit的2的补码表示的数值。样点精度值保存在 Format chunk中的wBitsPerSample属性中。例如一个16bit波形中的一个采样点是一个线性的16bit有符号整数,取值范围- 32768~32767.但是8bit的波形文件中,每一个样本点是一个线性的无符号数,范围0~255。上述带符号和无符号表示方法的差异,又是微软员 工的杰作。
绝大多数cpu的读写指令都是真对8bit的“字节”的,如果一个波形文件的采样点精度不是8的整数倍,它必须被凑到最接近的8的整数倍。1- 8bits/sample的波形文件用8bit保存一个样本,9-16bits/sample的波形文件用16bit保存一个样本……依此类推。
此外,数据比特是左对齐的,多余的填充bit都被设置为0。例如一个12bit的采样点,在wave文件中占用16bits,所以这两个字节的4-15号bit是数据,0-3号bit是填充的0。下图就是一个占据12bit的样点101000010111在内存中的情况:
| 1 0 1 0 0 0 0 1 0 1 1 1 0 0 0 0 |
|___|___|___|___|___|___|___|___|___|___|___|___|___|___|___|___|
<---------------------------------------------> <------------->
12 bit sample point is left justified rightmost
4 bits are
zero padded
应该注意,因为wave格式使用intel体系的little endian字节顺序,所以LSB(Least Significant Byte)在内存中出现在先:
___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
| | | | | | | | | | | | | | | | | |
| 0 1 1 1 0 0 0 0 | | 1 0 1 0 0 0 0 1 |
|___|___|___|___|___|___|___|___| |___|___|___|___|___|___|___|___|
<-------------> <-------------> <----------------------------->
bits 0 to 3 4 pad bits bits 4 to 11
对多声道声音,(例如一个立体声波形文件),来自每一个声道的采样点数据被交织存放。例如,对于一个立体声波形来说,有两个声道的数据。文件中并不是先把 左声道的全部采样点数据放在文件前半部分,然后把右声道的全部采样点数据集中放在文件后半部分。实际的做法是将两个声道的数据“混合”放置,即:
左声道的第一个样点,右声道的第一个样点,左声道的第二个样点,右声道的第二个样点,左声道的第三个样点,右声道的第三个样点……左右声道的样点交替出现,这就是我们所说的“交织数据”。这样,当播放这些数据的时候,应该被同时播放的数据在文件中的位置总是相邻的。
所有需要被同时“播放”(也就是发送到数-模转换器)的样点,总称为一个“采样帧”。对立体声波形来说,每两个采样点构成一个采样帧。如下图所示:
sample sample sample
frame 0 frame 1 frame N
_____ _____ _____ _____ _____ _____
| ch1 | ch2 | ch1 | ch2 | . . . | ch1 | ch2 |
|_____|_____|_____|_____| |_____|_____|
对于一个单声道的波形来说,一个采样帧就由一个采样点构成,无需任何交织。对多声道的波形来说,应该按照下面所示的习惯顺序,来安排一个采样帧中的各个采样点的顺序。下面的每一幅图,展示了一种多声道波形文件中的一个采样帧中,各个采样点出现的习惯顺序。
channels 1 2
_________ _________
stereo | left | right |
|_________|_________|
1 2 3
_________ _________ _________
3 channel | left | right | center |
|_________|_________|_________|
1 2 3 4
_________ _________ _________ _________
| front | front | rear | rear |
quad | left | right | left | right |
|_________|_________|_________|_________|
1 2 3 4
_________ _________ _________ _________
4 channel | left | center | right | surround|
|_________|_________|_________|_________|
1 2 3 4 5 6
_________ _________ _________ _________ _________ _________
| left | left | center | right | right |surround |
6 channel | center | | | center | | |
|_________|_________|_________|_________|_________|_________|
一个采样帧中的各个采样点之间没有填充字节,各个采样帧之间也没有填充字节。注意上面的讨论中我们所说的都是非压缩的data chunk中的数据格式。还有一些data chunk中可能存储压缩过的数据,显然这种情况下需要解压缩,解压缩之后的数据还是严格遵守上述格式的。
Format chunk
Format (fmt) chunk描述了波形文件的基本参数,比如抽样频率,bit精度,声道数量。
#define FormatID 'fmt ' /* chunkID for Format Chunk. 注意这个ID是四个字符,最后的t后面有一个空格! */
typedef struct {
ID chunkID;
long chunkSize;
short wFormatTag;
unsigned short wChannels;
unsigned long dwSamplesPerSec;
unsigned long dwAvgBytesPerSec;
unsigned short wBlockAlign;
unsigned short wBitsPerSample;
/* 根据wFormatTag的不同取值,这里还可能出现更多的fields */
} FormatChunk;
ID总是"fmt "。chunkSize是chunk的长度,单位是字节,这个长度不包括ID和Size两个域所占用的8字节。针对不同的wFormatTag的取值, chunkSize也可能取不同的值。Wave数据可以以非压缩的格式存放,也可以用很多不同的压缩格式存放在data chunk内。压缩格式存放时,每一个采样点占用的字节数可能是各不相同的。wFormatTag指明数据存储时是否经过压缩。如果经过压缩 (wFormatTag的取值不是1),FormatChunk末尾会被追加更多的域,用以说明压缩格式,以便解压缩。这些关于压缩格式的信息,首先是一 个unsigned short类型数据,指明这个unsigned short之后还有多少字节的附加数据。压缩格式的数据还必须包括一个Fact chunk,这个chunk包括了一个unsigned long数据指明数据被解压缩之后包含多少个样点。压缩格式太多了,详细信息参考微软站点。当wFormatTag=1,用的就是非压缩格式。
wChannels:该声音文件中的声道个数。1-单声道,2-立体声,4-四声道声音,等等。多声道情况下的采样点,采样帧及其格式参见上面的叙述。
dwSamplesPerSec:采样帧频率,即每秒播放的采样帧个数。标准的采样帧频率有11025, 22050, 44100 Hz。但是也有其它非标准的采样帧频率被使用。
dwAvgBytesPerSec:每秒钟有多少帧被播放。dwAvgBytesPerSec被音乐播放工具用于估计顺畅播放声音所需的RAM缓冲区大小。这个值应该用下面的公式计算并四舍五入到最接近的整数上:
dwSamplesPerSec * wBlockAlign
wBlockAlign该用下面的公式计算并四舍五入到最接近的整数上:
wChannels * (wBitsPerSample / 8)
这个值是一个采样帧的大小,单位是字节。例如一个16bit立体声声音文件中的一个采样帧大小是4字节。
The wBitsPerSample指定一个采样点的数据精度是多少个bit。
每个wave文件中有且只有一个format chunk。
举个例子说明format chunk。下面是windows标准的开机音乐的format chunk内容:
00000000h: 52 49 46 46 24 6B 07 00 57 41 56 45 66 6D 74 20 ; RIFF$k..WAVEfmt
00000010h: 10 00 00 00 01 00 02 00 22 56 00 00 88 58 01 00 ; ........"V..ˆX..
00000020h: 04 00 10 00 ; ....
从0ch开始是format chunk。
0ch-0fh是四个字节的chunkID: “fmt “。
10h-13h是chunkSize: 16。这个值是刨去chunkID和chunkSize之后format chunk的长度:16字节。
14h-15h是wFormatTag: 0x0001, 非压缩wave音频。
16h-17h是wChannels: 0x0002, 立体声音乐。
18h-1bh是dwSamplesPerSec: 0x5622=22050,即采样帧频率22050Hz.
1ch-1fh是dwAvgBytesPerSec: 0x015888=88200,采样帧频率22050,双声道,采样精度16bits=2bytes(见下面wBitsPerSample的取值),所以 每秒播放的字节数是22050*2*2=88200字节。有dwAvgBytesPerSec = dwSamplesPerSec * wBlockAlign。
20h-21h是wBlockAlign,0x0004,有wBlockAligh = wChannels * (wBitsPerSample / 8)。
22h-23h是wBitsPerSample,0x0010,16bits每样点。
继续说明data chunk的格式。它包含了真正的声音样点数据:
#define DataID 'data' /* chunk ID for data Chunk */
typedef struct {
ID chunkID;
long chunkSize;
unsigned char waveformData[];
} DataChunk;
Data chunk的chunkID总是data。chunkSize是chunk占据的字节数,不计ID和size两个域占用的8个字节,不计为了使chunk占用的字节数为偶数而添加的填充字节。下面的讨论都基于非压缩格式的wave数据。
waveformData数组中就是真正的波形数据。这些数据被划分为成为“采样帧”的单元,前面已经介绍过。从chunkSize属性可以知道波形数据 的长度,以字节为单位。Data chunk中保存的采样帧的个数,可以由data chunk的chunkSize除以format chunk中的wBlockAligh得到。Data chunk是必须有的,每个wave文件中必须有且只有一个data chunk。
波形文件的存储,远远不止这一种形式。且不论那些数不胜数的压缩格式,此外还有一些脑子进水了的人,把数据存储在wave文件中内嵌的IFF List中,这个list会包含多个data chunk和slnt chunk,此时Type ID是wavl。不要在你的程序里支持这种wave文件格式,除非你脑子里也进水了。