VAD模型简要概述

VAD 的核心任务是 判断一段音频中哪些时间段有人说话哪些时间段没有人说话(也就是他就是一个音频是不是有静音的检测器),也就是区分语音和非语音(或静音)部分。想象一下,在一个有背景噪音的环境中,VAD 就像一个智能“守门人”,它能准确识别什么时候有人在说话,什么时候是纯粹的环境噪音或沉默。

VAD模型工作原理

音频数字化过程:声音通过震动转化为电压的模拟信号,但是电脑只能处理数字信号(01等),因此在数字化信号处理过程中通过ADC(模数转换器)进行处理,其主要是如下几件事情:1、采样每隔固定时间测量一次声音幅度,常用指标 Hz表示每秒采样多少声音副本(常用的8Hz、16Hz);2、量化采样后数值结果是一批连续点,但是计算机存储数据范围有限如8bit就只有 2^8=256因此需要将数值进行量化处理如 0.123456V-->37,常用指标为 位深如8bit、16bit等;实际代码去理解音频处理过程(以模型训练中数据加载进行理解),在测试音频中通过绘制出波形图、Mel频谱图:
image
其中在波形图中,X 轴 = 时间,Y 轴 = 振幅(空气振动强度),密集振荡的区域 → 有声音,平坦区域 → 静音。在Mel频谱图中:X轴 = 时间,Y 轴 = Mel 频率(人耳感知的频率刻度,低频分辨更精细),颜色 = 能量 (dB)。它将一维时序信号变成二维热力图,同时展示什么频率在什么时候出现。

Mel 频谱图计算过程:
Step 1 — 预加重 (Pre-emphasis):$y_t’ = y_t - \alpha \cdot y_{t-1}$($\alpha \approx 0.97$),增强高频分量,补偿发声时声门和嘴唇对高频的衰减。
Step 2 — 分帧 (Framing):将一维信号切成重叠的小段。每帧长度通常 20~40ms(如 16kHz 下 512 点 = 32ms),帧移通常 10ms(如 160 点)。帧与帧之间有重叠,保证时间上的连续性。
Step 3 — 加窗 (Windowing):对每帧乘以汉明窗或汉宁窗,减少分帧带来的频谱泄漏(边缘截断效应):$x_n^{windowed} = x_n \cdot w_n$。
Step 4 — STFT / FFT:对每帧做短时傅里叶变换,得到该帧的功率谱:$P = |\text{FFT}(x)|^2$。此时得到的是线性频率刻度上的能量分布。
Step 5 — Mel 滤波器组 (Mel Filterbank):人耳对低频的分辨能力远高于高频。将线性频率 $f$(Hz)映射到 Mel 刻度:
\(m = 2595 \cdot \log_{10}\left(1 + \frac{f}{700}\right)\)
用 $N$ 个三角形滤波器(如 80 个)在 Mel 刻度上等距排列,对功率谱加权求和,得到 $N$ 维的 Mel 能量向量。
Step 6 — 转 dB (Log Compression):$\text{Mel}{\text{dB}} = 10 \cdot \log{10}\left(\frac{\text{Mel}}{\text{ref}}\right)$,取对数压缩动态范围,也更贴近人耳对响度的对数感知特性。最终得到一个 (n_mels × n_frames) 的二维矩阵——即 Mel 频谱图。

回到VAD模型处理过程中,理想的VAD模型效果就是在上述波形图中找到静音区域(尽量不受到背景噪音的干扰)然后切分出来(亦或者从Mel图中剔除掉db小的内容)

前置知识

理解了上面的 Mel 频谱图计算过程后,我们还需要掌握几个贯穿所有 VAD 模型的核心概念,才能理解后面每个模型的参数为什么这样设计、该怎样调。

一、采样率(Sample Rate)

采样率 = 每秒采集多少个声音振幅的”快照”,单位 Hz。16kHz 意味着每秒在波形上取 16,000 个点。

为什么 VAD 几乎全是 16kHz?

二、位深(Bit Depth)

位深 = 每个采样点用多少 bit 存储振幅值。8bit → $2^8=256$ 个可能值,16bit → $2^{16}=65,!536$ 个。16bit 是音频处理的标配——能表达从耳语到呐喊的动态范围而不过载或淹没在量化误差里。

三、分帧:帧长、帧移与重叠

