WebRTC 音频处理与 3A 算法

深入 WebRTC 音频前处理链:讲清 AEC 回声消除的参考信号与延迟对齐、双讲检测、ANS 降噪的谱减与维纳滤波、AGC 自动增益的目标电平与压缩曲线,并给出 AudioWorklet 自定义处理链、48 kHz 采样率约定、重采样损耗与 3A 开关策略的工程实践。

音频是实时通信里最不能出问题的一环。视频卡一下用户能忍,声音一旦带回声、断续或者忽大忽小,通话基本就进行不下去。而音频质量的绝大部分,在采集后的前几十毫秒里就已经被决定了,也就是所谓的音频前处理链。

业界把这条链上的三个核心模块统称为 3A:回声消除(AEC,Acoustic Echo Cancellation)、噪声抑制(ANS,Automatic Noise Suppression)、自动增益控制(AGC,Automatic Gain Control)。它们默认开启,浏览器把它们藏在 getUserMedia 的一个布尔参数后面,导致很多工程师从来没认真看过它们。

本文要把这层黑盒拆开。我们会先看处理链在整条音频通路上的位置,再分别深入 AEC 的延迟对齐与双讲、ANS 的降噪原理、AGC 的增益曲线,然后给出用 Web Audio 与 AudioWorklet 自建处理链的方法,最后讨论这些处理对音质与带宽的真实影响。

读完你应该能回答:为什么音乐教学场景必须关掉 3A?为什么戴耳机时 AEC 依然在跑?为什么 48 kHz 是必须遵守的约定?

目录

  1. 音频处理链的整体位置
  2. AEC 回声消除原理
  3. 延迟对齐与双讲检测
  4. ANS 噪声抑制
  5. AGC 自动增益
  6. 采样率与重采样
  7. Web Audio 处理链
  8. AudioWorklet 自定义处理
  9. 约束调优与 3A 开关策略
  10. 前处理对音质与带宽的影响

1. 音频处理链的整体位置

一条完整的音频通路可以切成四段:采集、前处理、编码、传输。前处理位于采集与编码之间,是唯一能在不增加带宽的前提下改善音质的环节。

麦克风 → 硬件采样 → [AEC → ANS → AGC] → Opus 编码 → RTP → 网络
                          ↑
                    参考信号(扬声器播放的远端音频)

关键点在于 AEC 需要一路「参考信号」,也就是本机扬声器正在播放的远端音频。因此 AEC 无法脱离 RTCPeerConnection 独立工作:参考信号来自接收侧的解码输出,而不是麦克风。这解释了为什么在纯录音(不播放)场景下 AEC 是空转的,也解释了为什么自建 Web Audio 处理链时如果绕开了 RTCPeerConnection,回声消除会失效。

前处理的具体实现因浏览器而异。Chrome 使用的是 WebRTC 原生音频处理模块(APM,Audio Processing Module),它由 C++ 实现并被编译为 WASM 用于部分场景;Safari 与 Firefox 各有自己的实现。这导致同一个页面在不同浏览器上的处理效果存在可感知差异,跨端体验一致性必须实测验证。

2. AEC 回声消除原理

2.1 回声是怎么产生的

免提或外放时,扬声器发出的声音会被麦克风重新拾取,传回远端后对方就听到了自己的声音延迟若干毫秒后的副本。这个延迟由三部分组成:音频设备缓冲、系统音频栈、网络往返。总延迟通常在 50 到 300 ms,远超人类对「自己声音延迟」的容忍度。

2.2 自适应滤波

AEC 的经典做法是自适应滤波:把参考信号 x(n) 通过一个估计的声学回声路径 h(n) 卷积,得到回声估计 y'(n),再从麦克风信号 d(n) 中减去。

麦克风信号   d(n) = s(n) + y(n) + v(n)
                            ↑      ↑
                        真实回声   背景噪声
回声估计     y'(n) = h'(n) * x(n)      h' 为估计的声学路径
残差         e(n) = d(n) - y'(n) ≈ s(n) + 残余回声 + v(n)

滤波器系数 h'(n) 用 NLMS(归一化最小均方)或频域分块自适应算法在线更新,目标是让残差能量最小。线性滤波能消除绝大部分回声,但对非线性失真(扬声器过载、小音箱的谐波失真)无能为力。

2.3 非线性残余抑制

线性滤波之后还有一道非线性处理(NLP,Non-Linear Processing),用残余回声抑制(RES)把剩余的弱回声进一步压掉。典型做法是基于回声估计的幅度谱计算一个时频掩码,对疑似回声的频点做衰减。

代价是双讲时的语音损伤:NLP 无法区分「弱回声」与「弱语音」,如果掩码过于激进,远端说话人的声音会被一并削弱。这就是为什么低端设备的 AEC 在双讲场景下听起来像「断断续续」。

