webrtc aecm回声消除模块

197 阅读10分钟

日期:2022年11月1日

一、AECM的算法介绍与整体框架

1.1:算法整体框架

AECM 属于 WebRTC 语音处理引擎(Voice Engine)的子模块,是为移动设备专门设计的回声消除处理模块,其内部有根据芯片类型进行汇编指令级的特殊优化。AECM 的主体工程文件可以从 WebRTC 工程根目录下的modules/audio_processing/aecm 子目录找到,包含 delay_estimator.c、delay_estimator_wrapper.c、aecm_core.c、aecm_core_c.c、echo_control_mobile.c 这五个回声消除的核心功能的 c 语言实现文件。

其中: delay_estimator.c、delay_estimator_wrapper.c 用于回声时延的精确估计;

aecm_core.c、aecm_core_c.c 则是回声消除的核心工作流程,包括将远端信号从缓冲区中取出、将信号转换到频域 、远端信号对齐、自适应滤波器更新以及回声消除等功能;

echo_control_mobile.c 则是对核心功能的一层包装,它提供对外直接调用的接口函数;

其 中 常 用 函 数 有 五 个 : WebRTCAecm_Create用于为核心数据结构分配内存,

WebRTCAecm_Init 用于核心数据结构以及部分函数指针的初始化 ,

WebRTCAecm_BufferFarend用于向回声消除模块写入远端信号,

WebRTCAecm_Process 用于写入向回声消除模块写入近端信号并同时得到处理完成后的输出信号。

WebRTCAecm_Free 在整个回声消除工作完成后释放内存,

其中WebRTCAescm_Process 是回声消除算法的主体部分的入口,由它的内部包装了核心功能的调用。

1.2:AECM的工作流程

从图可以看到,AECM模块的自适应滤波、滤波器系数的更新以及回声消除工作都在频域进行,然后将处理后的频域信号变换到时域后直接输出。

AECM回声消除的主体功能都由WebRTCAecm_Process 函数内部调用,因此可以通过一步步分析其调用链来查看和分析 AECM 的核心工作流程。下图为 WebRTCAecm_Process 函数调用链示意图,其中横向箭头表示函数间嵌套调用,纵向箭头表示函数内顺序调用或处理过程,整个调用链比较复杂,因此笔者选择其中比较重要的几个函数和处理过程进行简要分析。

二、算法分析

2.1:回声时延估计

AECM的回声延迟估计策略使用的是Bastiaan's algorithm;

WebRTCAecm_Process 只是一个上层的包装函数,它内部有一个容量为 4000 的环形缓存,用来缓存输入的远端信号,然后由传入的时延估计参数 msInSndCardBuf 计算出远端信号缓存的初始读取位置 readpos 用来完成远端信号和近端信号的初步对齐:

其中 f 为抽样频率。当远端信号缓存中的数据未达到初始读取位置 readpos 时,则直接将输入近端数据复制到输出并直接返回;当缓存数据量达到 readpos,则在缓存中将数据从 readpos 处读取一帧出来并和当前近端信号帧后输入 WebRTCAecm_ProcessFrame 函数。由于 readpos 的存在,导致后续处理过程的远端信号和近端信号之间始终有长度为readpos 的时间差,从而使两个信号之间完成初步的对齐。

2.2:将输入远、近信号变换到频域

频域变换的主要实现过程在函数 TimeToFrequencyDomain 内部,它使用快速傅里叶变换将远端信号和近端信号变换到频域。在进行频域变换的时也是使用的定点数计算,输出信号频域表示也是采用定点数的形式。算法内部默认使用长度为 128 点 FFT 且不可更改。考虑到 FFT 可能会引起的印谱泄露和栅栏效应[49],AECM 在变换之前对时域信号采用加汉宁窗的方法缓解这一效应。汉宁窗也成升余弦窗,长度为 N 的离散汉宁窗为:

转存失败,建议直接上传图片文件

2.3:计算回声时延并对齐远端频域数据块

回声时延的计算主要实现在函数 WebRTCAecm_DelayEstimatorProcessFix 内部,它首先从远端频域数据块的 65 个频点中选取序号为 12~43 的 32 个频点,将这 32 个频点值的均值作为门限 H,然后将这它们依次与门限值比较进行二值化,从而将每一个远端频域数据块转换为一个 32 位的无符号整型:

其中 Xi 是远端频域块的第 i 个频点值,Bi 为二值化后的第 i 位二进制。将二值化后的 32位整型插入远端二值化整型缓存的最前端,其他数据依次后移,整个过程如下图所示。

