g723源码详细分析(四) 感知加权与基音周期搜索

时间:2021-03-20 09:52:27

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的值

 

以下为一些声音频率的参考资料:

    人说话时基频范围大约为100Hz~300Hz

    深沉的男低音发出的最低音的频率可达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 于杭州