当前位置: 首页 > news >正文

webrtc之语音活动上——VAD能量检测原理以及源码详解

文章目录

  • 前言
  • 一、核心接口介绍
    • 1.VAD创建以及初始化
    • 2.模式控制
    • 3.VAD判断
      • 1)降采样
      • 2)长度校验
      • 3)VAD计算
  • 二、频带划分
    • 1.频带划分意义
      • 1)作用
      • 2)流程
    • 2.全通滤波器
      • 1)原理
      • 2)matlab代码
      • 3)总结
    • 3.高通滤波器
  • 三、能量计算
    • 1.能量统计介绍
    • 2.原理公式解析
    • 3.计算过程
      • 1)第一步:计算能量
      • 2)第二步:能量缩放
      • 3)第三步:对数处理
      • 4)第四步:计算能量
      • 5)能量总计
  • 总结


前言

语音活动检测(Voice Activity Detection, VAD)是语音处理链路中的关键环节,它负责在输入的音频流中区分“有声片段”和“静音/噪声片段”。一个可靠的 VAD 能有效降低带宽消耗、提升编解码效率,并为回声消除、降噪、语音识别等模块提供更稳定的输入。

在 WebRTC 中,VAD 的实现主要依赖两个核心:

  • 能量统计:通过分频带计算并量化语音能量,得到特征量;
  • 人声判定:利用高斯混合模型(GMM)及阈值策略,基于能量特征区分语音与非语音。

本文将首先介绍 VAD 的核心接口,随后重点解析 能量统计的算法原理与定点实现策略。至于人声判定的高斯模型细节与噪声跟踪逻辑,将在下一篇文章中单独展开。

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


一、核心接口介绍

VAD的接口主要在webrtc_vad,vad_core两个核心文件中,前者webrtc_vad是上层对外接口,vad_core则是具体的核心算法以及策略实现。

1.VAD创建以及初始化

  • WebRtcVad_Create:主要初始化一个VadInst的结构体,内部存放的是VAD的各种滤波状态、GMM模型参数、噪声/语音统计、特征等等参数。
  • WebRtcVad_Init:初始化VadInst结构体

结构体内部成员含义以及使用场景这篇文章不会涉及,将会在下篇文章使用高斯模型进行语音判断中讲解。

2.模式控制

WebRtcVad_set_mode:将会根据模式采用不同的高斯模型混合参数,主要有以下表格中的区别

模式名称特点适用场景
0Quality低误报,低灵敏度安静环境,录音质量要求高
1Low bitrate平衡模式普通 VoIP,低码率传输
2Aggressive高检测率,中等误报嘈杂语音通话
3Very aggressive极高检测率,高误报车载、远场、噪声大场景

3.VAD判断

WebRtcVad_Process:进行是否人声判断,内部主要做以下几件事

1)降采样

在vad算法检测中,所有的语音都统一将采用为8kHz进行处理,具体原因是:

  • VAD 算法设计基于 8kHz:WebRTC 的 经典 VAD 算法最初就是在窄带语音 (8kHz) 上开发的。
  • WebRTC 的这版 VAD 模型与阈值都是基于 8 kHz 训练/标定的
  • 统一输入,简化处理:不管输入多高,都转到 8kHz,这样核心 VAD 算法只需要维护一个版本
  • 效率与鲁棒性:人类语音的主要能量和可辨识特征都集中在0–4kHz 范围(8kHz 采样率可完整保留)。高于 4 kHz 的部分反而可能增加噪声干扰。

在vad核心初始化中:

int WebRtcVad_InitCore(VadInstT* self)
{
...WebRtcSpl_ResetResample48khzTo8khz(&self->state_48_to_8);
...
}

会先初始化48k重采样到8k的采样器,这是由于16kHz→8kHz、32kHz→8kHz分别只需要2:1 下采样和4:1下采样,而48kHz属于特殊情况,所以最开始进行初始化。

16kHz 和 32kHz 的情况是通过 逐级 2:1 下采样(16→8,32→16→8)完成的,因此 32kHz 会“调用两次 2:1 下采样核”,并不是同一个函数简单调用两次。核心接口如下:

