ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

WebRTC 3A 音频处理编译接入与调参实战

WebRTC 3A 音频处理编译接入与调参实战

简介:面向实时音视频开发者与算法学习者的 WebRTC 音频 3A 可编译参考代码,围绕自动增益控制 AGC、噪声抑制 ANS、回声消除 AEC 三项关键算法,适用于视频会议、在线教育、语音聊天等场景的音质优化、算法移植与工程化调试,帮助读者把握实时通信中清晰度、延迟与资源占用之间的平衡,对中高级开发者尤具参考价值。压缩包共 249 个文件、约 7.49MB,包含 C/C++ 源文件、头文件、CMake 与 Makefile 构建脚本、sample 示例、wav/pcm 测试音频及编译中间产物,目录覆盖音频采集、处理与播放模块,便于对照接口组织、参数配置和构建流程。已有 344 人学习下载。读者可在本地编译运行,结合示例与测试音频观察降噪、增益和回声消除效果,梳理 3A 模块的调用关系与排错思路,为移植到自有项目或二次开发提供可直接参考的实现基础。

1. 从一份无法编译的 apm demo 说起

某公司的跨平台语音 Demo 交接时,附件里塞了一份 webrtc audio 3a 代码,对方说“可编译、最新参考代码,拿去直接用”。结果我把压缩包解开,在 Linux 上敲完 ninja,第一个报错就卡在third_party/abseil-cpp,之后还有rtc_base/logging.h找不到、libaudio_processing.a缺符号、AudioProcessing::Create()已废弃等一连串问题。3A 这套代码(AEC 回声消除、ANS 降噪、AGC 自动增益)几乎每个做实时语音的项目都会遇到,但“代码能看懂”和“环境能跑起来”之间隔着一整套工具链。这篇记录的是我自己从零把这套 webrtc audio 3a 代码编译、接入、调通的全过程,版本怎么选、gn 参数怎么写、CMake 怎么剥依赖、装完怎么验证,一步步写下来。新手照着能走通,熟手能直接看参数取舍。

2. 环境准备:跑 WebRTC 3A 到底要装哪些东西

动手前先说清楚一件事:modules/audio_processing不是一个能单文件g++编过的模块。它向下依赖common_audio、rtc_base、system_wrappers、abseil、rnnoise 权重表,向上要求调用方严格按 10ms 帧、固定采样率喂数据。所以第一步不是写代码,而是先决定用哪条构建路线,再把工具链版本对齐。

2.1 三条落地路线,先选一条再动手

拿到一份 webrtc audio 3a 参考代码,能走的路其实只有三条,选错了会浪费掉一整天。

第一条是 depot_tools 全量 fetch + GN 构建。这是最接近上游的做法,gclient sync把整个 webrtc 仓库拉下来,gn gen生成 ninja 工程,再单独编audio_processing这个 target。优点是依赖零缺失、后续想升级直接git checkout换提交;缺点是仓库体积大、同步一次网络和时间成本都高,而且一旦同步到一半失败,.gclient_entries状态会残留,下次同步容易再次失败。

第二条是只保留目录、自己写 CMake 抽取编译。把modules/audio_processing、common_audio、rtc_base这几个目录拷出来,用一个CMakeLists.txt把所有.cc收进去编。优点是编译快、产物小、能直接嵌进现有的 CMake 工程;缺点是需要手工处理依赖闭包,编译期符号缺失是常态,得一个一个补。

第三条是用现成的二进制或系统包。这条路在正式项目里我不推荐,因为 3A 的调参深度依赖你能改源码、能读统计量,拿一个不透明的.so进去,出问题就是黑匣子。

我一般的判断标准是:只是想在本地验证效果、写个 demo,走第一条;要把 3A 集成进已经有 CMake 体系的产品工程,走第二条;只是短时间对个比、看下 AEC 效果,第三条够用。