3. 延迟对齐与双讲检测

3.1 延迟对齐

自适应滤波的前提是参考信号与麦克风信号在时间上对齐。实际系统里的延迟会漂移:声卡缓冲在不同负载下会抖动,蓝牙耳机(A2DP 编解码 + 传输)能引入 150 到 200 ms 的固定延迟。

WebRTC APM 用互相关搜索估计延迟,在一个窗口范围内找使两个信号最相关的偏移量。对齐窗口有上限(通常是几百毫秒),一旦真实延迟超出窗口,AEC 就完全失效——表现为「回声一点没消」。

典型延迟来源与量级:
  系统音频缓冲        5 ~ 40 ms
  A2DP 蓝牙耳机     150 ~ 200 ms   ← 最常超出对齐窗口
  USB 音频设备       10 ~ 30 ms
  网络往返(若参考信号取自远端)

工程上若必须支持蓝牙耳机,应在采集端优先选择低延迟编解码,或直接引导用户使用有线耳机与内置麦克风。

3.2 双讲检测

双讲(Double-Talk)指本端与远端同时说话。此时自适应滤波会被本端语音「带偏」,因为残差里既有本端语音也有回声,滤波器无法区分。标准做法是双讲检测器(DTD):当检测到双讲时冻结滤波器更新,只保留当前的滤波结果。

// 简化的双讲检测思路:比较远端能量与残差能量的比值
function isDoubleTalk({ farEndPower, residualPower, micPower }) {
  // 远端有信号,麦克风能量显著高于回声估计的预期 → 判定为本端在说话
  const echoReturnLoss = farEndPower > 0 ? micPower / farEndPower : 0;
  return farEndPower > 0.01 && echoReturnLoss > 1.5 && residualPower > 0.02;
}

双讲检测的准确率直接决定 AEC 的主观体验:漏检会让回声泄漏,误检会让本端语音被削弱。这也是各家 AEC 效果差异最大的环节。

4. ANS 噪声抑制

4.1 谱减法与维纳滤波

ANS 的基本假设是「噪声比语音更平稳」。谱减法估计噪声的功率谱,然后从带噪信号的功率谱中减去:

|S'(f)|² = max(|Y(f)|² - α·|N(f)|², β·|Y(f)|²)
  |Y(f)|²  带噪信号功率谱
  |N(f)|²  估计的噪声功率谱(来自静音段的递归平均)
  α        过减因子,略大于 1 以抑制残余
  β        谱底限,防止过度衰减产生音乐噪声

更现代的做法是维纳滤波或基于统计模型的 MMSE,输出一个 0 到 1 的增益掩码作用于每个频点。WebRTC 的 ANS 使用的是后者,同时提供低、中、高三档强度。

4.2 音乐噪声与语音损伤

谱减法的经典副作用是「音乐噪声」(musical noise):孤立的频点被过度衰减后,残差听起来像随机的鸟叫。缓解手段包括谱底限、时间平滑与增益下限。

对语音而言,过强的 ANS 会削掉辅音的高频成分,让「s」「sh」「f」变得含糊。这在高强度档位下非常明显。因此 ANS 强度不应默认拉满,应根据环境噪声水平自适应:安静环境用低档,嘈杂环境才用高档。

const stream = await navigator.mediaDevices.getUserMedia({
  audio: {
    noiseSuppression: { exact: true },   // 开关,强度由浏览器决定
    echoCancellation: { exact: true },
    autoGainControl: { exact: true },
  },
});

浏览器的约束只能控制开关,不能控制强度。若要精细控制,只能用自建处理链,见第 7 节。

5. AGC 自动增益

5.1 目标电平与增益曲线

AGC 的目标是把音量稳定在一个目标电平附近(WebRTC 默认目标约 -18 dBFS,数字满刻度以下)。它有两个方向:

  • 模拟增益:调整麦克风的硬件增益,范围有限但信噪比最好。
  • 数字增益:在数字域乘一个系数,范围大但会把噪声一起放大。
输入电平        目标电平        AGC 行为
-40 dBFS       -18 dBFS        +22 dB 数字增益(噪声同时被放大)
-30 dBFS       -18 dBFS        +12 dB
-18 dBFS       -18 dBFS         0 dB(死区,不动作)
-6 dBFS        -18 dBFS        -12 dB 衰减(防削波)

5.2 攻击与释放时间

AGC 的关键参数是攻击时间(增益上升的快慢)与释放时间(增益下降的快慢)。攻击太快会把语音的起始辅音吃掉,太慢则前几个字听不清;释放太慢会让渐弱的尾音被拉高,听起来像「喘气」。