为什么分帧? 语音是非平稳信号,频率成分随时间剧烈变化。直接对整段 10 秒音频做傅里叶变换会丢失所有时间信息——你不知道哪个频率发生在第几秒。所以在 Mel 频谱图计算的 Step 2 中,我们把长音频切成几十毫秒的小片段(帧),假设每帧内信号近似平稳,对每帧分别分析。

两个关键参数:

概念典型值含义
帧长 (Frame Length / Window Size)20~40ms(16kHz 下 320~640 点)每次分析截取多长的音频片段。太长 → 时间分辨率差(变化捕捉迟钝),太短 → 频率分辨率差

例如 FSMN-VAD 的 frame_out_ms=25(帧长 25ms)、frame_in_ms=10(帧移 10ms),WebRTC VAD 固定 30ms 帧长。

概念典型值含义
帧移 (Hop Length / Frame Shift)10ms(16kHz 下 160 点)相邻两帧起点间隔多少毫秒。帧移 < 帧长 → 帧之间有重叠,保证时间连续性,避免遗漏帧边界处的语音变化
重叠 (Overlap)帧长 − 帧移重叠越大 → 时间分辨率越高,但计算量也越大
帧长 25ms, 帧移 10ms:
|========帧0========|
          |========帧1========|
                    |========帧2========|
                              ...
|<-- 10ms -->|

四、特征:STFT 功率谱 vs Fbank(梅尔滤波器组能量)

不同 VAD 模型使用的输入特征不同,理解它们的区别很重要:

特征维度谁在用特点
STFT 功率谱257 维/frame(16kHz, 512 点 FFT)Silero VAD线性频率刻度,保留全部频域细节
Fbank (Filterbank)80 维/frame(80 个 Mel 滤波器)FSMN-VAD、FireRedVADMel 刻度 + 三角滤波压缩,维度低、去冗余、更贴合人耳

Fbank 就是 Mel 频谱图计算过程的 Step 1~5(跳过 Step 6 转 dB),得到的是 80 维能量值而非 dB。两者没有绝对的优劣:功率谱信息完整但维度高、冗余多;Fbank 压缩了维度,语音信息密度更高、模型更容易训练。

五、分贝(dB):为什么用对数尺度

音频处理中几乎处处用 dB 而不是线性能量值,原因有两条:

  1. 人耳对数感知:声压 10 倍 → 听感约 2 倍响。dB 就是对能量取对数,匹配人耳特性。
  2. 压缩动态范围:耳语和呐喊的能量差可达 $10^6$ 倍,线性值下小信号完全被淹没。转 dB 后变成 60dB 差距,算法更好处理。

两个常用 dB 公式:

\[\text{功率比 dB} = 10 \cdot \log_{10}\left(\frac{P}{P_{\text{ref}}}\right) \quad \text{振幅比 dB} = 20 \cdot \log_{10}\left(\frac{A}{A_{\text{ref}}}\right)\]

能量 VAD 中的 threshold_db=-30 意思是:帧 RMS 能量比最大 RMS 低 30dB 以下算静音。−30dB 约等于最大能量的 1/32,是一个比较保守的静音阈值。

六、VAD 后处理:滞后机制(Hysteresis)

这是理解 VAD 参数最核心的概念。模型输出的逐帧概率(0 或接近 1 = 语音,0 或 接近 0 = 静音)往往伴随频繁跳变——一帧语音、一帧静音、一帧语音……这是因为:

直接按帧级概率切分会得到大量碎片。所有 VAD 都会用一套滞后(Hysteresis)机制来平滑:

flowchart TD
    A["逐帧概率"] --> B{"概率 > 阈值 ?"}
    B -->|是| C["触发计数 +1"]
    B -->|否| D["静音计数 +1"]
    C --> E{"连续触发 ≥ min_speech ?"}
    E -->|是| F["语音段开始"]
    F --> G["持续记录..."]
    G --> H{"连续静音 ≥ min_silence ?"}
    H -->|否| G
    H -->|是| I["语音段结束"]
    I --> J{"本段时长 ≥ min_dur ?"}
    J -->|是| K["保留此语音段"]
    J -->|否| L["丢弃(太短)"]

对应的常见参数(不同模型叫法略有不同):

