三、ADPCM WAVE编码
我们知道,计算机的声卡相当于一个数字模拟信号的转换器。MIC先把声音转换为模拟电平信号,经过声卡的A/D转换后,变为数字电平信号。这个过程中,声卡需要对模拟信号进行采样,每秒采样的次数为采样频率。采样后得到的值进行量化,量化后对数据编码,编码过程有各种不同的算法,有些算法可以压缩数据。
WAVE文件结构里,”fmt” subchunk中有一个参数AudioFormat,大小是2字节。这个参数表示WAVE文件数据部分采用的编码方式。通常采用的PCM(Pulse Code Modulation脉冲编码调制),对应的AudioFormat参数值为0x0001;增量脉冲编码调制ADPCM,对应的AudioFormat参数值为0x0002;还有A-law,u-law编码等。可以参考连接:http://www.moon-soft.com/program/FORMAT/windows/wavec.htm。
一下内容参考链接:http://topic.youkuaiyun.com/u/20080407/15/aa98f445-9376-42a4-9d18-8181b206c6f0.html
1. 关于DPCM
DPCM是differential pulse code modulation的缩写,也就是差分脉冲编码调制的意思。他的主要思想是通过已知的数据预测下一个数据,然后传递预测值与实际值之间的差值。具体的细节可以在很多信号处理相关的书上找到。
一般的DPCM编码器都是采用的线性预测。假设传递的数据是X1,X2,...Xn,而下一个数据,Xn+1还是未知。可以通过前面的X1,X2,...Xn的加权和来预测Xn+1,也就是
Xn+1 = ∑(Ai*Xi),其中i属于1...n
为了简化计算,大部分编码的实现只取前两项,也就是,Xn+1 = a*Xn + b*Xn-1, 现在,最主要的事情就是如何对a,b进行取值,才能使得Xn+1的误差最小。
如果假设 x~i 是预测值,xi是实际值,那么,∑(x~i-xi)^2 最小的时候,a,b就是最优的。设 F=∑(X~i-Xi)^2,因为 X~i = a*X~i-1 + b*X~i-2,可以得出,F是关于a,b的二元函数.也就是 F=f(a,b) 。可以分别对a和b求偏导数,求出它的极值点。
f <sub>a </sub>(a,b) = 0 ;
f <sub>b </sub>(a,b) = 0 ;
可以得到
a * ∑(Xi-1)^2 + b * ∑(Xi-1)*(Xi-2) = ∑Xi*Xi-1
a * ∑(Xi-1)*(Xi-2) + b * ∑(Xi-2)^2 = ∑Xi*Xi-2
如果设
alpha = ∑(Xi-1)^2
beta = ∑(Xi-1)*(Xi-2)
gama = ∑(Xi-2)^2
m = ∑Xi*Xi-1
n = ∑Xi*Xi-2
上面的式子就可以写成
a*alpha + b*beta = m
a*beta + b*gama = n
算出alpha,beta,gama,m,n以后,a和b的值就可以计算出来了,实际上我们只需要一个循环遍历前n个数就能把它们都求出来。
2. ADPCM的思想
如果直接使用DPCM进行编码的话,是得不到什么压缩的效率的。缘故是,需要传输或保存的是预测值后的值与实际值之间的差值,这与原来的数据占用同样的空间。
为了满足我们原始的压缩数据的动机,你可以对这些差值进行各种各样的编码。因为,大部分情况下,差值都是像1,1,1,2,3,5,5,5之类的数。可以对它们进行通常的游程编码或者huffman编码,运气好的话能够得到很大的压缩比。
这样做会有一个很大的弊端。因为有些数据可能之间的联系会呈线性或者某种连续函数的性质。但是大部分情况下,数据的分布还是有一定的离散性的。当数据之间出现很大的跳跃的时候,这种方法就显得很苍白无力了。
我们可以这么做,每次对得到的差值用一个随着差值大小变化的数来除。这样就可以随着差值的变化,不断调整比例因子。这样出现较大的跳跃时也能把我们要存储的差值限定在一个较小的范围之内。
如果你现在有些迷惑,没事,我们换种方式来说明一下。
假设差值是 diff,也就是 diff = X~i - Xi,那么,diff就有可能变动很大,如果引入一个不断变化的因子iDelta,那么,diff' = diff / iDelta,而对于iDelta,每当diff变大的时候,他就变大比较大,当diff变得比较小的时候,他就相应的减小。这样,我们的diff'就能保持相对的稳定了。通过iDelta的引入,可以使得我们的DPCM编码自动的适应数据间大幅度的跳跃。这就是自适应脉冲编码调制,ADPCM的主要思想。
你现在可能会想,iDelta到底怎么变化,才能自动的匹配diff的变化? 一种可行的方法就是,把它定义为diff的一个函数,这个函数根据不同的diff的值的大小取不同大小的值。通常我们会做一个iDelta值的表,通过diff作为索引,这样,就可以根据不同的diff值,iDelta就可以作相应的变化了。
3. WAVE_FORMAT_ADPCM 的wav文件格式
再回到我们前面讨论的DPCM,对于预测后得到的差值diff,我们应该怎么处理它才能的比较好的压缩比?比如我们的数据是3,3,4,7,9,2...可以注意到,如果预测做的比较好的话,得到的差值可能会很小,甚至为0,假设我们的原始的音频数据时16位的,那么,如果仍然使用16位来存储这些数据,肯定是一种浪费。很直观的,你会想到减少每一个diff的存储空间,没错,这就是ADPCM格式压缩的wav文件采用的方法。
在压缩过的wav文件里,每一个diff使用4个bit来存储的,被称作一个nibble。这样,我们将一个16bit的原始数据缩减到4bit,可以得到一个稳定的4:1的压缩比。
因为我们采用的是预测编码,这就需要选择预测的系数a和b,我们在前面已经详细的推导过了a和b的计算方法。现在,我们需要解决的是,一个wav文件,我们是对整个文件计算来得出合理的a和b吗?显然,对整个文件采用相同预测系数并不实际,首先是计算起来麻烦,再一个,一个wav文件太长,如果对整个文件采用相同的a和b起不到什么效果,对于局部的数据,偏差仍然会是很大,这就丧失了我们的初衷。
不妨这么考虑,我们将音频数据分成不同的块,分别对每一块求不同的系数,来进行预测编码,一般声音都是连续的,在一个局部的小区域里变化很小,所以a和b就能很准确的预测出每一个值了。
在apple下系统下,每一个块称作一个packet,以64个sample为固定的大小。而在windows下被称作一个block,他的大小是可变的,所包含的sample的个数由nSamplesPerBlock来指出,后面我们会看到. 在WAVEFORMATEX结构里的nBlockAlign描述了每一个block所占的字节数.
不同的采样率会有不同的大小,下面是blockAlign在不同采样率大小下的值
nSamplesPerSec x Channels nBlockAlign
8k 256
11k 256
22k 512
44k 1024
这样,我们可以把压缩后的数据以block为单位存储在data chunk里.但是,除了压缩的数据以外,我们同样还需要存储当前块的a和b系数的值.
下面是ADPCM格式的format chunk的具体定义
typedef struct adpcmcoef_tag {
int iCoef1;
int iCoef2;
} ADPCMCOEFSET;
typedef struct adpcmwaveformat_tag {
WAVEFORMATEX wfxx;
WORD wSamplesPerBlock;
WORD wNumCoef;
ADPCMCOEFSET aCoeff[wNumCoef];
} ADPCMWAVEFORMAT;
这下子就很清楚了,ADPCMCOEFSET里的两个正是我们的系数a和b,ADPCMWAVEFORMAT扩展了WAVEFORMATEX的结构,添加了一些列自有的信息,wSamplesPerBlock表明了每一个block里含有多少个sample,而wNumCoef说明了后面的系数表的大小.
微软定义了一个7个的标准的系数表,当然,你也可以在后面添加自己的系数,下面是他的定义:
Coef Set Coef1 Coef2
0 256 0
1 512 -256
2 0 0
3 192 64
4 240 0
5 460 -208
6 392 -232
每一个coef是使用定点数来表示的,这样是为了方便存储和加快编解码的速度,使用的时候需要把它除以256得到实际的值
可以通过我们前面的方法算出来a和b,然后再在这里找出和a和b最近的一组数. 因为通常情况下,音频文件的数据量是相当大的,如果对每个样本都算出最优的a和b,他们的存储就会占相当大的空间, 如果我们只用这七个,可以把他们放在文件头里的一个表里面,然后对每一个block只需要存储他们最近的系数在表里的索引值就行了. ADPCMWAVEFORMAT里的aCoeff存储的就是这个系数表.
ADPCM压缩格式存储的wav文件,一般包含三个chunk,一个是format chunk,就是我们上面的那个ADPCMWAVEFORMAT结构. 接着会有一个fact chunk,这是必不可少的,现在它里面只有一个字段,就是整个文件含有的sample的个数.
接下来是最重要的data chunk, 它里面存储的就是我们的数据,里面是一个block接一个block存储的。
一般block由三部分组成,分别是header,data,padding。
header的定义:
typedef struct adpcmblockheader_tag {
BYTE bPredictor[nChannels];
int iDelta[nChannels];
int iSamp1[nChannels];
int iSamp2[nChannels];
} ADPCMBLOCKHEADER;
bPredictor就是我们前面说过的系数表的索引值。如果文件的channel不止一个,那么不同的channel就挨个向后排。
channels 1 2
_________ _________
| left | right |
stereo | | |
|_________|_________|
1 2 3
_________ _________ _________
| left | right | center |
3 channel | | | |
|_________|_________|_________|
1 2 3 4
_________ _________ _________ _________
| front | front | rear | rear |
quad | left | right | left | right |
|_________|_________|_________|_________|
1 2 3 4
_________ _________ _________ _________
| left | center | right | surround|
4 channel | | | | |
|_________|_________|_________|_________|
1 2 3 4 5 6
_________ _________ _________ _________ _________ _________
| left | left | center | right | right |surround |
6 channel | center | | | center | | |
|_________|_________|_________|_________|_________|_________|
ADPCMBLOCKHEADER结构里的iDelta是iDelta的初始值。iSample1和iSample2是用来预测后面数据的初始的两个sample value。需要注意的是,iSample1实际上是第二个sample的值,而iSample2则是第一个。这样做是为了方便编解码。
header后面紧接着就是数据了。data里面是4bit接4bit的sample数据。
如果存储的数据没有到整整一个block,那么就要在最后存一系列的0来使整体大小填满整个block,这被叫做padding
4. 编码和解码
首先要说明的是,我们通过预测之后的到的diff,需要再除上一个iDelta的比例因子,得到的值就是我们要存储的结果。这个值一般被称作iErrorDelta,他一般是一个可正可负的有符号数。因为我们需要把这个iErrorDelta存储到一个nibble里,需要对他作一定的裁剪,因为一个nibble是4bit的,如果用补码表示,他的范围从-8到7,很明显,我们的iErrorDelta只要大于7或者小于-8就都得用7或-8来表示,这样就丧失了一些精度。不过这些损失是很少的,因为如果我们的预测和iDelta比较好的话,大部分的iErrorDelta都会落在-8到7之间。即使丧失一点点音质,人的耳朵基本上听不出来,可以忽略它的影响。
每次产生新的iErrorDelta之后,都要根据iErrorDelta的值对iDelta的值进行更改。以使得iDelta能够自动适应sample的变化。因为一个iErrorDelta被限定到4bit,我们就完全可以做一个16项的表来表示它。下面的这个就是microsoft的标准的adaptation table
int AdaptationTable [] = {
230, 230, 230, 230, 307, 409, 512, 614,
768, 614, 512, 409, 307, 230, 230, 230
} ;
好了,我们现在已经把一切前提工作都做完了,下面就是具体的编码及解码步骤了,因为编解码对于每一个block都是相同的,我们仅仅使用一个block来说明。
编码:
对每一个block,编码的过程是通过下面的几个步骤进行的
确定需要使用的系数predictor
确定初始的iDelta值
输出block header
编码并输出数据
其中predictor的确定已经在开头部分详细的描写了。我们只要找出与a和b最相近的系数表的索引值就行了。
初始的idelta的值确定有很多种,你可以对每一个block都使用相同的iDelta值。也可以通过开始的几个数据来估计iDelta,比如,通过算出第一个sample的预测值跟实际值之间的diff,可以使用diff/4来表示iDelta,这样iErrorDelta的值就能刚好被限制在-8到7的比较靠中间的位置
每一个block的初始iDelta的值也可以使用前一个block的最后一个iDelta的值来确定。不过需要注意,第一个block的初始iDelta的值需要单独考虑。
当predictor和初始的iDelta确定之后,block header就可以写出了。
首先将每一个channel的predictor输出
再将每一个channel的iDelta输出
接着将16bit的第二个sample的PCM值输出(iSample1)
最后将16bit的第一个sample的PCM值输出(iSample2)
下面就是对剩下的数据编码了。
如果block里还有sample尚待编码
// 通过前两个sample预测下一个sample的值
lPredSamp = ((iSamp1 * iCoef1) + (iSamp2 *iCoef2)) / 256
// 计算iErrorDelta
iErrorDelta = (Sample(n) - lPredSamp) / iDelta
如果iErrorDelta大于7,把它设成7
如果iErrorDelta小于-8,把它设成-8
把iErrorDelta作为一个nibble输出
//算出使用iDelta和iErrorDelta预测得到的新的sample的值
lNewSamp = lPredSample + iDelta * iErrorDelta
把lNewSamp限定到16bit所允许的大小
//调整iDelta的值
iDelta = iDelta * AdaptionTable[ iErrorDelta] / 256
把iDelta限定到16bit所允许大小的范围内,并确保它不为0
//更新预测用的两个sample的值
iSamp2 = iSamp1;
iSamp1 = lNewSample.
重复上面的过程,直到block里没有需要再编码的sample
解码实际上就是上述过程的逆过程。如果你把上边的编码过程搞明白了,解码的过程就很简单,这里就不详细说了。
上面就是它的整个过程,如果你还有什么不明白,可以给我发邮件: mawenping@gmail.com
下面是相关的一些链接,可以参考一下。
Understanding The Differences Between Apple And Windows IMA-ADPCM Compressed Sound Files:
http://developer.apple.com/technotes/tn/tn1081.html
ADPCM reference implementation:
ftp://ftp.cwi.nl/pub/audio/adpcm.tar.gz
这个描述的比较详细一点:
http://www.moon-soft.com/program/FORMAT/windows/wavec.htm
libsndfile: http://www.zip.com.au/~erikd/libsndfile/
SoX Sound eXchange: http://sox.sourceforge.net
WAVE File Format:
http://www.borg.com/~jglatt/tech/wave.htm
http://ccrma.stanford.edu/courses/422/projects/WaveFormat/