void WebRtcVad_Downsampling(const int16_t* signal_in,int16_t* signal_out,int32_t* filter_state,size_t in_length)

而该下采样同样使用的是IIR滤波器,其原理在之前文章webrtc之子带分割上——All-pass QMF滤波器中已经介绍过。

2)长度校验

int WebRtcVad_ValidRateAndFrameLength(int rate, size_t frame_length)
  • 校验采样率是否在[8000,16000,32000,48000][8000,16000,32000,48000][8000,16000,32000,48000]之间
  • 校验输入的帧长是否在[10ms,20ms,30ms][10ms,20ms,30ms][10ms,20ms,30ms]

映射关系如下:

采样率10 ms20 ms30 ms
8k80160240
16k160320480
32k320640960
48k4809601440

3)VAD计算

  • WebRtcVad_CalculateFeatures:能量计算
  • GmmProbability:人声判断

二、频带划分

1.频带划分意义

1)作用

对于输入的语音信号进行6次非等宽划分,这么做是因为语音能量分布在不同频带是有差别的,并且这种划分可以减少后续人声判定的计算量。

频带编号频率范围 (Hz)说明
Band 00 – 200低频 (基音区),语音和噪声都可能有能量
Band 1200 – 400低频部分
Band 2400 – 800低中频,语音元音能量明显
Band 3800 – 1600中频,包含主要语音共振峰
Band 41600 – 3000高频部分,辅音特征明显
Band 53000 – 4000高频末端,区分噪声/语音的重要区域

2)流程

频带划分的总过程如下:主要使用全通滤波器和高通滤波器进行划分,然后对各个频带进行能量统计,如下图:
在这里插入图片描述

2.全通滤波器

1)原理

这里和SplittingFilter的原理基本一样,见文章webrtc之子带分割下——SplittingFilter源码分析。

2)matlab代码

这里使用matlab模拟webrtc中子带分割源码过程,然后对一段模拟正弦波语音进行分割

