在上一节中讲的是自适应码本搜索,经过自适应码本搜索后,
语音信号的时域相关性被极大地去除了,剩下的残差信号接近
于随机信号.
g723的第二级搜索,是按编码速率的不同,按两种方式进行的
在高速率的情况下,g723第二级搜索是按多脉冲编码方式进行的,
即对残差信号用N个脉冲来表示,搜索每个脉冲的位置与增益,
使之误差最小
在低速率编码情况下,残差信号是与一个伪随机的固定码本进行"匹配"
算出最佳的码本索引与增益
Find_Fcbk
先分析低速率的情况,即固定码本搜索
先来认识一下固本码本的构成
itu为了简化对固定码本的搜索计算量,做了一些简化的定义,
即一个码字里只有4个单位脉冲,并且它们只出现在固定的位置上.
4个脉冲的位置如下表:
符 号 位 置
±1 0, 8, 16, 24, 32, 40, 48, 56
±1 2, 10, 18, 26, 34, 42, 50, 58
±1 4, 12, 20, 28, 36, 44, 52, (60)
±1 6, 14, 22, 30, 38, 46, 54, (62) ---- <表1>
加括号的表示不存在的位置,脉冲不会出现在 60,62这样的地方
每个脉冲可以取正号或负号
并且所的脉冲可以同时移动一位,一起出现在奇数位置上
每个脉冲的位置采用3 比特来编码,并且每个脉冲的符号以1 比特来编码。这样对4 个脉冲给出了总
共16 比特。此外,额外的1 比特用来对位移进行编码,形成一个17 比特的码书。 --- 摘自itu文档
接下来,笔者为itu文档中出现的公式做一些推导,有助于对代码的理解
首先引入矩阵定义,注意到g723文档里对向量矩阵的定义是"竖"着来的
V:
定义码本向量Vi, 不同的i代码不同的码本,如果不考虑符号,总共应该是有 8*8*7*7*2个
H:
定义冲激响应矩阵H,如下
h[0], 0, 0, ..., 0
h[1], h[0], 0, ..., 0
h[2], h[1], h[0], 0, ..., 0
h[3], h[2], h[1], h[0], 0, ..., 0
....
h[59] h[58], h[57], h[h56], h[54] ..., h[0]
它是一个下三角的托布利兹矩阵
RR:
然后我们定义RR矩阵(在文档里它叫Φ)
它是由Ht(H的转置矩阵)乘H得到的
即 RR=Ht*H,很显然,RR是沿对角线对称的
T:
定义输入的信号,即搜索的目标向量矩阵为 T
(在代码中,它是Dpnt这个数组,在之前的编码中已经扣除了自适应激励成份了,
在g723的文档中,它是r)
D:
定义D = Ht*T
前戏做完,开始推倒(...^_^ 嘻嘻)
我们可以得到,冲激响应矩阵H与码本Vi的矩阵乘积后,得到矩阵的就是码本激励
滤波器产生的输入(同学们做一个矩阵的乘法,就自然能得到这个结论了)
那么相应的误差矩阵就由下式给出了
E = T - g*(Ht * Vi) g是增益,是一个数
我们把Ht*Vi暂时记作Y
E = T - g*Y
我们自然希望误差的均方最小,
即E*Et最小 (Et是E的转置矩阵)
也就是
T*Tt - g(T*Yt) - g(Tt * Y) + g^2(Y * Yt)
合并(T*Yt == Tt * Y,同学们可以自行验证之,笔者就跳过了)
T*Tt - 2g(T*Yt) + g^2(Y * Yt) -------------------------- <式1>
我们照例对这个式子对g进行求导,得到
T * Yt
g = -------- ------------------------------------------<式2>
Y * Yt
即,在选定的码本情况下,最佳的g取值为上式(这个式子与itu文档里式25,含义接近)
再将g代入<式1>
2(T * Yt)^2 (T * Yt)^2
T*Tt - ----------- + ------------
Y * Yt Y * Yt
T * Tt要同一子帧中是不变的,E的均方差最小时,
就是下式最大时
(T * Yt)^2
----------- <式3>
Y * Yt
它就是itu文档中的2.16节中出现的式27,
可以自行证明<式3>可以变形为:
(Dt * Vi)^2
--------------- <式4>
Vit * RR * Vi
这样就跟itu完全一致了,事实证明,变形成此式,将简化计算量
(RR只需要计算一次,而Vi又只是在某些固定的位置出现,这些将在下面进行描述)
完成了推导,就可以对源代码进行分析.
应特别注意由于防止定点数计算溢出引入的数值放大缩小问题.
直接跳得ACELP_LBC_code这个函数,这个函数完成了低速率下固定码本的搜索
首先如果基音周期小于一个子帧长度,是需要调整冲激响应的,这相当于把固定码本
做了调整,笔者这么理解的
V(z) * P(z) * H(z),线性系统满足结合律,相当于把固定码本按基音周期做个迭加
在之后的代码,也能看出是这么目的,相应的代码片段如下 ---- <标签1>
for (i = 0; i < SubFrLen; i++) /* Q13 --> Q12*/ //lsc 这里亦看出了冲激响应是放大2^13倍
h[i] = shr(h[i], 1);
//lsc epsi170表格中存在+2的现象,所以这里应减2,
//lsc 回顾一下lpc,二元激励,是基于基音周期的脉冲串,这里在做固定码本搜索时,有4个脉冲,当基音周期小于60时,应把基音周期的贡献也算上
//lsc 修正了冲激响应之后,在搜索码本时就不考虑基音周期的影响了
//lsc 对基音周期的调整是随机的
if (T0 < SubFrLen-2) {
for (i = T0; i < SubFrLen; i++) /* h[i] += gain_T0*h[i-T0] */
h[i] = add(h[i], mult(h[i-T0], gain_T0));//lsc 12 + 12 - 15 = ??? 这里的描述可能不正确,gain_T0在itu的注释里是2^12这个数量级的,代码与注释并不吻合
}
Cor_h:
Cor_h这个函数很长,但只要了解相应的数据存储规律,就很容易理解这段代码,此处完全不用考虑数据
放大缩小的问题,因为之后搜索并不需要考虑放大还是缩小,所有的值都是同步被放大或者缩小了的
计算冲激响应的相关矩阵,注意到固定码本的特点,只会在某几个位置有值,所以
并不是所以的自相关都要计算
我们看<式4>的分母:
Vit * RR * Vi
显然只要计算码本中所需要的位置的自相关即可
笔者举个例子,
假设搜索码本的4个位置取值分别为:
0 (第一组位置)
2 (第二组位置)
4 (第三组位置)
6 (第四组位置)
那么,Vit * RR * Vi,那么我们明显看到,会被使用到的RR矩阵中元素的位置
一定是
RR(0,0),RR(0,2),RR(0,4),RR(0,6),
RR(2,2),RR(2,4),RR(2,6),
RR(4,4),RR(4,6),
RR(6,6)
所以并不是所有的RR(i,i)都要被计算,这就极大地减少了计算量,
并且我们以前已经描述RR是沿对角线对称矩阵 即RR(0,2)=RR(2,0)
在程序中,RR是被存在这样的数组当中
Word16 rr[DIM_RR]; DIM_RR宏定义是416,
这里就要了解itu对RR的存储定义了,笔者结合代码得出这样的结论:
首先来看看416是怎么来的,它是 8 * 4 + 64 * 6
它的定义是因为<式4>的分母
Vit * RR * Vi
我们作矩阵乘法,就可以推出它的结果实际上是这样的
RR(m0,m0)
s(m0)*s(m1)*RR(m0,m1) + RR(m1,m1)
s(m0)*s(m2)*RR(m0,m2) + s(m1)*s(m2)*RR(m1,m2) + RR(m2,m2)
s(m0)*s(m3)*RR(m0,m3) + s(m1)*s(m3)*RR(m1,m3) + s(m2)*s(m3)*RR(m2,m3) + RR(m3,m3)
s(mi)表示对应脉冲位置的符号
Word16 rr[DIM_RR]前32个元素,以8个为一组(代码中的宏NB_POS=8)
总共4组,记录
RR(0,0),RR(8,8),RR(16,16), ... , RR(56,56)
RR(2,2), .... , RR(58,58)
RR(4,4), .... , RR(60,60)
RR(6,6), .... , RR(62,62)
即它们记录的是 RR(mi,mi) 这种位置的元素
接下来是6组,每组64个元素
因为每组位置的8个,两组位置组合,则会形成64种组合,
回顾<表1>,我们举个例子: RR(m0,m1)
第0组位置与第1组位置,进行组合,很明显是64个
具体的存储顺序,仍然以RR(m0,m1)这两组位置组合为例,笔者推出代码中是这么存的
rr(0,2) rr(0,10) rr(0,18) rr(0,26) rr(0,34) rr(0,42) rr(0,50) rr(0,58)
rr(8,2) rr(8,10) rr(8,18) rr(8,26) rr(8,34) rr(8,42) rr(8,50) rr(8,58)
...以此类推
rr(48,2) rr(48,10) rr(48,18) rr(48,26) rr(48,34) rr(48,42) rr(48,50) rr(48,58)
rr(56,2) rr(56,10) rr(56,18) rr(56,26) rr(56,34) rr(56,42) rr(56,50) rr(56,58)
其它位置的RR存储情况以此类推
再来看Cor_h函数的代码,首先是归一化
cor = 0;
for(i=0; i<SubFrLen; i++)
cor = L_mac(cor, H[i], H[i]);//lsc 12 +12 +1 cor是扩大2^25倍的
if(extract_h(cor) > 32000 ) {//lsc 根据需要,决定是否归一化 h里存储的是H归一化的结果,如果H的能量大于32000,则简化归一化操作,我们可以看出,搜索的式子实际上与H的放大倍数无关
for(i=0; i<SubFrLen; i++) h[i+4] = shr(H[i], 1);
}
else {
k = norm_l(cor);
k = shr(k, 1);
for(i=0; i<SubFrLen; i++) h[i+4] = shl(H[i], k);
}
不详细说了,之前的代码中有很多这样的处理
for(i=0; i<4; i++) h[i] = 0;
从这里看出,itu的算法实际上也处理的两个不存在的位置60,62,我们看到矩阵实际上是定义成64 * 64的
接下分初始化rr数组各个存储组合位置的指针:
rri0i0 = rr;
rri1i1 = rri0i0 + NB_POS;//lsc 根据固定码本的位置规律,Φ(mi, mi)只会有8个取值可能
rri2i2 = rri1i1 + NB_POS;
rri3i3 = rri2i2 + NB_POS;
rri0i1 = rri3i3 + NB_POS;//lsc 根据固定码本的位置规律,Φ(mi, mj)只会有64(8*8)个取值可能
rri0i2 = rri0i1 + MSIZE;
rri0i3 = rri0i2 + MSIZE;
rri1i2 = rri0i3 + MSIZE;
rri1i3 = rri1i2 + MSIZE;
rri2i3 = rri1i3 + MSIZE;
rri0i0表示第0组与第0组自身的组合,它只有8种可能,其它的类推
接下来计算各组位置的组合,分别为位置差4个循环分别计算
位置差为 0 + n*8, 2 + n*8, 4 + n*8, 6 + n*8,
计算 mi与mi的组合:
/*
* Compute rri0i0[], rri1i1[], rri2i2[] and rri3i3[]
*/
p0 = rri0i0 + NB_POS-1; /* Init pointers to last position of rrixix[] */
p1 = rri1i1 + NB_POS-1; //lsc rrixix对应一g723文档里的Φ(mi, mi) rri0i0即Φ(m0, m0)
p2 = rri2i2 + NB_POS-1;
p3 = rri3i3 + NB_POS-1;
//lsc H * H矩阵对角线上的元素特点, 它们的值 必定为 h[0] * h[0] + h[1] * h[1] + ... + h[60] * h[60], 见<矩阵定义1>
//lsc 可以想像左上角的元素是最"大"的 右下角的元素是最"小"的 因为右下角只有h[0] * h[0],沿着对角线依次添加 h[i] * h[i]直到 60
//lsc 以下这个循环就算出了 H[m0, m0] H[m1, m1] ... H[m2, m2]的所有取值可能,根据itu的随机码书的位置,只需要计算这么多
ptr_h1 = h;
cor = 0;
for(i=0; i<NB_POS; i++) {//lsc 总共60个,但计算64次数,从g723的固定码本表里看到,对角线上每隔2就需计算一次
cor = L_mac(cor, *ptr_h1, *ptr_h1); ptr_h1++;//lsc
cor = L_mac(cor, *ptr_h1, *ptr_h1); ptr_h1++;
*p3-- = extract_h(cor);
cor = L_mac(cor, *ptr_h1, *ptr_h1); ptr_h1++;
cor = L_mac(cor, *ptr_h1, *ptr_h1); ptr_h1++;
*p2-- = extract_h(cor);
cor = L_mac(cor, *ptr_h1, *ptr_h1); ptr_h1++;
cor = L_mac(cor, *ptr_h1, *ptr_h1); ptr_h1++;
*p1-- = extract_h(cor);
cor = L_mac(cor, *ptr_h1, *ptr_h1); ptr_h1++;
cor = L_mac(cor, *ptr_h1, *ptr_h1); ptr_h1++;
*p0-- = extract_h(cor);
}
计算"差2"这种坐标的元素:
/*
* Compute elements of: rri0i1[], rri0i3[], rri1i2[] and rri2i3[]
* lsc 此处如果这么注释,会令读者更加明了 rri0i1[], rri1i2[] and rri2i3[], rri0i3[], 也就是这里是计算"差2"这种坐标的元素
*/
/* lsc itu的码本表格 rri0i3显然与其它三个不同,是需要特殊处理的
表 1/G.723.1-ACELP激励码书
符 号位 置
±1 0, 8, 16, 24, 32, 40, 48, 56
±1 2, 10, 18, 26, 34, 42, 50, 58
±1 4, 12, 20, 28, 36, 44, 52, (60)
±1 6, 14, 22, 30, 38, 46, 54, (62)
同时需要注意的是,为了方便,这里实际是在计算一个 64 * 64 的矩阵,有一些多余的计算,即加括号的那两个位置似乎也被计算了
*/
l_fin_sup = MSIZE-1;
l_fin_inf = l_fin_sup-(Word16)1;
ldec = NB_POS+1;
ptr_hd = h;
ptr_hf = ptr_hd + 2;
for(k=0; k<NB_POS; k++) {
p3 = rri2i3 + l_fin_sup;
p2 = rri1i2 + l_fin_sup;
p1 = rri0i1 + l_fin_sup;
p0 = rri0i3 + l_fin_inf;//lsc 看了itu文档,自然就明白这个坐标为什么需要特别处理,因为这rri0i3的第一行明显是相差6的,应该在之后处理,这个循环rri0i3计算是的错开一个位置的那个元素
cor = 0;
ptr_h1 = ptr_hd;
ptr_h2 = ptr_hf;
for(i=k+(Word16)1; i<NB_POS; i++ ) {//lsc 4 * 8 = 32 把所有差值 为 2 + n * 8的元素算出,不需要计算重复的 [i,j] [j,i]值是一样的(h矩阵的对称性)
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;//lsc 在第一轮循环这里是"对角线" 60-62
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
*p3 = extract_h(cor);
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;//lsc 在第一轮循环这里是"对角线" 58-60
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
*p2 = extract_h(cor);
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;//lsc 在第一轮循环这里是"对角线" 56-58
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
*p1 = extract_h(cor);
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;//lsc 在第一轮循环这里是"对角线" 54-56 注意,这是Φ(m0, m3)错开位置的那个值
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
*p0 = extract_h(cor);
p3 -= ldec;//lsc 这里是减9,
p2 -= ldec;
p1 -= ldec;
p0 -= ldec;
/*
lsc 分析rr的存储顺序如下(以rr(m0, m1)为例):
rr(0,2) rr(0,10) rr(0,18) rr(0,26) rr(0,34) rr(0,42) rr(0,50) rr(0,58)
rr(8,2) rr(8,10) rr(8,18) rr(8,26) rr(8,34) rr(8,42) rr(8,50) rr(8,58)
...以此类推
rr(48,2) rr(48,10) rr(48,18) rr(48,26) rr(48,34) rr(48,42) rr(48,50) rr(48,58)
rr(56,2) rr(56,10) rr(56,18) rr(56,26) rr(56,34) rr(56,42) rr(56,50) rr(56,58)
这里注意
rr(48,2) = rr(2,48) 因为Ht * H 得到的矩阵是沿对角线对称的
另外,在这个循环里 rr(8,2) 这类差值非 2 + n * 8的元素是不计算的,在之后的循环中计算,
即,当前循环是计算"右上"半三角
被计算的元素如下:
rr(0,2) rr(0,10) rr(0,18) rr(0,26) rr(0,34) rr(0,42) rr(0,50) rr(0,58)
rr(8,10) rr(8,18) rr(8,26) rr(8,34) rr(8,42) rr(8,50) rr(8,58)
...以此类推
rr(48,50) rr(48,58)
rr(56,58)
综上所述,我们应该很容易看出,为什么是减9,每次循环,减9,都正好落到下一轮的差值为2的位置
当k=0时
第一轮是 从rr(56,58) 第二轮是 rr(48,50) 最后一轮是rr(0,2),总共计算了7次,在循环外再补一次,
这样就把对角线处所有差值为2的计算出来,跳出这轮循环,之后就是计算差值为10 .. 2 + k * 8,直到整个上三角的元素全
部计算完成
p3 - p1情况都是相似的
*/
/*
p0由于是计算"错开"的那个位置,起始处理略有不同,体现为循环之外,少算了一次
再来看p0的情况,它存储的是rr(m0, m3)对应的关联值
首先列出它存储的64个值:
rr(0,6) rr(0,14) rr(0,22) rr(0,30) rr(0,38) rr(0,46) rr(0,54) rr(0,62)
rr(8,6) rr(8,14) rr(8,22) rr(8,30) rr(8,38) rr(8,46) rr(8,54) rr(8,62)
...以此类推
rr(48,6) rr(48,14) rr(48,22) rr(48,30) rr(48,38) rr(48,46) rr(48,54) rr(48,62)
rr(56,6) rr(56,14) rr(56,22) rr(56,30) rr(56,38) rr(56,46) rr(56,54) rr(56,62)
从这里可以看出,我们需要计算的是左下半三角:
rr(8,6)
...以此类推
rr(48,6) rr(48,14) rr(48,22) rr(48,30) rr(48,38) rr(48,46)
rr(56,6) rr(56,14) rr(56,22) rr(56,30) rr(56,38) rr(56,46) rr(56,54)
列出这个三角后,对p0的指针移动就清楚了,在这个循环之内,往"前"跳的方式是一样的,
而且p0是少计算一次的,体现为在循环之外,为计算p0
在循环之外,p0只往"前"跳一个位置
*/
}
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
*p3 = extract_h(cor);
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
*p2 = extract_h(cor);
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
*p1 = extract_h(cor);
l_fin_sup -= NB_POS;//lsc这里自然是减8,跳到"前8个"存储位置
l_fin_inf--;//lsc 这里只要减1,移到下一条对角线
ptr_hf += STEP;//lsc 差值分别为 2 2+8 2+8+8
}
计算 相差为 4 + n * 8:
/*
* Compute elements of: rri0i2[], rri1i3[] //lsc 这里很明显可看出是计算 相差为 4 + n * 8,同样,分为"上半个三角"与"下半个三角"的计算区别
*/
ptr_hd = h;
ptr_hf = ptr_hd + 4;
l_fin_sup = MSIZE-1;
l_fin_inf = l_fin_sup-(Word16)1;
for(k=0; k<NB_POS; k++) {
p3 = rri1i3 + l_fin_sup;
p2 = rri0i2 + l_fin_sup;
p1 = rri1i3 + l_fin_inf;
p0 = rri0i2 + l_fin_inf;
cor = 0;
ptr_h1 = ptr_hd;
ptr_h2 = ptr_hf;
for(i=k+(Word16)1; i<NB_POS; i++ ) {
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
*p3 = extract_h(cor);
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
*p2 = extract_h(cor);
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
*p1 = extract_h(cor);
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
*p0 = extract_h(cor);
p3 -= ldec;
p2 -= ldec;
p1 -= ldec;
p0 -= ldec;
}
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
*p3 = extract_h(cor);
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
*p2 = extract_h(cor);
l_fin_sup -= NB_POS;
l_fin_inf--;
ptr_hf += STEP;
}
计算 6 + n * 8:
/*
* Compute elements of: rri0i1[], rri0i3[], rri1i2[] and rri2i3[] //lsc 计算 6 + n * 8 这个的计算元素位置,恰好与2 + n * 8的计算元素位置对应
*/
ptr_hd = h;
ptr_hf = ptr_hd + 6;
l_fin_sup = MSIZE-1;
l_fin_inf = l_fin_sup-(Word16)1;
for(k=0; k<NB_POS; k++) {
p3 = rri0i3 + l_fin_sup;
p2 = rri2i3 + l_fin_inf;
p1 = rri1i2 + l_fin_inf;
p0 = rri0i1 + l_fin_inf;
ptr_h1 = ptr_hd;
ptr_h2 = ptr_hf;
cor = 0;
for(i=k+(Word16)1; i<NB_POS; i++ ) {
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
*p3 = extract_h(cor);
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
*p2 = extract_h(cor);
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
*p1 = extract_h(cor);
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
*p0 = extract_h(cor);
p3 -= ldec;
p2 -= ldec;
p1 -= ldec;
p0 -= ldec;
}
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
cor = L_mac(cor, *ptr_h1, *ptr_h2); ptr_h1++; ptr_h2++;
*p3 = extract_h(cor);
l_fin_sup -= NB_POS;
l_fin_inf--;
ptr_hf += STEP;
}
这样完成了RR矩阵中所需要的位置的元素计算
Cor_h_X:
接下来计算D矩阵,它是目标向量T与H的相关矩阵,该函数比较简单,
不多描述了.
D4i64_LBC:
这个函数开始搜索<式4>的最大值了,在之前的计算,没有讨论数值放大的问题,
是因为这个函数不需要考虑数值放大的倍数,因为都是乘除法
由于每个脉冲有正负号之分,并且itu定义所有的脉冲可以同时移动一位至奇数位.
代码中做了简化处理,首先脉冲符号的取值是由对应位置的D(i) D(i+1)决定的,即哪个
绝对值大,使用哪个值的符号
而分母不管是使用偶数位置还是奇数位置,均取码本在偶数位置的值,
搜索的过程中,会在第三轮循环比较码本奇数位置与码本在偶数位置时的分子的值,哪个大,取哪个.
在第四轮时,并没有做除法,而是通过交叉乘的方式,绕开除法运算
itu定义了一个门限值,即前三轮的分子的值如果不大于门限值,是不会进入第四轮循环的,
并且进入第四轮循环的次数也是有限制的,这些都是为了限制运算量
调整符号:
/*
* Chose the sign of the impulse.
*/
for (i=0; i<SubFrLen; i+=2) {
if( add(Dn[i],Dn[i+1]) >= 0) {
p_sign[i/2] = 1;
}
else {
p_sign[i/2] = -1;
Dn[i] = -Dn[i];
Dn[i+1] = -Dn[i+1];//lsc 这里做了转换将dn转到了文档里的dn`
// lsc对这段的理解是,相邻的两个,如果正的那个绝对值大,那么取正号
//如果负的那个绝对值大,取负号,这样,就保证了绝对值大的那个永远是正号
//进行循环搜索时,这样会相对的使分子变大,而分母的符号也相应地按这个规则进行调整即可
}
}
p_sign[30] = p_sign[31] = 1;
接下来一段是计算门限值的,这里不列举相应代码了,
调整10组RR值的符号,RR(i,i)这种组合就不用调整了,容易看出,它们总是为正1
搜索<式4>的最大值的循环,总共有4轮,每轮固定一个位置进行循环比较
代码如下:
/* first pulse loop */
for (i0 = 0; i0 < SubFrLen; i0 += STEP) {
//lsc 从循环外看出,第一个循环固定"0"的位置(选择码本表里,第0组某个固定的位置,以下同)
ps0 = Dn[i0];
ps0a = Dn[i0+1];
alp0 = *ptr_ri0i0++;
/* Init. pointers that depend on second loop */
ptr_ri1i1 = rri1i1;
ptr_ri1i2 = rri1i2;
ptr_ri1i3 = rri1i3;
/* second pulse loop */
for (i1 = 2; i1 < SubFrLen; i1 += STEP) {
//lsc 第二个循环固定"1"的位置
ps1 = add(ps0, Dn[i1]);
ps1a = add(ps0a, Dn[i1+1]);
/* alp1 = alp0 + *ptr_ri1i1++ + 2.0 * ( *ptr_ri0i1++); */
alp1 = L_mult(alp0, 1);
alp1 = L_mac(alp1, *ptr_ri1i1++, 1);
alp1 = L_mac(alp1, *ptr_ri0i1++, 2);//lsc 在循环中完成自增,在循环未尾就不需要跳8格了
/* Init. pointers that depend on third loop */
ptr_ri2i2 = rri2i2;
ptr_ri2i3 = rri2i3;
/* third pulse loop */
for (i2 = 4; i2 < SubFrLen2; i2 += STEP) {
//lsc 第三个循 固定"2"的位置
ps2 = add(ps1, Dn[i2]);
ps2a = add(ps1a, Dn[i2+1]);
/* alp2 = alp1 + *ptr_ri2i2++
+ 2.0 * (*ptr_ri0i2++ + *ptr_ri1i2++); */
alp2 = L_mac(alp1, *ptr_ri2i2++, 1);
alp2 = L_mac(alp2, *ptr_ri0i2++, 2);
alp2 = L_mac(alp2, *ptr_ri1i2++, 2);
/* Decide the shift */
shift = 0;//分子哪个大,取哪个(奇位置,偶位置)
if(ps2a > ps2) {
shift = 1;
ps2 = ps2a;
}
/* Test threshold */
if ( ps2 > thres) {
/* Init. pointers that depend on 4th loop */
ptr_ri3i3 = rri3i3;
/* 4th pulse loop */
for (i3 = 6; i3 < SubFrLen2; i3 += STEP) {
//lsc 第四个循环,变动的是"3"的位置
ps3 = add(ps2, Dn[i3+shift]);
/* alp3 = alp2 + (*ptr_ri3i3++) +
2 x ( (*ptr_ri0i3++) +
(*ptr_ri1i3++) +
(*ptr_ri2i3++) ) */
alp3 = L_mac(alp2, *ptr_ri3i3++, 1);
alp3 = L_mac(alp3, *ptr_ri0i3++, 2);
alp3 = L_mac(alp3, *ptr_ri1i3++, 2);
alp3 = L_mac(alp3, *ptr_ri2i3++, 2);//lsc 在循环过程中自增即可
alp = extract_l(L_shr(alp3, 5));
ps3c = mult(ps3, ps3);//lsc 分子是平方
if( L_mult(ps3c, alpha) > L_mult(psc, alp) ) {//lsc 这里交叉相乘,绕开了除法,差点没看懂fuck
psc = ps3c;
alpha = alp;
ip0 = i0;
ip1 = i1;
ip2 = i2;
ip3 = i3;
shif = shift;
}
} /* end of for i3 = */
time --;
if(time <= 0 ) goto end_search; /* Max time finish */
ptr_ri0i3 -= NB_POS;//lsc 需要复位指针,因为循环一结束,要重置成原来的"0"位置
ptr_ri1i3 -= NB_POS;//lsc 需要复位指针,因为循环一结束,要重置成原来的"1"位置
} /* end of if >thres */
else {
ptr_ri2i3 += NB_POS;//lsc 没有进入循环,但指针得照走8格
}
} /* end of for i2 = */
ptr_ri0i2 -= NB_POS;//lsc 每次循环结束,复位,与新的"1"位置组合,再进行一次搜索
ptr_ri1i3 += NB_POS;//lsc 指针跳8格,表示在搜索下一个"1"的位置了
} /* end of for i1 = */
ptr_ri0i2 += NB_POS;
ptr_ri0i3 += NB_POS;
} /* end of for i0 = */
搜索过程中,确定了次优解,即4个脉冲的位置,符号(能过位置即可知晓符号),是否都移动1位至奇数位置
接下来的代码是一些善后处理,并且产生了码失量激励冲激响应H产生的响应,保存在rr数组当时(这个让人有点不习惯,读者应注意)
//lsc符号需要根据位置在p_sign里查找,这是显然的
i0 = p_sign[shr(ip0, 1)];
i1 = p_sign[shr(ip1, 1)];
i2 = p_sign[shr(ip2, 1)];
i3 = p_sign[shr(ip3, 1)];
/* Find the codeword corresponding to the selected positions */
for(i=0; i<SubFrLen; i++) cod[i] = 0;
//lsc移位,如果选择的是奇数位脉冲
if(shif > 0) {
ip0 = add(ip0 ,1);
ip1 = add(ip1 ,1);
ip2 = add(ip2 ,1);
ip3 = add(ip3 ,1);
}
//lsc 这里用了一个大大的内存记录脉冲的位置,这个数组很稀疏
cod[ip0] = i0;
cod[ip1] = i1;
if(ip2<SubFrLen) cod[ip2] = i2;
if(ip3<SubFrLen) cod[ip3] = i3;
/* find the filtered codeword */
for (i = 0; i < SubFrLen; i++) y[i] = 0;
//lsc 这里其实是在计算卷积,但因为是单位冲激,所以变成4轮加法了,注意:这结果将作为G_code函数的输入参数y
if(i0 > 0)
for(i=ip0, j=0; i<SubFrLen; i++, j++)
y[i] = add(y[i], h[j]);
else
for(i=ip0, j=0; i<SubFrLen; i++, j++)
y[i] = sub(y[i], h[j]);
if(i1 > 0)
for(i=ip1, j=0; i<SubFrLen; i++, j++)
y[i] = add(y[i], h[j]);
else
for(i=ip1, j=0; i<SubFrLen; i++, j++)
y[i] = sub(y[i], h[j]);
if(ip2 < SubFrLen) {
if(i2 > 0)
for(i=ip2, j=0; i<SubFrLen; i++, j++)
y[i] = add(y[i], h[j]);
else
for(i=ip2, j=0; i<SubFrLen; i++, j++)
y[i] = sub(y[i], h[j]);
}
if(ip3 < SubFrLen) {
if(i3 > 0)
for(i=ip3, j=0; i<SubFrLen; i++, j++)
y[i] = add(y[i], h[j]);
else
for(i=ip3, j=0; i<SubFrLen; i++, j++)
y[i] = sub(y[i], h[j]);
}
/* find codebook index; 17-bit address */
*code_shift = shif;
*sign = 0;
if(i0 > 0) *sign = add(*sign, 1);
if(i1 > 0) *sign = add(*sign, 2);
if(i2 > 0) *sign = add(*sign, 4);
if(i3 > 0) *sign = add(*sign, 8);
i = shr(ip0, 3);
i = add(i, shl(shr(ip1, 3), 3));
i = add(i, shl(shr(ip2, 3), 6));
i = add(i, shl(shr(ip3, 3), 9));
至此,完成了固定码本的搜索,得到了位置与符号,接下来就是求出增益,
根据<式2>来求.求增益的函数是G_code
G_code:
它的两个输入参数X是搜索的目标向量,Y是Cor_h函数最后阶段求出的,
该函数较为平铺直叙,是直接根据公式写代码的:
Word16 G_code(Word16 X[], Word16 Y[], Word16 *gain_q)
{//lsc 传进来的X是目标向量,传进来的Y是一个码激励滤波器产生的数组(即D4i64_LBC最后做的四轮加法产生的)
Word16 i;
Word16 xy, yy, exp_xy, exp_yy, gain, gain_nq;
Word32 L_xy, L_yy;
Word16 dist, dist_min;
/* Scale down Y[] by 8 to avoid overflow */
for(i=0; i<SubFrLen; i++)
Y[i] = shr(Y[i], 3);//lsc 现在变成放大2^9
/* Compute scalar product <X[],Y[]> */
L_xy = 0L;
for(i=0; i<SubFrLen; i++)
L_xy = L_mac(L_xy, X[i], Y[i]);//lsc L_xy 变成放大 2^10
exp_xy = norm_l(L_xy);//lsc 归一化后取出高16位
xy = extract_h( L_shl(L_xy, exp_xy) );
if(xy <= 0) {//lsc 太小了,不做增益
gain = 0;
*gain_q =FcbkGainTable[gain];
return(gain);
}
/* Compute scalar product <Y[],Y[]> */
L_yy = 0L;
for(i=0; i<SubFrLen; i++)
L_yy = L_mac(L_yy, Y[i], Y[i]);//lsc L_yy 变成放大 2^19
exp_yy = norm_l(L_yy);//lsc 归一化后取出高16位
yy = extract_h( L_shl(L_yy, exp_yy) );
/* compute gain = xy/yy */
xy = shr(xy, 1); /* Be sure xy < yy */
gain_nq = div_s( xy, yy);//lsc 除法取的是小数点后面的15位,意味着放大了2^15,所得结果放大了2^14
//lsc 24-19=5,这样就还原到除法的原值
i = add(exp_xy, 5); /* Denormalization of division */
i = sub(i, exp_yy);
gain_nq = shr(gain_nq, i);
//lsc 在增益码本表里搜索相应的增益,靠近哪个选哪个,并返回增益的索引
gain = (Word16) 0;
dist_min = sub(gain_nq, FcbkGainTable[0]);
dist_min = abs_s(dist_min);
for ( i = 1; i <NumOfGainLev ; i ++ ) {
dist = sub(gain_nq, FcbkGainTable[i]);
dist =abs_s(dist);
if ( dist< dist_min) {
dist_min = dist;
gain = (Word16) i ;
}
}
*gain_q = FcbkGainTable[gain];
return(gain);
}
接下来将产生的固定码矢量与增益相乘,形成最后的固定码本激励
代码片段如下:
//lsc code 是一个很稀疏的数组,记录增益加权后的4个伪随机脉冲
for (i = 0; i < SubFrLen; i++) {
code[i] = i_mult(tmp_code[i], gain_q);
}
//lsc如果基音周期小于子帧长度,把固定码本产生的激励"循环"
if(T0 < SubFrLen-2)
for (i = T0; i < SubFrLen; i++) /* code[i] += gain_T0*code[i-T0] */
code[i] = add(code[i], mult(code[i-T0], gain_T0));
tmp_code是在D4i64_LBC最后阶段形成的,就是位置与符号
并且根据基音周期是否小于子帧长度,会让产生的固定码本激励再通过一个
周期延迟滤波器,进一步修正激励,这与本文开头的<标签1>的描述内容是相对应的
现在完成了固定码本的搜索,并返回固定码本激励,
接下来的工作就是将固定码本与自适应码本相加,形成新的自适应码本,并更新内存
(由这里看出了,自适应码本是由固定码本一步一步迭代而来的)
到目前为止,g723低速率的下编码过程大部分已经分析完毕,
高速率情况下,对残差信号的编码是由多脉冲激励来编码的,即,循环地求出每一个最佳
脉冲的位置以及该脉冲的最佳增益.笔者将在下一章节分析这些内容
林绍川
2011.10.03 于杭州