十七、音频QOS-NetEQ
音频的QoS可以分:音频前处理3A算法、NetEQ两大类。

这张图从音频传输的全链路视角 ,展示了 WebRTC 中 NetEQ(网络均衡器)在接收端的核心作用 ------通过抖动消除(JB)和丢包补偿(PLC),解决网络波动导致的音频卡顿、失真问题,同时与发送端的 3A 算法形成闭环。以下从 NetEQ 的角度拆解每一环:
NetEQ 是 WebRTC 接收端的核心模块,负责处理网络传输带来的不确定性 (抖动、丢包),确保音频播放流畅。在图中,它对应红色虚线框内的 "抖动消除(JB)""丢包补偿(PLC)""压缩解码(Decoder)" 模块,是连接"网络传输"与"本地播放"的关键枢纽。
1. 发送端:3A 算法预处理(为 NetEQ 减负)
图中左上的 "AEC(回声消除)→ ANS(噪声抑制)→ AGC(自动增益控制)→ Encoder(压缩编码)" 是发送端的音频处理流程:
- 3A 算法先把原始音频"提纯"(消回声、降噪、稳音量),再通过编码器压缩。
- 这一步的意义是:让传输的音频本身质量足够高,减少 NetEQ 后续处理的压力(比如噪声少了,丢包补偿时生成的伪信号更自然)。
2. 网络传输:Internet 中的"不确定因素"
音频包在互联网传输时,会因网络拥堵、路由波动出现抖动 (包到达时间忽快忽慢)或丢包(包直接丢失)。这些问题由图中"Internet"模块体现,也是 NetEQ 要解决的核心痛点。
3. 接收端:NetEQ 的"三大武器"(红色虚线框)
这部分是 NetEQ 的主战场,通过三个模块依次处理,把"不稳定的网络包"变回"流畅的音频":
-
① 抖动消除(JB)
- 作用:解决"包到达时间不一致"的问题。
- 原理:NetEQ 会维护一个自适应抖动缓冲区(Jitter Buffer),把到达时间混乱的包先存起来,再按"匀速、有序"的节奏取出。
- 类比:就像把"时快时慢的流水"先放进水库,再以稳定的速度放出,避免播放时忽快忽慢。
- 源码关联:
neteq/delay_manager.cc负责统计网络抖动,动态调整缓冲区大小;neteq/buffer_level_filter.cc平滑计算抖动延迟。
-
② 丢包补偿(PLC)
- 作用:解决"包丢失"的问题。
- 原理:当检测到丢包时,NetEQ 会基于"历史收到的音频特征"生成伪音频帧(比如用前一帧的音频做插值、或生成舒适噪声),填补丢失的部分。
- 类比:就像拼图缺了一块,用旁边的图案推测出缺失的部分,让人看不出破绽。
- 源码关联:
neteq/decision_logic.cc判定丢包场景,neteq/expand.cc执行丢包补偿算法(如 WSOLA 算法实现"变速不变调"的伪帧生成)。
-
③ 压缩解码(Decoder)
- 作用:把发送端压缩的音频包还原成原始 PCM 数据。
- 原理:和发送端的编码器对应,是"压缩 - 解压缩"的逆过程。
- 补充:解码后的数据会进入 NetEQ 的算法缓冲区(Algorithm Buffer),供后续 DSP 处理(如加减速、融合)。
4. 本地播放:最终的"音频还原"
处理后的音频经过 "ANS(噪声抑制)→ AGC(自动增益控制)→ DAC(声音播放)" 输出:
- 这里的 ANS 和 AGC 是接收端的"二次优化"(可选),进一步确保播放时的音量稳定、噪声少。
- 最后通过 DAC(数模转换)把数字信号变回模拟声音,从扬声器播放出来。
NetEQ 的"协同逻辑":让音频"又稳又好"
从图中可以看出,NetEQ 不是孤立工作的:
- 发送端的 3A 算法"提纯"音频,减少 NetEQ 处理难度;
- NetEQ 处理网络波动,确保音频"不断、不卡、不跳";
- 接收端的 3A 算法"收尾",让最终播放的声音质量拉满。
这种"发送端预处理 + 网络端稳传输 + 接收端后处理"的闭环,是 WebRTC 音频体验的核心保障。
1、NACK丢包重传协议
详细原理
NACK(Negative Acknowledgment,否定确认)是TCP中"确认重传"机制在实时音频中的轻量化实现:当接收端检测到音频包丢失时,通过RTCP协议向发送端发送包含丢失包序号的NACK报文,请求重传。为避免重传导致延迟累积,仅对"近期丢失"的包(通常不超过1个RTT+100ms)发起请求,超时未收到则放弃。
核心作用
- 弥补少量丢包(1-2个连续包),避免因局部丢包导致音频卡顿;
- 平衡重传收益与延迟代价(只重传"值得重传"的包)。
WebRTC源码实现(neteq/nack.cc)
cpp
// 生成NACK请求列表(仅包含未超时的丢失包)
std::vector<uint16_t> Nack::GetNackList(int64_t round_trip_time_ms) {
std::vector<uint16_t> nack_list;
rtc::CritScope cs(&crit_); // 线程安全锁
for (auto it = missing_packets_.begin(); it != missing_packets_.end();) {
uint16_t seq = it->first;
int64_t loss_time_ms = it->second;
int64_t age_ms = clock_->TimeInMilliseconds() - loss_time_ms;
// 超时判断:仅保留RTT+100ms内的丢失包(kMaxNackRetransmissionAgeMs=100ms)
if (age_ms > round_trip_time_ms + kMaxNackRetransmissionAgeMs) {
it = missing_packets_.erase(it); // 超时包不再请求
} else {
nack_list.push_back(seq); // 加入请求列表
++it;
}
}
return nack_list;
}
// 接收新包时更新丢失包记录(通过序号连续性检测丢失)
void Nack::Update(uint16_t sequence_number) {
rtc::CritScope cs(&crit_);
if (last_sequence_number_ != -1) {
uint16_t start = static_cast<uint16_t>(last_sequence_number_ + 1);
uint16_t end = sequence_number;
// 记录[start, end)范围内的所有丢失包
for (uint16_t seq = start; seq != end; seq++) {
missing_packets_[seq] = clock_->TimeInMilliseconds();
}
}
last_sequence_number_ = sequence_number;
missing_packets_.erase(sequence_number); // 移除已接收的包
}
2、FEC冗余协议(Opus带内FEC)
详细原理
FEC(Forward Error Correction,前向纠错)通过在发送端添加冗余数据,使接收端无需重传即可恢复丢失的包。WebRTC中音频FEC基于Opus编码器的"带内FEC"(RFC 7845):编码器在当前音频帧中嵌入前一帧的压缩冗余数据(不增加额外包),接收端解码器若检测到当前帧丢失,可使用前一帧的冗余数据重建。
核心作用
- 解决瞬时丢包(1-2帧),避免NACK重传带来的延迟;
- 轻量化实现(无额外包开销),适合带宽受限场景。
WebRTC源码实现(codecs/opus/opus_encoder_impl.cc)
cpp
// 初始化Opus编码器并开启带内FEC
int OpusEncoderImpl::Init() {
// 创建Opus编码器(48kHz采样率,单声道,VOIP优化)
encoder_ = opus_encoder_create(48000, 1, OPUS_APPLICATION_VOIP, &error_);
if (error_ != OPUS_OK) return -1;
// 开启带内FEC(通过Opus控制接口)
if (enable_fec_) {
int fec_enabled = 1; // 1=开启
error_ = opus_encoder_ctl(encoder_, OPUS_SET_INBAND_FEC, fec_enabled);
if (error_ != OPUS_OK) return -1;
}
// 配置预期丢包率(影响冗余量,丢包率高则冗余数据多)
error_ = opus_encoder_ctl(encoder_, OPUS_SET_PACKET_LOSS_PERC, packet_loss_perc_);
return error_ == OPUS_OK ? 0 : -1;
}
// 编码时嵌入冗余数据(当前帧+前一帧冗余)
int OpusEncoderImpl::EncodeImpl(const int16_t* audio,
size_t input_len,
rtc::Buffer* encoded) {
std::vector<float> audio_float(input_len); // 转为浮点输入
for (size_t i = 0; i < input_len; ++i) {
audio_float[i] = static_cast<float>(audio[i]);
}
// 编码输出包含当前帧数据+前一帧冗余(若FEC开启)
encoded->Resize(kMaxEncodedBytes);
int len = opus_encode_float(encoder_, audio_float.data(), input_len,
encoded->data(), encoded->size());
if (len < 0) return -1;
encoded->Resize(len);
return len;
}
3、交织编码
详细原理
交织编码通过"拆分-重组"打破音频帧的连续性:将连续N个音频帧(如2帧)各拆分为M个小块(如2块),按"块索引"重组后传输(帧1块0→帧2块0→帧1块1→帧2块1)。接收端解交织时恢复原始顺序,即使丢失连续块,仍能从剩余块中恢复部分帧数据,避免长段音频丢失。
核心作用
- 对抗连续丢包(如3-4个包丢失),将"长段卡顿"分解为"分散杂音";
- 不增加数据量,仅改变传输顺序,适合突发丢包场景。
WebRTC源码实现(neteq/interleaver.cc)
cpp
// 交织:拆分帧并按块索引重组
void Interleaver::Interleave(const std::vector<rtc::Buffer>& input_frames,
std::vector<rtc::Buffer>* output_frames) {
output_frames->clear();
if (input_frames.empty()) return;
const size_t kInterleaveDepth = 2; // 交织深度(每帧拆2块)
size_t block_size = input_frames[0].size() / kInterleaveDepth;
// 按块索引重组:先所有帧的第0块,再第1块
for (size_t block_idx = 0; block_idx < kInterleaveDepth; ++block_idx) {
rtc::Buffer interleaved_block;
for (const auto& frame : input_frames) {
const uint8_t* block_start = frame.data() + block_idx * block_size;
interleaved_block.AppendData(block_start, block_size);
}
output_frames->push_back(interleaved_block);
}
}
// 解交织:恢复原始帧顺序
void Interleaver::Deinterleave(const std::vector<rtc::Buffer>& input_blocks,
std::vector<rtc::Buffer>* output_frames) {
output_frames->clear();
if (input_blocks.empty()) return;
const size_t kInterleaveDepth = input_blocks.size();
size_t num_frames = input_blocks[0].size() / (input_blocks[0].size() / kInterleaveDepth);
size_t frame_size = (input_blocks[0].size() * kInterleaveDepth) / num_frames;
// 拼接各块的对应部分为原始帧
for (size_t frame_idx = 0; frame_idx < num_frames; ++frame_idx) {
rtc::Buffer frame;
for (size_t block_idx = 0; block_idx < kInterleaveDepth; ++block_idx) {
const uint8_t* block_start = input_blocks[block_idx].data() + frame_idx * (frame_size / kInterleaveDepth);
frame.AppendData(block_start, frame_size / kInterleaveDepth);
}
output_frames->push_back(frame);
}
}
4、JitterBuff(抗网络抖动)
由网络延时统计 、缓冲BUF延迟统计 、控制命令决策判定三个子模块组成,协同实现抗抖动。
4.1. 网络延时统计算法
(delay_manager.cc)
原理
通过统计历史音频包的传输延迟(到达时间-发送时间),计算延迟分布的"95分位值"作为目标缓冲延迟------确保95%的包能在缓冲内等待至平稳输出,覆盖大部分网络抖动。
作用
- 动态估计网络抖动强度,为缓冲大小提供参考;
- 避免缓冲过小(导致卡顿)或过大(导致延迟过高)。
源码实现
cpp
// 更新网络延迟样本并计算目标缓冲
void DelayManager::Update(int64_t arrival_time_ms, uint32_t rtp_timestamp) {
int64_t send_time_ms = RtpTimestampToMs(rtp_timestamp); // RTP时间戳转发送时间
int64_t one_way_delay_ms = arrival_time_ms - send_time_ms; // 传输延迟
if (one_way_delay_ms < 0) one_way_delay_ms = 0; // 过滤异常值
// 保留最近kMaxDelaySamples个延迟样本(如100个)
delay_samples_.push_back(one_way_delay_ms);
if (delay_samples_.size() > kMaxDelaySamples) {
delay_samples_.pop_front();
}
// 目标延迟=95分位值(覆盖95%的抖动)
target_delay_ms_ = ComputePercentile(delay_samples_, 0.95);
}
4.2. 缓冲BUF延迟统计算法
(buffer_level_filter.cc)
原理
对当前缓冲的"水位"(已缓存音频的时长)进行低通滤波,平滑瞬时波动(如突发的包到达),避免因短期波动导致缓冲调整过于频繁(如频繁加速/减速)。
作用
- 稳定缓冲状态,减少不必要的平滑处理;
- 为决策模块提供可靠的缓冲水位数据。
源码实现
cpp
// 平滑缓冲水位(低通滤波)
void BufferLevelFilter::Update(int buffer_level_ms) {
const float alpha = 0.8f; // 平滑系数(经验值)
// 公式:当前平滑值 = 0.8*上一次平滑值 + 0.2*当前原始值
filtered_level_ms_ = alpha * filtered_level_ms_ + (1 - alpha) * buffer_level_ms;
}
4.3. 控制命令决策判定
(decision_logic.cc)
原理
综合网络目标延迟(DelayManager)、当前缓冲水位(BufferLevelFilter)、上一帧处理方式,输出五大处理命令(kNormal/kAccelerate/kExpand/kAlternativePlc/kMerge),决定音频帧的播放策略。
作用
- 动态平衡缓冲大小与播放延迟,确保音频流畅;
- 触发丢包补偿或平滑过渡,掩盖网络异常。
源码实现
cpp
// 生成处理命令
Operation DecisionLogic::GetDecision(int buffer_level_ms,
int target_delay_ms,
Operation last_operation) {
// 缓冲过高→加速播放(减少缓冲)
if (buffer_level_ms > target_delay_ms + kHighThresholdMs) {
return kAccelerate;
}
// 缓冲过低→减速播放(增加缓冲)
else if (buffer_level_ms < target_delay_ms - kLowThresholdMs) {
return kExpand;
}
// 检测到丢包→丢包补偿
else if (IsPacketLost()) {
return kAlternativePlc;
}
// 上一帧是融合→继续融合(平滑过渡)
else if (last_operation == kMerge) {
return kMerge;
}
// 正常状态→正常播放
else {
return kNormal;
}
}
5、音频平滑处理方法
根据JitterBuff的命令,对音频帧进行实时处理,确保播放流畅。
5.1. kNormal(正常播放)
原理 :直接输出解码后的音频帧,无额外处理。
作用 :缓冲状态正常时,保证音频原始质量。
源码 :NetEqImpl::ProcessNormal 直接复制解码数据到输出。
cpp
// 正常播放:直接输出解码后的音频帧
int NetEqImpl::ProcessNormal(const DecodedFrame& frame,
AudioFrame* output) {
// 复制解码后的PCM数据到输出帧
output->CopyFrom(frame.data(), frame.length(), frame.sample_rate_hz());
output->set_absolute_capture_timestamp_ms(frame.timestamp_ms());
return frame.length(); // 返回样本数
}
5.2. kAccelerate(加速播放)
原理 :采用WSOLA(波形相似叠加)算法,在不改变音调的前提下缩短音频时长(如10ms帧压缩至8ms),减少缓冲占用。
作用 :当缓冲过高时,快速消耗缓冲,避免延迟累积。
源码 :TimeStretch::Process 通过寻找相似波形片段并重叠相加,实现"变速不变调"。
cpp
// 加速播放处理
// input:输入音频(解码后)
// input_len:输入样本数
// output_len:目标输出样本数(小于input_len,实现加速)
// output:加速后的音频
int TimeStretch::Process(const int16_t* input,
size_t input_len,
size_t output_len,
int16_t* output) {
// 1. 对输入信号加汉宁窗(减少频谱泄漏)
std::vector<float> input_win(input_len);
ApplyHannWindow(input, input_len, input_win.data());
// 2. 寻找相似波形片段(确保拼接后平滑无跳跃)
size_t best_match_pos = FindBestMatch(input_win, input_len, output_len);
// 3. 重叠相加:将输入的前半段与相似片段的后半段叠加
OverlapAdd(input_win.data(), best_match_pos, output, output_len);
return output_len; // 返回加速后的样本数
}
5.3. kExpand(减速播放)
原理 :延长音频时长(如10ms帧扩展至12ms),通过线性预测(LPC)生成扩展部分,增加缓冲。
作用 :当缓冲过低时,缓慢消耗缓冲,避免后续包到达前缓冲为空(卡顿)。
源码 :Expand::ProcessSlow 基于LPC系数预测语音趋势,生成扩展片段。
cpp
// 减速播放(扩展帧)
// input:输入音频
// input_len:输入样本数
// output_len:目标输出样本数(大于input_len,实现减速)
// output:减速后的音频
int Expand::ProcessSlow(const int16_t* input,
size_t input_len,
size_t output_len,
int16_t* output) {
// 1. 分帧并提取语音特征(线性预测系数LPC)
std::vector<float> lpc_coeffs;
ComputeLpc(input, input_len, &lpc_coeffs);
// 2. 基于LPC系数生成扩展部分(预测语音趋势)
std::vector<float> extended(output_len);
ExtendUsingLpc(lpc_coeffs, input, input_len, extended.data(), output_len);
// 3. 重叠相加原始帧和扩展部分,保证平滑过渡
OverlapAddOriginalAndExtended(input, input_len, extended.data(), output_len, output);
return output_len;
}
5.4. kAlternativePlc(丢包补偿)
原理 :利用上一帧的语音特征(LPC系数)生成伪音频帧,填补丢失的帧,避免播放中断。
作用 :掩盖丢包导致的音频空洞,保证听感连续。
源码 :Expand::Process 用LPC预测生成伪帧,加入少量噪声提升自然度。
cpp
// 丢包补偿:生成伪音频帧
// last_received_frame:上一个正常接收的帧
// last_frame_len:上一帧样本数
// output_len:补偿帧的样本数
// output:生成的伪音频帧
int Expand::Process(const int16_t* last_received_frame,
size_t last_frame_len,
size_t output_len,
int16_t* output) {
// 1. 用LPC提取上一帧的语音特征(如共振峰频率)
std::vector<float> lpc_coeffs;
ComputeLpc(last_received_frame, last_frame_len, &lpc_coeffs);
// 2. 基于LPC系数预测丢失帧的波形(模仿上一帧的语音趋势)
std::vector<float> predicted(output_len);
PredictUsingLpc(lpc_coeffs, last_received_frame, last_frame_len, predicted.data(), output_len);
// 3. 加入少量舒适噪声,避免生成信号过于单调(提升听感)
AddComfortNoise(predicted.data(), output_len, 0.01f); // 噪声强度0.01
// 4. 转换为16位PCM格式
for (size_t i = 0; i < output_len; ++i) {
output[i] = static_cast<int16_t>(predicted[i]);
}
return output_len;
}
5.5. kMerge(融合处理)
原理 :将前一帧(如补偿帧)与当前帧(正常帧)的重叠区域加权融合,平滑过渡。
作用 :避免从补偿帧切换到正常帧时的听感跳变。
源码 :Merge::Process 对重叠区域加窗(前帧衰减、后帧增强)后叠加。
cpp
// 融合处理:平滑过渡两帧
// prev_frame:历史帧(如补偿帧)
// curr_frame:当前帧(正常解码帧)
// output_len:融合后样本数
// output:融合后的音频
int Merge::Process(const int16_t* prev_frame,
size_t prev_len,
const int16_t* curr_frame,
size_t curr_len,
size_t output_len,
int16_t* output) {
// 1. 计算重叠区域(取两帧的后半段和前半段)
size_t overlap_len = std::min(prev_len, curr_len) / 2;
// 2. 对重叠区域加窗(前帧用余弦窗衰减,后帧用余弦窗增强)
std::vector<float> prev_win(overlap_len);
std::vector<float> curr_win(overlap_len);
ApplyOverlapWindows(prev_win.data(), curr_win.data(), overlap_len);
// 3. 重叠区域融合:前帧*衰减窗 + 后帧*增强窗
std::vector<float> merged(overlap_len);
for (size_t i = 0; i < overlap_len; ++i) {
merged[i] = prev_frame[prev_len - overlap_len + i] * prev_win[i] +
curr_frame[i] * curr_win[i];
}
// 4. 拼接非重叠部分和融合部分
memcpy(output, prev_frame, (prev_len - overlap_len) * sizeof(int16_t));
memcpy(output + (prev_len - overlap_len), merged.data(), overlap_len * sizeof(int16_t));
memcpy(output + prev_len, curr_frame + overlap_len, (curr_len - overlap_len) * sizeof(int16_t));
return output_len;
}
6、音频NetEQ与视频NetEQ的细化异同
| 维度 | 音频NetEQ | 视频NetEQ |
|---|---|---|
| 核心目标 | 优先保证低延迟 (<500ms)和流畅性,容忍轻微音质损失 | 优先保证画质完整 和流畅性,可接受较高延迟(1-3s) |
| 数据特性 | 帧小(10-60ms/帧,<1KB)、时序敏感(断帧即卡顿) | 帧大(33ms/帧,10-100KB)、有冗余信息(如I帧可独立解码) |
| NACK机制 | 重传窗口极小(<200ms),超时立即放弃(避免延迟); 源码:neteq/nack.cc |
重传窗口大(<1s),允许多次重传(优先保画质); 源码:video_coding/jitter_buffer/nack_module.cc |
| FEC实现 | 仅支持带内FEC (Opus帧内嵌入冗余),开销<10%; 无额外包,适合带宽受限 | 支持带外FEC (单独FEC包),开销20%-50%; 可配置冗余度(如1:1保护),适合重要帧(I帧) |
| 交织策略 | 细粒度拆分(每帧拆2-4块),延迟影响可忽略; 源码:neteq/interleaver.cc |
粗粒度拆分(每GOP拆2块),增加50-100ms延迟; 源码:video_coding/interleaving.cc |
| JitterBuff | 缓冲小(<500ms),调整激进(频繁加速/减速); 目标延迟=95分位抖动 | 缓冲大(1-3s),调整保守(避免频繁画质波动); 目标延迟=固定值+动态补偿 |
| 平滑处理 | 基于音频波形特征(WSOLA、LPC),注重听感自然 ; 计算量小(毫秒级) | 基于图像运动特征(帧复制、运动补偿),注重视觉连贯 ; 计算量大(几十毫秒级) |
| 丢包影响 | 丢1帧即感知卡顿,需快速补偿(PLC) | 丢1帧可通过前后帧掩盖(如P帧依赖前帧),I帧丢失影响大 |
总结
音频NetEQ是WebRTC针对实时音频设计的"抗网络干扰引擎",通过NACK、FEC、交织编码抵御丢包,JitterBuff动态平衡延迟与缓冲,结合五种平滑处理命令保证播放流畅,核心是"低延迟优先"。与视频NetEQ相比,因音频对延迟更敏感、数据量更小,策略上更轻量化,实现上更注重实时性和听感自然,两者共同构成WebRTC实时媒体传输的质量保障体系。