提示:第二、三条路线都建立在对源码目录结构的正确理解上,所以不管选哪条,先把第 2.3 节的目录表看完。

2.2 编译工具链与系统依赖清单

WebRTC 的构建脚本对工具版本有硬性检查,版本对不上时不会给友好提示,而是直接在 GN 阶段报一堆看不懂的错误。下面是这套代码我实际跑通时用的清单,Ubuntu 20.04 / 22.04 和 macOS 都适用。

组件要求装错的表现
Python3.8 及以上,且python3在 PATH 里gclient启动即报语法错误或模块缺失
Git2.20 以上,需支持git submoduleDEPS拉取第三方库时中途失败
Ninja随 depot_tools 自带,不要用系统包版本过旧导致gn gen生成的 build.ninja 不兼容
GN随 depot_tools 自带手工装 gn 容易出现与源码不匹配
编译器GCC 9+ / Clang 12+,需 C++17大量std::相关报错或-std=c++17未生效
pkg-config必须存在CMake 抽取路线下找不到系统库

Linux 上把基础依赖一次装齐,命令如下。这段我建议整段复制执行,缺一个包后面都会以奇怪的形式报出来。

# 基础构建工具,缺任何一个 GN 阶段都可能失败 sudo apt-get update sudo apt-get install -y \ git python3 python3-pip pkg-config \ build-essential clang lld \ libglib2.0-dev libgtk-3-dev libnss3-dev \ libasound2-dev libpulse-dev libx11-dev # 确认版本,GCC 低于 9 就要换 clang gcc --version clang --version python3 --version

这里libasound2-dev和libpulse-dev是音频设备层依赖,很多人抽 3A 时会觉得“我只是做算法,不碰设备”就跳过,结果编译system_wrappers里的音频设备相关.cc时直接失败。libnss3-dev、libgtk-3-dev看起来跟音频毫无关系,但它们是 GN 里rtc_build_examples打开时的传递依赖,如果你后面把 examples 关掉,这两个可以不装。

macOS 上用xcode-select --install装命令行工具即可,不需要完整的 Xcode。但要注意一点:macOS 上如果之前装过 Homebrew 的ninja或gn,一定要把它们从 PATH 前面挪走,depot_tools 自带的版本优先级必须更高,否则你会遇到“gn 能跑、ninja 报未知规则”这种玄学问题。

2.3 代码目录辨认:3A 的实体在 modules/audio_processing

很多人拿到代码后第一反应是全局搜WebRtcAec,然后在common_audio/里找到一堆aec老实现,误以为那就是要用的东西。实际上现行版本的 3A 全部收在modules/audio_processing下,老的common_audio/signal_processing里只剩少量数学工具。

目录作用能不能删
modules/audio_processing3A 入口与调度层,对外暴露AudioProcessing不能
modules/audio_processing/aec3现行回声消除实现,替代旧的 aec/aecm不能
modules/audio_processing/ns降噪,含基于 RNN 的语音活动检测不能
modules/audio_processing/agc2自动增益,含自适应数字增益不能
modules/audio_processing/agc老版 AGC1,模拟增益新工程建议关掉
common_audioFFT、重采样、环形缓冲、向量运算不能
rtc_base日志、检查宏、线程、时间不能
system_wrappers采样率、CPU 特性探测视情况
third_party/rnnoiseRNN VAD 的权重数据用 RNN 降噪就必须留

判断你手上这份代码是新是旧,有个很快的办法:看modules/audio_processing/aec3是否存在且非空。存在说明是 AEC3 时代之后的代码,配置接口走AudioProcessing::Config;不存在、只有aec和aecm,那就是很老的版本,接口还是AudioProcessing::Create()加一堆SetXXX方法,网上找到的示例代码大概率对不上。

