简介:WebRTC音频3A处理参考代码包,面向实时音视频开发者与音频算法研究者,聚焦自动增益控制(AGC)、噪声抑制(ANS)和回声消除(AEC)三大核心模块,提供可直接编译的最新实现。代码包基于Google开源的WebRTC项目整理,适用于视频会议、在线教育、语音聊天等场景,帮助开发者快速理解并集成高质量音频前端算法。
资源共249个文件,包体约7.49MB,以C/C++源码(c、cc、cpp)和头文件(h)为主,同时包含CMake/Makefile构建脚本、编译中间产物(o、d、bin)、配置与说明文档(txt、md),以及少量测试音频(wav、pcm)和Git仓库管理文件。目录结构覆盖AGC、ANS、AEC等模块,关键源码如noise_suppression.c、agc.c等,便于按模块检索与二次开发。
目前已有342人学习下载。通过可编译的完整工程,读者既能直接查看最新算法实现,也能修改优化后嵌入自身项目,是学习WebRTC音频处理和提升工程能力的实用参考。
1. WebRTC Audio 3A:这版可编译参考代码,建议直接拉下来跑一遍
做实时音视频的开发者,十有八九被回声和噪声折磨过:扬声器放出去的声音又被麦克风收回来,对方听到自己的回音,会议直接变成噪音现场。WebRTC Audio 3A(AEC 回声消除、ANS 噪声抑制、AGC 自动增益控制)是解决这套问题的经典组合,但这套代码在 WebRTC 工程里埋得很深,模块依赖多,直接拽出来编经常卡在第一步。这版代码把 3A 模块单独拆了出来,可编译、可链接,接口紧凑,想研究算法实现、或者把 3A 接进自己的音频采集链路,它都值得认真过一遍。适合做语音通话、直播连麦、会议系统、甚至录音后处理的开发者,尤其是想绕过繁琐构建、直接拿到一套能跑的 3A 流程的人。
2. 3A 链路的工程真相:先把回声、噪声、增益的边界划清楚
2.1 AEC 不是简单“减掉回声”,而是延迟估计、自适应滤波与非线性抑制的合体
回声消除在 3A 里最难,也是这套代码的核心价值所在。很多人以为回声消除就是把参考信号反向叠加到麦克风信号上,实际上远端参考信号到达扬声器、再被麦克风采集回来,中间隔着声卡延迟、声学路径、扬声器失真,简单相减只会把近端人声一起消掉。
WebRTC 的 AEC 分成三个环节:延迟估计负责找到参考信号和回声信号之间的时间偏移,这一步错了后面全废;自适应滤波用 NLMS 类算法估计声学回声路径,逐帧更新滤波器系数;非线性处理把残余回声压制到听觉阈值以下,代价是可能轻微损伤近端语音。这套代码把三部分拆在独立文件里,延迟估计模块可以单独替换,这点在接入非标准声卡时非常有用。
2.2 ANS 的噪声底估计和 AGC 的增益策略,决定了听感是“干净”还是“发闷”
噪声抑制的难点不在算法本身,而在噪声底怎么估计。WebRTC 的 ANS 用的不是固定阈值,而是对频带做递归平滑,区分平稳噪声和语音段,在语音段保留更多细节。这套代码里噪声估计的更新速率、起始抑制深度都是可调参数,你在安静会议室和嘈杂马路边需要的参数完全不同。
AGC 的工程坑通常在“该压的时候压不住,该放的时候放不开”。WebRTC 的 AGC 有模拟增益和数字增益两段,模拟增益负责粗调,数字增益负责精修。这套代码把两段分开配置,允许你设定目标语音电平,还能限制最大增益倍数,避免把背景噪声一起放大。
2.3 模块之间的调用顺序为什么是 AEC→ANS→AGC,而不是反过来
3A 的串联顺序不是随便排的。先做 AEC,是因为回声的能量通常远大于远端噪声不清除的话,后面的噪声估计会把回声当作语音保护起来;再做 ANS,是在回声消除后的干净信号上估计噪声,准确度更高;AGC 放最后,是因为它要评估的信号里已经不含回声和大部分稳态噪声,测出来的语音电平才可信。这套代码的调用入口 AudioProcessing 接口严格按这个顺序处理,你如果接进来后擅自调整顺序,跑出来的效果大概率是灾难。
2.4 为什么要选这版代码当参考:模块隔离、可编译、接口收敛
WebRTC 主仓库里 3A 相关的代码散落在多个目录,牵一发动全身。这版代码的做法是把 AEC、ANS、AGC 各自封装成独立模块,再用一个类似 AudioProcessing 的门面类串起来。编译层面,它把对 WebRTC 基础库的依赖降到最低,核心算法文件独立成静态库,只依赖 abseil 之类的通用库,这让 CMake 构建流程简单很多。接口层面,输入输出都是标准的 10ms 帧,SampleRate、Channel 在初始化时固定,数据流清晰,适合照着它的函数签名写自己的封装。
2.5 什么时候不该用它:低算力嵌入式场景要单独看
这套代码默认按桌面级 CPU 的算力来设计,滤波器的阶数、噪声估计的频带分辨率都偏“完整实现”。如果你要跑在单片机或者低端 DSP 上,直接搬这套代码会面临实时性压力,NEON 指令集能帮一部分忙,但整体内存占用仍然偏高。这种场景更适合参考它拆出来的算法流程,自己重写一版精简实现,而不是直接链接这套代码。
3. 把工程编译出来:从 CMake 配置到第一个 10ms 音频帧跑通
3.1 环境准备与仓库结构说明
编译这版代码不需要 WebRTC 的完整构建环境。我常用的组合是 Ubuntu 22.04 加 CMake 3.16 以上版本,编译器用 GCC 9 或 Clang 12 都可以。Windows 上用 MSVC 2019 也能编,但音频设备接入部分的代码,在 Linux 下验证最省事,因为 ALSA 的设备枚举比 Windows 的 WASAPI 直接。
代码目录结构通常是这样:src/modules/audio_processing/aec/放 AEC 实现,ans/放噪声抑制,agc/放增益控制,include/放对外头文件,test/放离线测试工具。你先确认这版代码的 CMakeLists 是否把三个模块分别编译成静态库,这决定了你后续链接的粒度。
3.2 编译三步走:CMake 配置、构建、装库
mkdir build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release \ -DWEBRTC_BUILD_AUDIO_PROCESSING=ON \ -DCMAKE_INSTALL_PREFIX=/usr/local make -j$(nproc) sudo make install先解释CMAKE_BUILD_TYPE=Release:3A 算法内部有大量浮点运算,Debug 模式下滤波器系数收敛速度会受影响,实时性也容易出问题,直接 Release 编。WEBRTC_BUILD_AUDIO_PROCESSING是这版代码的开关,有些分支会把 3A 和 AEC3 新旧两套实现都编进去,你只需要其中一套时,可以用这个选项把另一套关掉,缩减二进制体积。
如果 CMake 提示找不到 abseil,就用系统包管理装一下,或者把 abseil 源码放到third_party/目录下,CMake 的add_subdirectory会自动找到。装完库后头文件会落在include/,静态库在lib/下,pkg-config文件也会生成,方便后续项目引用。
3.3 初始化一个 3A 实例:参数摆出来逐个说
#include "modules/audio_processing/include/audio_processing.h" webrtc::AudioProcessing* apm = webrtc::AudioProcessingBuilder().Create(); webrtc::AudioProcessing::Config config; config.echo_canceller.enabled = true; config.echo_canceller.mobile_mode = false; config.noise_suppression.enabled = true; config.noise_suppression.level = webrtc::NoiseSuppression::Level::kHigh; config.gain_controller1.enabled = true; config.gain_controller1.mode = webrtc::GainController1::Mode::kAdaptiveAnalog; config.gain_controller1.target_level_dbfs = -3; config.gain_controller1.compression_gain_db = 12; apm->ApplyConfig(config);逐行说明。mobile_mode是 AEC 的精简模式,桌面端不开,因为移动端算力有限会启用带宽更窄的滤波器,桌面上开它反而会损失回声消除的频段覆盖。noise_suppression.level有 kLow/kModerate/kHigh 三档,实际会议场景 kModerate 更稳,kHigh 在安静环境会让人声发干。gain_controller1是旧的 AGC 实现,新版的gain_controller2是实验性的,参考代码里放在另一个目录。target_level_dbfs = -3意味着把语音峰值压到 -3dBFS,太接近 0 会削波,太低会让后面接的编码器量化噪声变大,-3 是语音通信的常见选择。
3.4 丢 10ms 帧进去:处理流程和返回值判断
int apm->ProcessStream( far_end_frame, capture_frame, capture_config, capture_output);这行代码是 3A 的核心入口。far_end_frame是远端参考信号,也就是扬声器正在播放的那一帧;capture_frame是麦克风采集到的一帧。长度都是 480 个采样点(48kHz 采样率下对应 10ms)。capture_config里要指定采样率和声道数,如果初始化时用的 48kHz 双声道,这里也得保持一致。
注意ProcessStream内部的处理顺序:先 AEC、再 ANS、最后 AGC,这段逻辑封装在AudioProcessingImpl里,你不需要手动调用三个模块。返回值非 0 时,优先检查capture_config的采样率是否和初始化一致,这是最常见的翻车点。
3.5 编译自己的调用工程:链接库和头文件路径
g++ my_app.cpp -o my_app \ -I/usr/local/include \ -L/usr/local/lib \ -lapm_3a -labsl_strings -labsl_synchronizationapm_3a是这版代码编译出来的静态库,名称可能带版本后缀,你以实际生成的.a文件名为准。abseil 的库必须一起链,因为AudioProcessing::Config内部用了它的字符串和同步原语。如果出现未定义引用,多半是链接顺序问题,把-lapm_3a放在源文件后面再试一次。
4. 把 3A 接进自己的音频链路:替换旧模块的实操套路
4.1 先梳理现有采集链路再动手,别一上来就改代码
很多开发者拿到 3A 代码后第一件事就是往自己的采集回调里塞ProcessStream,结果回声消除完全没生效,因为麦克风采集的数据流和扬声器播放的数据流根本不是同一台机器上的。接 3A 之前,先把音频流图画出来:采集端是声卡还是网络输入,参考信号从哪取,播放延迟多少毫秒,这些都明确后,再决定far_end_frame的数据源。
常见做法是给播放线程加一个环形缓冲,每写一帧到扬声器就同时拷贝一份到参考缓冲。3A 在处理采集帧时会从这个缓冲里取对应的参考帧。缓冲区长度要覆盖声卡延迟加上系统调度抖动,一般 200ms 足够,太长反而会让延迟估计模块收敛变慢。
4.2 参数不是调一次就完事,按场景分三组配置
| 场景 | AEC mobile_mode | ANS level | AGC target_level_dbfs | compression_gain_db |
|---|---|---|---|---|
| 视频会议(近讲) | false | kModerate | -3 | 12 |
| 直播连麦(远讲) | true | kHigh | -6 | 18 |
| 录音笔后处理 | false | kLow | -1 | 6 |
视频会议场景,麦克风距离说话人 30~50cm,回声路径相对稳定,关掉 mobile_mode 获得全频段回声消除。直播连麦场景,手机离嘴近,环境噪声复杂,ANS 开高挡,AGC 目标电平降低,留出动态余量,压缩增益加大以提升弱说话声的可懂度。录音笔场景,声源距离远且移动,AGC 应该保守一点,compression_gain_db设小,避免音量忽大忽小。
4.3 双声道采集怎么接:参考信号和采集信号必须在同一个时钟域
双声道麦克风阵列在会议设备里很常见。接入 3A 时注意一点:参考信号和采集信号如果来自不同的声卡、不同的时钟源,即使采样率都标着 48kHz,实际频率也可能差几十个 ppm,时间长了延迟估计模块会慢慢漂移,回声消除效果逐渐劣化。解决办法是把 AEC 的参考信号也接到同一块声卡的播放回采(loopback)上,保证时钟同源。
如果设备不支持回采,就要在应用层做时钟补偿,每处理 N 帧就重新校准一次参考缓冲的读写位置。这个过程在代码里表现为定时检查ProcessStream返回的延迟信息,和本地播放帧计数做对比。
4.4 用这套代码替换自己旧版 3A 时的兼容性检查清单
替换旧模块前过一遍这份清单:旧代码的音频帧格式是不是 S16 交错存储,这版代码内部处理用的是浮点平面格式,需要在入口做转换;旧代码的采样率如果只有 16kHz,会导致这版代码的 AEC 高频段失效,最好统一升到 48kHz;旧代码里 AGC 的目标电平单位如果用的是百分比,需要换算成 dBFs 再填进去。每一项都是实际接项目时踩过的,缺一个就能让整体效果不如旧的。
4.5 实时性保障:处理耗时超过 10ms 会是什么后果
3A 的处理必须在 10ms 内完成,否则音频采集中断就会欠载。实测这版代码在 i5 处理器上单声道 48kHz 的处理耗时大约是 2~3ms,余量充足。但如果你的工程里还做了降噪、语音活动检测、编码,这些叠加起来就可能超时。排查方式是在ProcessStream调用前后加高精度时钟打点,统计最坏耗时而不是平均耗时。
如果最坏耗时超过 8ms,优先检查滤波器的长度设置。这版代码的 AEC 默认滤波器长度按 100ms 混响时间设计,如果你所在的场景是小型会议室混响短,可以尝试把滤波器长度缩短一半。修改后回声消除的性能会略降,但实时性风险解除。
5. 避坑实战:编译没过、AEC 无效、音量过冲的排查记录
5.1 现象:AEC 完全没有效果,远端声音原样出现在采集流里
原因基本集中在参考信号上。排查路径是:先确认far_end_frame里真的有音频数据,且和播放内容一致;再确认参考缓冲的读写索引是否同步,常见的低级错误是播放线程写缓冲的速度慢于采集线程读的速度,导致参考帧落后太多;最后检查采样率对齐,48kHz 的采集配 44.1kHz 的参考信号,延迟估计模块会直接跑飞。
解决:参考信号改用声卡回采获取,绕开系统混音器可能引入的路径不一致问题。如果非要用应用层播放拷贝,就把环形缓冲区加大到 500ms,同时监控缓冲水位,跌到阈值以下时主动清空重来。
5.2 现象:AEC 在人声停顿期间放出“咕噜”声的残余回声
原因是回声路径突变,比如说话人转身、门打开,自适应滤波器来不及收敛。有些场景是参考信号裁剪导致的,比如播放前做了淡入淡出处理,回声路径里多了非线性成分。
解决:检查参考信号是不是原始 PCM,不能是经过音量自动调节或 EQ 处理后的数据;同时把 AEC 的convergence_time_ms调短一些。代价是稳态下滤波器抖动变大,但突变场景恢复更快。噪声场景中会伴随高频嘶嘶声,多半是参考信号经过压缩编码又解码,编码失真让滤波器把编码噪声当成了回声路径,无解,只能改取参考信号的位置。
5.3 现象:ANS 开启后,语音尾部被切断,每个字都像被“咬”了一下
原因是噪声估计的更新速率跟不上语音尾部迅速变化的信噪比。尾音被抑制器当成了残余噪声。这版代码的噪声估计器对平稳噪声效果很好,但对语音尾部这种瞬态信号反应偏慢。
解决:把noise_suppression.level从 kHigh 降到 kModerate,尾部保留更多细节。如果应用场景允许 1~2ms 延迟,可以把处理帧长从 10ms 改成 20ms,让噪声估计有更长的时间窗观察语音段分布。
5.4 现象:AGC 在安静环境下音量正常,一进嘈杂环境音量疯狂跳动
原因是 AGC 的模拟增益和数字增益打架了。模拟增益根据环境噪声自动调整,数字增益又在模拟增益基础上补一刀,两段叠加导致增益波动幅度过大。此外对环境噪声的响应速度太快,说话声音稍微大一点就迅速降增益,造成泵浦效应。
解决:固定模拟增益,只做数字增益;把target_level_dbfs改成固定值,关闭 AGC 自适应模式。这套代码里对应的参数是把mode改成kFixedDigital,效果上牺牲了一点弱语音的放大能力,但换来了音量的稳定。
5.5 现象:编译时链接 abseil 报各种未定义符号,头文件里却看得见声明
原因是 abseil 的符号是 inline 命名空间管理的,编译器和链接器的 abseil 头文件版本不一致,导致符号名被修饰成不同版本。这类错误信息里通常带absl::lts_2023或类似后缀。
解决:统一编译器和 abseil 版本,最稳妥的做法是全部用系统包管理器安装的版本,不再混着源码目录里的旧 abseil。改完后清理 build 目录重新配置一次,CMake 的缓存经常把旧路径记在里面。
6. 验证 3A 效果的实操技巧:用离线文件测明白再上真机
先准备三份测试素材:近端语音文件(模拟说话人)、远端参考文件(模拟扬声器播放内容)、混合文件(近端语音叠加远端回声)。混合文件可以用远端参考文件通过卷积一个室内冲激响应生成,模拟回声路径。这样测出来的回弹损失增强(ERLE)指标完全可复现。
写一个离线测试脚本,把这版 3A 处理混合文件,对比输出和近端原声。方法如下。
ffmpeg -i mix.wav -ar 48000 -ac 1 mix_mono.pcm ffmpeg -i far.wav -ar 48000 -ac 1 far_mono.pcm ./test_3a --input=mix_mono.pcm --ref=far_mono.pcm --output=out.pcm逐条说明。ffmpeg先把 WAV 转成纯 PCM,避免 3A 内部读取 WAV 头带来的格式问题。test_3a是这版代码 test 目录下的离线测试程序,输入混合文件和参考文件,输出处理后的 PCM。
参数说明:--input是麦克风采集信号,--ref是扬声器参考信号,两个文件采样率必须一致。如果你想模拟真实设备延迟,可以先用sox给参考信号延迟一个固定毫秒数,观察输出 ERLE 的变化,这是检验延迟估计模块是否正常最直接的手法。
测完 ERLE 再测语音质量,准备一份干净语音,用工具叠加上不同信噪比的噪声,分别过 3A 后计算 PESQ 分数。你会发现 ANS 在低信噪比下提升明显,但信噪比高于 20dB 后反而可能轻微下降,这就是为什么不能无条件把 ANS 开最高档的原因。
主观听感测试也有固定流程:找一段 30 秒的对话,一半安静、一半嘈杂,分别在开启和关闭 AGC 的条件下听。重点听音量忽大忽小是否明显、句首句尾是否有增益过冲。
从那以后,我每次把 3A 接进新项目,都强制自己先跑一遍这套离线测试流程,确认 ERLE 和 PESQ 达标了才肯上真机联调。真机上声学环境千奇百怪,离线数据至少能把算法层的错误提前筛掉大半,剩下的就是设备适配问题了。
这套 3A 代码的可贵之处在于它把算法和工程分离得足够清晰,编译能过、接口能调、参数能查,是你研究 WebRTC 3A 落地细节的一份可靠参考。希望帮到你。
本文还有配套的精品资源,点击获取