前言

对于webrtc中AGC1的整体架构已经在上一篇文章webrtc之AGC1上——整体架构与设计逻辑中进行介绍。值得注意的是webrtc中的AGC1 本质不是“让声音变大”,而是:把语音统计分布拉向目标响度分布。

本篇文章将继续对于 AGC1 中“如何分析语音统计量,并最终决定模拟增益档位”的完整策略链路,结合源代码进行详细介绍。

|版本声明:山河君,未经博主允许,禁止转载


一、自动增益详细流程

自动增益的全过程可以分为三个阶段:分析阶段、处理阶段、应用阶段。而整体的核心部分如下:

10ms帧
   ↓
VAD → RMS → Histogram
   ↓
计算 loudness (响度)
   ↓
error_db (RMS误差)
   ↓
┌───────────────┐
│ 数字增益承担  │
└───────────────┘
       ↓
┌───────────────┐
│ 模拟增益承担  │
└───────────────┘
       ↓
平滑
       ↓
应用

1.预处理阶段

该阶段整体核心做了两件事情:

  • 多通道增益聚合
  • 削波检测 → HandleClipping → 降低模拟增益

该阶段主要会调整模拟增益音量,并且确定最大增益范围,以防止后面增益出现爆音现象。整体流程如下:

AgcManagerDirect::AnalyzePreProcess
 ├─> AggregateChannelLevels() 预设置模拟电平(根据各个通道中最小或者最大增益设置)
 └─> ComputeClippedRatio() 计算该通道中峰波和谷波统计占总比例,即32767或者-32768
 └─>state_ch->HandleClipping() 处理削波数值
 │    └─>SetMaxLevel() 调整最大允许音量
 │    └─>SetLevel() 降低模拟电平等级,一个等级是15,范围0~255
 └─>AggregateChannelLevels() 更新模拟电平数值

2.处理阶段

该阶段整体核心:

  • VAD检测:chunkwise RMS & 语音概率
  • 更新响度直方图:长期响度统计
  • 确定数字增益范围:快速调节、半步更新、压缩器
  • 确定模拟增益范围:慢速调节、处理残余误差

这个阶段是为了管理“模拟音量档位”的调节策略,决定mic_volume = ?,而不是直接修改样本,整体流程如下:

AgcManagerDirect::Process()
 ├─> AggregateChannelLevels():聚合当前通道电平
 ├─> GetSpeechLevelErrorDb():已废弃
 ├─> MonoAgc::Process():单声道自动增益控制
	└─> Agc::Process()
		└─> VoiceActivityDetector::ProcessChunk():vad计算
		└─> VoiceActivityDetector::chunkwise_rms():获取各段语音能量
		└─> VoiceActivityDetector::chunkwise_voice_probabilities():获取语音概率
		└─> LoudnessHistogram::Update():更新直方图
	└─> Agc::GetRmsErrorDb():获取增益误差
	└─> UpdateGain():决定增益责任
	└─> UpdateCompressor():平滑数字增益
 ├─> AggregateChannelLevels():再次聚合当前通道电平

3.应用阶段

其实际上就是通过分析阶段得到的数字增益结果,进行系数转换最终进行数字增益

agc_manager->GetDigitalComressionGain()
      └─> 获取数字增益目标
gain_control->set_compression_gain_db
      └─> 设置数字增益
gain_control->ProcessCaptureAudio()
      └─> WebRtcAgc_Analyze():实际应用数字增益
      └─> ApplyDigitalGain():实际应用数字增益

其核心部分代码其实就是确定增益系数后的一次振幅调整,代码如下

 for (int n = 0; n < kNumSubSections; ++n, ++sample) {
        RTC_DCHECK_EQ(k * kNumSubSections + n, sample);
        out_band[sample] *= gain;
        out_band[sample] =
            std::min(32767.f, std::max(-32768.f, out_band[sample]));
        gain += delta;
      }

二、分析阶段

1.最大增益策略

该策略实际上协调多通道的模拟增益推荐值,得出一个统一的、安全的输入音量,并记录当前控制增益的主导通道(遍历所有通道,选取最小推荐增益)。核心代码如下:

...
for (size_t ch = 1; ch < channel_agcs_.size(); ++ch) {
    int level = channel_agcs_[ch]->recommended_analog_level();
    if (level < new_recommended_input_volume) {
      new_recommended_input_volume = level;
      channel_controlling_gain_ = static_cast<int>(ch);
    }
  }
...
 if (analog_controller_enabled_) {
    recommended_input_volume_ = new_recommended_input_volume;
  }