还有一处容易踩:modules/audio_processing/include/audio_processing.h是唯一该 include 的头文件,但AudioProcessing::Config这个结构体在不同提交里字段增删频繁,比如echo_canceller.mobile_mode在部分版本里已经移除,写死config.echo_canceller.mobile_mode = false;就会编译不过。任何从网上抄来的配置代码,先打开本地的audio_processing.h对一遍字段名再贴进去,这一步能省掉大量“代码明明是对的却编不过”的困惑。

3. 编译步骤:GN 全量构建与 CMake 抽取两条路

环境和目录都确认过,接下来进入真正产生.a文件的阶段。这一章两条路线都写,按你的场景挑一条走完即可,但建议都读一遍,因为后面排查问题时,很多现象是两条路线共有的。

3.1 用 depot_tools 拉代码并固定分支

depot_tools 是 WebRTC 官方那套拉代码工具的集合,gclient、gn、ninja都在里面。第一步是把它拿到本地并加到 PATH 最前面。

# 1) 拿到 depot_tools,放到一个不会误删的目录 cd ~ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git # 2) 加到 PATH 最前面,注意是“最前面” export PATH=~/depot_tools:$PATH # 3) 验证三个工具都来自 depot_tools which gn ninja gclient

export PATH=...:$PATH这个顺序不能反,很多人写成$PATH:~/depot_tools,结果gn仍然指向系统里那个旧版本,表现就是gn gen报一堆未知参数。which的输出里如果能同时看到gn、ninja、gclient三个都落在~/depot_tools/下,才算成功。建议把这一行写进~/.bashrc,否则每开一个新终端都要重新设。

接着拉代码。如果手上有完整仓库,直接git clone或者拷过来更省事;如果是走官方流程,用fetch拉一个干净的工作区。

# 建一个工作目录,fetch 会在里面生成 src/ mkdir -p ~/webrtc_ws && cd ~/webrtc_ws # 拉取主干代码,这一步耗时长,网络不稳建议挂后台跑 fetch --nohooks webrtc cd src # 固定到你要用的提交,别一直用 HEAD git checkout <你手上这份参考代码对应的提交或分支> # 同步第三方依赖,这一步最容易中断 gclient sync -D

gclient sync -D的-D表示同步完成后把多余的依赖删掉,保证工作区干净。这一步失败是很常见的事,失败后不要直接重跑,先把src/.gclient_entries删掉再gclient sync,否则它会从断点残骸上继续,报出各种文件已存在的错误。这条血泪经验是踩过两次才总结出来的。

3.2 gn args 逐条解释,别直接抄别人给的字符串

GN 的参数决定了产物形态,网上流传的参数串大多是别人在自己的机器上跑通的,直接抄过来,轻则编得慢,重则链接不上。这里给一份偏保守、只为编出audio_processing的配置。

# 放在 out/apm/args.gn 里,用 gn gen out/apm 生成 is_debug = false # 要性能就关掉,调试符号用 is_debug=true 单独一个目录 is_component_build = false # 必须是 false,否则 aec3 内部静态符号会外露成动态符号 rtc_build_examples = false # 不编示例,省掉一大堆 UI 和网络依赖 rtc_build_tools = false # 不编测试工具 rtc_include_tests = false # 关掉单测,能少编上千个文件 rtc_include_internal_audio_device = false # 只要算法,不要设备层 use_rtti = false # WebRTC 默认不用 RTTI,打开会引入额外符号 use_custom_libcxx = false # Linux 上建议关,避免 libc++ 与系统 libstdc++ 冲突 target_cpu = "x64"

逐条说一下为什么这么设。is_component_build = false是关键,开成 true 时 aec3 里一些内部类的符号会变成动态导出,你在别的工程里链接时会出现“同名符号重复定义”,报错位置还都在第三方库里,很难定位。use_custom_libcxx = false是 Linux 上的经验值,WebRTC 默认会拉一份自己的 libc++ 进来编,如果你最终要和系统里的 libstdc++ 工程混链,ABI 对不上就是运行期崩溃,不是链接期报错,排查成本极高。