clc;clear;
fs = 8000;
t = 0:1/fs:1-1/fs;
f1 = 500;
f2 = 2800;a_1 = 20972; %Q15
a_2 = 5571; %Q15x_total = int32((sin(2*pi*f1*t) + sin(2*pi*f2*t))*32767); %Q0 模拟真实short类型信号filter_state = bitshift(0,16); %Q15
x_up = zeros(1,length(x_total)/2, 'int32'); %Q-1for i = 1:2:length(x_total)tmp32 = filter_state + int32(a_1) * int32(x_total(i)); %Q15x_up((i+1)/2) = int32(bitshift(tmp32, -16));%Q-1filter_state = bitshift(int32(x_total(i)), 14) - int32(a_1) * int32(x_up((i+1)/2)); %Q14filter_state = bitshift(filter_state,1); %Q15
endfilter_state = bitshift(0,16); %Q15
x_low = zeros(1,length(x_total)/2, 'int32'); %Q-1
for i = 2:2:length(x_total)tmp32 = filter_state + int32(a_2) * int32(x_total(i)); %Q15x_low(i/2) = int32(bitshift(tmp32, -16));%Q-1filter_state = bitshift(int32(x_total(i)), 14) - int32(a_2) * int32(x_low(i/2)); %Q14filter_state = bitshift(filter_state,1); %Q15
end%% 原始信号频谱
N = length(x_total);
X = fft(double(x_total).*hamming(N)');   % 加窗避免泄漏
f = (0:N-1)*(fs/N);                      % 频率坐标
figure;
subplot(3,1,1);
plot(f(1:N/2),abs(X(1:N/2)));
xlabel('Frequency (Hz)'); ylabel('Magnitude (dB)');
title('原始信号频谱');
grid on;%% 上支路信号频谱
N_up = length(x_up);
X_up = fft(double(x_up).*hamming(N_up)'); 
f_up = (0:N_up-1)*(fs/2/N_up);  % 下采样一半 → 采样率 fs/2
subplot(3,1,2);
plot(f_up(1:N_up/2),20*log10(abs(X_up(1:N_up/2))));
xlabel('Frequency (Hz)'); ylabel('Magnitude (dB)');
title('上支路输出频谱');
grid on;%% 下支路信号频谱
N_low = length(x_low);
X_low = fft(double(x_low).*hamming(N_low)'); 
f_low = (0:N_low-1)*(fs/2/N_low);
subplot(3,1,3);
plot(f_low(1:N_low/2),20*log10(abs(X_low(1:N_low/2))));
xlabel('Frequency (Hz)'); ylabel('Magnitude (dB)');
title('下支路输出频谱');
grid on;

输出结果如下图:
在这里插入图片描述

3)总结

webrtc中的滤波器系数是Q15形式:

static const int16_t kAllPassCoefsQ15[2] = { 20972, 5571 };

只是和SplittingFilter不同的是这里的核心是计算出能量分布,而并不需要保证完美重构,使用的并不是三个一阶滤波器进行级联,而是只有一个一阶IIR滤波器。

3.高通滤波器

为了抑制直流与超低频分量(~80 Hz 以下),VAD 使用二阶 IIR 高通,和webrtc之高通滤波——HighPassFilter源码及原理分析文章中一样,而vad中的高通滤波器使用的是给定的滤波器系数而不是零极点,所以它的公式为:
y[n]=b0x[n]+b1x[n−1]+b2x[n−2]−a1y[n−1]−a2y[n−2]y[n]=b_0x[n]+b_1x[n-1]+b_2x[n-2]-a_1y[n-1]-a_2y[n-2]y[n]=b0x[n]+b1x[n1]+b2x[n2]a1y[n1]a2y[n2]
源码核心:

for (i = 0; i < data_length; i++) {// All-zero section (filter coefficients in Q14).tmp32 = kHpZeroCoefs[0] * *in_ptr;tmp32 += kHpZeroCoefs[1] * filter_state[0];tmp32 += kHpZeroCoefs[2] * filter_state[1];filter_state[1] = filter_state[0];filter_state[0] = *in_ptr++;// All-pole section (filter coefficients in Q14).tmp32 -= kHpPoleCoefs[1] * filter_state[2];tmp32 -= kHpPoleCoefs[2] * filter_state[3];filter_state[3] = filter_state[2];filter_state[2] = (int16_t) (tmp32 >> 14);*out_ptr++ = filter_state[2];}
  • filter_state[0]:存储x[n−1]x[n-1]x[n1]
  • filter_state[1]:存储x[n−2]x[n-2]x[n2]
  • filter_state[2]:存储y[n−1]y[n-1]y[n1]
  • filter_state[3]:存储y[n−2]y[n-2]y[n2]
  • tmp32 >> 14:这是由于滤波器系数为Q14格式

三、能量计算

1.能量统计介绍

在语音数字信号处理——计算pcm分贝文章中,已经介绍过分贝和能量的原理,这里不再赘述,webrtc的VAD更为看重的是能量变化而不是分贝,并且最终是以Q4的格式进行存储,也就是说log_energy的计算公式如下
Edb=10⋅log⁡10(E)<<4=24⋅10⋅log⁡10(E)=160⋅log⁡10(E)E=∑n=0N−1x[n]2E_{db} = 10 \cdot \log_{10}(E) << 4 = 2^4 \cdot 10 \cdot \log_{10}(E) = 160 \cdot \log_{10}(E) \\ E = \sum_{n=0}^{N-1}x[n]^2 Edb=10log10(E)<<4=2410log10(E)=160log10(E)E=n=0N1x[n]2

这里就引入两个问题:

  • EEE:计算时可能超过计算机存储大小
  • log⁡\loglog:在 DSP(尤其是老设备、嵌入式里)不好算

2.原理公式解析

webrtc中为了解决上述问题,将上述计算能量的公式进行变形以保证不溢出并且易于计算:
Edb=160⋅log⁡10(E)=160⋅log⁡10(Es⋅2shift)=160⋅log⁡10(2)⋅log⁡2(Es⋅2shift)=160⋅log⁡10(2)⋅(log⁡2(Es)+log⁡2(2shift))=160⋅log⁡10(2)⋅(log⁡2(Es)+shift)E_{db}=160 \cdot \log_{10}(E) = 160 \cdot \log_{10}(E_s \cdot 2^{shift}) \\ = 160 \cdot \log_{10}(2)\cdot \log_{2}(E_s \cdot 2^{shift}) \\ = 160 \cdot \log_{10}(2)\cdot \big(\log_{2}(E_s)+\log_2( 2^{shift})\big) \\ =160 \cdot \log_{10}(2)\cdot \big(\log_{2}(E_s)+shift \big) Edb=160log10(E)=160log10(Es2shift)=160log10(2)log2(Es2shift)=160log10(2)(log2(Es)+log2(2shift))=160log10(2)(log2(Es)+shift)

3.计算过程

1)第一步:计算能量

在接口WebRtcSpl_Energy先计算出能量,此时使用int32_t类型存储,并防止数据溢出进行右移,右移大小保存在scale_factor

int32_t WebRtcSpl_Energy(int16_t* vector,size_t vector_length,int* scale_factor)

2)第二步:能量缩放

根据能量大小判断是缩放还是放大进行移位,最终将int32存储的能量缩放到[214,215][2^{14},2^{15}][214,215]区间,然后统计总移位数,即

bit15 bit14 ........ bit00    1   xxxxxxxxxxxxxx

这样做的好处是能量可以表示为:
E=(214+frac)⋅2shift;Es=214+fracE = (2^{14}+frac)\cdot 2^{shift}; \quad E_s = 2^{14}+fracE=(214+frac)2shift;Es=214+frac

对应代码:

int normalizing_rshifts = 17 - WebRtcSpl_NormU32(energy); //计算移位个数tot_rshifts += normalizing_rshifts; //总移位数
if (normalizing_rshifts < 0) {energy <<= -normalizing_rshifts; //左移放大
} else {energy >>= normalizing_rshifts; //右移缩小
}

3)第三步:对数处理