值得注意的是选取最小值的原因为:因为所有通道共享同一个硬件模拟增益,为了避免任何一个通道因增益过高而削波失真,必须选择所有通道推荐值中的最小值(即最“敏感”的通道所需的增益),这样能确保所有通道都不会过载。

2.削波策略

首先,AGC会对于每一帧进行失真记录,核心代码如下

...
for (size_t i = 0; i < samples_per_channel; ++i) {
      RTC_DCHECK(audio[ch]);
      if (audio[ch][i] >= 32767.0f || audio[ch][i] <= -32768.0f) {
        ++num_clipped_in_ch;
      }
    num_clipped = std::max(num_clipped, num_clipped_in_ch);
...
  return static_cast<float>(num_clipped) / (samples_per_channel);

而每过30s后统计,如果失真比率超过阈值(默认clipped_ratio_threshold_ = 0.1f),说明当前输入信号已失真。代码会调用 HandleClipping 降低麦克风电平,并同步下调最大允许电平,同时增加压缩增益上限作为补偿。

SetMaxLevel(std::max(clipped_level_min_, max_level_ - clipped_level_step));
SetLevel(std::max(clipped_level_min_, level_ - clipped_level_step));

这种“严厉”处理是为了防止重复出现裁剪回声,确保信号始终工作在安全范围内。

二、响度直方图策略

1.目的

响度直方图LoudnessHistogram本质上是一个“长期响度统计器”,为了防止出现突然爆音或者静音,webrtc中需要进行长期统计,并且找到主流响度:

LoudnessHistogram= 用直方图统计历史响度 → 找到主要响度区间 → 决定目标增益应该往哪里调

2.策略过程

1)VAD能量计算

计算VAD

在Agc::Process()中,将对 10 毫秒音频进行 VAD 分析,提取每个子块的 RMS 和语音概率,并用这些数据更新长期统计直方图。在webrtc之语音活动上——VAD能量检测原理以及源码详解中,我们解释了webrtc_vad的核心接口,最终调用的地方就在这里:

  vad_.ProcessChunk(audio.data(), audio.size(), sample_rate_hz);
记录语音概率

在经过vad计算后,获取到各个子带的能量以及语音概率,最终更新到直方图里,核心代码如下:

const std::vector<double>& rms = vad_.chunkwise_rms();
  const std::vector<double>& probabilities =
      vad_.chunkwise_voice_probabilities();
  RTC_DCHECK_EQ(rms.size(), probabilities.size());
  for (size_t i = 0; i < rms.size(); ++i) {
    histogram_->Update(rms[i], probabilities[i]);
  }

2)确认响度

其主要确定响度的策略为:

  • 将每一帧的能量划分到一个区间内进行计数
  • 计算每个区间出现语音的概率,取中心对应的线性 RMS 值,进行相加,得到最终mean_val 即为加权平均的 RMS

计数代码如下:

...
    double rms_log = log(rms);
    ...
    double b = 0.5 * (kHistBinCenters[index] + kHistBinCenters[index + 1]);

确认平均响度代码如下:

if (audio_content_q10_ > 0) {
    double p_total_inverse = 1. / static_cast<double>(audio_content_q10_);
    for (int n = 0; n < kHistSize; n++) {
      p = static_cast<double>(bin_count_q10_[n]) * p_total_inverse;
      mean_val += p * kHistBinCenters[n];
    }
  }

三、增益划分策略

1.目标

webrtc中的增益策略实现的是一种 “数字优先,模拟兜底” 的双层增益控制策略。这是由于

  • 模拟增益(音量滑块):范围大、步进粗、可能有物理延迟,适合长期平均电平调整。
  • 数字增益(压缩器):范围小、响应快、无物理延迟,适合短期波动和精细调整。

由此AGC的设计办法为:

  • 压缩器优先:
    • 不影响用户滑块
    • 不会有硬件噪声变化
  • 压缩器更新是“半步走”:
    • 避免intra-talkspurt pumping
  • 模拟增益有最大步长限制
    • 避免突然音量跳变
  • 改模拟增益必须 reset

看起来就是模拟增益用于慢速调节,承担增益较小,而数字增益用于快速调节,承担增益较大,并且整体增益是非线性增长 (小声提升多一点,大声提升少一点)

          ┌───────────────┐
Mic --->  │ Analog Gain   │  ← 慢速调节
          └───────────────┘
                  ↓
          ┌───────────────┐
          │ Compressor    │  ← 快速调节 
          └───────────────┘

2.实现过程

1)确定增益大小

这里的误差是指当前响度和目标响度的差,计算方式如下:

