G.729编码算法介绍
电话线路上的模拟语音信号,经话路带宽滤波(符合ITU-T G.712建议)后,被8kHz采样,量化成16bit线性PCM数字信号输入到编码器。该编码器是基于线性预测分析合成技术,尽量减少实际语音与合成语音之间经听觉加权后差分信号的能量为准则来进行编码的。编码器的结构框图如图1所示,其主要部分有:
·线性预测分析和LPC系数的量化;
·开环基音周期估计;
·自适应码本搜索;
·固定码本搜索;
·码本增益量化。
下面分别描述这五部分的主要技术。
·线性预测分析与LPC系数的量化
首先对信号进行加线性预测分析窗,分析窗由两部分组成。第一部分是半个汉明窗,第二部分是四分之一个余弦信号。将加窗后的语音信号通过LevinsonDurbin算法获得线性预测滤波器系数ai i=1......10。由于线谱对参数比线性预测系数具有更好的内插特性和量化特性[3], G.729将LPC参数转换成相应的线谱对参数,最后对线谱对参数进行量化。
·开环基音分析
为了减少自适应码本搜索的复杂度,需要计算开环基音Top,使自适应码本搜索在开环基音值附近进行。基音值的范围在18~145个样本点之间。
·自适应码本搜索
G.729对每个子帧进行闭环基音搜索,它通过最小化原始语音信号和重构语音信号的加权均方误差来进行闭环基音搜索。对子帧1的闭环基音T1的搜索是局限在开环基音Top的一个小范围内,对子帧2的闭环基音T2的搜索是局限在闭环基音T1的一个小范围内。这样可以减少基音搜索的复杂度。
·固定码本搜索
G.729固定码本矢量含有四个非0脉冲,每个脉冲的幅度要可以是+1或-1。通过最小化加权语音信号和加权重构语音信号的均方误差来进行固定码本的搜索。
·增益量化
G.729对自适应码本增益和固定码本增益采用二级共轭结构码本进行矢量量化。在码本搜索时,采用预搜索策略,使得运算量只为全搜索算法的1/4。
G.729解码算法介绍
G.729的解码也是按帧进行的,主要是对符合G.729协议的码流进行解码,得到相应的参数,根据语音产生的机理,合成语音。解码的方框图如图2所示,其主要部分为:参数解码;后滤波处理。
G.729解码过程如下。
·参数解码
首先解码得到线谱对参数,并将线谱对参数转换为线性预测系数。然后解码出基音周期,获得自适应码本矢量V(n)。解码出固定码本矢量的四个脉冲的位置和符号,计算出固定码本矢量c(n)。
·后滤波处理
后滤波处理主要是自适应后滤波。自适应后置滤波器是由三个滤波器级连而成:长时后置滤波器Hp(z),短时后置滤波器Hf(z),频谱倾斜补偿滤波器Ht(z),后面接着进行一个自适应增益控制过程。后置 滤波器的系数每一个子帧更新一次。后置滤波能够有效地改善合成出的语音质量。
G.729编解码器的实现
我们在Analog Device的定点数字处理芯片ADSP-2181[4][5]上实时实现了符合ITU-T G.729的全部功能,进行编解码总共用了22MIPS(ADSP-2181处理速度为33MIPS)。实时实现了G.729的编解码功能,主要有以下的难点。
·数据在DSP中的安排。在DSP中只要是声明的变量,DSP的连接程序就会分配数据空间。如果我们象C语言编程那样定义局部变量,就会浪费大量的存储空间。
·数据精度的处理。在16位的定点信号处理芯片上实现一些浮点算法时,为了使运算速度加快,往往要针对定点芯片的特点,用定点数来表示浮点数。虽然速度提高了,但是很可能导致运算精度不够。
·有限计算资源的利用。ADSP2181只有33MIPS,而且不能使所有的MIPS都用来实现G.729的编解码功能。
·DSP高速运行时,DSP与主机的数据交互。
·语音信号和编解码缓冲区的维护。这些缓冲区至少都有两者要去存储,如语音信号缓冲区, 一方面语音编码模块要调用,另一方面采集中断程序也要调用它。而在语音编码模块调用的过程中,该缓冲区不应该被其它模块所改变,所以中断程序不应该此时去存储这个缓冲区,这就存在着矛盾。
1. 软件设计
软件设计主要包括三个部分。
· 命令解释器
命令解释器主要是用于解释主CPU发来的各种命令,如发送或接收编解码数据、查询编解码状态以及启动、停止编解码操作等。该模块不直接与主CPU打交道,而是通过接口功能模块,间接实现与主CPU的数据信息交换。
· G.729数据引擎
我们在ADSP-2181上完成了G.729的实时编/解码工作。
ADSP-2181不仅包含了ADSP-2100系列的基本结构(三个运算单元、数据地址发生器和一个程序序列器),还含有两个串行口、一个16位的内部IDMA口,一个8位的BMDA口、一个可编程定时器、标志输入输出(Flag I/O、外部中断能力以及片内程序和数据存储器等周边设备。ADSP-2181片内集成了共80K字节的存储器,它们分别是16K字(24bit)程序存储器和16k字(16bit)的数据存储器,大量的片内存储器使得复杂的G.729编解码算法能够全部放入ADSP-2181片内,无须增加任何的片外RAM,简化了硬件设计和接口。
· 接口功能模块
该模块实现ADSP-2181与主CPU的数据实际交换工作。该模块包括DSP的主控程序和数据传输两部分。DSP的主控程序主要负责不断将采集到的语音数据分帧,送入编码器,并将接收到的码流分类后送入解码器模块。数据传输部分负责采集数据和与主CPU的数据交换。
2. 硬件设计
本系统考虑了各CPU之间的数据交换与协调等问题。ADSP2181具有一个16位的IDMA
口,在处理器全速运行的情况下,ADSP-2181可以自动完成IDMA口的接收和发送数据,这为系统的设计带来了很大的便利。利用ADSP-2181的这个性质,我们通过IDMA口来实现ADSP-2181与主CPU的数据交互。
我们设计了ADSP-2181与PC机ISA总线接口的主从系统。在这个主从系统中,PC机为主CPU,ADSP-2181为从CPU。在启动时,由PC机通过IDMA口将程序装载入ADSP-2181内部存储器中。在ADSP-2181全速运行时,主机可以查询从机的运行状态、读取压缩后的G.729码流,也可以送入待解码的G.729码流等。
在该主从系统中,PC机通过ADSP-2181的IDMA口来读写它的内部存储器。PC总线通过GAL译码,形成IACK信号,与ADSP-2181的IDMA口连接,这样就实现了在ADSP-2181全速运行的情况下,PC机仍能访问到ADSP-2181内部存储器。
图3是主从系统中的ADSP-2181功能与接口的框图。语音信号由ADSP1847采集,通过ADSP-2181的串口0送入到ADSP-2181进行编码。编码数据通过IDMA口送到主机进行存储。需要解码的数据从主机由IDMA口送到ADSP-2181进行解码,解码后获得的语音信号通过串口0送至ADSP1847。
3 .难点的解决
数据在DSP中的安排。维护好一张变量表,每次进入一个模块时,首先使用已经分配但暂时不用的变量。只在不够的情形下,再去分配额外的变量,同时将这些新分配的变量计入到变量表中,供其它模块使用。另外维护好一个局部变量堆栈,使得各个模块的内部局部变量分配在堆栈中,当从该模块退出时,就从堆栈中弹出这些局部变量,释放空间。
数据精度的处理。对精度要求比较高的地方,将计算的中间变量采用32位来表示,运算结束后,再转换成16位表示,这样指令条数增加不多,但精度却大大提高了。在某些精度要求比较高的模块,采用尾数和指数来表示浮点数,自己编写一套指数和尾数的运算库,由于尾数和指数表示法有足够大的精度范围,完全满足要求。但只能在少许地方采用这种方法,否则运算量就会上去,给实时实现带来困难。
有限计算资源的利用。利用ADSP强大的多功能指令可以非常好地利用有限计算资源。
DSJP高速运行时,DSP与主机的数据交互。由于ADSP-2181具有一个16位的IDMA口,在处理器全速运行的情况下,ADSP-2181可以自动完成IDMA口的接收和发送数据,这为系统的设计带来了很大的便利。利用ADSP-2181的这个性质,我们通过IDMA口来实现ADSP-2181与主CPU的数据交互。
语音信号和编解码缓冲区的维护。本系统的数据交互都是采用双缓冲的工作方式。双缓冲的好处在于数据的交互不会影响到其他工作的正常进行。
本系统的数据采集与播放采用双缓冲的工作方式。采集时采用cod-ad和cod-work两个缓冲区,播放时采用decod-work和decod-work两个缓冲区。cod-ad用于ADSP-2181的串口数据采集,cod-work用于ADSP-2181的编码。当cod-ad采完一帧数据后与cod-work互换。decod-work用于ADSP-2181的串口数据播放,decod-work用于ADSP-2181的解码。当播放完一帧数据后,decod-ad与decod-work互换。采集与播放程序放在ADSP-2181的中断服务程序中。本系统只开放ADSP-2181的接收中断,ADSP-2181每接收一个数据,同时就播放一个数据。
另外G.729的编码器的编码数据和解码器的解码数据都有两个缓冲区,缓冲区的工作原理与数据采集和播放的双缓冲区的工作原理相同。
综上所述,ITU-T的G.729协议是一个ITU-T的8Kbps语音编解码协议,它具有高音质和低延时的特点。软件设计主要包括三个部分:命令解释器、G.729数据引擎、接口功能模块。我们用ADSP-2181实时实现了G.729协议,在ADSP-2181的数据采集/播放和ADSP-2181与主CPU接口的数据交互方面都采用了双缓冲方式。硬件设计主要是通过ADSP-2181的IDMA口,来实现主CPU与ADSP-2181的数据交互。
针对实时实现G.729的一些难点,我们提出了一系列解决的方法,通过这些解决方法,我们已经在Analog Device的定点数字处理芯片ADSP-2181上实时实现了符合ITU-T G.729的全部功能,并且已经通过了ITU-T G.729的全部测试矢量。该编解码器已经在数字语音记录仪和H.323网上多媒体通信系统中推广应用。
在TMS320C6x系列DSP上G.729A算法的优化
1. 引言
语音通信是现代多媒体通信中一个重要的组成部分,语音压缩又是实现低速率语音通信的关键技术。国际电信联盟(ITU)于1996年提出了一种共轭结构代数码激励线性预测(CS-ACELP)的语音编码算法—G.729。该算法在8kbits码率下具有较好的语音编码质量,且延迟较短,因此在IP电话、移动通信、多媒体网络通信以及各种手持设备中具有广泛应用。G.729A是在G.729基础上进行了一部分简化,使得编码的复杂度降低,对硬件的要求更低,而编码质量并没有明显降低[1][2][3]。
2.G.729A的DSP软件开发流程
在编写和调试C6000程序时,为了使C6000代码获得最好的性能,我们需要按照软件编程的3个阶段进行,每个阶段完成的任务如下[4]:
第一阶段:开始可以不考虑C6000的有关知识,完全根据任务编写C语言程序。在CCS环境下用C6000的代码产生工具,编译产生在C6000内运行的代码,证明其功能正确。然后再用CCS的调试工具,如debug和profiler等,分析确定代码可能存在的、影响性能的低效率段。为进一步改进代码性能,需要进入第二阶段。
第二阶段:利用内联函数、CCS编译选项和其他具体优化方法改进C语言程序。重复第一阶段,检查所产生的C6000代码性能。如果产生的代码仍不能达到所期望的性能,则进入第三阶段。
第三阶段:从C语言程序中抽出对性能影响很大的程序段,用线性汇编重新编写,再用汇编优化器优化,链接,直到达到所期望的性能要求。
具体到G.729A标准编解码器的实时要求,第三阶段是工作的重点,而且线性汇编的重新编写要求对程序代码和DSP的特性有充分的了解。
3. G.729A代码的剖析
CCS集成开发环境为软件开发人员提供了高效的开发、调试工具。特别是它提供了评价器( profiler)的优化工具,通过收集在指定代码区间程序执行的统计性能,分析确定程序中各个段、各个子函数所花费的处理器时间,从而把程序的优化集中在对程序性能影响最大的代码段上去[5]。其两种不同的测试方法是:
(1) 在需要测定复杂度的程序段的开头和结尾处设定两个断点,打开时钟窗口,运行程序。在第一个断点处执行停止,这时双击时钟窗口使之清0,接着继续执行程序,在第二个断点处停止,这时,时钟窗口显示的值便是该段代码的复杂度。这在测试程序中一个函数的复杂度是非常有用的。
(2) 先打开统计窗口,在需要测试的程序段头尾设置统计点((Probe Point)。程序运行结束后,统计窗口内该程序段后面的统计值便是该代码段的复杂度。这种方法较简单,统计点自动收集统计信息,无需手工干涉,这在测定程序多段代码的复杂度是非常有用。
4. 线性汇编的优化
线性汇编是TI提供的一种汇编语言,其指令系统和汇编语言的指令系统完全相同,但在编写时不需要指定寄存器和操作单元,也不需要考虑延时的问题,因此编写线性汇编相对要容易一些 [6]。
经过第一阶段和第二阶段的优化后,音频编码程序在DM642上的运行状况有了很大改善,但是经测试仍然没有到达实时效果,而高级语言的效率几乎发挥到了极致,测试的速度达到了36.5帧/s,是未优化之前的10倍。这时,我们采用线性汇编语言重新编写C代码的低效率段程序,进一步提高程序的执行效率和充分利用DM642的硬件资源,最终按设计要求在DM642实时实现G.729A编码。在前面的DSP开发流程已经提过,DSP开发的最后一个手段是用汇编重写C代码,它是唯一可以既提高程序执行速度又可以减少程序体积的方法。由于针对并行处理器编写汇编的难度很大,一般采取的是混合编程的方法,即程序的主要部分用C代码,部分耗时较大的函数可以用线性汇编改写。
在编写线性汇编优化代码的过程中,为了提高代码执行效率,我们需要遵循以下原则[7]:
(1)写并行代码:通过使用汇编指令并行执行的方法减少循环内的执行周期数,优化线性汇编代码。这里的关键问题是弄清指令相关性,只有不相关的指令才能并行执行。辨别指令是否相关,可以使用相关图。
(2)处理跳转指令和转移指令:汇编程序的一大特点就是频繁地跳转,当满足不同的条件时,要求程序进行不同的操作,或跳到相应的位置。对于“大于”、“大于等于”、“小于”、“小于等于”等较为接近的逻辑判断和处理,应慎重对待,否则将产生逻辑性错误,并且很难调试。当发生溢出需进行相应处理时,这种现象尤为突出。
(3)尽量减少循环体内的指令数:G.729A的算法实现,有许多是在循环内部完成的,有些地方如固定码本搜索过程中,为了确定四个非0脉冲的位置和幅度,还用到了多重循环。在循环内部,特别是在嵌套较深的循环内部,减少一条指令可以大大降低程序的操作次数。例如,对于一个每重循环8次的四重嵌套循环,在最内层循环每减少一条指令,整个程序可以少执行84=4096语句。因此在设计程序时,能够放在循环体外执行的语句,尽量放在循环体外执行。
(4)展开程序体:在一定条件下,尽量展开程序,以减少子程序的调用和返回次数,牺牲空间换取时间。
G.729A算法中的LPC模块、LSP量化及激励码本搜索耗时最多,为进一步提高代码效率,对相关计算、FIR滤波等部分函数用线性汇编语言进行了改写,并用画相关图等方法有针对性的进行优化。经汇编优化器优化后,代码效率比C语言直接编译有明显提高。
5. 优化工作的创新点
在对G.729A的优化中,本文在前人研究成果的基础上,针对TMS320DM642 DSP系列芯片提出了一些有价值的新方法。这些创新点在不同程度上提高了代码的优化速度和执行效率,在语音编解码的DSP实时实现中起到了关键性作用。下面,以举例的方式阐明一些经典的方法。
5.1 绘制分析图,掌握函数结构
对于一个语句较多、结构复杂的函数,为了充分了解其逻辑结构和语句的相关性,我们通常采用画分析图的方法。分析图的形式比较灵活,可以根据具体的情况选用不同的制图工具。在编写线性汇编的时候,需要考虑存取数组中的元素,数据打包操作和数据相关性等问题,分析图有助于正确处理这些问题。
在对函数Cor_h_X( )优化过程中,我们遇到了一定的困难,原因在于其中有一个双层的循环体,内层的次数与外层有关,外层的循环次数为40,并且循环内部的语句有先后的相关性。这样的结构如果用循环展开的方法将会用到大量的寄存器,数目超出了64个,需要开辟额外的内存空间去存放临时变量,而读写内存会消耗较多的时间,因此这样执行效率不会有充分的提高。对此,我们利用分析图描述了函数中关键代码的数组X[ ],h[ ]的使用情况,如图1所示:
图1 cor_h_X( )函数分析图(部分)
图1直观地反映了数组16位h[ ]和16位X[ ]之间的乘加关系,从函数cor_h_X( )中可知,两个数组的乘积之和要对应的保存在临时数组32位Y[ ]中。通过研究此分析图,我们发现h[ ]与X[ ]中的一些元素进行乘积和处理之后就不再被使用,那么存储这些元素的寄存器可以存放中间结果(Y[]的元素),这样就可节省寄存器的使用个数,免去了开辟内存空间和中间变量的存取指令。
对于函数cor_h_X( ),利用上述思想编写线性汇编,只需要定义57个寄存器就可以完成所用的操作,存取指令从1760条优化到30条,仅为原来的1/60。同时执行速度从390072个时钟减少到35871个,降为原来的1/10。
绘制的分析图可以包含相关图,相关表等,使资源安排更加合理。该方法在其他函数的改写中也多次使用到。
5.2 功能相似的函数或代码段合并为一个函数
线性汇编在提高代码效率的同时也成倍的增加了代码尺寸,以上述cor_h_X( )为例,它在该写后代码尺寸从660条增大到7776条(该数据由CCS剖析工具分析所得)。在工程应用中,对于有限的内存程序区,我们会适当减少程序占用的空间。合并功能相似的函数可以达到这一要求。
在LSP量化处理中,源代码中给出了2个LSP选择函数:Lsp_select_1( )和Lsp_select_2( ),而我们发现它们具有相同的功能和相似的结构,因此,在对两者的线性汇编改写中,我们只需编写一个函数(命名为Lsp_select)即可实现LSP量化处理中这两个模块的功能。
另外,在对于一些数组拷贝,数组初始化的代码,我们同样可以用此方法,编写一个函数实现,这样可以在提高执行效率的同时,减少程序占用的内存空间。
5.3 多个循环合并为一个循环
C代码改写线性汇编的时候,我们常常会发现,只要作一些调整,两个或多个循环完成的操作完全可以由一个循环来完成。以LPC子模块240点加窗语音的自相关计算Autocorr()函数为例,经过优化改写的C代码(部分)如下:
for(i=0; i<L_WINDOW; i++) //第一个循环体
y[i] = (_smpy(x[i], hamwindow[i])+0x00008000L)>>16;
sum = 1; //避免为0的情况
for(i=0; i<L_WINDOW; i++) //第二个循环体
sum = _sadd(sum,_smpy(y[i], y[i]));
这段代码包含了两个for循环,在CCS中直接编译运行并行度很差,利用线性汇编重写代码。我们发现两个循环体的循环次数均为60(L_WINDOW=60),所处理的数组不同,并且两个循环没有相关性,可以把第一和第二个循环合并成一个循环。前者的功能是对语音信号进行加窗;后者是实现乘累加(Mac)。两者合并后采用线性汇编编写,其代码如下:
mvk 60,i //设置循环次数
loop1: lddw *ham++,hamih:hamil //hamwindow[]指针
lddw *x++,xih:xil //x[]指针
smpy2 hamil,xil,yi1:yi0 //两对16位操作数相承,并行执行
smpy2 hamih,xih,yi3:yi2
sadd yi0,con0x8000,yi0
sadd yi1,con0x8000,yi1
sadd yi2,con0x8000,yi2
sadd yi3,con0x8000,yi3
packh2 yi1,yi0,yl //数据打包技术
packh2 yi3,yi2,yh
stdw yh:yl,*y++ //双字存取,提高执行效率
smpy2 yl,yl,yi1:yi0
sadd sum0,yi1,sum0
sadd sum0,yi0,sum0
smpy2 yh,yh,yi3:yi2
sadd sum0,yi3,sum0
sadd sum0,yi2,sum0
add i,-1,i
[i] b loop1 //把第一和第二个循环合成一个大循环,减少转移次数
产生的汇编代码并行流水性能大大增加,耗费的时钟周期数从1310000减少到15000,少于改编前的1/8。
6. 结束语
关于编解码器执行的时钟周期,在线性汇编改写前后,文件版本通过CCS的profiler剖析工具得知:每10帧(100ms)从159700000降至68500000,仅为原来的42%。硬件版本进行测试得:编解码的帧数提高到了88帧/s以上,鉴于编码、解码的时间比例为5:1,所以,本系统编码已经达到100帧/s,完全符合实时通信的要求。