rtc_include_tests = false对编译时间影响最大,一个完整测试目标编下来能占掉总时间的一半以上。rtc_include_internal_audio_device = false则是明确告诉构建系统“我不要设备采集播放那一层”,这对纯算法集成是正解,能让产物依赖减少一大截。

注意:不同提交里可用的 gn 参数名会有增删。写 args.gn 之前先跑gn args out/apm --list,把里面实际存在的键名对一遍,别硬写。

生成工程并编译:

cd ~/webrtc_ws/src # 第一次生成,会检查工具链,这一步报错基本都出在编译器版本上 gn gen out/apm --args="$(cat out/apm/args.gn)" # 只编 audio_processing 这一个目标,不编整个 webrtc autoninja -C out/apm audio_processing

gn gen的--args也可以直接传字符串,我这里用文件方式是因为参数多,写文件便于版本管理和复查。autoninja是 depot_tools 提供的包装,会自动按 CPU 核数选并发度,比裸ninja稳,不容易因为内存被吃满而导致编译器被 OOM kill——现象是编译到某个.cc时突然internal compiler error或者进程直接消失。

3.3 只编 audio_processing 目标并拿到产物

编完之后产物不是一个,是一串静态库。先定位:

# 找出所有相关静态库,路径和文件名跟提交版本有关,不要硬编码 find out/apm -name "libaudio_processing.a" -o -name "libcommon_audio.a" -o -name "librtc_base.a" # 看下 audio_processing 依赖了哪些目标 gn desc out/apm //modules/audio_processing:audio_processing deps --all

find出来的libaudio_processing.a一般落在out/apm/obj/modules/audio_processing/下面,libcommon_audio.a在out/apm/obj/common_audio/下面。这些路径在不同提交里会变,所以我不建议在 CMake 或 Makefile 里写死,用find一次性定位、再把结果写进你的构建脚本。

gn desc ... deps --all这条命令非常有用,它会把audio_processing的完整依赖树打出来。你做 CMake 抽取时,依赖闭包就照这个输出抄,比自己去翻BUILD.gn快得多,也不会漏。

集成到你自己的工程时,编译选项要带上头文件路径:

# 头文件搜索路径,三个都必须有 -I$WEBRTC_ROOT -I$WEBRTC_ROOT/third_party/abseil-cpp -I$WEBRTC_ROOT/third_party/rnnoise/src

third_party/abseil-cpp是必带的,因为rtc_base里大量使用absl::容器,缺这个路径会在 include 阶段就失败。third_party/rnnoise/src只在你的代码用到 RNN 降噪时才需要,但它同时影响libaudio_processing.a本身的符号完整性,所以一般一起带上。

3.4 CMake 抽取编译:依赖闭包与 CMakeLists 骨架

要把 3A 塞进已有的 CMake 工程,走 GN 全量构建就不合适了,产物太大而且构建流程割裂。抽取编译的思路是:只保留必要目录,用一个 glob 把所有.cc收进来,再用 filter 把测试、mock、dump 这些剔除。