参数含义调大效果调小效果
threshold / speech_threshold单帧判为语音的概率/能量门槛更保守(更少判语音)更激进(更多判语音)
min_speech_duration_ms连续多少毫秒判语音才确认”语音段开始”过滤短噪音导致的误触发更快响应,但可能误触发
min_silence_duration_ms连续多少毫秒静音才确认”语音段结束”语音段更长(容忍更长的句中停顿)语音段更短,句中停顿即切分
speech_pad_ms在语音段首尾额外多保留多少毫秒避免切掉开头/结尾,语音段更完整边界更精确,但可能截断尾音

这四个参数的组合决定了 VAD 的激进程度——是宁可多切(保守,漏语音少但杂音多)还是宁可不切(激进,杂音少但可能漏语音)。

七、端点检测(Endpoint Detection)

FSMN-VAD 参数中大量的 start_pointend_pointlookbacklookahead 参数,就是端点检测的具体实现。核心思想与滞后机制一致,但更精细:

八、其他参数速查

概念出现在含义
SNR (信噪比)FSMN-VAD语音能量 / 噪声能量,< 阈值则判噪声,用于筛除能量低但噪音大的帧
chunk / 分块推理FireRedVAD、FSMN-VAD将长音频切成固定时长(如 30,000 帧)逐块推理,控制显存/内存占用
平滑窗口 (Smooth Window)FireRedVAD对连续 N 帧的概率取平均,消除单帧抖动
CMVN (倒谱均值方差归一化)FireRedVAD对 Fbank 特征做零均值单位方差归一化,消除信道/录音设备差异

有了这些前置知识,再看下面每个模型的参数表,就能理解每个参数控制的是哪一步、调大会产生什么效果。

常用VAD模型架构

WebRTC VAD — 高斯混合模型 (GMM)

Google 在 WebRTC 项目中开源的经典 VAD 1,浏览器内置,极轻量零依赖,是实时通信场景的事实标准。采用纯信号处理方法,无需神经网络。

模型结构

特征提取1:每帧提取 6 个子带(80Hz~4kHz 按对数刻度划分)的能量,得到一个 6 维特征向量。
模型结构:每个子带用 2 个高斯分布 分别建模语音和噪声的能量分布,共 12 个高斯分量。对每帧的 6 维特征向量,计算其在语音 GMM 和噪声 GMM 下的似然比:

\[\text{LLR} = \log \frac{P(\mathbf{x} \mid \text{语音})}{P(\mathbf{x} \mid \text{噪声})} = \sum_{k=1}^{6} \log \frac{\mathcal{N}(x_k; \mu_k^s, \sigma_k^s)}{\mathcal{N}(x_k; \mu_k^n, \sigma_k^n)}\]

LLR > 阈值则判为语音帧。GMM 参数通过 EM 算法离线训练,推理仅需查表 + 指数运算。

flowchart LR
    A[16kHz PCM] --> B[30ms 分帧]
    B --> C[6 子带能量]
    C --> D[语音 GMM 似然]
    C --> E[噪声 GMM 似然]
    D --> F[似然比 > 阈值?]
    E --> F
    F --> G[语音 / 静音]

模型使用

参数默认值含义
aggressiveness2激进程度(0~3),越大越容易判为语音
from AudioVADProcess import VADProcess

vad = VADProcess.create("webrtc", aggressiveness=2)
segments = vad.detect("audio.wav")

image

Silero VAD — CRNN(CNN + GRU)

Silero 团队开源的企业级 VAD 2,训练数据覆盖 6000+ 语种,模型仅 2MB,单 CPU 线程 <1ms/帧,社区最活跃的开源 VAD(GitHub 9.6k Stars)。

模型结构

特征提取2:输入 16kHz 音频 → 512 点 STFT(32ms 窗,10ms 帧移)→ 257 维功率谱。
模型结构(前向计算流):

STFT (257, T)
  ↓
Conv1d(k=3, 257→128) + ReLU
  ↓
Conv1d(k=3, 128→64) + ReLU
  ↓
2× GRU(hidden=64, bidirectional)
  ↓
Linear(128→1) + Sigmoid
  ↓
每帧语音概率 ∈ [0, 1]

CNN 负责提取局部时频模式(如共振峰、谐波结构),双向 GRU 捕捉前后时间上下文(如语音段边界过渡),最后 Sigmoid 输出逐帧概率。总参数约 2MB,纯 PyTorch JIT,可导出 ONNX。

