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
:将会根据模式采用不同的高斯模型混合参数,主要有以下表格中的区别
模式 | 名称 | 特点 | 适用场景 |
---|---|---|---|
0 | Quality | 低误报,低灵敏度 | 安静环境,录音质量要求高 |
1 | Low bitrate | 平衡模式 | 普通 VoIP,低码率传输 |
2 | Aggressive | 高检测率,中等误报 | 嘈杂语音通话 |
3 | Very 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 ms | 20 ms | 30 ms |
---|---|---|---|
8k | 80 | 160 | 240 |
16k | 160 | 320 | 480 |
32k | 320 | 640 | 960 |
48k | 480 | 960 | 1440 |
3)VAD计算
WebRtcVad_CalculateFeatures
:能量计算GmmProbability
:人声判断
二、频带划分
1.频带划分意义
1)作用
对于输入的语音信号进行6次非等宽划分,这么做是因为语音能量分布在不同频带是有差别的,并且这种划分可以减少后续人声判定的计算量。
频带编号 | 频率范围 (Hz) | 说明 |
---|---|---|
Band 0 | 0 – 200 | 低频 (基音区),语音和噪声都可能有能量 |
Band 1 | 200 – 400 | 低频部分 |
Band 2 | 400 – 800 | 低中频,语音元音能量明显 |
Band 3 | 800 – 1600 | 中频,包含主要语音共振峰 |
Band 4 | 1600 – 3000 | 高频部分,辅音特征明显 |
Band 5 | 3000 – 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[n−1]+b2x[n−2]−a1y[n−1]−a2y[n−2]
源码核心:
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[n−1]filter_state[1]
:存储x[n−2]x[n-2]x[n−2]filter_state[2]
:存储y[n−1]y[n-1]y[n−1]filter_state[3]
:存储y[n−2]y[n-2]y[n−2]tmp32 >> 14
:这是由于滤波器系数为Q14格式
三、能量计算
1.能量统计介绍
在语音数字信号处理——计算pcm分贝文章中,已经介绍过分贝和能量的原理,这里不再赘述,webrtc的VAD更为看重的是能量变化而不是分贝,并且最终是以Q4的格式进行存储,也就是说log_energy
的计算公式如下
Edb=10⋅log10(E)<<4=24⋅10⋅log10(E)=160⋅log10(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=10⋅log10(E)<<4=24⋅10⋅log10(E)=160⋅log10(E)E=n=0∑N−1x[n]2
这里就引入两个问题:
- EEE:计算时可能超过计算机存储大小
- log\loglog:在 DSP(尤其是老设备、嵌入式里)不好算
2.原理公式解析
webrtc中为了解决上述问题,将上述计算能量的公式进行变形以保证不溢出并且易于计算:
Edb=160⋅log10(E)=160⋅log10(Es⋅2shift)=160⋅log10(2)⋅log2(Es⋅2shift)=160⋅log10(2)⋅(log2(Es)+log2(2shift))=160⋅log10(2)⋅(log2(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=160⋅log10(E)=160⋅log10(Es⋅2shift)=160⋅log10(2)⋅log2(Es⋅2shift)=160⋅log10(2)⋅(log2(Es)+log2(2shift))=160⋅log10(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⋅log10(2)⋅(log2(Es)+shift)E_{db} = 160 \cdot \log_{10}(2)\cdot \big(\log_{2}(E_s)+shift \big)Edb=160⋅log10(2)⋅(log2(Es)+shift)
此时单独将EsE_sEs带入,那么:
log2(214+frac)=14+log2(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格式转换:
log2(214+frac)=14+log2(frac_Q15)=>log2(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)Q10≈14<<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⋅log10(2)≈48.245160 \cdot \log_{10}(2) \approx 48.245160⋅log10(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_energy
:Q9⋅Q10=Q19Q9\cdot Q10 =Q19Q9⋅Q10=Q19,右移19位变成Q0tot_rshifts * kLogConst
:Q0⋅Q9=Q9Q0\cdot Q9 =Q9Q0⋅Q9=Q9,右移9位变成Q0
值得注意的是160⋅log10(2)160 \cdot \log_{10}(2)160⋅log10(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的接口与实现,重点解析了能量统计的整个过程:
- 统一重采样至 8 kHz,确保统计量与阈值的一致性;
- 通过 all-pass 与高通滤波器进行 六频带划分,并在各子带上计算能量;
- 使用定点近似方法完成 能量的对数量化,在保证效率的同时避免溢出和复杂运算;
- 将能量结果以 Q4 格式保存,分别作为 log_energy(细粒度特征)和 total_energy(粗粒度能量)供后续判定使用。
VAD 的输入信号经过降采样、分频带和能量特征提取,已经具备了进一步判定语音/非语音的基础条件。下一篇文章将继续深入 GMM 模型与判定逻辑,详细介绍WebRTC VAD 如何基于这些能量特征完成最终的人声检测。
反正收藏也不会看,不如点个赞吧!