简介:这是从最新WebRTC代码库中整理出的音频三A处理参考实现,聚焦自动增益控制、噪声抑制和回声消除三大核心模块,代码可独立编译,适合实时音视频开发者、音频算法学习者以及需要集成或定制音质优化功能的工程师。资源包共二百四十九个文件,压缩后约七点四九兆字节,除了C和C++源文件之外,还包含CMake构建脚本、Makefile配置文件、头文件、编译生成的目标文件以及少量WAV和PCM格式测试音频,既便于理清完整构建流程,也能直接进行编译验证。已有三百四十人学习下载。借助配套的示例音频和可运行代码,读者可对照分析算法处理前后的效果差异,深入理解增益调节、噪声抑制和回声消除的具体实现;也可修改参数或替换模块来适配不同应用场景,并结合最新代码结构,为二次开发和算法优化提供直接参考。 很多做音频算法或者实时通信开发的工程师,碰到“回声、噪声、音量忽大忽小”这类问题时,第一反应都是去 WebRTC 里找 3A 参考实现。这里的 3A 指的是音频前处理三件套:AEC(回声消除)、AGC(自动增益控制)、NS(噪声抑制)。但真正去对照最新源码的时候,不少人都卡在了同一道门槛上:代码最新版和网上教程对不上,API 改了;或者代码摆在那里,不知道怎么编译成一个能跑的示例。这篇文章就是围绕“可编译、最新参考代码”这个目标,把 WebRTC 音频 3A 从源码定位、编译环境到最小调用示例一次理清,适合正在做实时音频 SDK 集成、想借鉴 3A 算法思路、或者刚接手音频模块的开发者。

1. “最新参考代码”究竟指哪一份,为什么要强调可编译

先说结论:目前最值得当作参考的,就是 WebRTC 主干仓库里 modules/audio_processing 目录下的源码,也就是 Google 官方维护的那一份。很多人习惯去看一些第三方开源项目里复制出来的旧版本 3A 代码,那些代码看起来结构很清晰,但和最新的算法实现相比,可能差了好几个大版本。尤其是 2023 年以后,WebRTC 对 AEC3、增益控制器和噪声抑制模块都做过结构调整,API 也变了,如果以旧代码为参考去做二次开发,大概率会把第二梯队的参数搬回到新工程里,结果就是实时场景下表现不理想,还找不到原因。

强调“最新”,是因为 WebRTC 的代码版本策略很特别。它不像某些项目每半年发一个 tag,而是要求开发者跟着主干同步走。官方也提供了 branch-heads 这种稳定分支,但实际去看源码提交记录,很多算法修复都只会合到主干,分支上只会挑部分修复。所以如果你的目标是“参考最新算法思路”,看主干代码最准确。

强调“可编译”,则是因为 WebRTC 本身的构建系统对不少人是第一道坎。它使用 GN + Ninja,源码仓库里绑定了大量第三方依赖,包括 abseil、boringssl、libyuv 等,直接拷贝一个 .cc 文件到自己的工程里往往编译不过。所谓“参考代码”,如果不能先做成一个可运行的 demo,就谈不上真正理解模块间的调用关系。因此我后面所有步骤,都以“能编译、能调用、能跑通”为准绳。

从经验上讲,普通人需要的并不是把整个 WebRTC 工程集成到产品里,而是希望有一份能够编译成功的最小示例,用来观察 3A 模块在真实音频数据上的处理效果。这篇文章的目标就是带你拿到这份代码,并把它跑起来。

2. WebRTC 音频 3A 的模块拼图:AEC、AGC、NS 与 APM 的职责划分

很多人把 3A 理解成“一段处理代码”,但阅读 WebRTC 源码时你会发现,真正统一的入口是一个叫 AudioProcessing 的类,简称 APM。3A 并不是三个独立接口,而是通过 APM 内部配置组织起来的处理流水线。理解这层关系,比背下一百个 API 函数更重要。

2.1 AEC 模块:WebRTC 回声消除的核心