WebRTC 的 AGC 采用固定目标电平加动态范围压缩,并区分「语音段」与「非语音段」——只在检测到语音时更新增益,避免把背景噪声当成信号放大。

5.3 为什么音乐场景必须关掉 AGC

音乐的核心是动态。一段钢琴曲从极弱到极强的动态范围可能超过 40 dB,AGC 会把这个范围压成 10 dB 以内,听感上「平的」。同理,ANS 会把乐器的谐波当噪声削掉,AEC 会在有伴奏时误判。这就是在线乐器教学、音乐直播必须全关 3A 的原因。

6. 采样率与重采样

Opus 内部以 48 kHz 工作,支持 8、12、16、24、48 kHz 的输入,但只有 48 kHz 能覆盖全带宽音频(20 kHz 奈奎斯特)。若采集端给出 44.1 kHz,就需要重采样到 48 kHz,这个转换是有损的。

const ctx = new AudioContext({ sampleRate: 48000 });
console.log("实际采样率:", ctx.sampleRate); // 多数设备为 48000

const stream = await navigator.mediaDevices.getUserMedia({
  audio: {
    sampleRate: { ideal: 48000 },
    channelCount: { ideal: 1 },
    latency: { ideal: 0.01 },
  },
});

三个要点:

  • 显式请求 48 kHz 可避免浏览器内部重采样,减少一次音质损失与额外 CPU。
  • 通话用单声道,立体声会让 Opus 码率翻倍却对语音无增益。
  • latency 是建议值,实际生效值要用 getSettings() 回读确认。

若必须做重采样(例如接入 44.1 kHz 的外部音源),应使用带限插值而不是简单的线性插值,后者会引入明显的混叠失真。AudioContext 的 sampleRate 一旦创建就不可更改,需要不同采样率时必须新建 context。

7. Web Audio 处理链

getUserMedia 得到的流可以直接送进 RTCPeerConnection,也可以先经过 Web Audio 图处理后替换回轨道:

const ctx = new AudioContext({ sampleRate: 48000 });
const source = ctx.createMediaStreamSource(rawStream);

const highpass = ctx.createBiquadFilter();
highpass.type = "highpass";
highpass.frequency.value = 80;      // 滤掉低频隆隆声

const compressor = ctx.createDynamicsCompressor();
compressor.threshold.value = -24;
compressor.ratio.value = 4;
compressor.attack.value = 0.003;
compressor.release.value = 0.25;

const destination = ctx.createMediaStreamDestination();
source.connect(highpass).connect(compressor).connect(destination);

// 用处理后的轨道替换原始轨道,无需再协商
const processedTrack = destination.stream.getAudioTracks()[0];
await sender.replaceTrack(processedTrack);

replaceTrack 不触发再协商,是接入自定义处理链的标准方式,这一点与 媒体采集与设备管理 中切换设备的做法一致。

一个常见误区是把 Web Audio 的处理链接在 RTCPeerConnection 之后:这样做会绕过浏览器的内置 3A,且参考信号丢失,回声消除失效。正确顺序永远是「先采集,再处理,后入 PC」。

8. AudioWorklet 自定义处理

ScriptProcessorNode 已废弃,它在主线程上跑音频回调,容易造成爆音与卡顿。现代做法是 AudioWorklet:处理代码运行在独立的音频线程上,不受主线程阻塞影响。

// noise-gate-processor.js(作为独立模块加载)
class NoiseGateProcessor extends AudioWorkletProcessor {
  static get parameterDescriptors() {
    return [{ name: "threshold", defaultValue: 0.02, minValue: 0, maxValue: 1 }];
  }

  process(inputs, outputs, parameters) {
    const input = inputs[0];
    const output = outputs[0];
    if (!input || input.length === 0) return true;

    const threshold = parameters.threshold[0];
    for (let ch = 0; ch < input.length; ch++) {
      const inp = input[ch];
      const out = output[ch];
      for (let i = 0; i < inp.length; i++) {
        const sample = inp[i];
        // 简单噪声门:低于阈值则衰减 30 dB,而不是直接静音以避免突兀
        out[i] = Math.abs(sample) < threshold ? sample * 0.03 : sample;
      }
    }
    return true;
  }
}

registerProcessor("noise-gate", NoiseGateProcessor);
await ctx.audioWorklet.addModule("/noise-gate-processor.js");
const gate = new AudioWorkletNode(ctx, "noise-gate", {
  parameterData: { threshold: 0.015 },
});
source.connect(gate).connect(destination);

三个工程要点:

  • process 每 128 帧调用一次(48 kHz 下约 2.7 ms),必须保持极轻量,禁止分配对象或发起网络请求。
  • 处理单元要返回 true 才能持续被调用,返回 false 会终止该节点。
  • 参数用 AudioParam 传递而不是闭包变量,才能在主线程侧平滑调节。