cmake_minimum_required(VERSION 3.16) project(webrtc_apm CXX) set(CMAKE_CXX_STANDARD 17) # WebRTC 现行代码要求 C++17 set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_POSITION_INDEPENDENT_CODE ON) # 要链进动态库就必须开 set(WEBRTC_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/webrtc) # 只收四个目录,第三个是 rnnoise 权重表所在 file(GLOB_RECURSE APM_SOURCES ${WEBRTC_ROOT}/modules/audio_processing/*.cc ${WEBRTC_ROOT}/common_audio/*.cc ${WEBRTC_ROOT}/rtc_base/*.cc ${WEBRTC_ROOT}/third_party/rnnoise/src/*.cc ) # 剔除策略:测试、mock、fuzz、dump 一律不要 list(FILTER APM_SOURCES EXCLUDE REGEX ".*/(test|tests|mock|fuzzers|fuzzer)/.*") list(FILTER APM_SOURCES EXCLUDE REGEX ".*_unittest\\.cc$") list(FILTER APM_SOURCES EXCLUDE REGEX ".*aec_dump.*") list(FILTER APM_SOURCES EXCLUDE REGEX ".*/platform_file.*") # 依赖文件系统,纯算法不需要 add_library(webrtc_apm STATIC ${APM_SOURCES}) target_include_directories(webrtc_apm PUBLIC ${WEBRTC_ROOT} ${WEBRTC_ROOT}/third_party/abseil-cpp ${WEBRTC_ROOT}/third_party/rnnoise/src ) # 平台宏,缺了会导致 get_cpu_features 之类走错分支 target_compile_definitions(webrtc_apm PUBLIC WEBRTC_LINUX=1 WEBRTC_POSIX=1 WEBRTC_APM_DEBUG_DUMP=0 )

file(GLOB_RECURSE ...)会匹配到几类不该进来的文件,所以后面四行list(FILTER ... EXCLUDE ...)是必须的,不是可选的清理。aec_dump那几个文件会引用调试转储相关的类型,抽出来编会缺一堆符号;platform_file依赖rtc_base/platform_file.h,在纯算法场景里完全是负担。

WEBRTC_APM_DEBUG_DUMP=0这个宏要显式定义为 0,不能干脆不定义。因为部分版本的代码里写的是#if WEBRTC_APM_DEBUG_DUMP,没定义时按 0 处理看似没问题,但在某些头文件里会触发“宏未定义”的警告,配合-Werror直接编不过。

编出来之后链接报错是很正常的,下一章会专门讲怎么补符号。

4. 参数配置:Config 里真正影响听感的几项

编译通过只是第一步,3A 的效果九成取决于配置。同一份代码,参数配错时的表现不是“效果差一点”,而是完全没效果,或者更糟——把正常语音也削掉。

4.1 初始化顺序与 StreamConfig 一致

现行接口的初始化顺序是有讲究的:先建实例,再ApplyConfig,最后Initialize三个流。顺序错了,配置会被忽略且不报错,这是最坑的地方。

#include "modules/audio_processing/include/audio_processing.h" #include "api/scoped_refptr.h" // 1) 建实例,新版本用 Builder,老的 Create() 已不推荐 rtc::scoped_refptr<webrtc::AudioProcessing> apm = webrtc::AudioProcessingBuilder().Create(); // 2) 先配置再初始化,顺序反了配置会被静默丢弃 webrtc::AudioProcessing::Config config; config.echo_canceller.enabled = true; apm->ApplyConfig(config); // 3) 三个流参数:输入、输出、远端参考 webrtc::AudioProcessing::StreamConfig stream; stream.sample_rate = 16000; // 必须和实际喂进来的数据一致 stream.num_channels = 1; apm->Initialize(stream, stream, stream);

webrtc::AudioProcessingBuilder().Create()返回的是scoped_refptr,不是裸指针,注意别按老代码习惯去delete。StreamConfig里的sample_rate必须和实际数据严格一致,填错时不会报错,只会表现为 AEC 完全不起作用——因为帧长对不上,滤波器根本收敛不了。三个参数分别是输入流、输出流、反向(远端)流,最常见的用法是三个都填同一个值,如果你要做重采样,可以让输入输出填不同采样率,AudioProcessing内部会帮你转换。

4.2 AEC、NS、AGC2 的开关与档位

配置项看着多,真正影响听感的是下面这几个。我把常用取值和效果差异列成表,方便对照着调。