AEC 是 3A 里最复杂、也最影响体验的模块。WebRTC 里目前有两套回声消除实现:一套是旧的 EchoCancellerImpl ,主要给移动端低功耗场景用,另一套是 AEC3,面向高质量通话场景,算法更完整。

AEC3 在最新代码里的路径在 modules/audio_processing/aec3 ,它做的事情可以粗略分成三步:首先做延迟估计,也就是算出扬声器信号到达麦克风要经过多长时间;然后用自适应滤波器估计回声路径,把麦克风信号里的线性回声部分减掉;最后再做非线性处理,压制扬声器失真引起的残余回声。这个过程中最关键的一个概念是“参考信号”,通话场景下,参考信号就是远端声音播放出去的内容,你必须在处理近端麦克风信号时把这个参考信号也喂给 APM,否则 AEC 无法工作。

最新 AEC3 的代码里引入了很多细腻的控制逻辑,比如 RenderBuffer 和 CaptureBuffer 的分离开关,以及针对低延迟场景的 EchoPathVariability 检测。如果你只是照着旧教程调用 SetAecmMode ,在最新代码里基本是找不到的,因为接口已经整合到了 AudioProcessing::Config 里。

2.2 AGC 与 NS 模块的常见误读

AGC 的作用不是简单地把音量拉高,而是把不同距离、不同说话习惯的人声,归一化到一个相对稳定的电平范围。WebRTC 里有 gain_controller1 和 gain_controller2 两套增益控制。

gain_controller1 是传统 AGC,偏保守,通常配合模拟增益和数字增益一起工作,适合需要与底层设备增益联动的场景。 gain_controller2 (GC2)是相对较新的实现,思路更像是“智能响度控制”,它会根据语音活动检测和信号电平估计动态调整增益,减少背景噪声被过度放大的问题。但注意,这两套不能同时开启,否则等于给信号做了两次增益,音频会忽大忽小,甚至出现严重的削波失真。

NS 噪声抑制模块相对单纯,核心是把平稳噪声从信号里分离出去。WebRTC 的 NS 在低信噪比场景下比较保守,宁愿保留一点噪声也不愿意把语音弄“破”。有人误以为把 NS 的等级调到最高就能解决一切噪声问题,实测下来反而会让语音发闷,正确做法是区分平稳噪声和非平稳噪声,对风扇声、空调声这类平稳噪声用 NS 压制,对键盘声、关门声则更多依赖 VAD 和舒适噪声生成。

2.3 最新源码中 APM 的封装逻辑

AudioProcessing 在最新代码里的使用方式和三年前已经有明显不同。以前你可能需要分别创建 EchoCancellation 、 GainControl 、 NoiseSuppression 这些子模块,再逐个开关启用。现在这些配置都集中在一个 webrtc::AudioProcessing::Config 结构体里,通过 ApplyConfig 一次推入。这种设计的好处是让模块间的参数依赖关系更容易约束,比如 AGC 需要知道是不是在 AEC 之后工作,以避免控制环路打架。

APM 对外暴露的核心接口有三个: AnalyzeReverseStream 用来送入扬声器参考信号, ProcessStream 用来处理麦克风采集到的近端信号, ProcessReverseStream 则用于需要做“回环处理”的场景。理解这三个接口的名称和调用顺序,是后面写最小示例的基础。

3. 编译准备:源码获取与 GN 构建的完整链路

3.1 获取源码时的关键操作

获取 WebRTC 源码的标准工具是 depot_tools ,官方 README 里的流程大致是:

git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH="$PWD/depot_tools:$PATH"

然后建立一个文件夹,在里面执行:

mkdir webrtc-audio-3a
cd webrtc-audio-3a
fetch --nohooks webrtc
gclient sync

