本文简要地分析一下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,
<p_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于杭州
算法实现主要在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,
<p_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于杭州