rms_error_db = 目标RMS电平(dBFS) - 当前RMS电平(dBFS)

计算的前提为:每当存在100帧,并且需要vad检测有30帧是语音活动时才判断增益数值,否则不进行增益改变:

if (histogram_->num_updates() < kNumAnalysisFrames) {
    return false;
  }

  if (histogram_->AudioContent() < kNumAnalysisFrames * kActivityThreshold) {
    return false;
  }

此时在比较当前响度(从直方图获取)和目标电平响度(用户初始化AGC时设置进入)的大小,输出修正电平大小

double loudness = Linear2Loudness(histogram_->CurrentRms());
*error = std::floor(Loudness2Db(target_level_loudness_ - loudness) + 0.5);

2)数字增益部分

补偿

由于AGC期望数字增益使用处于工作状态,所以压缩器在最开始时一定会保证有2db的增益

rms_error += kMinCompressionGain;
  int raw_compression =
      rtc::SafeClamp(rms_error, kMinCompressionGain, max_compression_gain_);
去激进化

为了防止音量突然变化导致增益过高

一句话里突然大声 → 增益瞬间变化 → 听感 pumping

AGC数字增益目标大小每一次都会走一半,也就是target = target + 0.5 * (new_target - target)

target_compression_ =
    (raw_compression - target_compression_) / 2 + target_compression_;
增益步长

目标有了之后,最终确定本次数字增益的大小是按照固定步长一点点增益

if (target_compression_ > compression_) {
  compression_accumulator_ += kCompressionGainStep;
} else {
  compression_accumulator_ -= kCompressionGainStep;
}

直到压缩器当前增益已经等于目标增益,则直接返回,不做任何调整

if (compression_ == target_compression_) return;

3)模拟增益部分

const int residual_gain =
    rtc::SafeClamp(rms_error - raw_compression,
                   -kMaxResidualGainChange,
                   kMaxResidualGainChange);

这里实际上就是将剩余误差交给模拟增益。

残余误差 = 总误差 - 压缩器承担的部分

最终会在计算完成后直接设置

 SetLevel(LevelFromGainError(residual_gain, level_, min_mic_level_));

四、补充

对于GainControlImpl::AnalyzeCaptureAudio也有可能有同学会用到,这里也做一个介绍吧

1.历史介绍

该接口是在AGC1中对于原始信号能量进行分析,最终确认增益大小,而上文中说过在2018年后为了解耦引入了AgcManagerDirect,该接口虽然保留但不会使用,如果读者使用的是在2018之前的版本,那么还是使用的,并且该接口里面的策略还是值得学习的。

在SetupDigitalGainControl中会强制设置GainControlImpl的模式是kFixedDigital

void AgcManagerDirect::SetupDigitalGainControl(
    GainControl* gain_control) const { 
    ....
  if (gain_control->set_mode(GainControl::kFixedDigital) != 0) {
    RTC_LOG(LS_ERROR) << "set_mode(GainControl::kFixedDigital) failed.";
  }
  ....
 }

而GainControlImpl::AnalyzeCaptureAudio只对模拟增益和数字增益做处理

int GainControlImpl::AnalyzeCaptureAudio(const AudioBuffer& audio)
{
...
  if (mode_ == kAdaptiveAnalog) {
}else if (mode_ == kAdaptiveDigital) {}
...

2.流程概述

GainControlImpl::AnalyzeCaptureAudio
 ├─> 模拟增益模式
 │    └─>ExportSplitChannelData() 提取子带音频
 │    └─>WebRtcAgc_AddMic() 模拟增益分析
 └─> 数字增益模式
 │    └─>ExportSplitChannelData() 提取子带音频
 │    └─>WebRtcAgc_VirtualMic() 数字增益分析
 │    		└─>低能量门限
 │    		└─>过零率统计
 │    		└─>模拟硬件增益
 │    		└─>确定增益电平

3.低能量检测策略

1)策略作用

这是一种粗略估计的能量估计,它的作用是:

  • 它控制后续 AGC 模块是否进行增益更新;
  • 如果为 1,AGC 模块通常 跳过当前帧的增益估计;
  • 这样可以避免:
    • 放大静音/底噪;
    • 放大高频杂音;
    • 因误判而拉低整体增益策略。

其部分代码截取:

  uint32_t frameNrgLimit = 5500; //能量门限