执行完 fetch 以后,源码会放在 webrtc/src 目录下。这一步有两个常见问题。第一, fetch 和 gclient sync 必须在国内网络环境下稍等一会儿,源码整体体积比较大,包含十几个 G 的提交历史和依赖缓存,如果只想要最新代码,可以考虑用 --shallow 减少历史记录,但首次同步时有些依赖模块仍需要完整拉取。第二, fetch --nohooks webrtc 后面一定要跟一个 gclient sync 来完成依赖的下载,否则直接进 src 目录编译大概率缺头文件。

从官方维护性来说,我建议不要轻易去改仓库里的 DEPS 文件来“裁剪”依赖,否则升级代码时会非常痛苦。我的实际做法是完整同步一份代码,用来学习和编译;真正运行时,只在构建产物里链接自己需要的 audio_processing 相关静态库。

3.2 用 GN 生成构建文件

WebRTC 使用自己那套 GN 构建系统,进入 src 目录后,典型操作是:

cd webrtc/src
gn gen out/Default --args='is_debug=false is_component_build=false use_custom_libcxx=false'

其中 is_debug=false 是 release 构建, is_component_build=false 会把 WebRTC 编译成静态库,方便后续链接到自己的工程里。 use_custom_libcxx=false 是为了避免 WebRTC 自带的 libc++ 版本干扰系统的默认编译环境,尤其是你会把示例代码和系统编译器混着编的情形。

生成完构建文件以后,编译音频处理核心模块:

ninja -C out/Default audio_processing

如果编译过程中出现 absl 相关错误,先检查是不是没有执行 gclient sync 。WebRTC 的 GN 构建会自动拉取依赖,但不一定会在显式错误信息里提示你“依赖缺失”。另外, audio_processing 只是模块目标,如果后续要运行自定义可执行文件,通常还要编译 webrtc 里自带的测试框架,不过那不是本文的目标。

3.3 编译目标的选择与裁剪思路

我们真正关心的目标主要是:

  • audio_processing :APM 核心模块,包含 AEC、AGC、NS 在内的一组对象文件
  • audio_processing/api :公共头文件和 API 目标
  • rtc_base :WebRTC 的基础工具库, AudioFrame 、日志、线程等都在这里

如果你不希望编译整个 WebRTC,只想快速验证 3A 效果,可以在 src/webrtc/examples 或自己的目录下新建一个 GN target,专门编译最小示例。下面是一个最小的 GN 文件写法:

import("//build/config/c++/c++.gni")

static_library("audio_3a_minimal") {
  sources = [ "audio_3a_minimal.cc" ]
  deps = [
    "//modules/audio_processing:audio_processing",
    "//modules/audio_processing:api",
    "//rtc_base:rtc_base_approved",
  ]
}

这样构建时只需要执行:

ninja -C out/Default audio_3a_minimal

就能把 3A 相关依赖一起编进来,不需要关心 audio_processing 内部还有多少细粒度的 target,这对快速验证是最省事的。

4. 最小可编译示例:初始化 APM 并跑通 10ms 音频帧

建一个 audio_3a_minimal.cc ,目的只有一个:读入一段 16kHz 单声道的原始 PCM,经过 APM 的 3A 处理后输出到另一个文件,同时用打印日志的方式确认模块确实生效。

4.1 初始化 AudioProcessing 实例

使用时先创建实例并配置 3A:

#include <fstream>
#include <iostream>
#include <memory>

#include "modules/audio_processing/include/audio_processing.h"
#include "modules/audio_processing/include/audio_processing_builder.h"
#include "rtc_base/checks.h"

int main(int argc, char* argv[]) {
  // 创建一个 AudioProcessing 实例
  std::unique_ptr<webrtc::AudioProcessing> apm(
      webrtc::AudioProcessingBuilder().Create());

  webrtc::AudioProcessing::Config config;
  config.echo_canceller.enabled = true;
  config.echo_canceller.mobile_mode = false;   // false 表示使用 AEC3
  config.gain_controller1.enabled = true;       // 使用传统 AGC
  config.noise_suppression.enabled = true;
  config.noise_suppression.level =
      webrtc::NoiseSuppression::Config::Level::kModerate;
  apm->ApplyConfig(config);

  // 设置处理帧的参数:16kHz、单通道、10ms
  constexpr int kSampleRate = 16000;
  constexpr int kNumChannels = 1;
  constexpr int kFrameLength = 160;  // 16000 * 10ms / 1000
  constexpr int kStreamDelayMs = 50; // 模拟延迟,按实际设备调整
  apm->set_stream_delay_ms(kStreamDelayMs);
  ...
}