根据公式:
Edb=160⋅log⁡10(2)⋅(log⁡2(Es)+shift)E_{db} = 160 \cdot \log_{10}(2)\cdot \big(\log_{2}(E_s)+shift \big)Edb=160log10(2)(log2(Es)+shift)
此时单独将EsE_sEs带入,那么:
log⁡2(214+frac)=14+log⁡2(1+frac214)\log_2(2^{14}+frac) = 14+\log_2(1+\frac{frac}{2^{14}})log2(214+frac)=14+log2(1+214frac)
我们知道fracfracfrac是一个低14bit的数,所以frac214\frac{frac}{2^{14}}214frac是一个[0,1][0,1][0,1]区间的小数,那么加上1后实际上就是1+frac214=frac_Q151+\frac{frac}{2^{14}} = frac\_Q151+214frac=frac_Q15的Q15的小数(实际小数位为14)。所以对其做Q格式转换:
log⁡2(214+frac)=14+log⁡2(frac_Q15)=>log⁡2(Es)Q10≈14<<10+frac_Q1524\log_2(2^{14}+frac) = 14+\log_2( frac\_Q15) => \\ \log_2(E_s)_{Q10} \approx14<<10+\frac{ frac\_Q15}{2^{4}}log2(214+frac)=14+log2(frac_Q15)=>log2(Es)Q1014<<10+24frac_Q15
对应代码:

static const int16_t kLogEnergyIntPart = 14336;  // 14 in Q10
int16_t log2_energy = kLogEnergyIntPart ;

而webrtc中将log⁡(1+y),y∈[0,1]\log(1+y),\quad y \in [0,1]log(1+y),y[0,1]进行一阶近似,而不是严格的换底公式,最终的能量计算为:

log2_energy += (int16_t) ((energy & 0x00003FFF) >> 4);

4)第四步:计算能量

对于160⋅log⁡10(2)≈48.245160 \cdot \log_{10}(2) \approx 48.245160log10(2)48.245,实际精度不够,需要转化为Q9计算static const int16_t kLogConst = 24660; // 160*log10(2) in Q9.,此时最终能量计算为