if (stt->fs != 8000) {
    frameNrgLimit = frameNrgLimit << 1;
  }

	//能量累加
  frameNrg = (uint32_t)(in_near[0][0] * in_near[0][0]);
  for (sampleCntr = 1; sampleCntr < samples; sampleCntr++) {
    if (frameNrg < frameNrgLimit) {
      nrg = (uint32_t)(in_near[0][sampleCntr] * in_near[0][sampleCntr]);
      frameNrg += nrg;
    }

    // 过零率统计
    numZeroCrossing +=
        ((in_near[0][sampleCntr] ^ in_near[0][sampleCntr - 1]) < 0);
  }
if ((frameNrg < 500) || (numZeroCrossing <= 5)) {
    stt->lowLevelSignal = 1;
  } else if (numZeroCrossing <= kZeroCrossingLowLim) {
    stt->lowLevelSignal = 0;
  } else if (frameNrg <= frameNrgLimit) {
    stt->lowLevelSignal = 1;
  } else if (numZeroCrossing >= kZeroCrossingHighLim) {
    stt->lowLevelSignal = 1;
  } else {
    stt->lowLevelSignal = 0;
  }

2)能量统计

对于能量RMS算法,可以看语音数字信号处理——计算pcm分贝这篇文章,其中重要公式为:
E = ∑ i = 0 N − 1 x [ i ] 2 E=\sum_{i=0}^{N-1}x[i]^2 E=i=0∑N−1​x[i]2
其门限为uint32_t frameNrgLimit = 5500;,该数值由来其实是一个经验值,对于8kHz采样率的音频信号

  • 假设背景噪声(低电平)下的 RMS 值为 10~30;
  • 则单个样本的平方约为 100∼900;
  • 80 个样本总能量约为 8000∼72000;
  • 5500 是远低于这个范围的。

值得注意的是,如果是16k或者及其以上的采样率,这个门限就放大了一倍,并且会在能量累积超过门限时停止统计。这是一种 一种弱判别型的 energy-based VAD 门限。

3)过零率统计

((in_near[0][sampleCntr] ^ in_near[0][sampleCntr - 1]) < 0)判断当前样本与前一个样本是否符号相反,即是否发生“零交叉”,而其门限是15:const int16_t kZeroCrossingLowLim = 15。

4)低电平判定

条件判定理由
frameNrg < 500 或 numZeroCrossing <= 5lowLevelSignal = 1极端低能量/低频,静音或平直信号
numZeroCrossing <= kZeroCrossingLowLimlowLevelSignal = 0正常低频,可能是语音
frameNrg <= frameNrgLimit (5500)lowLevelSignal = 1能量偏低
numZeroCrossing >= kZeroCrossingHighLimlowLevelSignal = 1高频噪声可能性高
其他lowLevelSignal = 0正常能量和频率,可能是语音帧

3)确定增益索引策略

该策略很简单,这里做一个大致介绍

WebRtcAgc_VirtualMic()
 ├─> mono_agcs_[ch]->state 各子带WebRtcAgc句柄
 ├─> 计算模拟的麦克风电平
 └─> 设置 gain 索引(默认值)
 └─> 检测物理音量是否突然变化
 └─> 各子带处理,检测是否爆音,爆音则衰减一个步长
 └─> 记录实际使用的增益索引

五、补充二

webrtc中的数字增益是模拟硬件增益来做的,这里介绍一下方便理解代码:

  • 硬件音量映射到 [ 0 , 255 ] [0,255] [0,255]
  • 数字增益划分 [ 0 , 127 ] [0,127] [0,127]为衰减, [ 127 , 255 ] [127,255] [127,255]是放大

整体增益大小见表格,在webrtc中源码可以看到一组明确的kGainTableVirtualMic和

区域含义增益范围(约)说明
左侧(<127)数字抑制~0.2x 到 1x用于模拟音量调低或信号太大时抑制
中点(=127)单位增益1.0x默认起始点,不变倍增益
右侧(>127)数字放大1x 到 ~5.7x用于模拟提升麦克风音量
表名类型范围功能说明
kGainTableAnalogQ140 ~ 127模拟“物理增益”对信号的效果
kGainTableVirtualMicQ100 ~ 127虚拟数字增益(放大区)
kSuppressionTableVirtualMicQ100 ~ 127虚拟数字增益(衰减区)
kTargetLevelTableint0 ~ 63不同目标电平对应的能量阈值

如同下图:

在这里插入图片描述


总结

本文完整梳理了 WebRTC AGC1 的自动增益决策链路,从统计分析到增益分配,再到最终应用,贯穿了一条清晰的策略主线。

如果用一句话概括 AGC1 的核心思想,那就是:通过长期响度统计,动态分配数字与模拟增益责任,将语音分布稳定拉向目标响度区间。

反正收藏也不会看,不如点个赞吧!

Logo

邀请您加入社区

更多推荐