这里有两个容易忽略的细节。

第一, mobile_mode = false 表示启用 AEC3,而 true 会走旧的移动端回声消除实现。AEC3 更注重音质,但计算复杂度更高;如果你在做嵌入式低功耗设备,可以设成 true 来对比。

第二, set_stream_delay_ms 必须调用。这个数值代表从扬声器播放到麦克风采集之间的系统延迟,数值不对,AEC 的延迟估计模块会陷入混乱,回声消除效果会很差。拿不准时可以先设一个估计值,再根据实测去校准。

4.2 读取 PCM 并处理每一帧

接下来用 std::ifstream 读取原始 PCM 数据。由于 16kHz 单声道 16bit 格式下,10ms 对应 160 个采样点,也就是 320 字节。处理时用 webrtc::AudioFrame 存放数据:

  std::ifstream in_file("input.pcm", std::ios::binary);
  std::ofstream out_file("output.pcm", std::ios::binary);
  if (!in_file.is_open() || !out_file.is_open()) {
    std::cerr << "cannot open pcm file" << std::endl;
    return -1;
  }

  webrtc::AudioFrame capture_frame;
  capture_frame.sample_rate_hz_ = kSampleRate;
  capture_frame.num_channels_ = kNumChannels;
  capture_frame.samples_per_channel_ = kFrameLength;

  while (in_file.peek() != std::ifstream::traits_type::eof()) {
    int16_t input[kFrameLength];
    in_file.read(reinterpret_cast<char*>(input), sizeof(input));
    if (in_file.gcount() < static_cast<std::streamsize>(sizeof(input))) {
      break;
    }

    memcpy(capture_frame.data(), input, sizeof(input));

    // 如果没有远端参考信号,这里可以简单把同一帧作为参考。
    // 实际通话场景中,_必须_ 使用远端即将播放的音频数据。
    webrtc::AudioFrame render_frame = capture_frame;
    apm->AnalyzeReverseStream(&render_frame);

    int result = apm->ProcessStream(&capture_frame);
    if (result != webrtc::AudioProcessing::kNoError) {
      std::cerr << "ProcessStream error: " << result << std::endl;
      return -1;
    }

    out_file.write(reinterpret_cast<const char*>(capture_frame.data()),
                   sizeof(int16_t) * kFrameLength);
  }

这段代码要注意:把同一个文件既当作参考信号又当作近端麦克风信号,只是为了验证代码能跑通,不具备实际的回声消除意义。真要测试 AEC 效果,需要准备两路声音:一路是扬声器实际会播出的远端文件,另一路是包含回声和近端说话的麦克风录音文件,两者时间对齐后才能看出 AEC 的真实表现。

4.3 运行示例并验证输出

在 WebRTC 源码目录里新建一个子文件夹,把上述代码放进去,配置好 GN target 后编译运行:

ninja -C out/Default audio_3a_minimal
./out/Default/audio_3a_minimal

得到的 output.pcm 应该比输入在响度上更稳定,背景噪声变得更低。如果没有变化,先检查 config 中的开关是否真的设置成功;如果编辑配置后没有调用 ApplyConfig ,则修改不会生效,这类低级问题在实际开发中经常出现。

5. 参数调优与真实数据调试心得

5.1 AEC 参数在不同场景下的取舍

AEC3 在最新代码里并没有像旧版那样暴露大量“暴露级”参数,更多是封装在 Config.echo_canceller 里。实际调试时,真正影响效果的是几个外部参数:

  • mobile_mode :适合资源受限的低功耗设备,算法做了一定降级。
  • 采样率:越高,延迟估计越精细,但计算量也越大。建议先用 16kHz 验证逻辑,再切到 48kHz。
  • 是否启用 convergence_check 等内部开关:有些开关默认开启,出现误杀或残留回声时,可以关闭后对比测试。

