speex源码分析-1-lpc分析

本文简要地分析一下speex窄带的编码算法


算法实现主要在nb_celp.c这个文件里,看名字,大概可以猜出其它编码框架与g723等算法是极其类似的


在分析前,先来看一下怎么用speex编解码算法


  //初始化编解码器:
void *st;
void *dec;
SpeexBits bits;


st = speex_encoder_init(speex_lib_get_mode(SPEEX_MODEID_NB));
dec = speex_decoder_init(speex_lib_get_mode(SPEEX_MODEID_NB));
speex_bits_init(&bits);

short in_short[160] = {0};
short out_short[160] = {0};
char cbits[200];
int nbBits = 0;
int bitCount = 0;

while(1)
{
lcx_audio_record_read(in_short, sizeof(in_short));//这个不必理会,是笔者封装的waveInXXX windows api,就是读取输入的pcm码(8k 16bit 单声道)

speex_bits_reset(&bits);


speex_encode_int(st, in_short, &bits);
nbBits = speex_bits_write(&bits, cbits, 200);
bitCount+=bits.nbBits;


speex_bits_rewind(&bits);


//speex_bits_read_from(&bits, input_bytes, nbBytes);
speex_decode_int(dec, &bits, out_short);
speex_bits_reset(&bits);


lcx_audio_play_write(out_short, sizeof(out_short));//这个是调用waveOutXXX window api,放音
}


speex的窄带编码算法是基于 8k 16bit 单声道,每帧数据160个采样
speex内部将160个采样点,分成了4个子帧,每个子帧40个(...)


从代码上看,speex代码风格更多地引入了面向对象的设计方式,
带来的结果就是,代码显得更绕了.一些编码参数的设置值,往往不能一眼看出来


先来看
st = speex_encoder_init(speex_lib_get_mode(SPEEX_MODEID_NB));
这个是编码器初始化


speex_lib_get_mode 就是选择speex的编解码器的工作模式,
这里笔者选择了SPEEX_MODEID_NB,就是本文关注的窄带编码算法


#define speex_lib_get_mode(mode) ((mode)==SPEEX_MODEID_NB ? &speex_nb_mode : speex_lib_get_mode (mode))
是这个宏
即,窄带模式下,算法的配置参数取自全局变量 speex_nb_mode


const SpeexMode speex_nb_mode = {
   &nb_mode,
   nb_mode_query,
   "narrowband",
   0,
   4,
   &nb_encoder_init,
   &nb_encoder_destroy,
   &nb_encode,
   &nb_decoder_init,
   &nb_decoder_destroy,
   &nb_decode,
   &nb_encoder_ctl,
   &nb_decoder_ctl,
};
可以看出,这个全局变量包含一系列的回调函数,以及配置项nb_mode
static const SpeexNBMode nb_mode = {
   160,    /*frameSize*/
   40,     /*subframeSize*/
   10,     /*lpcSize*/
   17,     /*pitchStart*/
   144,    /*pitchEnd*/
#ifdef FIXED_POINT
   29491, 19661, /* gamma1, gamma2 */ ?
#else
   0.9, 0.6, /* gamma1, gamma2 */
#endif
   QCONST16(.0002,15), /*lpc_floor*/
   {NULL, &nb_submode1, &nb_submode2, &nb_submode3, &nb_submode4, &nb_submode5, &nb_submode6, &nb_submode7,
   &nb_submode8, NULL, NULL, NULL, NULL, NULL, NULL, NULL},
   5,
   {1, 8, 2, 3, 3, 4, 4, 5, 5, 6, 7}
};


FIXED_POINT 这个是控制代码是否采用定点数缩放来规避浮点数计算,在所有代码中均可忽略.
对算法的理解没有影响,speex编译默认是采用浮点数的(windows版本)


从这个配置项里,大概看出的一些信息是,帧长,子帧长,lpc为10阶,基音周期的搜索是从17-144,以及感知
加权的两个系数分别为0.9和0.6,其它的配置项暂时先不管,会在代码中相应是说明
注意一下子模式,默认被启用的子模式是 nb_submode5
如下
/* 15 kbps high bit-rate mode */
static const SpeexSubmode nb_submode5 = {
   -1,
   0,
   3,
   0,
   /*LSP quantization*/
   lsp_quant_nb,
   lsp_unquant_nb,
   /*Pitch quantization*/
   pitch_search_3tap,
   pitch_unquant_3tap,
   &ltp_params_nb,
   /*Innovation quantization*/
   split_cb_search_shape_sign,
   split_cb_shape_sign_unquant,
   &split_cb_nb,
   QCONST16(.3,15),
   300
};
子模式里包含的回调函数,实际为lsp量化/反量化,
自适应激励编码(pitch_search_3tap)/解码(pitch_unquant_3tap)
split_cb_search_shape_sign固定码本编码
split_cb_shape_sign_unquant固定码本解码