在此之后,用相同的方法计算当前近端频域数据块的二值化整型 Db,然后遍历远端二值化整型缓存。将近端二值化整型与缓存中第 i 个历史数据 Xbi相做异或运算得到 di,取缓存中 di 最小的那个整型,它所代表的远端数据块与当前近端数据块的频谱差异最小,然后根据其在缓存中的位置来计算回声时延 delay,单位为 ms:

2.4:语音活动检测(VAD)

AECM 在 WebRtcAecm_CalcEnergy 函数中对远端信号进行 VAD。

AECM 中远端信号的 VAD 有两个目的:第一个是判定远端信号中是否含有语音,将一个块内远端信号的以 2 为底的对数域能量值 Elog与判决门限 T1 的值进行比较,如果 E 大于 T1 则认为远端信号中存在语音,反之则认为远端语音不存在;另一个目的是控制滤波器系数的更新,对远端频谱的每个频点的值,在该点对应频域点的幅值设置一个门限 T2,在系数更新时会进行判决,只有当该点频域幅度值大于 T2 才会对该点的滤波器系数进行系数更新。

除了利用远端信号对数域能量值 Elog 进行 VAD,WebRTCAecm_CalcEnergy 还根据该值计算并保存了一些用于后续计算的能量中间状态值,如远端能量最大值Emax、和远端能量最小值 Emin,其迭代计算规则如下:

其中E m a x 和E m i n 的初始值分别设置为-32767 和 32767。

2.5:滤波器步长计算及系数更新

AECM 的滤波器系数更新采用的是频域变步长 NLMS 算法(AEC使用的估算回声路径计算)。在更新滤波器之前首先根据计算得带的时延通过调用函数 WebRTCAecm_CalcStepSize 计算以 2 为底的负对数域步长 mu,如果 VAD 的结果为不存在语音,则将步长设置为 0。然后将步长参数和远、近端频域数据块传入WebRTCAecm_UpdateChannel 函数进行滤波器系数的更新。、

需要注意的是,AECM 内部默认采用双滤波器的结构进行系数更新。关于具体的更新算法可以参考 NLMS 算法的详细阐述,这里重点只介绍步长的计算方法。

根据上一小节中计算的能量中间状态值E m a x 和E m i n ,负对数域的步长 mu 的计算式如下:

其中μ m a x和μ m i n 都是常数,分别是负对数域步长的最大值和最小值,AECM 内分别设定为 1 和 10。

2.6:后续处理

2.6.1:NLP(非线性滤波)

WebRTC采用了维纳滤波器。此处只给出传递函数的表达式,设估计的语音信号的功率谱为Ps(w),噪声信号的功率谱为Pn(w),则滤波器的传递函数为:

2.6.2:CNG(舒适噪声产生)

WebRTC采用的舒适噪声生成器比较简单,首先生成在[0 ,1 ]上均匀分布的随机噪声矩阵,再用噪声的功率谱开方后去调制噪声的幅度。

三、使用webrtc注意的点

1)延时要小,因为算法默认滤波器长度是分为12块,每块64点,按照8000采样率,也就是12*8ms=96ms的数据,而且超过这个长度是处理不了的。

2)延时抖动要小,因为算法是默认10块也进行计算一次参考数据的位置(即滤波器能量最大的那一块),所以如果抖动很大的话找参考数据时不准确的,这样回声就消除不掉了。

3)算法基础方法WebRTCAecm_Process方法的样本数只能是80、或者160。

