[原创]桓泽学音频编解码(7):MP3 和 AAC 中huffman解码原理,优化设计与参考代码中实现
1 不同标准中的huffman解码原理
1.1标准MP3的huffman解码原理
在MP3即mpeg-1 audio标准中,无噪声编码模块的输入是一组576个己量化的频谱数据。无噪声编码首先对频谱进行无噪声的动态范围压缩。编码模块最多可以分别对四个模值超过1的系数进行编码,并且在量化系数队列中保留符号位,用来传送符号。
为了使格组量化频谱系数所需的比特数最少,无噪声编码把一组576个量化频谱系数分成3个region,每个region一个霍夫曼码书。由低频到高频分别为big_value区,count1区,rzero区,big_value区一个huffman码字表示2个量化系数,使用32个huffman表,count1区一个huffman码字表示4个量化系数,一共使用了2本码书。Nzero区不用解码。表示余下子带谱线值全为0。
1.2标准MPEG2 AAC的Huffman解码原理
在13818-7即AAC标准中,无噪声编码模块的输入是一组1024个己量化的频谱数据。一共有13个有效的码本,1个用于scalefactor解码,其余的12个用于量化谱线的解码。
霍夫曼编码利用一个霍夫曼码表示4量化系数或2个量化谱线系数。编码时,一共使用了12本码书。利用这些码书,对系数的模值进行霍夫曼编码,而非零系数的符号位添加到码字中去。
为了使格组量化频谱系数所需的比特数最少,无噪声编码把一组1024个量化频谱系数分成一些区(section),每个区由一个或多个比例系数频段组成,使用同一本霍夫曼码书。这样,对比例系数频段的每个分区都必须传输该区的长度以及该区所用码书的序号。
对scalefactor的解码,第一个比例系数代表了量化器的全局步长,称为全局增益值,也就是公共比例因子。它被编码为一个8位无符号整数PCM值,全局增益的动态范围足以表示一个24位PCM音源的所有值。后面所有的比例因子都利用一个特殊的霍夫曼码书(对第一个比例系数,则与全局增益)进行差分编码。为了提高压缩比,系统不传输系数值全为零的比例系数频段的比例系数。
标准区别 |
对SCF |
码本组织 |
码本个数 |
MP3 |
查分定长编码 |
低频2个系数一组,中频4个系数一组 |
有效码本8个 |
AAC |
Huffman编码 |
低频4个系数一组,中频2个系数一组 |
有效码本10个 |
2 Huffman的解码优化设计
Huffman的解码实现主要有以下几种:
- 线性搜索法
线性搜索法按码字非减的顺序间码本排成一个表,每次读取一个比特,然后看排序的表中是否有完全匹配,如有则找到索引,没有则继续寻找。它的优点是所用的表比较小,但是搜索较长码表的时候所需的时间太长,且不易扩展.
- 二叉树搜索法
二叉树搜索法要根据码表建立一个二叉树,叶节点表示相应的索引,左右子树分别用1 ,0表示,如图3-10(b)所示。进行搜索时,每次读入一个比特,当读入的值为1时进入左子树,为0时进入右子树,直到找到叶子节点。
- 直接查表法
直接查表法就是根据码字逆向建表,解码时每次读入码表中码字的最大长度个比特,查表后便可找到相应的索引。这种算法只需一次查表即可完成,是所有算法中速度最快的,但是因为需要建立庞大的码表而变得不可取。
- 分步搜索法等。
分步查表法避免了直接查表法中占用内存大的缺点,它灵活地把查表分为几次完成。这样就需要建几个表,前一个表相当于后一个表的索引,最后的表记录了相应码字的索引,如图3-10(a)所示的是两步查表法。它实际上是二叉树搜索法与直接查表法的折衷,当各个表的位宽为1时就是二叉树搜索法,当位宽为最长码长的长度时就变成直接查表法。所以分布查表法是各种解码方法中最灵活的,可以根据不同的应用限制制定相应的表的组织形式。
3 huffman解码算法模块在不同参考软件中的实现方法
Mp3
参考软件1:11172-5_1998(E)_Software_Simulation
顶层函数
III_hufman_decode
子函数1
initialize_huffman
从文件中读入huffman表
子函数2
huffman_decoder,解码big value和count1
解码一个码字的算法
/* 查找huffman树的方法. */
do {
if (h->val[point][0]==0) { /*end of tree*/
*x = h->val[point][1] >> 4;
*y = h->val[point][1] & 0xf;
error = 0;
break;
}
if (hget1bit()) {
while (h->val[point][1] >= MXOFF) point += h->val[point][1];
point += h->val[point][1];
}
else {
while (h->val[point][0] >= MXOFF) point += h->val[point][0];
point += h->val[point][0];
}
level >>= 1;
} while (level || (point < ht->treelen) );
参考软件2:libmp3dec
Libmp3dec的解码函数是从ffmpeg中提取出来,原意是想适应多个标准的huffman解码,所以书写比较复杂,但是效率较高.
有初始化表
init_vlc->调用build_table建立huffman表,其目的是通过建立huffman表减少huffman表在rom中的存储空间,一个huffman表最少有3个部分组成,码字,数据,码长.但是libmp3dec的方法省略了数据的存储,静态表只有码字和码长,这样的好处是省略了大量的数据的rom存储空间,而改用ram存储,这样多个标准存储的情况换成了1个标准执行存储ram的情况.尤其对音频这种组码的情况更加有效.
解码顶层
huffman_decode中
if (code_table) {
code = get_vlc(&s->gb, vlc);
if (code < 0)
return -1;
y = code_table[code];
x = y >> 4;
y = y & 0x0f;
} else {
x = 0;
y = 0;
}
这段代码用来解码一个码字.其中调用函数get_vlc解码, get_vlc函数设计的十分巧妙.但是这种巧妙主要用于多标准适应(可能包括视频),是来自ffmpeg的一个函数.里边调用一个关键的宏定义GET_VLC解码一个码字
#define GET_VLC(code, name, gb, table, bits, max_depth)\
{\
int n, index, nb_bits;\
\
index= SHOW_UBITS(name, gb, bits);\ //bits=8(一般的情况下),step1,提取8位码流
code = table[index][0];\ //查找表
n = table[index][1];\ //
\
if(max_depth > 1 && n < 0){\ //n<0表示没找到, max_depth表示最大查找步长
LAST_SKIP_BITS(name, gb, bits)\
UPDATE_CACHE(name, gb)\
\
nb_bits = -n;\
\
index= SHOW_UBITS(name, gb, nb_bits) + code;\ //再次取数据
code = table[index][0];\
n = table[index][1];\
if(max_depth > 2 && n < 0){\ //还没找到,再查
LAST_SKIP_BITS(name, gb, nb_bits)\
UPDATE_CACHE(name, gb)\
\
nb_bits = -n;\
\
index= SHOW_UBITS(name, gb, nb_bits) + code;\
code = table[index][0];\
n = table[index][1];\
}\
}\
SKIP_BITS(name, gb, n)\
}
三步法完成查找.
参考软件3 Melo
使用与faad相似的2步法完成查表。
AAC
参考软件1: 13818-5_2005_Reference_Software
也是用分布查找法,没有init表,表是静态全局变量.
与mp3不同的是,scalefactor也是用的huffman解码,mp3中scalefactor不是的.
程序中是按如下定义的
顶层huffdecode
核心子函数1
get_ics_info
核心子函数2
Getics
Getics中调用huffcb解码section data
Getics中调用hufffac解码scale factor data
Getics中调用huffspec解码量化谱线系数
Hufffac和huffspec都调用了函数decode_huff_cw解码一个码字
其函数体是
i = h->len;
cw = getbits(i);
while (cw != h->cw) {
h++;
j = h->len-i;
i += j;
cw <<= j;
cw |= getbits(j);
}
Step1:解码huffman表中的index值.
下面是13818 -7 ISO官方参考代码中解码一个huffman码字的函数的流程图与注释:
int decode_huff_cw(Huffman *h)函数的流程图与注释
Step2:实现index值到量化谱线的映射
标准中的伪码
unsigned = 数组unsigned_cb[i]的布尔值, 见表2的第二列.
当unsigned=0时表示有符号数, unsigned=1表示无符号数.
dim = 码本的维数, 见表2的第三列.
lav = LAV,最大可编码量化谱线系数的绝对值, 见表2的第四列..
idx = 码字索引
if (unsigned) {
mod = lav + 1;
off = 0;
}
else {
mod = 2*lav + 1;
off = lav;
}
if (dim == 4) {
w = INT(idx/(mod*mod*mod)) - off;
idx -= (w+off)*(mod*mod*mod)
x = INT(idx/(mod*mod)) - off;
idx -= (x+off)*(mod*mod)
y = INT(idx/mod) - off;
idx -= (y+off)*mod
z = idx - off;
}
else {
y = INT(idx/mod) - off;
idx -= (y+off)*mod
z = idx - off;
}
注:这里的实际公式是
4-tuple : Idx = (w+off)*mod³ + (x+off)*mod² + (y+off)*mod + z + off
2-tuple: Idx = (y+off)*mod + z + off
之所以采取分组的整体huffman编码的方法是为了进一步压缩帧内的相关性节省码字.其实在编码端或或是解码端完全可以直接选用(w,x,y,z)制表,查表,但这样的查阅不便,加入了index的做法使思路清晰.但也给我们提出了优化的方向.
ISO参考代码中的unpack_idx函数(注:程序初始化阶段就计算出每个码本的mod和off值)
if(dim == 4){
qp[0] = (idx/(mod*mod*mod)) - off;
idx -= (qp[0] + off)*(mod*mod*mod);
qp[1] = (idx/(mod*mod)) - off;
idx -= (qp[1] + off)*(mod*mod);
qp[2] = (idx/(mod)) - off;
idx -= (qp[2] + off)*(mod);
qp[3] = (idx) - off;
}
else {
qp[0] = (idx/(mod)) - off;
idx -= (qp[0] + off)*(mod);
qp[1] = (idx) - off;
}
Step3:获取符号位
标准中伪码
if (y != 0)
if (one_sign_bit == 1)
y = -y ;
if (z != 0)
if (one_sign_bit == 1)
z = -z;
ISO参考代码
q:刚进行huffman解码的量化谱线数据
n:码本的维数
void get_sign_bits(int *q, int n)
{
while (n) {
if (*q) {
if (getbits(1)) { //1表示为负
*q = -*q;
}
}
n--;q++;
}
}
-----scale_factor_data部分的解码
输入:scale_factor_data部分码流
输出:差分scalefactor数据或差分intensity位置数据
Step1:
解码scalefactor差分值, 同解码量化谱线系数中的step1.index既是差分值.其他步骤参见scalefactor章节.
解码intensity位置的差分值, 同解码量化谱线系数中的step1.index既是差分值. 其他步骤参见intensity章节.
11.3 C参考代码:
Step1:
获得global_gain
Getics函数
*global_gain = getbits(LEN_SCL_PCM);
计算sf[g][sfb]
Hufffac函数
fac = global_gain;
t = decode_huff_cw(hcw);
fac += t - MIDFAC; /* 1.5 dB */
if(fac >= 2*maxfac || fac < 0)
return 0;
factors[i] = fac;
Step2 和 step3
来自huffspec函数
{
int sbk, nsbk, sfb, nsfb, fac, top;
Float *fp, scale;
i = 0;
fp = coef;
nsbk = info->nsbk;
for (sbk=0; sbk<nsbk; sbk++) {
nsfb = info->sfb_per_sbk[sbk];
k=0;
for (sfb=0; sfb<nsfb; sfb++) {
top = info->sbk_sfb_top[sbk][sfb];
fac = factors[i++]-SF_OFFSET;
//注释:小于TEXP的使用查找表
if (fac >= 0 && fac < TEXP) {
scale = exptable[fac];
}
else {
if (fac == -SF_OFFSET) {
scale = 0;
}
else {
scale = pow( 2.0, 0.25*fac );
}
}
for ( ; k<top; k++) {
*fp++ *= scale;
}
}
}
}
参考软件2:faad
Faad中调用huffman_scale_factor解码scalefactor数据,使用的是二叉树法
while (hcb_sf[offset][1])
{
uint8_t b = faad_get1bit(ld
DEBUGVAR(1,255,"huffman_scale_factor()"));
offset += hcb_sf[offset][b];
if (offset > 240)
{
// stop_huffman_timer();
/* printf("ERROR: offset into hcb_sf = %d >240!\n", offset); */
return -1;
}
}
调用huffman_spectral_data函数解码谱线数据使用的是2步法解码
总结
|
AAC |
Mp3 |
|||
|
官方 |
Faad or melo |
官方 |
Libmp3dec |
Lame |
解码谱线 |
分步查表 |
码字宽度小于12的用2步查表 码字宽度大于等于12的用二叉树 |
二叉树 |
3步查表 |
二叉树 |
解码scalefactor |
分步查表 |
二叉树 |
|
|
|
Huffman表状态 |
静态表 |
静态表 |
静态表 |
动态表 |
静态表 |
参考文献
【1】 基于risc的mpeg-4 aac编解码研究
【2】 略