配置项常用取值说明
echo_canceller.enabledtrue有远端参考就必须开
echo_canceller.enforce_high_pass_filteringtrue去掉直流分量,避免低频累积
noise_suppression.levelkLow / kModerate / kHigh越高降噪越狠,但语音细节损失越多
gain_controller1.enabledfalse老模拟增益,和 AGC2 同时开会互相冲突
gain_controller2.enabledtrue新数字增益,建议只开这个
gain_controller2.adaptive_digital.enabledtrue按语音能量自适应,电话场景建议开
high_pass_filter.enabledtrue80Hz 高通,去电源嗡声
transient_suppression.enabledtrue键盘敲击、点击声抑制
webrtc::AudioProcessing::Config config; // 回声消除:有反向参考信号才开,纯播放端不要开 config.echo_canceller.enabled = true; config.echo_canceller.enforce_high_pass_filtering = true; // 降噪:会议场景 kHigh,电话场景 kModerate 更自然 config.noise_suppression.enabled = true; config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kHigh; // 增益:AGC1 和 AGC2 二选一,同时开必然冲突 config.gain_controller1.enabled = false; config.gain_controller2.enabled = true; config.gain_controller2.adaptive_digital.enabled = true; config.high_pass_filter.enabled = true; config.transient_suppression.enabled = true; apm->ApplyConfig(config);

最容易踩的是 AGC 那两行。AGC1 是老的模拟增益控制,AGC2 是新的数字增益,两者内部都有对信号包络的估计,同时打开会出现“增益来回跳”的现象,听感上像音量忽大忽小。我一般只留 AGC2,把 AGC1 显式关掉,别指望默认值——不同提交里默认值不一样。

第二个坑是noise_suppression.level直接拉到kVeryHigh。这个档位在安静环境下确实干净,但在有背景人声的会议室里会把远处说话的人一起抹掉,表现为“只有靠近麦克风的人能被听见”。线上出过这类反馈,后来改回kHigh就正常了。

提示:noise_suppression.level的枚举名在不同提交里可能是kLow/kModerate/kHigh/kVeryHigh,也可能少了kVeryHigh,编译前先在头文件里确认。

4.3 延迟对齐:set_stream_delay_ms 与双向调用节奏

这一节是最容易被跳过、但直接决定 AEC 有没有效果的环节。AEC 的原理是用远端参考信号去估计回声路径,如果你的调用节奏和真实延迟对不上,滤波器永远收敛不了,表现为“回声一点没消”。

先看调用节奏。远端数据每来一帧,必须调一次ProcessReverseStream;近端采集到一帧,调一次ProcessStream。两者都是 10ms 一帧,采样率一致。

constexpr int kSampleRate = 16000; constexpr int kFrameSamples = kSampleRate / 100; // 10ms = 160 点 webrtc::AudioProcessing::StreamConfig stream{kSampleRate, 1}; // 远端参考:收到一帧就处理一帧,不要攒批 apm->ProcessReverseStream(far_in, stream, stream, far_out); // 近端采集:处理前告诉它当前的回环延迟 apm->set_stream_delay_ms(current_delay_ms); apm->ProcessStream(near_in, stream, stream, near_out);

set_stream_delay_ms填的是“远端信号从播放出去,到被本地麦克风重新采集回来”这段系统延迟,单位毫秒。这个值不是随便填 0 就行:填 0 时 AEC3 会尝试自己估计,冷启动阶段会有几秒的适应期,期间回声消除效果很差;如果你能给出接近真实的值,收敛会快很多。

怎么估这个值?我的做法是把播放缓冲、设备输出缓冲、声学传播时间三段加起来。比如播放端缓冲 40ms、设备输出 20ms、扬声器到麦克风约 5ms,就先填 60ms 左右,跑起来之后看统计量再微调。

// 读统计量,判断 AEC 收敛情况 webrtc::AudioProcessingStats stats = apm->GetStatistics(); // echo_return_loss_enhancement 越大说明消除得越好 // divergent_filter_fraction 接近 1 说明滤波器发散了,基本等于没工作