*log_energy = (int16_t)(((kLogConst * log2_energy) >> 19) +((tot_rshifts * kLogConst) >> 9));
  • kLogConst * log2_energyQ9⋅Q10=Q19Q9\cdot Q10 =Q19Q9Q10=Q19,右移19位变成Q0
  • tot_rshifts * kLogConstQ0⋅Q9=Q9Q0\cdot Q9 =Q9Q0Q9=Q9,右移9位变成Q0

值得注意的是160⋅log⁡10(2)160 \cdot \log_{10}(2)160log10(2)本身就是Q4格式,所以最终结果是Q4

5)能量总计

  • log_energy:这个能力相当于这段语音当前频带的精细能量,用于更细粒度的 GMM/阈值比较。
  • total_energy:这段语音的粗略能量,更像门限快速通道,帮助尽快越过‘显著有声’门槛。

所以这里的策略是:total_energy 在能量较大时迅速“冲过门槛”,在能量较小时再逐步精确积累,对应代码比较容易理解,这里不再赘述

if (*total_energy <= kMinEnergy) {if (tot_rshifts >= 0) {*total_energy += kMinEnergy + 1;} else {*total_energy += (int16_t) (energy >> -tot_rshifts);  // Q0.}}

总结

本文首先介绍了 WebRTC VAD的接口与实现,重点解析了能量统计的整个过程:

  1. 统一重采样至 8 kHz,确保统计量与阈值的一致性;
  2. 通过 all-pass 与高通滤波器进行 六频带划分,并在各子带上计算能量;
  3. 使用定点近似方法完成 能量的对数量化,在保证效率的同时避免溢出和复杂运算;
  4. 将能量结果以 Q4 格式保存,分别作为 log_energy(细粒度特征)和 total_energy(粗粒度能量)供后续判定使用。

VAD 的输入信号经过降采样、分频带和能量特征提取,已经具备了进一步判定语音/非语音的基础条件。下一篇文章将继续深入 GMM 模型与判定逻辑,详细介绍WebRTC VAD 如何基于这些能量特征完成最终的人声检测。

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

http://www.xdnf.cn/news/1452961.html

相关文章:

  • 桌面应用开发语言与框架选择指南
  • android seekbar显示刻度
  • Python_occ 学习记录 | 细观建模(2)
  • 【C语言】第二课 位运算
  • QT6 配置 Copilot插件
  • Pycharm 试用
  • Spring简单的读取和存储对象
  • 君正T31学习(7)- 启动流程
  • 当有鹿机器人读懂城市呼吸的韵律——具身智能如何重构户外清洁生态
  • 2025变现打法:AI+IP实现高效变现|创客匠人
  • 第十四届蓝桥杯青少组C++国赛[2023.5.28]第二部分编程题(4、 数独填数)
  • JS中正则表达式的运用
  • android Thread线程—HandlerThread
  • 汽车v型推力杆总成三维5自由度性能及疲劳测试系统
  • 追觅科技举办2025「敢梦敢为」发布会,发布超30款全场景重磅新品
  • 【iOS】 懒加载
  • 每日工作计划管理工具:核心功能详解
  • 《Java餐厅的待客之道:BIO, NIO, AIO三种服务模式的进化》
  • UE5 制作游戏框架的部分经验积累(持续更新)
  • Mybatis入门、操作数据、配置xml映射、数据封装
  • 深入探讨AI三大领域的核心技术、实践方法以及未来发展趋势,结合具体代码示例、流程图和Prompt工程实践,全面展示AI编程的强大能力。
  • leetcode21.合并两个有序链表
  • 来自AI的背包系统
  • solar应急响应-7月
  • 怎样让外网计算机访问局域网计算机?通过公网地址访问不同内网服务的设置方法
  • Web 与 Nginx 网站服务介绍与nginx安装
  • 泛型-泛型方法
  • C++工程实战入门笔记10-面向对象之静态成员变量和成员函数、构造函数和析构函数
  • 【C++设计模式】第二篇:策略模式(Strategy)--从基本介绍,内部原理、应用场景、使用方法,常见问题和解决方案进行深度解析
  • 联软科技:以“韧性安全”守护数字世界,致敬抗战胜利80周年的坚韧精神