看完这些数据结构,熟悉celpc的朋友,对speex的算法框架恐怕已经心中有数了


接下来,上代码,分析


nb_encoder_init
这个是编码器初始化函数
主要做一些编码器参数的赋值,编码过程中要用到的内存分配等


speex_encode_int 这个函数的名字令人迷惑,它调用nb_encode进行编码.


现在来看nb_encode,即speex的窄带编码函数


首先,将之前一帧信号的激励往前挪,它们将作为当前帧(第一子帧)的自适应码本,以及第二,三,四子帧的自适应码本的一部分
   SPEEX_MOVE(st->excBuf, st->excBuf+st->frameSize, st->max_pitch+2);//lsc 保留历史的 144+2个激励
   
这里笔者回顾一下nb_encoder_init 中的一行内存分配代码:
   st->excBuf = (spx_word16_t*)speex_alloc((mode->frameSize+mode->pitchEnd+2)*sizeof(spx_word16_t));//lsc 目测为冲激响应保存的缓冲区
   st->exc = st->excBuf + mode->pitchEnd + 2;


st->exc指向地址,就用来保存当前帧的激励


保存历史感知加权的解码语音信号,用来计算零输入响应
   SPEEX_MOVE(st->swBuf, st->swBuf+st->frameSize, st->max_pitch+2);//lsc 保留历史的 144+2个感知加权语音信号


高通滤波   
   if (st->highpass_enabled)
      highpass(in, in, st->frameSize, (st->isWideband?HIGHPASS_WIDEBAND:HIGHPASS_NARROWBAND)|HIGHPASS_INPUT, st->mem_hp);//lsc 高通滤波,去除低频噪声
      
接下来,第一步就是lpc分析
计算lpc系数所需要的11个自相关系数,_spx_autocorr
用莱文森德宾递推方法计算出lpc系数 _spx_lpc
再将lpc系数转换成lsp系数(看注释像是用了契比雪夫多项式) lpc_to_lsp


相应的代码就不分析了,可以参考笔者之前对lpc算法以及lpc->lsp转换算法的推导
(g723源码分析系列与g729源码分析系列:
莱文森德宾递推公式证明: http://blog.csdn.net/lsccsl/article/details/6306667 
g723 lpc代码分析: http://blog.csdn.net/lsccsl/article/details/6325006
g729 lpc转lsp代码分析: http://blog.csdn.net/lsccsl/article/details/7448689)
有兴趣的读者,可以自行对speex的lpc模块代码做一个推导,笔者此处不赘述


如前所述,然后进行lsp插值
(前一帧的lsp系数,与当前帧的lsp系数按一定比较,进行插值)做法类似到g723...
lsp_interpolate,函数比较简单,不详细说明了


lsp_enforce_margin 保证lsp系数的稳定性,直观地看,就是让相邻的两个共振峰不要靠得太近
代码上的处理,就是将相邻的两个lsp系数保持一个最小的距离(lsp系数都在单位圆上,实际就是角度值)


插值完的lsp系数再转换成lpc系数保存在 interp_lpc 数组里


将原始的语音信号,通过interp_lpc所表征的系统,进行滤波,
这样就可以得到一个精确的激励:
      /*Compute "real" excitation*/
      SPEEX_COPY(st->exc, st->winBuf, diff);
      SPEEX_COPY(st->exc+diff, in, st->frameSize-diff);//lsc 原始语音拷贝至st->exc
      fir_mem16(st->exc, interp_lpc, st->exc, st->frameSize, st->lpcSize, st->mem_exc, stack);//lsc 逆向滤波,得到残差信号(也就是原始激励:"real" excitation)


计算这组激励的能量:
         spx_word16_t g = compute_rms16(st->exc, st->frameSize);//lsc 计算平均能量


这个能量,将被用做自适应激励的增益系数,在自适应激励编码时,会用到,ol_gain.


至此,完成了lpc的分析的大部分工作,接下去是lsp量化,以及激励编码(同样是两级,自适应激励与固定码本激励)
笔者将在下一节进行分析




                                                      






                                        林绍川 2012.10.29于杭州



































posted @ 2012-10-29 22:44  飞天大蟾蜍  阅读(222)  评论(0编辑  收藏  举报