webrtc之AGC1下——自动增益源码流程与策略分析
文章目录
前言
对于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−1x[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 <= 5 | lowLevelSignal = 1 | 极端低能量/低频,静音或平直信号 |
numZeroCrossing <= kZeroCrossingLowLim | lowLevelSignal = 0 | 正常低频,可能是语音 |
frameNrg <= frameNrgLimit (5500) | lowLevelSignal = 1 | 能量偏低 |
numZeroCrossing >= kZeroCrossingHighLim | lowLevelSignal = 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 | 用于模拟提升麦克风音量 |
| 表名 | 类型 | 范围 | 功能说明 |
|---|---|---|---|
kGainTableAnalog | Q14 | 0 ~ 127 | 模拟“物理增益”对信号的效果 |
kGainTableVirtualMic | Q10 | 0 ~ 127 | 虚拟数字增益(放大区) |
kSuppressionTableVirtualMic | Q10 | 0 ~ 127 | 虚拟数字增益(衰减区) |
kTargetLevelTable | int | 0 ~ 63 | 不同目标电平对应的能量阈值 |
如同下图:

总结
本文完整梳理了 WebRTC AGC1 的自动增益决策链路,从统计分析到增益分配,再到最终应用,贯穿了一条清晰的策略主线。
如果用一句话概括 AGC1 的核心思想,那就是:通过长期响度统计,动态分配数字与模拟增益责任,将语音分布稳定拉向目标响度区间。
反正收藏也不会看,不如点个赞吧!
更多推荐
所有评论(0)