g723源码详细分析-4-感知加权与基音周期搜索

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 于杭州







posted on 2011-10-08 14:18  lsccsl  阅读(218)  评论(0编辑  收藏  举报

导航