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

发布于:2025-09-06 ⋅ 阅读:(14) ⋅ 点赞:(0)


前言

语音活动检测(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]之间
  • 校验输入的帧长是否在 [ 10 m s , 20 m s , 30 m s ] [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; %Q15

x_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-1

for i = 1:2:length(x_total)
	tmp32 = filter_state + int32(a_1) * int32(x_total(i)); %Q15
	x_up((i+1)/2) = int32(bitshift(tmp32, -16));%Q-1
	filter_state = bitshift(int32(x_total(i)), 14) - int32(a_1) * int32(x_up((i+1)/2)); %Q14
	filter_state = bitshift(filter_state,1); %Q15
end

filter_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)); %Q15
	x_low(i/2) = int32(bitshift(tmp32, -16));%Q-1
	filter_state = bitshift(int32(x_total(i)), 14) - int32(a_2) * int32(x_low(i/2)); %Q14
	filter_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 ] = b 0 x [ n ] + b 1 x [ n − 1 ] + b 2 x [ n − 2 ] − a 1 y [ n − 1 ] − a 2 y [ 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的计算公式如下
E d b = 10 ⋅ log ⁡ 10 ( E ) < < 4 = 2 4 ⋅ 10 ⋅ log ⁡ 10 ( E ) = 160 ⋅ log ⁡ 10 ( E ) E = ∑ n = 0 N − 1 x [ n ] 2 E_{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

这里就引入两个问题:

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

2.原理公式解析

webrtc中为了解决上述问题,将上述计算能量的公式进行变形以保证不溢出并且易于计算:
E d b = 160 ⋅ log ⁡ 10 ( E ) = 160 ⋅ log ⁡ 10 ( E s ⋅ 2 s h i f t ) = 160 ⋅ log ⁡ 10 ( 2 ) ⋅ log ⁡ 2 ( E s ⋅ 2 s h i f t ) = 160 ⋅ log ⁡ 10 ( 2 ) ⋅ ( log ⁡ 2 ( E s ) + log ⁡ 2 ( 2 s h i f t ) ) = 160 ⋅ log ⁡ 10 ( 2 ) ⋅ ( log ⁡ 2 ( E s ) + s h i f t ) 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存储的能量缩放到 [ 2 14 , 2 15 ] [2^{14},2^{15}] [214,215]区间,然后统计总移位数,即

bit15 bit14 ........ bit0
  0    1   xxxxxxxxxxxxxx

这样做的好处是能量可以表示为:
E = ( 2 14 + f r a c ) ⋅ 2 s h i f t ; E s = 2 14 + f r a c E = (2^{14}+frac)\cdot 2^{shift}; \quad E_s = 2^{14}+frac E=(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)第三步:对数处理

根据公式:
E d b = 160 ⋅ log ⁡ 10 ( 2 ) ⋅ ( log ⁡ 2 ( E s ) + s h i f t ) E_{db} = 160 \cdot \log_{10}(2)\cdot \big(\log_{2}(E_s)+shift \big) Edb=160log10(2)(log2(Es)+shift)
此时单独将 E s E_s Es带入,那么:
log ⁡ 2 ( 2 14 + f r a c ) = 14 + log ⁡ 2 ( 1 + f r a c 2 14 ) \log_2(2^{14}+frac) = 14+\log_2(1+\frac{frac}{2^{14}}) log2(214+frac)=14+log2(1+214frac)
我们知道 f r a c frac frac是一个低14bit的数,所以 f r a c 2 14 \frac{frac}{2^{14}} 214frac是一个 [ 0 , 1 ] [0,1] [0,1]区间的小数,那么加上1后实际上就是 1 + f r a c 2 14 = f r a c _ Q 15 1+\frac{frac}{2^{14}} = frac\_Q15 1+214frac=frac_Q15的Q15的小数(实际小数位为14)。所以对其做Q格式转换:
log ⁡ 2 ( 2 14 + f r a c ) = 14 + log ⁡ 2 ( f r a c _ Q 15 ) = > log ⁡ 2 ( E s ) Q 10 ≈ 14 < < 10 + f r a c _ Q 15 2 4 \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.245 160 \cdot \log_{10}(2) \approx 48.245 160log10(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 Q 9 ⋅ Q 10 = Q 19 Q9\cdot Q10 =Q19 Q9Q10=Q19,右移19位变成Q0
  • tot_rshifts * kLogConst Q 0 ⋅ Q 9 = Q 9 Q0\cdot Q9 =Q9 Q0Q9=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 如何基于这些能量特征完成最终的人声检测。

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


网站公告

今日签到

点亮在社区的每一天
去签到