不同场景下,AEC 的收敛速度和稳态效果存在矛盾。会议场景希望快速收敛,因为说话人位置相对固定;K歌或者外放游戏场景则希望稳态时能应对扬声器音量的突然变化,这时 AEC3 的 EchoPathVariability 检测会不断调整滤波器,参数上不能一味追求“压得干净”。

5.2 用真实采集数据而不是单测数据验证

WebRTC 官方自带的测试音频大多是合成的或者专门录制的,干净得过头。自己调试时,一定要录制真实硬件环境下的音频:

  • 用手机外放一段音乐,让麦克风近距离采集,模拟回声场景
  • 在空调房、风扇附近录一段带平稳噪声的语音
  • 说话人距离变化,模拟音量忽大忽小

然后把这些数据放入示例程序,观察输出频谱图或者直接听。实际操作中,我发现很多“AGC 把噪声放大了”的问题,并不是 AGC 算法本身的问题,而是增益控制器把很低电平的噪声当成信号提升了。处理这类问题通常是把 NS 的等级调高一点,或者调整 AGC 的 target_level_dbfs ,让 AGC 更不容易对弱信号产生灵敏反应。

5.3 从音频数据管理到稳定性防御

在调试 3A 时,我还会做一件很基础但有效的事:把每次输入输出的 PCM 都保存下来,命名带上时间戳。这样一旦听众反馈“有回声”“声音很小”,我可以快速找到当时那一段音频,用相同的参数重新跑一遍离线测试,判断是算法参数问题,还是网络丢包、设备切换等外部因素。这也是“参考代码可编译”之外最有价值的工程习惯。

6. 落地改造时的实战经验

如果你已经跑通上面的示例,下一步大概率是想把 3A 集成到自己的实时音频链路里。这里分享几个我踩过的坑。

6.1 不要直接搬整个 WebRTC

WebRTC 内部为了支持浏览器场景,做了很多和 3A 本身无关的依赖绑定,例如线程模型、任务队列、网络模块。直接把整个 WebRTC 作为 SDK 引入会带来巨大的体积和复杂度。更实际的做法是只搬运 modules/audio_processing 和它依赖的底层库,比如 common_audio 、 rtc_base 、 absl ,然后用 GN 编译成静态库,再对外暴露一层自己的 C 接口。这个过程有点繁琐,但只要 GN target 梳理清楚,维护起来会比“用一个第三方魔改版本”放心得多。

6.2 与系统自带 3A 的处理顺序

很多平台本身也提供 AEC、AGC、NS 实现,比如 Android 系统的 AudioEffect。如果 WebRTC 的 3A 和系统 3A 同时开启,会出现“双重回声消除”和“双重增益控制”的怪问题。实际经验是二选一:要么用系统处理硬件层面的回声,把比较干净的信号交给 WebRTC 做专业优化;要么关掉系统效果,把原始信号完整交给 WebRTC。千万不要两级都开着,否则排查时你会被奇怪的声音变化绕晕。

6.3 推荐在最终项目里保留的调试接口

不管你在什么平台上集成 3A,建议至少保留两个调试口:一个是实时打印每个处理帧的 RMS 电平,另一个是能把处理前后的 PCM 数据导出来。前者用来在线上粗糙判断 AGC 是否异常,后者用来离线复现问题。很多团队成员一开始嫌麻烦,等出了线上反馈时才发现没有现场数据,只能靠猜。

如果你做的是商业音频 SDK,还可以把 WebRTC 3A 的配置文件做成动态下发,这样即使某个会议场景下算法表现不好,也能远线程调整参数,而不是要求用户升级版本。WebRTC 的源码足够专业,把它当作“参考代码”去理解,而不是当作“黑盒库”去调,才是长期演进最省力的姿势。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

Logo

邀请您加入社区

更多推荐