echo_return_loss_enhancement(ERLE)是我最常看的一个指标,稳定在 20dB 以上说明 AEC 工作正常;如果在 0 到 5dB 之间徘徊,八成是延迟填错了,或者ProcessReverseStream调用节奏和近端对不齐。divergent_filter_fraction是个 0 到 1 的比例,接近 1 就是发散,先查参考信号是不是真的从扬声器放出去了——有些场景下远端数据走的是耳机,那本地根本采不到回声,这时候开 AEC 反而会误伤。

还有一个细节:ProcessStream和ProcessReverseStream必须在同一个线程里按时间顺序调用,或者你自己保证严格的先后关系。多线程乱序调用不会崩,但会让内部时间戳错乱,效果直线下降。

5. 常见问题排查:3A 能编过但没效果的五个坑

下面五条都是我实际遇到过的,按“现象 → 原因 → 解决”记录。前两条偏构建,后两条偏运行,最后一条是两者交界。

5.1 构建与链接层面的三个坑

坑一:链接时报一堆undefined reference to rtc::LogMessage::...

现象是静态库编出来了,但在自己的工程里链接阶段爆出几十条未定义符号,全部指向rtc_base里的日志和检查宏。

原因是rtc_base的.cc文件没有全部收进 CMake,或者被list(FILTER ... EXCLUDE ...)误剔除了。rtc_base目录下有一批文件名带logging、checks的文件,它们提供RTC_CHECK、RTC_LOG的实现,缺一个就全链路报错。

解决办法是先看 GN 的依赖输出:gn desc out/apm //modules/audio_processing:audio_processing deps --all,照着输出把rtc_base下的源文件逐个对一遍。如果嫌麻烦,另一个做法是把rtc_base整目录收进来,只在 filter 里排除test和platform_file,宁可多编几个文件也别漏。链接时如果还缺,加-Wl,--allow-multiple-definition是错的方向,先把符号补全再说。

坑二:audio_processing.h里某字段不存在,配置代码编译不过

现象是你从某份网上示例里抄的config.echo_canceller.mobile_mode = false;,编译报“no member named mobile_mode”。

原因是 WebRTC 的AudioProcessing::Config结构在这几年改动频繁,mobile_mode在新提交里已经被移除,旧提交里又有。同理还有gain_controller2.fixed_digital.gain_db这类字段,位置和名字都变过。

解决办法只有一个:打开本地modules/audio_processing/include/audio_processing.h,搜struct Config,把实际存在的字段列出来,以本地代码为准,网上的示例只当思路参考。我现在的习惯是每次换版本,先花五分钟把这个头文件里的Config结构从头到尾读一遍,比出问题后翻报错快得多。

坑三:编过了,但一调Create()就崩溃

现象是webrtc::AudioProcessing::Create()返回空或直接段错误,堆栈落在构造函数里。

原因是走 CMake 抽取路线时,缺少了third_party/rnnoise/src里的权重数据源文件,而降噪模块在构造阶段就会初始化 RNN VAD,权重表为空就崩。另一个可能是WEBRTC_LINUX、WEBRTC_POSIX这些平台宏没定义,CPU 特性探测走进错误分支。

解决办法是确认third_party/rnnoise/src下的.cc都被收进来了(尤其是带weights字样的那个),并把WEBRTC_LINUX=1、WEBRTC_POSIX=1显式加到target_compile_definitions里。新版接口建议直接用AudioProcessingBuilder().Create(),老Create()在很多提交里已经是废弃状态,行为不一定可靠。

5.2 效果与运行层面的两个坑

坑四:AEC 开了,但回声一点没消

现象是配置里echo_canceller.enabled = true,代码也编进去了,实际通话仍有明显回声。读GetStatistics(),echo_return_loss_enhancement长期在 0 到 3dB 之间。

原因通常有三个,按概率从高到低:一是set_stream_delay_ms填的值和真实延迟差距太大,滤波器无法收敛;二是ProcessReverseStream喂的不是真正播放出去的那路信号,比如喂的是解码后的远端数据,但实际播放时又经过了重采样或混音;三是近端和远端的采样率不一致且只在一侧做了重采样,帧长对不上。

