Microsoft ADPCM 编码解码算法
http://tech.cuit.edu.cn/forum/redirect.php?tid=4054&goto=lastpost
因为种种原因,最近需要把原始的wav文件压缩成ADPCM格式。但是网上几乎搜不到相关的中文资料。花了相当长的时间,七拼八凑的从一些文章中得到了些信息,终于搞定了它。为了方便遇到跟我一样麻烦的人,我决定把它详细的写下来。 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. WAV文件的格式 IMA-ADPCM压缩的音频文件并没有一个统一的格式。我们现在只考虑微软的自己的wav格式。apple公司的网站上有一篇写得很不错的technical note, 可以看后面的链接地址。 wav文件是微软定义的一系列资源文件中的一个。这些文件通常是由一系列的chunk组成。所有的文件都以RIFF标记开头,然后指出文件的大小。接着表明类型,比如WAVE,MIDI等. 一个wav文件的结构大致如下 __________________________ | RIFF WAVE Chunk | | groupID = 'RIFF' | | riffType = 'WAVE' | | __________________ | | | Format Chunk | | | | ckID = 'fmt ' | | | |__________________| | | __________________ | | | Sound Data Chunk | | | | ckID = 'data' | | | |__________________| | |__________________________| 一般每一个chunk都有一个id和一个size来表明chunk的类型和大小,这样就可以很容易的将chunk读出。id一般是四个字节,用ASCII 码的值来标记,比如data chunk的id就是'data',而format chunk的id就是'fmt '.要注意的是,size表明的大小是以字节为单位的,而且不包括id和size字段本身所占的空间。 原始的wave文件一般只有两个chunk,也就是fmt和data,原则上,你可以添加任何的其他chunk,用来添加不同的信息.这也是wav文件出现很多变种的原因. windows下一般有两种格式的wav文件,一种是普通未压缩的原始数据,另一种就是采用了ADPCM压缩了的。无论哪一种,你都可以使用现存的播放器播放。 未压缩的存储格式比较简单,只需要一个format chunk来描述格式信息,然后再用一个data chunk来存储数据。 现在的wave文件按规定都必须包括一个fact chunk,而且在fmt chunk里必须包含一个扩展了的format description,这通过一个WAVEFORMATEX的结构来描述,但是以WAVE_FORMAT_PCM为格式的文件并不需要这些额外的信息。 下面是WAVEFORMATEX的定义 typedef struct waveformat_extended_tag { WORD wFormatTag; /* format type */ WORD nChannels; /* number of channels (i.e. mono, stereo...) */ DWORD nSamplesPerSec; /* sample rate */ DWORD nAvgBytesPerSec; /* for buffer estimation */ WORD nBlockAlign; /* block size of data */ WORD wBitsPerSample; /* Number of bits per sample of mono data */ WORD cbSize; /* The count in bytes of the extra size */ } WAVEFORMATEX; 其中wFormatTag标明了文件的类型,这样我们就可以判断后面的数据是以什么方式来存储和表示的了,比如,#define WAVE_FORMAT_PCM 0x0001 这样,WAVE_FORMAT_PCM表示的就是普通的原始wav文件格式。再比如,#define WAVE_FORMAT_ADPCM 0x0002 我们就可以知道,wFormatTag的值为WAVE_FORMAT_ADPCM就是以ADPCM压缩的格式了。具体的文件的类型有很多种,详细的定义可以在mmreg.h头文件里找到。 WAVEFORMATEX是所有的format所共有的一个头部,不同格式会在后面添加自己的相关的数据段,添加的段的字节数可以通过cbSize来得到。 cbSize用来描述不同的格式后面添加的多余的字节数。比如WAVE_FORMAT_ADPCM格式的format的这个值就是32,表明还有32个字节在后面。 另外一些字段会在后面解释。 fact chunk是一个值得注意的chunk,现在的wav文件,无论是否压缩,都必须包含一个fact chunk,用来存储文件文件相关的信息。但是现在它里面只定义了一个字段,用来指出文件里一个有多少个sample。 4. 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 5. 编码和解码 首先要说明的是,我们通过预测之后的到的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/ 后面附带上我写的一个对8000采样率,单声道16位的wav格式压缩成ADPCM的代码
|