WebRTC音频3A可编译代码实战:从源码拉取到工程搭建
简介:这是面向音视频开发者的WebRTC音频3A可编译参考代码,聚焦AGC自动增益控制、ANS自动噪声抑制与AEC自动回声消除三大核心算法,适合优化实时通话、在线会议或直播音质的开发者参考与二次开发。资源包共249个文件、约7.49MB,除c/cc源文件与h头文件外,还有CMake及Makefile构建脚本、sample示例、wav/pcm测试音频,以及o/bin等中间产物,便于理解工程组织与验证流程。已有340人学习,代码属于较新参考版本,可在本地编译运行,结合示例音频观察3A效果,也可修改算法参数或移植到自有项目。对研究WebRTC音频链路、开展课题或产品集成的开发者,这套代码能省去自行整理的时间,是实用的学习与工程素材。 做音频开发的兄弟应该都知道,WebRTC里有一套非常经典的音频前端处理模块,核心就是行业里常说的“3A”:AEC回声消除、AGC自动增益、ANS降噪。可真正想把这套代码从WebRTC源码里抠出来,整理成一份“可编译、能跑通、方便自己改”的参考代码,很多人会在拉源码、搭工程、补依赖这几个环节上卡上好几天。这篇文章就基于我最近一次在最新主干上整理 webrtc audio 3a 代码的实操过程,把可编译参考代码的获取路径、工程组织方式和排查思路完整写出来,给准备研究或移植这套算法的朋友一条能直接抄的近路。
1. WebRTC音频3A是什么:先弄清楚你要编译的这坨代码解决什么问题
1.1 AEC、AGC、ANS各自负责什么
3A不是某一个文件,而是一套音频前处理链路。AEC负责从麦克风信号里消除扬声器播放出去的参考信号,解决“对方听到自己回声”的问题;AGC负责把音量自动拉到一个合适区间,不管说话人离麦克风远近,输出音量都不会忽大忽小;ANS负责压制环境噪声,让语音更干净。三者叠加之后,网络电话、会议系统、语音助手等场景里才能有基本的听感保障。
WebRTC的3A代码集中在 src/modules/audio_processing 目录下。最新代码里AEC已经是AEC3,支持桌面和移动端两种模式;AGC拆成了 gain_controller1 和 gain_controller2 两套实现;ANS则基于噪声估计器做谱减/增益衰减。值得注意的是,这些模块不像普通开源项目那样 README 一拉就能跑,它深度依赖WebRTC的公共基础库、ABSL库和音频帧封装,这也是我标题里强调“可编译”的原因。
1.2 为什么“可编译”今天仍然是个坎
网上搜“webrtc 3a”能拿到一堆源码片段,但大部分是直接从 audio_processing.h 里截取的调用头文件,真正能独立编译成库、放到自己项目里跑的非常少。原因有三:一是仓库体积大,完整拉一次源码动辄几个G;二是编译系统用的是GN/Ninja,很多人不熟悉;三是模块之间有隐藏依赖,比如 audio_processing 会引入 common_audio 、 rtc_base 、 absl ,漏一个就编不过。
所以“可编译”这个需求背后,其实是两个问题:怎么高效拿到一份最新的、完整的参考源码,以及怎么用最小代价把这些依赖关系理顺。这两件事搞清楚,后面编译就是顺水推舟。
2. 锁定代码入口:从WebRTC源码里找到3A模块
2.1 拉取源码的推荐路径
我自己的习惯是直接用depot_tools拉取,这是Chromium/WebRTC官方推荐的方式。流程大致是:
# 先安装depot_tools并加入PATH
mkdir webrtc
cd webrtc
fetch --nohooks webrtc
gclient sync --no-history
拉完之后源码会在 src 目录下, src/modules/audio_processing 就是我们关心的3A主目录。 --no-history 是一个很实用的参数,能省下不少磁盘空间和同步时间,适合我们只想要“最新参考代码”做学习编译的场景。如果不需要测试数据,还可以在 gclient sync 时控制部分子模块下载,但WebRTC的GN构建系统默认会检查DEPS,建议第一次还是按完整流程来,后续再自己裁剪。
2.2 目录结构里谁才是3A
进入 modules/audio_processing 后,先看几个关键目录:
-
include/audio_processing.h:对外主接口,所有3A调用都从这里走。 -
aec3/:最新一代回声消除实现。 -
agc/和agc2/:自动增益控制的老版与新版实现。 -
ns/:噪声抑制模块。
还有一个容易被忽略的 api/ 目录,里面定义了 AudioProcessing::Config 结构体,所有3A开关、模式、目标电平都通过这个结构体传入。编译时,默认依赖的GN target是 modules/audio_processing:audio_processing ,它会自动把 AEC3、AGC、NS 等子模块一起编进去。
2.3 用GN目标把依赖补齐
如果你不想在WebRTC全量工程里动手,想自己建一个独立目录引用3A代码,可以这么建一个BUILD.gn:
import("//webrtc.gni")
rtc_static_library("audio_3a_ref") {
sources = [
"audio_3a_ref.cc",
"audio_3a_ref.h",
]
deps = [
"//modules/audio_processing:audio_processing",
"//rtc_base:rtc_base",
]
}
这里的核心就是把 audio_processing 这个target作为依赖挂上。它内部已经帮你处理了AEC3、AGC、NS之间的依赖,不用再手动逐个添加。如果是Rust、JS等其他语言的封装项目,通常也是先把这份C++代码编成一个静态库,再去做FFI绑定。
3. 搭一个能编译的3A参考工程
3.1 最小工程长什么样
我建议一开始不要贪多,先做一个最小demo:读一个16-bit PCM文件,经过3A处理,再写回一个新的PCM文件。这样做的好处是能快速验证“AEC/AGC/ANS是否真的生效”,也能避开实时音频设备回调的复杂性。
工程目录可以这样组织:
webrtc/audio_3a_demo/
BUILD.gn
main.cc
把 main.cc 放在WebRTC源码树的 audio_3a_demo 目录下,main函数里只做四件事:初始化 AudioProcessing 、配置3A参数、按10ms帧读取PCM、调用 ProcessStream 。
3.2 BUILD.gn怎么写得少又稳
参考下面的写法:
import("//webrtc.gni")
rtc_executable("audio_3a_demo") {
sources = [
"main.cc",
]
deps = [
"//modules/audio_processing:audio_processing",
"//rtc_base:rtc_base",
]
}
这里我用了 rtc_executable 而不是 rtc_static_library ,目的是为了能直接生成一个可执行文件,方便命令行验证。如果你想把它编成动态库或静态库给上层App用,改成 rtc_static_library 或 rtc_shared_library 即可,依赖不用变。
3.3 main函数里怎么调用3A
核心调用代码大概长这样:
#include "modules/audio_processing/include/audio_processing.h"
using webrtc::AudioProcessing;
using webrtc::StreamConfig;
int main(int argc, char* argv[]) {
// 输入输出配置:48kHz,双声道
StreamConfig input_config(48000, 2, false);
StreamConfig output_config(48000, 2, false);
// 创建AudioProcessing实例
std::unique_ptr<AudioProcessing> apm(AudioProcessingBuilder().Create());
// 3A配置
AudioProcessing::Config config;
config.echo_canceller.enabled = true; // AEC
config.echo_canceller.mobile_mode = false; // 桌面端用AEC3
config.gain_controller1.enabled = true; // AGC
config.gain_controller1.mode =
AudioProcessing::Config::GainController1::kFixedDigital;
config.noise_suppression.enabled = true; // ANS
config.noise_suppression.level =
AudioProcessing::Config::NoiseSuppression::kHigh;
apm->ApplyConfig(config);
// 按10ms帧读PCM并处理
// int16_t input_block[480 * 2];
// int16_t output_block[480 * 2];
// while (fread(...)) {
// apm->ProcessStream(input_block, input_config,
// output_config, output_block);
// fwrite(output_block, ...);
// }
return 0;
}
注意 ProcessStream 要求每次处理固定10ms的数据,48kHz双声道就是 480 * 2 个int16样本。如果你给它传了任意长度的数据,很可能会直接返回错误码,或者出现做一些“有声音但处理效果不对”的怪问题。
3.4 编译命令与产物
在WebRTC源码根目录执行:
gn gen out/Demo --args='is_debug=false is_component_build=false'
ninja -C out/Demo audio_3a_demo
编译完成后,可执行文件会出现在 out/Demo/audio_3a_demo 。整个编译过程比一般人想象的要久,即使是增量编译,第一次也可能要十几分钟甚至更久,建议编译时用Ninja多线程(默认就是多核并行),不要开着其他重型任务抢CPU。
4. 最新参考代码里容易踩的粒度与配置坑
4.1 AudioProcessing接口变化
最新主干上 AudioProcessing 的接口比老版本收敛了很多。过去 AudioProcessing::ProcessStream 有多个重载,现在主要保留基于 StreamConfig 的方式。这意味着输入格式和输出格式必须显式声明,对习惯老接口的人来说是一个需要适应的点。
另外, Config 结构体的字段也发生了变化。比如 echo_canceller 已经不再是简单的布尔值,还包含了 mobile_mode ; noise_suppression 除了 enabled 还有 level 枚举;AGC则拆成 gain_controller1 和 gain_controller2 两个子配置。代码只要写错一个字段名,编译期就能报错,这其实是个好消息,反倒不容易运行时出错。
4.2 配置参数与10ms帧的强约束
3A里面的时间对齐和帧长约束非常强,尤其是AEC。它内部维护了一根参考信号延迟线,输入帧不固定,延迟估计就会乱,回声也就消不干净。因此,无论你的采集线程怎么回调,送到 ProcessStream 的数据块大小都要保持为 sample_rate / 100 * channels 。
如果你要做实时音频设备接入,建议参照WebRTC自带的 AudioTransport 或 media_file 相关代码,把设备回调先缓冲成10ms块,再交给3A。我一个朋友第一次写接入时,把音频设备一次给的20ms数据直接拆成两次调用,结果回声残留很明显,最后查了半天才发现是帧边界没对上。
4.3 从参考代码到自研工程
拿到参考代码能编译通过只是第一步。绝大多数人最终目标是把3A集成到自己的RTC引擎、耳机、音箱或者App里。我的建议是: 先原样跑通,再逐模块裁剪 。WebRTC的3A与它自己的音频设备模块、编解码器没有强耦合,是可以独立用的,但接口上会依赖 rtc_base 的日志、线程、事件等基础能力。想要彻底去掉这些基础依赖,需要做不少胶水层工作。如果是做商业产品,不要一开始就想着“源码都拿来了,把不需要的文件删掉就行”,依赖关系远比你表面看到的复杂。
5. 编译和运行中的典型问题排查
5.1 gclient同步和依赖问题
最常遇到的是 gclient sync 过程中因为子模块更新失败导致源码不完整。现象是编译时突然报某个头文件找不到,比如 absl/base/config.h 找不到,或者 modules/audio_processing/include/audio_processing.h 不存在。解决办法不是去网上乱下头文件,而是回到 src 目录重新执行:
gclient sync
如果同步了几次还不行,可以检查一下 DEPS 文件里对应子模块是否被本地分支锁住,必要时用 gclient config 重新生成 .gclient 文件。
5.2 编译通过但运行崩溃
这类问题多半不是3A本身的问题,而是你给的数据格式与配置不一致。比如配置的是48kHz双声道,实际传入的 StreamConfig 却写成了16kHz单声道, ProcessStream 内部就会出问题。还有一些崩溃是因为没有实例化 AudioProcessingBuilder 或忘记 ApplyConfig ,在空配置的默认状态下运行。建议在调用 ProcessStream 前后加返回值检查,WebRTC返回非0就要立刻打印日志。
5.3 效果不对:爆音、没消噪、回声
效果不对时先别怀疑算法,先确认三点:
- 输入信号是否已经削顶失真,AGC开启后会不会进一步放大爆音。
- AEC是否真的拿到了参考信号,而不是把扬声器信号和麦克风信号接反。
- 降噪等级是否设置过高,导致语音也跟着被削弱。
我实际测试过同一段带噪声语音, kLow 和 kVeryHigh 的听感差异非常明显。做产品调参时,建议先把三个模块单独开关跑一遍,确定每个模块在目标场景里的贡献,再组合起来调,否则你根本不知道是AEC压掉了语音还是ANS压掉了噪声。
这套代码我已经在最新主干上完整编过一遍,参考价值确实比网上那些零散片段高很多。如果你也准备从零整理一份 webrtc audio 3a 的可编译工程,我最后的建议是:先别急着改代码,把 modules/audio_processing 目录下各个子模块的BUILD依赖画一遍,搞清楚 audio_processing 到底拉起了多少东西,再决定你是全量编还是自己裁剪。实际弄下来,最花时间的往往不是写编译脚本,而是搞清楚依赖边界、接口版本和10ms帧约束这三个细节。把这三个细节理通了,后续移植到任意平台都会顺手很多。
火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。
更多推荐
所有评论(0)