类介绍 EchoRemover 、EchoRemoverImpl
EchoRemoverImpl::ProcessCapture
-
echo_path_variability
:回声路径的变化性,用于指示回声路径的稳定性程度。 -
capture_signal_saturation
:捕获信号是否存在饱和。 -
external_delay
:外部提供的延迟估计值,用于指定回声估计的延迟。 -
render_buffer
:渲染缓冲区,包含了回声估计所需的渲染信号。 -
linear_output
:线性输出的指针,用于存储线性输出的结果。 -
capture
:捕获数据的指针,用于存储处理后的捕获信号。
Step1:初始化
-
首先分配变量空间。如果声道数为大于2,则分配空间不够,按照实际声道数分配空间。
-
根据传入的爆音状态变量capture_signal_saturation更新AEC里面的爆音状态。
-
判断回声路径是否发生变化(模拟麦克风增益是否发生变化),若发生变化则进行进一步操作,否则走第三步。
-
回声路径变化依据echo_path_variability中的两点,两者中有一个符合即为发生变化:一为gain_change改变,即回声大小发生改变;二为delay_change不为kNone。
-
如果是gain_change为true的这种情况,是需要每三帧(gain_change_hangover)做一个判断,若已经满三帧了,则gain_change为true不会改变,并且将gain_change_hangover重置为3;若没有满3帧,则gain_change置为false。
-
完成上述操作后,对subtractor的主、副滤波器进行clear,对aec_state也进行clear。
-
如果是delay_change不为kNone这种情况,则将initial_state设置为true。然后若gain_change_hangover大于0,则对其进行自减。
Step2:分析参考信号
分析参考信号分为两步:第一步是识别具有窄特征的本地频段,第二步是标识信号是否具有单个强窄带分量:
-
远端信号中某个频率点的幅度大于左右频点幅度的3倍,则认为是弱窄带频率分量;
-
远端信号中某个频率点的幅度大于左右频点的100倍,则认为该频率点是强窄带分量。
Step3:判断是否为transition
若当前Initial_state 为false且前一帧的initial_state为true,则认为需要进行状态转换了,线性滤波器substractor的main_gains、shadow_gains、main_filters、shadow_filter都退出初始化,suppression_gain_也设置initialstate为false。
Step4:线性滤波器处理
AEC3的线性滤波器部分使用了主辅滤波器结构(即main_filters和shadow_filter),一个是卡尔曼滤波器,另一个是NLMS滤波器,卡尔曼滤波器更新比较慢,而NLMS更新比较快,这种方法取代了双讲检测。整体来说思路,主滤波器会通过计算失调和步长的变化来保证主滤波器不会发散,但是缺点是可能误差较大;辅助滤波器会一直更新,跟踪回声的路径,当辅助滤波器发散的时候,会将主滤波器系数拷贝到辅助滤波器。
这两个滤波器在双讲时卡尔曼滤波器很稳健,不易发散,而NLMS会发散;但是回声路径发生变化时NLMS可以较快收敛,两者结合取最小输出。
主要都在subtractor_.Process过程中。
第一步,是对播放信号进行处理。
若主辅滤波器长度一致,则令same_filter_sizes为true,否则为false。若same_filter_sizes为true,代表两个滤波器长度一致,实际上代表他们是一样的,然后把当前参考帧的频谱加到X2_refined;若两者长度不一致,则X2_coarse是X2_coarse_data的别名,然后把当前参考帧的频谱分别加到X2_refined和X2_coarse。
第二步,滤波。
refined_filters_[ch]->Filter(render_buffer, &S);
PredictionError(fft_, S, y, &e_main, &output.s_main);
coarse_filter_[ch]->Filter(render_buffer, &S);
PredictionError(fft_, S, y, &e_shadow, &output.s_shadow);
Filter是滤波,实际上就是S=W^HX。其中X代表参考信号频谱,W代表主滤波器或者辅助滤波器的滤波器系数,S代表滤波后的结果频谱。PredictionError实际上就是下面的公式:
e(t)=y(t)-s(t)、o(t)=kscale*e(t)
其中e为e_main或e_shadow。o为output.s_main或output.s_shadow。kscale=1.0f / kFftLengthBy2。
第三步,更新主辅滤波器。
对于主滤波器来说,其主要计算的公式如下:
mu = H_error / (0.5* H_error* X2 + n * E2)
H_error = H_error – 0.5 * mu * X2 * H_error (H_error自更新)
G = mu * E 、H_error = H_error + factor * erl
H_error = H_error + factor * erl
H(t+1) = H(t) + G(t) * conj(X(t))
对于辅滤波器来说,其主要计算的公式如下:
如果 X2[k] > current_config.noise_gate,那么 mu[k] = current_config_.rate / X2[k];否则mu[k]=0。
G = mu * E
H(t+1)=H(t)+G(t)*conj(X(t))
其中k是第k个频点。
第四步,选择最佳的线性滤波器输出。
首先,选择主滤波器或辅助滤波器输出中最适合传递给抑制器的那一个,并且在切换的时候进行平滑过渡。然后计算采集信号的加窗FFT为Y,线性输出结果的加窗FFT为E,计算残余回声的功率谱S2_linear((Y-E)^2)。计算Y的频谱Y2,E的频谱E2。
Step5:更新 AEC状态信息
v输入以下变量
-
external_delay
:外部延迟的可选值,表示从延迟估计器报告的外部延迟。 -
adaptive_filter_frequency_responses
:自适应滤波器的频率响应,以二维数组的形式提供,其中每个通道都是一个包含kFftLengthBy2Plus1个浮点数数组。 -
adaptive_filter_impulse_responses
:自适应滤波器的冲激响应,以二维数组的形式提供,其中每个通道都是一个包含浮点数数组。 -
render_buffer
:渲染缓冲区的引用,用于获取渲染信号的相关数据。 -
E2_refined
:经过改进的E2数据,以二维数组的形式提供,其中每个通道都是一个包含kFftLengthBy2Plus1个浮点数数组。 -
Y2
:Y2数据,以二维数组的形式提供,其中每个通道都是一个包含kFftLengthBy2Plus1个浮点数数组。 -
subtractor_output
:减法器输出数据,以SubtractorOutput结构体的数组形式提供。
对aec_state中10个估计器和变量作更新,具体见AecState::Update变量。
Step6:选择线性滤波后的输出Y_fft,并分高低频段计算舒适噪声
Step7:估计残余噪声
第一步就是更新参考信号的噪声功率,它分为以下两步:
1.得到参考信号的功率谱render_power;
2.以最小统计方式估计稳态噪声功率。
第二步就是生成残余回声估计。它依赖于一个变量分为了两部分,就是是否使用线性处理的结果来做残余回声估计,决定这一点依赖于两个条件,分别是filter_quality_state.LinearFilterUsable() 和config.filter.use_linear_filter,两者缺一不可。
1.若使用线性处理的结果来做残余回声估计,则先判断参考信号是否爆音,若爆音了,则认为残余回声估计就是参考信号,这样做是因为我们认为回声足够大到淹没了我们感兴趣的说话声,所以直接全部去掉就可以了,这样的缺点是在有少许近端说话声音的时候会造成无声;若没爆音,则根据aec_state.ErleUncertainty()得到erle_uncertainty,对于爆音情况下来说,erle_uncertainty=1.0f(但是我感觉有了前面那个条件,这里也不可能走到这个分支啊,存疑),否则的话erle_uncertainty为nullpot。然后就开始估计了,对于爆音情况下,使用S2_linear和1.0f来计算得到R2,对于其他情况下,则使用S2_linear和计算出来的Erle来估计R2。
下面重点讲一下使用线性处理结果估计R2的函数LinearEstimate,其本质上是一样的实现,只不过一个里面erle的一项为固定的float数据,另一个就是erle的数组了,所以这里讲解我都以通用形式的输入参数为S2_linear、erle来解释。它的计算过程就是对于每一个通道ch,对于每一个频点k,R2= S2_linear / erle。在完成这些操作之后,将混响的估计功率添加到残余回声功率,至此R2计算初步完成。
2.若不适用线性处理的结果来做残余回声估计。则执行下面的步骤:
第一步是执行GetEchoPathGain得到echo_path_gain。若是透传模式,则gain_amplitude=0.01,否则为config的默认值,然后返回gain_amplitude*gain_amplitude。
第二步依旧是判断是否爆音,若爆音则残余回声估计就是参考信号;否则的话,估计回声产生信号功率(将回声生成信号功率估计为一个时间窗口内的门控最大功率)X2,若不用平稳特性,则对回声产生功率应用软噪声门限,然后减去静态噪声功率以避免静态噪声导致过度的回声抑制得到最终的X2,然后使用NonLinearEstimate估计R2,如果是非透传模式,则在R2加上混响。
第三步,如果使用平稳特性的话,则根据回声可听度缩放回声。
Step8:非线性处理
若使用线性滤波输出,对E2作限制,取E2和Y2各个频点的最小值赋值到E2,赋值nearend_spectrum、echo_spectrum为非线性处理作准备;
非线性处理也是分为低频带和高频带。
对于低频带,其处理位于LowerBandGain()函数中,首先让gain全部置1,通过限制先前增益的增益增加来计算最大增益。然后对于每一个通道来说,将线性输出求解平均,保存在nearend中。然后以可懂度为条件来加权前面计算出来的残余回声得到weighted_residual_echo,然后使用加权后的残余回声、上一帧近端、上一帧回声、low_noise_render计算得到min_gain【仅在爆音情况下更新,否则为0】。最后的关键就是计算增益。
增益的计算过程如下:对于每一个频点来说,首先计算ENR(估计的残余回声能力与线性滤波器输出能力的比值)和EMR(回声能量与舒适噪声能量的比值)。然后如果enr大于一个门限值且emr大于一个门限值的话,增益gain为(p.enr_suppress[k]-ENR)/(p.enr_suppress[k] – p.enr_transparent[k])和p.emr_transparent[k] / emr两者的最大值。p.enr_suppress是ENR的一个较小门限,enr_transparent是一个较大门限。emr_transparent_ / emr是抑制到底噪水平对应的gain, voip场景可以不保留底噪。 这种简单计算目前发现的问题是信噪比低时容易剪切,非线性失真较严重时回声消得不太干净。
计算完增益就是对其进行一些限制,避免出现问题。
EchoRemoverImpl::FormLinearFilterOutput
该函数的作用是根据一些条件选择线性滤波器的输出数据,并将结果存储在 output
数组中。
函数接受两个参数:
-
const SubtractorOutput& subtractor_output
:一个结构体对象,包含了几个成员变量,如e_refined
、e_coarse
、e2_coarse
、e2_refined
、y2
、s2_refined
和s2_coarse
。这些变量用于计算和比较条件,以选择滤波器的输出。 -
rtc::ArrayView<float> output
:一个浮点数类型的数组,用于存储滤波器的输出结果。
函数的执行逻辑如下:
-
首先,函数使用断言
RTC_DCHECK_EQ
来确保subtractor_output.e_refined
和output
的大小相等,以及subtractor_output.e_coarse
和output
的大小相等。 -
接下来,函数定义一个布尔变量
use_refined_output
并将其初始化为true
。 -
如果use_coarse_filter_output_为真,则根据一些条件判断是否选择粗滤波器的输出。具体的条件判断包括:
-
subtractor_output.e2_coarse
小于0.9f * subtractor_output.e2_refined
。 -
subtractor_output.y2
大于30.f * 30.f * kBlockSize
。 -
subtractor_output.s2_refined
大于60.f * 60.f * kBlockSize
,或者subtractor_output.s2_coarse
大于60.f * 60.f * kBlockSize
。 如果这些条件都满足,则将use_refined_output
设置为false
。
-
-
如果上述条件判断不满足,那么判断是否选择细滤波器的输出。具体的判断条件是:
-
subtractor_output.e2_coarse
小于subtractor_output.e2_refined
。 -
subtractor_output.y2
小于subtractor_output.e2_refined
。 如果这些条件都满足,则将use_refined_output
设置为false
。
-
-
最后,调用
SignalTransition
函数,根据选择的滤波器输出,将subtractor_output.e_refined
或subtractor_output.e_coarse
的数据传递给output
数组。同时,将refined_filter_output_last_selected_
的值更新为use_refined_output
。
总结起来,这段代码根据一些条件判断选择线性滤波器的输出数据,并将结果存储在指定的数组中。具体选择细滤波器输出还是粗滤波器输出的条件包括功率比较和阈值判断。选择的结果会影响下一次函数调用时滤波器输出的选择。