模型使用

参数默认值含义
use_onnxFalse是否使用 ONNX 推理(需 onnxruntime
threshold0.5语音概率阈值(0~1),高于此值判为语音
min_speech_duration_ms250最短语音持续(毫秒),短于此的片段被过滤
min_silence_duration_ms100最短静音间隔(毫秒),小于此的间隔被合并
speech_pad_ms30语音段首尾填充(毫秒),避免切掉开头结尾
from AudioVADProcess import VADProcess

vad = VADProcess.create("silero", threshold=0.6, min_speech_duration_ms=300)
segments = vad.detect("audio.wav")

image

FSMN-VAD — 前馈序列记忆网络

https://modelscope.cn/models/iic/speech_fsmn_vad_zh-cn-16k-common-pytorch/summary

模型结构

阿里达摩院在 FunASR 中开源的 VAD 模块 3,基于前馈序列记忆网络(FSMN),专为中英文语音场景优化,仅 0.4M 参数。其具体模型结构:
image

特征提取3:80 维 Fbank(梅尔滤波器组能量),帧长 25ms,帧移 10ms。
模型结构:在传统的序列建模模型RNN/LSTM等在第 t 帧的结果依赖第 t−1 帧的隐藏状态,所以无法并行化,训练和推理都很慢,FSMN 的核心洞察是:序列建模不一定需要递归。如果你能让每一帧直接”看到”前后若干帧的信息,就能用纯前馈网络(可并行)达到类似的上下文建模效果,因此FSMN 的核心思想是在标准前馈网络(FFN)的隐藏层中插入 可学习的记忆模块 (Memory Block)

\[\tilde{h}_t = h_t + \sum_{i=0}^{N} a_i \odot h_{t-i} + \sum_{j=1}^{M} c_j \odot h_{t+j}\]

其中 $h_t$ 是当前帧的隐藏向量,$a_i, c_j$ 是前向/后向记忆权重。相比 LSTM/GRU,FSMN 无需序列化递归计算,所有帧可并行前向传播,训练和推理都更快。对于上述个公式在具体的模型结构设计中对于输入特征维度 $(B,T,D)$ 直接可以通过循环去对时间维度 $T$ 进行读取然后进行编码即可,实际上更加高效的计算直接通过卷积进行实现:

class MemoryBlock(nn.Module):
  """FSMN 核心:可学习的固定窗口上下文加权模块。
  
  公式: h̃_t = h_t + Σ a_i ⊙ h_{t-i} + Σ c_j ⊙ h_{t+j}
  
  关键设计:a_i, c_j 是 d 维向量(不是矩阵),参数量仅 O(d × (N+M)),
  远小于自注意力的 O(d²)。
  """
  
  def __init__(self, dim: int, forward_order: int, backward_order: int):
      """
      Args:
          dim: 隐藏层维度 d
          forward_order: 前向阶数 N(看过去多少帧)
          backward_order: 后向阶数 M(看未来多少帧)
      """
      super().__init__()
      self.N = forward_order   # 前向记忆阶数
      self.M = backward_order  # 后向记忆阶数
      
      # 可学习的记忆权重向量,形状 (N+1, dim) 和 (M, dim)
      # +1 是因为 i=0 也包含在内(当前帧自身的 weight)
      self.forward_weights = nn.Parameter(torch.randn(forward_order + 1, dim) * 0.02)
      self.backward_weights = nn.Parameter(torch.randn(backward_order, dim) * 0.02)
  
  def forward(self, x: torch.Tensor) -> torch.Tensor:
      """
      Args:
          x: (B, T, dim) — 批量 × 时间帧 × 隐藏维度
      Returns:
          (B, T, dim) — 融合上下文后的增强表示
      """
      B, T, d = x.shape
      out = torch.zeros_like(x)
      
      # ---------- 逐帧计算(可优化为 conv1d,这里保持公式的直观性)----------
      for t in range(T):
          # --- 前向部分:Σ a_i ⊙ h_{t-i},i = 0..N ---
          forward_sum = torch.zeros(B, d, device=x.device)
          for i in range(self.N + 1):
              idx = t - i
              if idx >= 0:
                  forward_sum = forward_sum + self.forward_weights[i] * x[:, idx, :]
          
          # --- 后向部分:Σ c_j ⊙ h_{t+j},j = 1..M ---
          backward_sum = torch.zeros(B, d, device=x.device)
          for j in range(1, self.M + 1):
              idx = t + j
              if idx < T:
                  backward_sum = backward_sum + self.backward_weights[j - 1] * x[:, idx, :]
          
          out[:, t, :] = x[:, t, :] + forward_sum + backward_sum
      
      return out


class FastMemoryBlock(nn.Module):
  """MemoryBlock 的高效卷积实现版本。
  
  将逐元素加权求和等价为 depthwise 1D 卷积:
  - 前向部分等价于 causal conv1d (groups=dim)
  - 后向部分等价于 anti-causal conv1d (groups=dim)
  """
  
  def __init__(self, dim: int, forward_order: int, backward_order: int):
      super().__init__()
      self.N = forward_order
      self.M = backward_order
      
      # 前向权重:shape (dim, 1, N+1),作为 depthwise conv 的 kernel
      # 卷积核排列:[w_0, w_1, ..., w_N](从左到右 = 从当前到过去)
      fwd_kernel = torch.randn(dim, 1, forward_order + 1) * 0.02
      self.register_buffer('fwd_kernel', fwd_kernel)
      
      # 后向权重:shape (dim, 1, M),右侧填充 0 作为 anti-causal
      bwd_kernel = torch.randn(dim, 1, backward_order) * 0.02
      self.register_buffer('bwd_kernel', bwd_kernel)
      
      # 用 Parameter 方式(支持训练):
      self.fwd_weight = nn.Parameter(torch.randn(dim, 1, forward_order + 1) * 0.02)
      self.bwd_weight = nn.Parameter(torch.randn(dim, 1, backward_order) * 0.02)
  
  def forward(self, x: torch.Tensor) -> torch.Tensor:
      """
      x: (B, T, dim) → permute → (B, dim, T) 作为 depthwise conv 的输入
      """
      B, T, d = x.shape
      x_t = x.permute(0, 2, 1)  # (B, dim, T)
      
      # 前向 causal conv:padding 在左侧,只看过去
      fwd_out = F.conv1d(
          x_t, self.fwd_weight, groups=d,
          padding=self.N  # 左侧 pad N 个零(保证 causal)
      )[:, :, :T]  # 裁掉右侧多余的
      
      # 后向 anti-causal conv:padding 在右侧,只看未来
      bwd_out = F.conv1d(
          torch.flip(x_t, [2]),  # 时间反转
          self.bwd_weight, groups=d,
          padding=self.M
      )[:, :, :T]
      bwd_out = torch.flip(bwd_out, [2])  # 再反转回来
      
      out = x_t + fwd_out + bwd_out
      return out.permute(0, 2, 1)  # (B, T, dim)

不过在具体的“时序”数据中因为未来时间信息是未知的所以训练/推理过程中比较有效的策略:1、离线推理过程:训练过程中双向+推理使用双向,不过训练过程中直接使用 Dropout 随机不让 未来 信息融入;2、实时推理:此时需要将模型退化为因果FSMN($tilde{h}t = h_t + \sum{i=0}^{N} a_i \odot h_{t-i}$),而为了保证实时推理的有效一般策略是使用 状态累计策略,通过固定采集音频进行输入而后对采集的音频中进行 “静音片段检测”当片段超过累计则进行放回,具体流程如下:
image

FSMN-VAD 仅 0.4M 参数,是当前最小的高精度 VAD。
image

模型使用

音频特征参数:

参数默认值含义
sample_rate16000音频采样率(Hz)
frame_in_ms10帧移(ms),每帧间隔
frame_out_ms25帧长(ms),每帧包含的音频时长
window_size_ms200推理滑动窗口长度(ms)

端点检测参数:

参数默认值含义
detect_mode0检测模式,1 更严格,0 更宽松
do_start_point_detectionTrue是否启用语音起点检测
do_end_point_detectionTrue是否启用语音终点检测
max_start_silence_time0起点前最大静音(ms),0 不限制
max_end_silence_time0终点后最大静音(ms),0 不限制
lookback_time_start_point0起点回溯时间(ms),防止切掉开头
lookahead_time_end_point0终点提前时间(ms),减少延迟
sil_to_speech_time_thres150静音→语音切换所需连续触发时长(ms)
speech_to_sil_time_thres150语音→静音切换所需连续静音时长(ms)
do_extend1是否延长语音段尾部,避免截断尾音

分段控制:

参数默认值含义
max_single_segment_time15000单段最大时长(ms),超长自动切分

噪声与能量门限:

参数默认值含义
snr_mode0信噪比模式,0 关闭,1 启用
snr_thres−100信噪比阈值(dB),低于此值判为噪声
noise_frame_num_used_for_snr100SNR 估计所用噪声帧数
decibel_thres−100分贝阈值,低于此值的帧判为静音
speech_noise_thres0.8语音/噪声能量比阈值,控制噪声过滤灵敏度
speech_noise_thresh_low−1双阈值下限
speech_noise_thresh_high1双阈值上限
speech_2_noise_ratio0.5语音/噪声比例,越大越严格
fe_prior_thres1e-4特征能量先验阈值,小于此的帧直接判静音

推理环境:

参数默认值含义
device"cpu"推理设备,"cuda""cpu"
from AudioVADProcess import VADProcess

vad = VADProcess.create("fsmn", speech_noise_thres=0.9, sil_to_speech_time_thres=100,
                        max_single_segment_time=10000, device="cuda")
segments = vad.detect("audio.wav")

pyannote.audio — SincNet + LSTM + Self-Attention

法国 CNRS 开源的说话人日志工具包 4,学术基准长期 SOTA,其语音活动检测(SAD)模块端到端从原始波形学习,无需手工特征。

模型结构

特征提取:直接用 SincNet 从原始波形学习滤波器,无需 STFT:

\[g[n, f_l, f_h] = 2f_h \cdot \text{sinc}(2\pi f_h n) - 2f_l \cdot \text{sinc}(2\pi f_l n)\]

$f_l, f_h$ 是可学习的低/高频截止频率,相当于在频域学习一组带通滤波器。
模型结构

原始波形 (1, T)
  ↓
SincNet (学习 64 个带通滤波器)
  ↓
2× LSTM (hidden=128, bidirectional)
  ↓
Multi-Head Self-Attention (4 heads)
  ↓
Linear(128→1) + Sigmoid
  ↓
逐帧语音概率

SincNet 替代手工 Fbank/STFT,端到端从波形学习;LSTM 建模长程时序依赖;自注意力聚合全局上下文,捕捉整段音频的语音分布模式。

模型使用

参数默认值含义
tokenHF_TOKEN 环境变量HuggingFace 访问令牌,需先接受用户协议
from AudioVADProcess import VADProcess

vad = VADProcess.create("pyannote", token="hf_xxx")
segments = vad.detect("audio.wav")

FireRedVAD

模型结构

小红书开源VAD模型5模型参数上只有0.6M基于DFSMN同时支持流式和非流式的使用,FSMN模型结构介绍

模型使用

参数默认值含义
model_dirpretrained_models/FireRedVAD/VAD模型路径,首次自动下载
speech_threshold0.4语音概率阈值(0~1),低于此值判为静音
use_gpuFalse是否启用 GPU 推理
smooth_window_size5概率平滑窗口大小(帧数),减少抖动
min_speech_frame20最短语音持续帧数,短于此的片段被过滤
max_speech_frame2000最长语音持续帧数,超长自动切分
min_silence_frame20最短静音间隔帧数,小于此的间隔被合并
merge_silence_frame0合并间距帧数,相邻段间隔小于此值则合并
extend_speech_frame0向外扩展帧数,在语音段首尾各延伸
chunk_max_frame30000分块推理的最大帧数,控制显存/内存占用
from fireredvad import FireRedVad, FireRedVadConfig

config = FireRedVadConfig(
    use_gpu=use_gpu, speech_threshold=speech_threshold,
    smooth_window_size=smooth_window_size,
    min_speech_frame=min_speech_frame, max_speech_frame=max_speech_frame,
    min_silence_frame=min_silence_frame, merge_silence_frame=merge_silence_frame,
    extend_speech_frame=extend_speech_frame, chunk_max_frame=chunk_max_frame,
)
vad = FireRedVad.from_pretrained(model_dir, config)
result, _ = vad.detect(audio)

image

能量 VAD — RMS 阈值

最简单的 VAD 实现,仅依赖信号处理计算帧能量,零模型依赖、零网络请求、零额外存储,适合快速原型和噪声可控的环境。

模型结构

最朴素的方法:计算每帧 RMS → 转 dB → 与固定阈值比较。

\[\text{RMS}_t = \sqrt{\frac{1}{N} \sum_{n=0}^{N-1} x_t[n]^2}, \quad \text{dB}_t = 20 \log_{10} \left( \frac{\text{RMS}_t}{\text{RMS}_{\max}} \right)\]

$\text{dB}_t > \theta$(如 −30dB)判为语音。零模型依赖,但阈值固定,无法应对动态噪声环境。

模型使用

参数默认值含义
threshold_db−30能量阈值(dB),高于此值判为语音
frame_ms25帧长(毫秒)
hop_ms10帧移(毫秒)
min_speech_ms200最短语音持续(毫秒),短于此的片段被过滤
min_silence_ms300最短静音间隔(毫秒),小于此的间隔被合并
from AudioVADProcess import VADProcess

vad = VADProcess.create("energy", threshold_db=-25, min_speech_ms=100)
segments = vad.detect("audio.wav")

auditok — 能量 + 频谱

基于能量和频谱特性的纯算法 VAD,Python 生态中轻量级音频切分工具,无需训练,开箱即用。

模型结构

基于能量和频谱特性的传统信号处理方法。对音频流进行实时分帧,计算每帧短时能量,当能量连续超过阈值且持续足够时长时标记为语音段。核心逻辑为状态机:静音 → 检测到能量 → 语音段开始 → 能量持续低于阈值 + 静音超时 → 语音段结束。无需训练数据,纯算法实现。

模型使用

参数默认值含义
energy_threshold50能量阈值,高于此值视为有声音
min_dur0.2最短语音持续(秒),短于此的片段被过滤
max_dur15最长语音持续(秒),超长自动切分
max_silence0.3最大静音容忍(秒),静音超过此值则切分
from AudioVADProcess import VADProcess

vad = VADProcess.create("auditok", energy_threshold=45, min_dur=0.5)
segments = vad.detect("audio.wav")

image

VAD模型补充介绍

训练/推理过程

音频数据结构一般对于输入的音频读取过程中,假设直接通过torchaudio去读取 waveform, sample_rate = torchaudio.load("test.wav")对于两部分返回内容分别表示:1、sample_rate:采样率;2、waveform[channel, time]表示通道数量以及音频采样点数量。比如说16Hz,3秒,单声道返回结果就是 torch.Size([1, 48000])。一般而言模型输入数据主要是两种:1、直接输入waveform数据;2、直接输入输入Mel/Fbank特征(会将音频的 waveform 数据进行转换):对于输入为 [1, 48000]会直接转换为 [T,F]比如说 [300, 80]那么这样一来音频就变成 [bs, 300, 80]

如果音频长度不一致一般就直接用padding策略即可

标签构建过程对于输入音频只需要标记出来中间那些位置有音频,比如说10s的音频设置10ms(每一秒100帧),那么0.80s ~ 3.50s对对应帧 80 到帧 349,全部标1依次类推这样一来标签就变成了[0,0,...,1,...1,0...0],但是对于训练数据如何去对音频截取窗口就需要考虑:因为很可能一段音频里面大部分都是静音的,这样就会导致模型效果不佳。一般的处理策略很多比如说有些ASR模型会返回每个字所对应的时间戳直接去根据这个时间戳截取即可。

损失函数构建可以直接使用二元交叉熵损失函数处理即可

对于输入音频进行读取获取1维的数组(如果双通道直接取平均即可)假设得到数据维度为 [1, 3s x 16000hz],而后将1维的波形特征转化为2维矩阵特征假设得到维度信息为:[1, 300帧, 80]而后直接将这个音频输入到模型这哦你进行推理输出每一帧的概率 [1,300,1]。但是输出结果中可能出现在 t帧位置判断为静音(可能是错误判断),但是不能根据这个又把音频切分成两段,因此需要一个后处理过程最简单的就是直接静音帧累计策略(静音帧数累计>30就认为应该切分了)

除此之外有些时候还会使用 speech_pad 策略就是将预测的时间范围向前向后补充部分帧避免截断为题出现

除此之外还需要考虑超长音频吹过程(不可能直接全部输入到模型中,显存处理不下)具体过程如下(假设模型结构为FSMN):
image

参考