在 AudioWorklet 里实现完整的 AEC 并不现实(需要参考信号与自适应滤波),但实现噪声门、自定义 AGC 曲线、音效与混音是合适的。

9. 约束调优与 3A 开关策略

不同场景下的推荐配置:

场景echoCancellationnoiseSuppressionautoGainControl备注
语音会议(外放)开开开默认配置
语音会议(耳机)开(无副作用)开开参考信号仍在,AEC 空转
音乐教学关关关保留动态与谐波
屏幕共享加语音开开开以人声为主
专业录音上云关关关后期处理
async function getAudioStream({ music }) {
  return navigator.mediaDevices.getUserMedia({
    audio: {
      echoCancellation: { exact: !music },
      noiseSuppression: { exact: !music },
      autoGainControl: { exact: !music },
      sampleRate: { ideal: 48000 },
      channelCount: { ideal: music ? 2 : 1 },
    },
  });
}

用 exact 而不是 ideal 是刻意的:3A 开关必须被严格遵守,若浏览器忽略了 ideal: false 仍然开着 ANS,音乐场景的音质损伤就无法避免。代价是某些设备可能因不支持而抛 OverconstrainedError,需要准备降级路径。

10. 前处理对音质与带宽的影响

3A 的处理是不可逆的:一旦 AGC 把动态压平、ANS 把高频削掉,编码前的信号就已经损失了信息,后续再高的码率也救不回来。因此前处理的激进程度应该与实际环境噪声成反比。

对带宽的间接影响体现在 Opus 的码率决策上。Opus 有两种模式:CELT 适合音乐与低延迟,SILK 适合语音。前处理后的信号更「干净」,VBR 编码时静音段可以用极低码率(甚至 DTX 不发送),这是 AGC 与 VAD 协同带来的带宽收益。反之,未做降噪的嘈杂环境会让 VAD 一直判定为「有语音」,DTX 失效,码率居高不下。

具体的编码参数与拥塞响应策略见 音视频编解码与拥塞控制 ,而弱网下这些参数如何动态调整则见 弱网对抗与自适应码率 。

权衡取舍

取舍点开启 3A关闭 3A
回声基本消除外放时完全无法使用
噪声明显改善保留全部环境音
音量稳定忽大忽小
音乐保真严重损伤完整保留
带宽DTX 生效,更省静音段仍占码率
实现复杂度零(浏览器内置)需自建处理链

结论是按内容类型切换,而不是全局一刀切。产品上应该把「音乐模式」做成显式开关,而不是靠自动检测——自动检测一旦误判,用户感受到的是「声音变闷了」,且无法自行恢复。

常见坑清单

  • 在音乐场景保留默认 3A,AGC 把动态压平,钢琴与弦乐听感发闷。
  • 自建 Web Audio 链时把处理节点接在 RTCPeerConnection 之后,AEC 失去参考信号而失效。
  • 用 ideal: false 关 3A,被浏览器忽略后仍开着 ANS,问题难定位。
  • 采集端不指定 48 kHz,浏览器做隐式重采样,白白损失一次音质。
  • 通话用立体声,Opus 码率翻倍而语音听感无变化。
  • 用 ScriptProcessorNode 做实时处理,主线程一卡就爆音。
  • 在 AudioWorklet 的 process 里分配对象或做同步 IO,造成周期性爆音。
  • 蓝牙耳机引入 200 ms 延迟超出 AEC 对齐窗口,误判为 AEC 失效。
  • 把 AGC 当作「万能音量修复」,掩盖了麦克风增益本身设置错误的问题。
  • 忽略不同浏览器 APM 实现差异,只在 Chrome 验证就上线。

小结

3A 是实时音频质量的地基,它决定了编码器拿到的信号有多干净。理解 AEC 依赖参考信号与延迟对齐、ANS 以噪声平稳性为假设、AGC 以固定目标电平为约束,就能解释绝大多数「声音听起来怪」的现象。

工程上的三条实用准则:语音场景用浏览器默认配置,音乐场景显式全关并改用自建处理链,自建处理链务必遵循「先采集、再处理、后入 PC」的顺序。采样率上坚持 48 kHz 与单声道,能在不花额外成本的前提下保住音质。

需要更细粒度控制时,AudioWorklet 是唯一正确的落点;但要清楚它能做的是增益、门限、混音这类逐样本运算,而不是重建一套 AEC。音频前处理之后,信号的命运就交给了编码与网络,而采集侧的设备切换与权限处理则是这条链路的起点。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「实时通信」更多文章

  1. 实时通信的端到端测试与压测
  2. 信令服务与房间水平扩展
  3. 低延迟直播:LL-HLS 与 WebRTC 直播