4)AECM两个关键的方法:

  /**
     * 设置远端信号参考帧
     * set the far-end signal of AECM instance.
     *
     * @param farendFrame
     * @param frameLength
     * @return the {@link AEC AEC} object itself or null if farendBuffer() is called on an unprepared AECM instance
     * or you pass an invalid parameter.
     */
    public AEC farendBuffer(short[] farendFrame, int frameLength) {
        // check if AECM instance is not initialized.
        if (!mIsInit) {
            Log.d(TAG, "farendBuffer() is called on an unprepared AECM instance or you pass an invalid parameter");
            return null;
        }
        if (nativeBufferFarend(mAecmHandler, farendFrame, frameLength) == -1) {
            Log.d(TAG, "farendBuffer() failed due to invalid arguments");
            return null;
        }
            return this;
    } 

    /**
     * 回声消除的控制方法,nearendClean为近端的认为干净没有回声的帧
	 * core process of AECM instance, must called on a prepared AECM instance. we only support 80 or 160 sample blocks
     * of data.
     *
     * @param nearendNoisy
     *            - In buffer containing one frame of reference nearend+echo signal. If noise reduction is active,
     *            provide the noisy signal here.
     * @param nearendClean
     *            - In buffer containing one frame of nearend+echo signal. If noise reduction is active, provide the
     *            clean signal here. Otherwise pass a NULL pointer
     *            or just call {@link #echoCancellation(short[] nearendNoisy, int numOfSamples, int delay)}.
     * @param numOfSamples
     *            - Number of samples in nearend buffer. Must be <= Short.MAX_VALUE and >= Short.MIN_VALUE
     * @param delay
     *            - Delay estimate for sound card and system buffers <br>
     *            delay = (t_render - t_analyze) + (t_process - t_capture)<br>
     *            where<br>
     *            - t_analyze is the time a frame is passed to farendBuffer() and t_render is the time the first sample
     *            of the same frame is rendered by the audio hardware.<br>
     *            - t_capture is the time the first sample of a frame is captured by the audio hardware and t_process is
     *            the time the same frame is passed to echoCancellation(). Must be <= Short.MAX_VALUE and >= Short.MIN_VALU
     * @return one processed frame without echo or null if echoCancellation() is called on an unprepared AECM instance
     *         or you pass an invalid parameter.
     */
    public short[] echoCancellation(short[] nearendNoisy, short[] nearendClean, int numOfSamples, int delay) {
        // check if AECM instance is not initialized.
        if (!mIsInit) {
            Log.d(TAG, "echoCancellation() is called on an unprepared AECM instance or you pass an invalid parameter");
            return null;
        }

        if (numOfSamples > Short.MAX_VALUE) {
            Log.d(TAG, "echoCancellation() numOfSamples > Short.MAX_VALUE, Short.MAX_VALUE will be used instead");
            numOfSamples = Short.MAX_VALUE;
        } else if (numOfSamples < Short.MIN_VALUE) {
            Log.d(TAG, "echoCancellation() numOfSamples < Short.MIN_VALUE, Short.MIN_VALUE will be used instead");
            numOfSamples = Short.MIN_VALUE;
        }

        if (delay > Short.MAX_VALUE) {
            Log.d(TAG, "echoCancellation() delay > Short.MAX_VALUE, Short.MAX_VALUE will be used instead");
            delay = Short.MAX_VALUE;
        } else if (delay < Short.MIN_VALUE) {
            Log.d(TAG, "echoCancellation() delay < Short.MIN_VALUE, Short.MIN_VALUE will be used instead");
            delay = Short.MIN_VALUE;
        }

        return nativeAecmProcess(mAecmHandler, nearendNoisy, nearendClean, (short) numOfSamples, (short) delay);
    }

四、AEC、AECM、AEC3的对比

1、aecm是移动端的回声消除模块,用于计算能力较弱的移动端或是额嵌入式设备而开发的,默认更少的CPU占用,aec是电脑端的回声消除模块,但AECM同时也带来了自己的劣势。

2、aec是最早期的版本,后续迭代更新中,aec3的出现替代了aec在webrtc中的地位。

3、关于信号处理流程,AECM算法包含了延迟估计策略,回声幅度包络估计,非线性回声抑制3个部分;与AEC不同,AECM是使用噪声抑制那一套方式抑制回声,即通过估计维纳滤波的增益值来进行工作;而AEC是通过估计回声路径,从而重构出原始回声来进行回声消除,因此在理论上AEC的处理效果就比AECM要好,虽然AECM中也有使用自适应滤波,但是它不是用来估计回声路径的而是回声幅度包络,其最终也是为估计维纳滤波的增益值做铺垫;由于是估计回声幅度包络,因此在计算量上就比原来的AEC就要小很多,并且使用了子带的方式来对回声幅度包络进行估计,相较于使用全频带可以大大减少计算量,同时处理后效果与全频带相差不大;

五、代码Demo仓库地址

gitee.com/edwinZwp/ae… (OSC-edwin / AECMProject)

参考资料

1、blog.csdn.net/qq_44085437… (WebRTC中AECM算法简介)

2、zhuanlan.zhihu.com/p/377750095 (WEBRTC-AECM算法浅析)

3、www.ktanx.com/blog/p/4739 (为何一直推荐WebRTC)

4、https://lequ7.com/guan-yu-webrtc-shen-ru-qian-chu-webrtcaec-sheng-xue-hui-sheng-xiao-chu.html** (关于webrtc:深入浅出-WebRTC-AEC声学回声消除)**

5、https://zhuanlan.zhihu.com/p/335570792** (硬货专栏 |深入浅出 WebRTC AEC(声学回声消除))**