5 感知加权与基音周期
Mem_Shift
这个函数的作用是
把先前保存的120个输入信号,与当前的240信号值,整成一个360的缓冲区buf,
并把当前的最后120个输入信号存入PrevData,
取buf的第60至299样值块(也就是一帧240)来做分析.
Wght_Lpc
构造感知加权滤波器
用lpc系数来构造形式如下
10 10
(1 - Σa(i) * 0.9^i * z^-1) / (1 - 1 - Σa(i) * 0.5^i * z^-1)
i=1 i=1
Error_Wght
构240个样值送入感知加权滤波器,得到感知加权后的语音信号
其计算过程不详述了,分为iir 和 fir两部分来运算,
代码完全根据加权滤波器的公式,照本宣科
然后做一些缓冲区拼接,将先前142个历史值,与当前的240个样值,拼起来(142跟基音周期有关,见下文)
/* Construct the buffer */
for ( i = 0 ; i < PitchMax ; i ++ )
Dpnt[i] = CodStat.PrevWgt[i] ;
for ( i = 0 ; i < Frame ; i ++ )
Dpnt[PitchMax+i] = DataBuff[i] ;
Vec_Norm
把信号归一化
Estim_Pitch
接下来就是基音周期搜索了.
采用的是自相关法来搜索基音周期的.
根据柯西定量 a^2+b^2 > 2ab 这样.
我们可以知道,一个语音信号的自相关值,一定会在它的基音周期处,达到最大值
(还有一种基音周期的估算法,叫做短时平均幅度差法,与自相关法不同的是,它在基音周期处,是谷值)
妇女150-300赫兹,儿童200-300赫兹);成年男子的声带长而厚,所以说话声音就低一些(60-200赫兹) 对应为采样率为
8000时 基音周期分别为26-133 (8000/60=133 8000/300=26),
itu对基音周期的搜索为 18-142
即PitchMin PitchMax的值
以下为一些声音频率的参考资料:
人说话时基频范围大约为100Hz~300Hz
深沉的男低音发出的最低音的频率可达65.4Hz。
花腔女高音发出的最高音的频率可达1177.2Hz。
人和一些动物的发声频率范围和听觉频率范围
名称 发声频率范围Δf/Hz 听觉频率范围Δf/Hz
人 65~1 100 20~20 000
狗 450~1 800 15~50 000
猫 760~1 500 60~6 500
蝙蝠 10 000~150 000 1 000~200 000
海豚 7 000~120 000 150~150 000
知更鸟 2 000~13 000 250~20 000
鱼 40~2 000 --- 鱼能发声吗?从没听过,呵呵
回到代码中来,723将语音帧分成两截分别求基音周期,每截为120样值点
j = PitchMax ;
for ( i = 0 ; i < SubFrames/2 ; i ++ ) {
Line.Olp[i] = Estim_Pitch( Dpnt, (Word16) j ) ;
VadStat.Polp[i+2] = Line.Olp[i] ;
j += 2*SubFrLen ;
}
Estim_Pitch采用的是自相关算法,即第一个峰值点的索引,就是基音周期了
即计算
n=119 n=119
( (Σ s[n] * s[n - j])^2 ) / (Σ s[n - j] * s[n - j]) 18<=j<=142
n=0 n=0
可以看出分母即能量 分子就是自相关函数
这里为了避开昂贵的除法运算,实际代码在比较是做了变形,
我们假设搜索目标值 分子为Da, 分母为Db, 当前搜索到的最大值分子为Ma, 分母为Mb
代码要做比较时,实际是这样的 Da*Mb - Db*Ma 结果大于零,来判断相应的值大小,推导很简单
根据不等式的性质直接推出
下面来看Estim_Pitch函数的实现过程
首先就是计算能量初始值,不用每次都将分母计算一遍,只需要在循环中更新能量即可(即添头,去尾)
/* Init the energy estimate */
Pr = Start - (Word16)PitchMin + (Word16)1 ;
Acc1 = (Word32) 0 ;
for ( j = 0 ; j < 2*SubFrLen ; j ++ )
Acc1 = L_mac( Acc1, Dpnt[Pr+j], Dpnt[Pr+j] ) ;
添头,去尾,达到更新能量的目的,也就是分母
/* Energy update */
Acc1 = L_msu( Acc1, Dpnt[Pr+2*SubFrLen], Dpnt[Pr+2*SubFrLen] ) ;
Acc1 = L_mac( Acc1, Dpnt[Pr], Dpnt[Pr] ) ;
计算自相关,这个也就是分子组成部分
/* Compute the cross */
Acc0 = (Word32) 0 ;
for ( j = 0 ; j < 2*SubFrLen ; j ++ )
Acc0 = L_mac( Acc0, Dpnt[Start+j], Dpnt[Pr+j] ) ;
接下来的代码比较绕,但是还是很容易理解的,
计算自相关的平方,得到了分子,并将其归一化
/* Compute Exp and mant of the cross */
Exp = norm_l( Acc0 ) ; //lsc 计算归一化分子所需要的左移位数
Acc0 = L_shl( Acc0, Exp ) ; //lsc 归一化分子
Exp = shl( Exp, (Word16) 1 ) ; //lsc 因为平方,所以指数在扩大两倍..
Ccr = round( Acc0 ) ;
Acc0 = L_mult( Ccr, Ccr ) ; //lsc 这里计算平方
Ccr = norm_l( Acc0 ) ; //lsc 再次归一化得到的结果
Acc0 = L_shl( Acc0, Ccr ) ;
Exp = add( Exp, Ccr ) ;
Ccr = extract_h( Acc0 ) ;
分母,即能量归一化
/* Do the same with energy */ //注意归一化后得到的指数的符号与原值是相反的,所以最大,相应为最小
Acc0 = Acc1 ;
Enr = norm_l( Acc0 ) ;
Acc0 = L_shl( Acc0, Enr ) ;
除法,对应分子分母的指数相减
Exp = sub( Exp, Enr ) ;
Enr = round( Acc0 ) ;
真值大于"1"的情况,所以右移一位后,又是归一化了,指数相应减少(因为是左移,这点要记住,否则你会认为指数应该加1)
if ( Ccr >= Enr ) {
Exp = sub( Exp, (Word16) 1 ) ;
Ccr = shr( Ccr, (Word16) 1 ) ;
}
接下来就是一段繁琐的比较代码,笔者分析了如下.大意就是比较大小
//lsc 指数小,说明值大,因为左移的
if ( Exp <= Mxp ) {
//lsc 绝对小,保存最大的自相关值以及相应的索引 大于1.25db的情况 即应该比最大值大1.33倍
if ( (Exp+1) < Mxp ) {//lsc 这是大4倍的情况,直接保留索引
Indx = (Word16) i ;
Mxp = Exp ;
Mcr = Ccr ;
Mnr = Enr ;
continue ;
}
if ( (Exp+1) == Mxp )//lsc 这是大两倍,需要整到同一个数量级,所在右移
Tmp = shr( Mcr, (Word16) 1 ) ;
else
Tmp = Mcr ;//lsc 指数一样,这就不用移了,可以直接相乘再相减判断大小
/* Compare with equal exponents */
Acc0 = L_mult( Ccr, Mnr ) ;
Acc0 = L_msu( Acc0, Enr, Tmp ) ;
if ( Acc0 > (Word32) 0 ) {
if ( ((Word16)i - Indx) < (Word16) PitchMin ) {//lsc 位置差别小于18,只要大,就选成
Indx = (Word16) i ;
Mxp = Exp ;
Mcr = Ccr ;
Mnr = Enr ;
}
else {//lsc 位置差别大于18,则还要考虑是否大了1.33倍,但这里似乎成了1.5倍...笔者怎么算都不符
Acc0 = L_mult( Ccr, Mnr ) ;
Acc0 = L_negate(L_shr( Acc0, (Word16) 2 ) ) ;
Acc0 = L_mac( Acc0, Ccr, Mnr ) ;
Acc0 = L_msu( Acc0, Enr, Tmp ) ;
if ( Acc0 > (Word32) 0 ) {
Indx = (Word16) i ;
Mxp = Exp ;
Mcr = Ccr ;
Mnr = Enr ;
}
}
}
}
最后返回得到的索引值 Indx 它就是基音周
为何要做这些?基音周期体现了语音数据的相关性,实际上后继的自适应码本,就是在基音周期的基础上进行搜索的,
通过对基音周期周围的历史5个激励源进行加成,得到自适应激励,笔者将在下一章分析这些,待续
林绍川
2011.05.16 于杭州