实时音频处理链路
从贝尔实验室发明电话后,实时音频处理的技术不断发展,至今基于 VoIP(Voice over Internet Protocol,基于 IP 的语音传输)的实时音频互动已经慢慢成为人们日常音频在线交流的主流方法。图 1 展示的就是这样一个“现代版”的基于 VoIP 的实时音频处理链路。
实时音频可以分为上行链路和下行链路两个部分,其中上行链路中的音频处理步骤可以对应图 1 中的上半部分。我们可以看到,在采集模块(Recording)之后就是我们常说的“3A”处理,包括了 AEC(回声消除)、ANS(降噪)、AGC(音频增益控制)这 3个部分。之后在 Voice Beautifier and Effects(美声音效)模块我们可能会对人声做一些美化或者加入一些音效,来提升音频的听感。最后把音频进行编码(Encode)传输,至此
上行链路也就结束了。
下行链路主要的流程是先从网络中获取音频包进行解码(Decode)。由于网路传输过程中可能会出现网络抖动、丢包等现象,而下行链路中的 NetEQ 模块就是用来解决这些网络带来的问题。随后在播放端可能会需要一些个性化的空间音频渲染(Spartial Audio Rendering ),把音频转换成符合空间听感的多声道音频。最后通过播放模块(Playback)将音频通过播放设备播放出来。
好的,了解了实时音频的处理步骤,我们再聚焦下 AEC 的位置。AEC 的位置放在紧挨着采集模块的地方。上节课我们介绍过 AEC 需要对回声路径做估计,如果有别的模块放在AEC 之前就会导致回声路径无法收敛到正确的位置。比如将 AGC 放在 AEC 之前,那么AGC 模块导致的音量波动就可能导致回声信号忽大忽小,从而 AEC 中滤波器的系数就很难收敛了。但这样的顺序安排是否就没问题了呢?
在链路式的处理方法中上游的算法会对下游的算法产生影响。AEC 在回声消除的时候如果对近端的声音也造成了损伤,比如,近端的部分噪声被当做回声消除了。之前降噪的部分我们讲过降噪模块由于也需要输入信号中的噪声来做噪声估计,如果输入信号中的噪声不是连续的(有部分被 AEC 干掉了),那降噪模块可能就会估计出一个偏小的噪声。这种情况下,回声虽然没有泄漏,但噪声的残余却增加了。
另外,回声消除的远端信号实际上需要的是扬声器播放的信号,也就是说需要在靠近Playback 模块之前获取。如果信号在送往扬声器之后被系统再处理一遍(譬如动态范围控制,简称 DRC),就会干扰到 AEC 的效果。
所以音频的体验是一个需要整体考虑的问题,我们在音频模块的改进和修改的时候一定要对音频链路有一个整体的认识和理解,不然很可能会出现一些意想不到的体验问题。
回声消除算法案例分析
下面我们就结合 2 个常见的案例来分析分析,了解一下回声消除算法可能出现的问题以及回声消除算法的一些改进方向。
案例 1:混响
现在办公室的房间里有很多周围是玻璃墙的会议室,而在这些房间里回声泄漏的现象出现的概率很高。这是为什么呢?这里需要一点声学知识,即光滑且表面坚硬的墙壁,声波在其表面反射后能量衰减较小。也就是说声波可以在房间墙面多次反射,而能量依旧不会完全衰减。那这些不断反射的声音就形成了一个持续时间很长的混响,也就是所谓的“余音绕梁”。
我们知道,在回声消除中自适应滤波器中滤波器的长度是固定的。如果混响持续的时间超过了滤波器的长度,那么回声路径就不能被完整估计出来,从而就会导致回声泄漏。这种情况下,你可能会想,如果我们发现这些玻璃房间的混响时间太长,是不是把自适应滤波器的长度拉长就可以解决问题了呢?回答这个问题之前,我们先看一下下面的混响衰减曲线图。
如图 2 所示,混响时间我们一般用 RT60 来表示,也就是房间混响(Reverberation Time)衰减 60dB 所需要的时间。普通房间的混响的 RT60 大约在 50~200ms 左右,但有的玻璃房间或者大房间的 RT60 可能高达 200ms 甚至更多。假设我们音频的采样率是16kHz,也就是说我们的自适应滤波器至少需要 3200 的长度,如果我们直接使用 NLMS来计算,那可能实时性就没有办法得到满足。
为了克服滤波器长度太长造成的算力过大的问题,我们一般用分块频域自适应滤波器(Partitioned Block Frequency Domain Adaptive Filter, 简称 PBFDAF)来解决。自适应滤波器耗时最长的就是卷积计算。而卷积计算是可以采用 FFT 来进行加速计算的。
PBFDAF 的思想就是先把滤波器的权重和输入向量都分成等长的多块,然后通过 FFT 变换到频域来进行频域滤波,之后再通过 IFFT 得到滤波后的时域信号,最后把所有小块的结果叠加起来就可以得到滤波器的输出向量了。
卷积的算力是 O(n²),而 FFT 的算力是 O(nlog(n)),这样就实现了卷积计算的加速。实际上现在主流的开源算法像WebRTC 和Speex,在计算 NLMS 时都是采用类似这样的频域计算来对 NLMS 进行加速的。
回到我们之前的问题,即混响情况下,如果滤波器长度不够就会导致回声泄漏,但是自适应滤波器的长度太长也会导致收敛缓慢。就算是使用了 PBFDAF 做加速,滤波器的长度也不能随意设置。因此,在实际中我们可以先估算一下场景中 RT60 的分布。
例如,一般办公场景中正常办公室 RT60 很少超过 200ms,但可能会经常出现需要快速收敛的场景,比如房间的切换,那么滤波器可以短一些。但一些课程直播或者娱乐场景中场地有很长的 RT60 时间,但场景一般不会切换,这时则可以把滤波器长度调整得稍微长一些。
案例 2:非线性
Speex 作为较早开源的实时音频库,有不少实时音频系统都在使用它。但我们在实际使用的过程中会发现,Speex 在一些采集播放设备的失真比较大的时候效果就比较差了。
在上一讲中我们知道 NLMS 其实只能解决线性部分的变化,而扬声器、麦克风导致的非线性变化则可能需要非线性处理(Non-linear Process,NLP)来帮助解决。而 Speex 并没有 NLP 模块来处理这种非线性的回声残留,这也正是 Speex 的效果在不同设备上表现不稳定的原因。
相比较于 Speex 这种主要为 DSP 设备服务的算法,WebRTC 中的 AEC 由于考虑到要适配不同的设备终端,所以相比于 Speex,其主要增加了延迟估计和非线性处理这两个步骤来提高 AEC 算法的鲁棒性。那么 WebRTC 中 AEC 算法里的非线性处理是怎么做的呢?
WebRTC AEC 代码中的 NLP 的做法如图 3 所示,主要是利用信号之间的相干性来做回声抑制的判断。这里的相干性主要是指近端信号 d(n)和残差信号e(n) 之间的相干性 ,以及近端信号 和远端信号 之间的相干性 。
其实这很容易理解:如果 Cde越大,则代表近端信号和残差信号相似度高,也就是说回声很小,那么就越不需要去抑制;而如果 Cdx越大,则代表近端信号和远端信号很相近,需要更多地抑制回声。
注意,这里由于非线性部分的回声可能是出现在某些频段上的,所以需要先把频带分解成多个的子带,且在每个子带上都做一个相干性的判断,然后再综合各个子带的相干性来计算回声抑制因子。而根据不同的回声状态,比如双讲情况下线性部分回声泄漏比较大,则需要增加一些 overDrive 的操作。
比如,本来可能算出来需要抑制 50% 的频谱能量,但由于判断此时是回声泄漏比较多的场景,这里再多做个 0.2 倍,变成抑制 60%。这里的 overDrive 就是作为经验值进一步防止回声泄漏。
值得注意的是,WebRTC 中 NLP 的处理在双讲情况下很容易把近端的噪声也一起干掉了,从而会影响到后续降噪模块的处理。所以我们一般会在 NLP 之后再补一个舒适噪声,用来保证降噪模块中噪声估计不会受到太大的影响