北京理工大学 20981 陈罡
原文:http://www.61ic.com/Services/Course/C6000/200912/24768.html
随着互联网络的发展,基于网络的多媒体通信越来越引起人们的关注,多媒体通信的基础是语音通信,为此国际电信联盟电信组(ITU-T)创立了G.711,G.723,G.729等多个语音编码的标准,其中G.729及其附录A以其较低的编解码复杂度,较高的语音质量和很低的编解码延时获得了人们的青睐。
多媒体终端是通信网络的重要组成部分,一个多媒体通信终端需要融合系统、传输、图像、语音、数据等多种功能,这就要求其核心处理器具有强大的处理能力。
1、语音编码原理和G.729A编解码器
语音编码算法从上个世纪七八十年代兴起,已经发展得相当成熟和完善。语音编码一般分为三种类型:波形编码,参数编码和波形参数混合编码。波形编码是以逼近声音波形为目标的,其代表的算法有G.711,它的声音清晰度好,语音的自然度高,但是压缩效率比较差,常在32kbps以上。参数编码是把人的声道抽象成一个发声模型,对这个模型的参数进行编码,特点是语音压缩效率高,但是自然度比较差,可以在极低速率进行编码。波形参数混合编码结合了以上两者的优点,代表算法有G.723,G.729等,能在4-16kbps的速率上进行高质量的语音合成。
G.729采用的是共轭结构的代数码激励线性预测算法(Conjugate Structure Algebraic Code Excited Linear Prediction,CS-ACELP),这是一种基于CELP编码模型的算法。由于G.729编码器能够实现很高的语音质量(MOS分4.1)和很低的算法延时,被广泛地应用于数据通信的各个领域,如IP Phone和H.323系统等。G.729是对8KHz采样16bit量化的线性PCM语音信号进行编码,压缩后数据速率为8Kbps,具有16:1的高压缩率。
CS-ACELP编码器是以码本激励线性预测编码(CELP)模型为基础的。编码器对10ms长的语音帧进行处理,逐帧地提取CELP模型参数(LP滤波器系数、自适应码本和固定码本索引和增益),然后对这些参数进行编码和传输。编码参数的比特分配显示在表1中。
500)this.width=500;" border=0>
表1 G.729码字分配表
在解码器端,这些参数用来恢复激励信号和合成滤波器参数。重建语音是使激励信号通过线性预测(LP)滤波器滤波后得到的,如图1所示。其中,短时合成滤波器是一个10阶线性预测滤波器,而长时合成滤波器,或称基音合成滤波器,是利用自适应码本的方法来实现的。计算出合成语音后,还要进行后滤波以提高语音质量。
500)this.width=500;" border=0>
图1 G729解码器示意图
G.729以10ms为一帧,编码时需要前一子帧作为参考帧,所以编码延时只有15ms,特别适用于实时通信系统。
G.729A是G.729的简化版本,两者的编解码结构相同,码流可以互通。
主要的简化有:
感觉加权滤波器采用量化的LP滤波系数,
简化了自适应和固定码本的搜索,在解码器中只使用整数延迟,
简化了谐波后滤波器。
经过这些简化,G.729A的运算量比G.729减少约一半,而语音质量只有很小的下降(MOS分3.9),因此在实际的工程应用中一般都采用G.729A。
2、ITU-T G.729A Speech Coder使用方法
对于G.729A语音编码算法有了一些了解以后,作为工程师来说最关心的还是如何使用这个编码器。目前采用标准c语言编写的G.729A语音编码器是可以下载到源代码的,有兴趣的朋友只要用"ITU-T G.729A 语音编码"作为关键字google一下,就可以找到很多关于它的代码。这里需要说明的是,网上下载的G.729A是无法直接用于工程项目的,首先就是运行效率,由于是itu-t作为原理性阐述的代码发布,它的c语言代码是以大家能够看懂为目的设计的,而不是考虑运行效率和速度;其次,itu-t的speech codec在编码完毕后,进行了“串行化”的操作。
我仔细研究ITUG729代码后发现:
编码器的输出及解码器的输入不是编码生成的参数向量,而是经过1bit->Word16(即2字节)转换的bit流
,从而使得编码器输出数据不是原始PCM的1/16(理论上g.729编码和wav的文件的大小比例约为1:16),而实际上,我们采用它的coder编码出来的文件由于上述“串行化”的原因和原始wav几乎一样大,而且甚至比原始的wav还要大个十几K。
ITU-T为了在标准化方针中进行丢帧隐藏测试,对语音编解码器参考软件的码流格式一般要求为ITU-T G.192中规定的格式,即用16位的0x007F表示1个比特'0',用0x0081表示1个比特'1',每个帧头会有同步字和包的长度。对于同步字,0x6B20表示该帧为坏帧,0x6B21表示该帧为好帧。这样固然很好,但是。。。导致了编码后数据的增大。
那么如何来解决上述问题呢?解决的方法就是——去掉串行化代码,或者重新编写串行化代码。
我们打开bits.c,就可以看到里面定义的如下4个函数:
static void int2bin(int value, int no_of_bits, INT16 *bitstream);
static int bin2int(int no_of_bits, INT16 *bitstream);
void prm2bits_ld8k(int prm[], INT16 bits[]) ;
void bits2prm_ld8k(INT16 bits[], int prm[]) ;
这个文件就是编码后文件大小没有什么变化的关键所在了,可以用如下的代码替换:
static void bit2byte(Word16 para,int bitlen,unsigned char * bits,int bitpos) ;
static Word16 byte2bit(int bitlen,unsigned char * bits,int bitpos) ;
void prm2bits_ld8k(Word16 *para,unsigned char *bits)
{
int i;
int bitpos = 0;
for (i = 0;i<PRM_SIZE;i++) {
bit2byte(*para++,bitsno[i],bits,bitpos);
bitpos += bitsno[i];
}
}
void bit2byte(Word16 para,int bitlen,unsigned char * bits,int bitpos)
{
int i;
int bit = 0;
unsigned char newbyte = 0;
unsigned char *p = bits + (bitpos / 8);
for (i = 0 ;i < bitlen; i++) {
bit = (para >> (bitlen - i -1) ) &0x01;
newbyte = (1 << (7 - bitpos % 8));
if (bit == 1)
*p |= newbyte;
else
*p &= ~newbyte;
bitpos++;
if (bitpos % 8 == 0)
p++;
}
}
void bits2prm_ld8k(unsigned char *bits,Word16 *para)
{
int i;
int bitpos = 0;
for (i = 0;i<PRM_SIZE;i++) {
*para++ = byte2bit(bitsno[i],bits,bitpos);
bitpos += bitsno[i];
}
}
Word16 byte2bit(int bitlen,unsigned char * bits,int bitpos)
{
int i;
int bit = 0;
Word16 newbyte = 0;
Word16 value = 0;
unsigned char *p = bits + (bitpos / 8);
for (i = 0 ;i < bitlen; i++) {
bit = (*p >> (7 - bitpos % 8)) &0x01;
if (bit == 1) {
newbyte = (1 << (bitlen - i -1));
value |= newbyte;
}
bitpos++;
if (bitpos % 8 == 0)
p++;
}
return value;
}
通过上述的修改,已经可以保证,每次一帧160个字节的pcm16音频数据流,可以编码成为22个字节的编码参数prm,经过改写过的prm2bits_ld8k处理后,会转换成为10个字节的最终语音编码数据,这个时候,才真正体现出g.729a的威力。
编码器的代码可以采用如下的修改方法
(1)修改coder.c:
unsigned char serial[SERIAL_SIZE]; /* 输出数据的数据类型由Word16改为unsigned char */
(2)修改coder.c文件,对编码器调用方式进行修改(关键部分代码):
frame =0;
while( fread(new_speech, sizeof(Word16), L_FRAME, f_speech) == L_FRAME)
{
printf("Frame =%d/r", frame++);
Pre_Process(new_speech, L_FRAME);
Coder_ld8a(prm);
prm2bits_ld8k( prm, serial);
fwrite(serial, 1, SERIAL_SIZE, f_serial);
}
return (0);
(3)把ld8a.h头文件中关于“串行化”长度的常量定义修改为10个字节:
#define SERIAL_SIZE 10
(4)解码器decoder.c进行类似的修改(关键部分代码):
frame = 0;
while( fread(serial, 1, SERIAL_SIZE, f_serial) == SERIAL_SIZE)
{
printf("Frame =%d/r", frame++);
bits2prm_ld8k(serial, &parm[1]); /* 注意这里一定要是&parm[1] */
parm[0] = 0; /* 假设没有丢帧 */
parm[4] = 0 ; /* 假设数据效验正常 */
Decod_ld8a(parm, synth, Az_dec, T2);
Post_Filter(synth, Az_dec, T2);
Post_Process(synth, L_FRAME);
fwrite(synth, sizeof(short), L_FRAME, f_syn);
}
return (0) ;
通过上面的修改,标准的ITU-T的G.729语音编码器和解码器就基本可以达到1:16的编码效率了。
修改bits2prm_ld8k和prm2bits_ld8k之后的编码的数据其实也不是1/16的,因为编码器中的比特分配并不是均匀的,有的时候一个参数需要用5bit来表示,但是他并不是在内存中用5bit表示,而是用了一个word16来表示的。所以这样编码后得到的数据还是要比1/16大一些,原理上来讲,编完码后的prm[]大小应该是11(共11个参数),但是编码器的bits2prm()在其前面又加了一位校验是否丢包的校验位,这样1个block编码后的prm[0]这位实际上是一个校验位。你要注意一下,解码器在读取编码后文件内容的时候,不是直接读取的字节,而是通过一个函数read_frame(),进行读取的,一个block读取prm[]大小为12。这样如果你把bits2prm去掉,编码后一个block用11个word16表示,但是解码的时候一次读取12个,这样就错了。所以需要对解码器作一下相应的修改,使它不再去关注那个prm[0],一次读入11个数据。你看一下解码函数也会发现,在读入12个prm后,开头就是这么一句:bfi = *prm++,你把这个bfi作为参数传入函数,使其不再通过prm[0]得到,应该就可以了。
3、编解码器的优化问题
由于语音编码的特点,编解码的函数都是由一些基本的加减乘除的简单函数组织而成的,这些函数定义在BASIC_OP.C和OPER_32B.C两个文件里面,如果能够对这些简单函数进行嵌入式汇编的优化,就能达到事半功倍的效果。例如:
#define mult_r(var1, var2)
#define L_add(L_var1,L_var2)
#define L_mult(var1,var2)
#define L_shl(L_var1,var2)
等等,可以大大地加快速度,同时调整编译选项,采用最优化的编译模式,g.729a的代码有很大一部分的运算量集中在循环体计算中,因而针对循环的优化也很有必要,尤其是深层循环,如果有能力都改成arm汇编,就可以适用于手机voip等发展方向的应用。国外经过优化的g.729a的库,在dm642 dsp芯片上运行的时候,仅仅占有0.7%的cpu,而且是实时语音编码,真是很难设想他们都是怎么优化的(不过价钱也很高,而且只给二进制库和调用头文件,不给源代码)。
好了,这些是我对g.729a库使用的一点心得,希望对大家有用。