解决办法是按顺序验证:先把延迟估一个近似值填进去,观察 ERLE 是否上升;再核对喂给ProcessReverseStream的数据是否和实际送往扬声器的数据完全一致(包括是否被音量调节改过);最后检查两路采样率,如果不一致,让Initialize的StreamConfig分别填正确值,让内部自己转,而不是在外面转完再喂进去。

坑五:降噪开了,人声也被削了

现象是通话中对方反馈“你的声音断断续续,像被吃掉了一半”,本地听自己的录音也发现字头字尾变弱。

原因是noise_suppression.level设得过高,配合 AGC 的强增益,在低信噪比环境下会把语音判成噪声。另一个诱因是 AGC1 和 AGC2 同时打开,增益控制互相打架,语音包络被压缩得过狠。

解决办法是把 NS 档位降到kHigh或kModerate,关掉 AGC1,只留 AGC2 的adaptive_digital。如果场景是近距离麦克风的会议终端,kModerate就够;如果是远场拾音,宁可开transient_suppression也别把 NS 拉满。

6. 装完怎么验证:三段式自测与统计量读数

编译通过、配置写完,最后一步是证明这套 3A 真的在工作。我习惯用三段式自测,从离线到在线逐步加码,每一段都有明确的通过标准,不过标准就往下查,别跳过。

第一段是离线自测。准备一段干净的近端语音和一段远端语音,用代码把两路按 10ms 帧喂进去,把ProcessStream的输出写成 wav。判断标准很直接:把输出波形和输入波形对比,回声成分应该被压掉 15dB 以上,语音本身不能有明显断裂。这一段不涉及任何设备,纯粹验证代码链路和参数是否生效。

第二段是统计量自测。跑一段真实通话录音,每处理 100 帧采一次GetStatistics(),把关键指标打出来。

指标正常范围异常含义
echo_return_loss_enhancement15dB 以上低于 5dB 说明滤波器没收敛
divergent_filter_fraction接近 0接近 1 说明滤波器发散
residual_echo_likelihood0.3 以下偏高说明仍有残余回声
输出帧能量不为 0恒为 0 说明流程根本没走通
// 每 100 帧采样一次,避免日志刷屏 static int frame_count = 0; if (++frame_count % 100 == 0) { auto stats = apm->GetStatistics(); // ERLE 是判断 AEC 是否工作的第一指标 RTC_LOG(LS_INFO) << "ERLE: " << stats.echo_return_loss_enhancement.value_or(-1) << " divergent: " << stats.divergent_filter_fraction.value_or(-1); }

注意echo_return_loss_enhancement这些字段在不同版本里是absl::optional或者std::optional,取值前要判空,别直接.*访问——在某些运行条件下它就是没有值,硬取会崩。value_or(-1)是我常用的写法,-1 一眼就能看出“这次没采到”。

第三段是端到端验证。把编译出来的库链进实际工程,用两台设备做真实通话,重点听三件事:对方能不能听到自己的回声、背景键盘声还在不在、你的音量是否忽大忽小。三件事分别对应 AEC、NS、AGC,哪一项不对就回到第 4 章对应的配置项去调。这一步我用过最有效的方法是录下处理前后的双轨音频,用任意音频软件对齐后对比听,比在线通话时凭感觉判断准得多。

从那以后我每次换版本、换提交,都会强制走一遍这三段:离线跑一遍看波形,统计量采一轮看 ERLE,端到端听三件事再上线。中间省掉的任何一步,最后都会以“线上回声消不掉”的形式还回来。这套 webrtc audio 3a 参考代码本身是完整的,可编译的前提是把工具链和参数配对,跑通的关键是把延迟和调用节奏对齐,剩下的就是照着统计量慢慢调。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进