取消大小周了,周末有了更多的时间来自己学习。给自己立个flag,两周内把fullaec.m里面的线性滤波器、NLP等部分弄懂,发博客;再2~3周的时间看webrtc的AEC3的代码,同样发博客整理;然后再2~3周的时间看一下speex里面的AEC算法。

本次继续更新非线性处理部分。在学习过程中,参考了《实时语音处理实践指南》相关内容和网上有关博客的内容,在此对相关作者表示感谢。

非线性处理部分

计算完hnled,接下来开始计算ovrd。
hnlLocalMin是对hnlPrefAvgLow的最小值跟踪,其初始值为1,在满足了下面判断条件下,实际上就是发现了更小的值(并且这个最小值符合条件),就会对其更新,然后将hnlNewMin设置为1,hnlMinCtr重新置0。
设置hnlNewMin为1其实更接近于一个bool值,他其实是指有没有更新hnlLocalMin,hnlMinCtr其实更类似于一个计数器,当hnlNewMin=1(也就是更新了之后),往下走就会更新hnlMinCtr=2,若此时下一个循环过来了没有更新hnlPrefAvg,则实际满足了hnlNewMin=1且计数的hnlMinCtr=2,这个时候就开始更新抑制等级ovrd。
if hnlPrefAvgLow < hnlLocalMin & hnlPrefAvgLow < 0.6
hnlLocalMin = hnlPrefAvgLow;
hnlMin = hnlPrefAvgLow;
hnlNewMin = 1;
hnlMinCtr = 0;
end
if hnlNewMin == 1:
hnlMinCtr = hnlMinCtr + 1;
end
if hnlMinCtr == 2:
hnlNewMin = 0;
hnlMinCtr = 0;
ovrd = max(log(0.00000001)/log(hnlMin +1e-10),3);
end
这里更新的ovrd最小值为3。
除了hnlLocalMin是对hnlPrefAvgLow的最小值跟踪外,cohxdLocalMin是对hnlSort的最小值跟踪。这里为了防止值大于1,对其进行了处理。
hnlLocalMin = min(hnlLocalMin + 0.0008/mult, 1);
cohxdLocalMin = min(cohxdLocalMin + 0.0004/mult, 1);
最后平滑更新ovrdSm。
if ovrd < ovrdSm
ovrdSm = 0.99*ovrdSm + 0.01*ovrd;
else
ovrdSm = 0.9*ovrdSm + 0.1*ovrd;
end

接下来是进行发散处理。
首先对Se和Sd(分别是误差信号的功率谱和近端信号的功率谱)按行求和,得到误差信号的能量ekEn和近端信号的能量dkEn。然后进行发酸处理。如果divergeState为0,期望误差能量大于近端能量,则用df(近端信号频谱)更新ef(误差信号频谱),并将发散处理装divergeState置为1,否则继续往下走。如果divergeState为1,则判断近端信号能量是否大于误差信号能量的1.05倍,若是则将divergeState置为0,否则用df(近端信号频谱)更新ef(误差信号频谱),divergeState仍为1。
ekEn = sum(Se);
dkEn = sum(Sd);
if divergeState == 0
if ekEn > dkEn
ef = df;
divergeState = 1;
end
else
if ekEn*1.05 < dkEn
divergeState = 0;
else
ef = df;
end
end
如果误差信号比近端信号大约13dB(差不多误差能量大于近端能量的19.95倍),则认为此时滤波器已经发散了,就要重新更新滤波器,即将滤波器系数WFb置为全零矩阵。
if ekEn > dkEn*19.95
WFb=zeros(N+1,M); % Block-based FD NLMS
end
如果滤波器系数每发散,或者已经将WFb置零了之后,就需要将相应的能量存放在相应的向量中,将hnlLocalMin/cohxdLocalMin/hnlMin等保存在相应向量中。
ekEnV(kk) = ekEn;
dkEnV(kk) = dkEn;
hnlLocalMinV(kk) = hnlLocalMin;
cohxdLocalMinV(kk) = cohxdLocalMin;
hnlMinV(kk) = hnlMin;

接下来开始平滑滤波器系数及抑制指数,计算NLP的权重了。
首先使用权重曲线weight平滑hnled。wegiht是一个频率在较低的时候值很低,随着频率点变大值变大的曲线(如下图所示)。我们用hnlPrefAvg和hnled对应频点的较小值来更新这一次的hnled,更新后的值乘上weight,更新之前的值(上一次)乘1-weight的和来作为最终的hnled。结合weight的曲线我们发现,更新的时候存在这样一个现象:频率越高的点,使用本次更新的hnled的值的占比越大;频率越低的点,使用上一次的值平滑的结果占比越大。
aggFact = 0.3;
wCurve = [0; aggrFact*sqrt(linspace(0,1,N))’ + 0.1];
weight = wCurve;
hnled = weight.*min(hnlPrefAvg, hnled) + (1 – weight).*hnled;
接下来利用ovrdSm来生成od,实际上是ovrdSm*(1+sqrt(x)),其中x是0~1之间等分65份的线性分布。od用来更新hnld的幂指数。
od = ovrdSm *sqrt(linspace(0,1,N+1))’ + 1;
sshift = ones(N+1,1);
hnled = hnled.^(od.*sshift);
最后就是hnl系数与误差信号的频谱点乘,在时域相当于卷积,也就是将误差信号通过这样一个滤波器最后得到NLP处理后的感兴趣信号。最后存储一些有关的变量值。
hnl=hnled;
ef = ef.*(hnl);
ovrdV(kk) = ovrdSm;
hnledAvg(kk) = 1-mean(1-cohed(echoBandRange));
hnlxdAvg(kk) = 1-mean(cohxd(echoBandRange));
hnlSortQV(kk) = hnlPrefAvgLow;
hnlPrefAvgV(kk) = hnlPrefAvg;
至此,整个aec的主要流程已经结束。后面就是舒适噪声的生成和将频域信号经过IFFT和重叠相加变回时域信号,然后切到下一帧进行处理,直到所有音频处理完成。

至此,fullaec.m的第一遍阅读笔记就整理结束了。总的来说,经过这次整理我弄明白了AEC里面很多部分的计算和更新的思想是什么,收获颇丰。但是这里并没有DTD模块等其他模块的代码整理,我看在fullaec.m里面也没有这部分代码,我想在后面我在阅读webrtc aec3的时候可以进行进一步的整理。同时我还想提到的是,我阅读的代码只是其中的一个版本,实际上包括od、hnled等多个参数中间都可以有多种方式的特殊处理,以提升AEC的效果,这些我都没要整理在代码串讲里面。接下来我打算趁热打铁,好好看看webrtc 里面aec3代码是如何写的,并且整理出一份代码串讲来,作为下面多期代码串讲的内容。

博客内容排版有些混乱,如需访问原版,请点击这里访问,访问密码请关注个人公众号【小奥的学习笔记】,回复fullaec获取。