文章目錄

  • 前言
  • 一、高斯混合模型介紹
  • 1.高斯模型舉例
  • 1)定義
  • 2)舉例説明
  • 2.高斯混合模型(GMM)
  • 1)定義
  • 2)舉例説明
  • 3)一維曲線
  • 二、VAD高斯混合模型
  • 1.模型訓練介紹
  • 1)訓練方法
  • 2)訓練結果
  • 2.噪聲高斯模型分佈
  • 1)matlab代碼
  • 2)結果
  • 2.人聲高斯模型分佈
  • 三、VAD人聲判斷
  • 1.高斯分佈計算
  • 1)公式
  • 2)對應代碼
  • 2.人聲判定策略
  • 1)判斷機制
  • 2)重要閾值確認
  • 3)整體流程
  • 第一步:計算噪聲/人聲概率分佈
  • 第二步:噪聲/人聲比較
  • 第三步:判斷單個閾值
  • 第四步:全局判定
  • 四、模型更新
  • 1.計算條件概率
  • 2.獲取噪聲基線和均值
  • 1)噪聲基線的作用
  • 2)確定噪聲基線
  • 3)獲取噪聲均值
  • 3.更新噪聲均值
  • 1)短時更新
  • 2)平滑修正
  • 3)邊界約束
  • 4)策略意義
  • 4.更新人聲均值
  • 5.更新人聲方差
  • 1)更新公式
  • 2)代碼解析
  • 3)邊界約束
  • 6.更新噪聲方差
  • 7.模型分離
  • 1)計算全局均值
  • 2)計算差值
  • 3)如果模型太近,強行拉開
  • 4)限制模型漂移範圍
  • 五、hangover 機制
  • 1.策略作用
  • 2.整體流程
  • 3.對應代碼
  • 1)非語音處理
  • 2)語音
  • 總結

前言

在上一篇文章中,介紹了VAD對於能量的計算原理和策略介紹。本篇文章中講進一步介紹VAD中的人聲/噪聲決策,這是基於高斯混合模型,也就是GMM進行計算的。

本篇文章將會對照源碼、結合圖像進行介紹:

  • GMM模型介紹(可以跳過)
  • webrtc中VAD的高斯模型(包括來源以及使用)
  • 人聲/噪聲決策以及在線更新機制
  • hangover機制

本篇文章比較長,可以耐心觀看。

|版本聲明:山河君,未經博主允許,禁止轉載


一、高斯混合模型介紹

如果對於高斯模型很熟悉那麼可以跳過這一節,這裏需要明白的是概率密度、區間概率和條件概率的區別。

1.高斯模型舉例

1)定義

高斯分佈(又叫正態分佈)是一種常見的概率分佈,它的概率密度也叫做似然函數(PDF)是:
WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#音視頻

  • WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#算法_02為均值:分佈的中心位置,數據大部分集中在它附近。
  • WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#算法_03表示方差:分佈的寬度,數值越大,圖像分佈越平;數值越小,分佈越尖。

而使用高斯分佈計算區間概率公式為:
WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#音視頻_04

2)舉例説明

假設有一門數學考試,100 分滿分,有5個學生的成績分別是WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#webrtc_05

  • 均值WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#算法_02WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_高斯混合模型_07
  • 方差WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_高斯混合模型_08

那麼現在還有一個學生

  • 他的成績為WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#音視頻_09分的概率帶入公式最終結果為:WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#webrtc_10
  • 他的成績在WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#信號處理_11分之間的概率帶入公式為:WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#音視頻_12

2.高斯混合模型(GMM)

1)定義

GMM 就是多個高斯分佈的加權和,它的概率密度公式是:
WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#信號處理_13

  • WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#信號處理_14:高斯分佈的個數
  • WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#信號處理_15:每個分佈的權重(比例,所有權重相加 = 1)
  • WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#信號處理_16:第k 個高斯分佈的均值和方差
  • WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#算法_17:第k個高斯分佈的概率密度函數 (PDF)

而此時我們計算某一個值屬於哪一個高斯分佈的過程叫做條件概率(責任度)
WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_高斯混合模型_18

2)舉例説明

上文中是一個以成績的例子進行計算高斯分佈,但顯然現實中要考慮的因素會更多:

  • 一部分學生是學霸WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#webrtc_19,集中在WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#信號處理_20分附近
  • 一部分學生是普通學生WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#webrtc_21,集中在WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#信號處理_22分附近

那麼由此可以得到:

  • 學霸羣體:均值WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#信號處理_23,標準差WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_高斯混合模型_24,權重WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#算法_25
  • 普通學生:均值WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#webrtc_26,標準差WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#算法_27,權重WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_高斯混合模型_28

此時有一個學生成績為70,那麼他的條件概率分別為:

  • WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#信號處理_29
  • WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#webrtc_30

很明顯,該學生大概率屬於普通學生

3)一維曲線

使用matlab畫出曲線

x = linspace(30, 100, 500);

% 學霸羣體參數
mu1 = 85; sigma1 = 5; w1 = 0.3;
pdf1 = w1 * normpdf(x, mu1, sigma1);

% 普通學生羣體參數
mu2 = 65; sigma2 = 10; w2 = 0.7;
pdf2 = w2 * normpdf(x, mu2, sigma2);

% 混合分佈
pdf_mix = pdf1 + pdf2;

% 繪製
figure;
plot(x, pdf1, 'b--', 'LineWidth', 1.5); hold on;
plot(x, pdf2, 'g--', 'LineWidth', 1.5);
plot(x, pdf_mix, 'r-', 'LineWidth', 2);
grid on;
legend('學霸羣體 N(85,5^2)*0.3', ...
       '普通學生 N(65,10^2)*0.7', ...
       '混合分佈 (GMM)');
title('考試成績的高斯混合模型 (GMM)');
xlabel('成績');
ylabel('概率密度');

WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#音視頻_31

二、VAD高斯混合模型

1.模型訓練介紹

1)訓練方法

webrtc關於VAD的高斯混合模型訓練是一種離線訓練,是通過以下幾個步驟得到的:

  • 準備數據
  • 收集了大量的語音數據(各種語言、男女聲、不同音量)
  • 收集了大量的噪聲數據(白噪聲、街道、辦公室、電話線路噪聲等)
  • 特徵提取
  • 對每一幀音頻(10ms ~ 30ms)提取簡單特徵:子帶能量、總能量、過零率
  • 特徵維度很低,不像是MFCC會有多個維度,但適合在嵌入式環境運行
  • 擬合高斯分佈
  • 把“語音幀特徵”丟進一個高斯混合模型訓練器
  • 把“噪聲幀特徵”丟進另一個 GMM 訓練器
  • 使用EM算法迭代得到每個分量的均值、方差、權重
  • 量化和存儲
  • 得到的均值、方差、權重定點數存儲(int16_t),方便在 C 代碼裏運行
  • 該套參數固定寫死在代碼表裏,運行時雖然會根據實際情況修改,但不會在運行後進行存儲

由此我們也可以直覺看出來這套模型的明顯的優缺點:

特性

優點

缺點

計算效率

高,適合實時


模型複雜度

低,易實現

表達能力有限

多模態表示

可以區分靜音/語音

只適合簡單多模態

魯棒性

可自適應噪聲變化

對異常噪聲敏感

時序建模


需要額外平滑

2)訓練結果

webrtc中訓練後的結果是以Q7格式分別存儲在:

  • kNoiseDataWeights:噪聲的兩個高斯分佈權重,表格中的G0和G1
  • kNoiseDataMeans:噪聲的兩個高斯分佈均值
  • kNoiseDataStds:噪聲的兩個高斯分佈的方差
  • kSpeechDataWeights:噪聲的兩個高斯分佈權重,表格中的G0和G1
  • kSpeechDataMeans:噪聲的兩個高斯分佈均值
  • kSpeechDataStds:噪聲的兩個高斯分佈的方差
  • G0:低幅度、常見特徵,是頻帶特徵裏的主分量
  • G1:高幅度、變化性更大的特徵,是頻帶特徵裏的補充分量

頻帶

高斯

Noise 權重

Speech 權重

Noise 均值

Speech 均值

Noise Std

Speech Std

0

G0

34

48

6738

8306

378

555

G1

62

82

4892

10085

1064

505

1

G0

72

45

7065

10078

493

567

G1

66

87

6715

11823

582

524

2

G0

53

50

6771

11843

688

585

G1

25

47

3369

6309

593

1231

3

G0

94

80

7646

9473

474

509

G1

66

46

3863

9571

697

828

4

G0

56

83

7820

10879

475

492

G1

62

41

7266

7581

688

1540

5

G0

75

78

5020

8180

421

1079

G1

103

81

4362

7483

455

850

在代碼中,將會在WebRtcVad_InitCore接口中存儲在VadInstT結構體中。

2.噪聲高斯模型分佈

下面將根據具體的參數,畫出6個子帶的兩個高斯分佈G0,G1圖像,然後再根據權重進行G0和G1加權後的高斯混合圖像。

1)matlab代碼

clc; clear; close all;

% 原始 Q7 數據
kNoiseDataMeans_Q7   = [6738, 4892, 7065, 6715, 6771, 3369, 7646, 3863, 7820, 7266, 5020, 4362];
kNoiseDataStds_Q7    = [378, 1064, 493, 582, 688, 593, 474, 697, 475, 688, 421, 455];
kNoiseDataWeights_Q7 = [34, 62, 72, 66, 53, 25, 94, 66, 56, 62, 75, 103];

% 轉為 Q0(浮點)
kNoiseDataMeans   = double(kNoiseDataMeans_Q7) / 128;
kNoiseDataStds    = double(kNoiseDataStds_Q7) / 128;
kNoiseDataWeights = double(kNoiseDataWeights_Q7);   % 權重本身可以直接使用

kTableSize = length(kNoiseDataMeans);
numBands = kTableSize / 2;

figure;

for band = 1:numBands
    % 提取 G0, G1 的參數
    mean0 = kNoiseDataMeans(2*band-1);
    mean1 = kNoiseDataMeans(2*band);
    std0  = kNoiseDataStds(2*band-1);
    std1  = kNoiseDataStds(2*band);
    
    weight0 = kNoiseDataWeights(2*band-1);
    weight1 = kNoiseDataWeights(2*band);
    
    % x 範圍,取 ±4σ
    x_min = min(mean0-4*std0, mean1-4*std1);
    x_max = max(mean0+4*std0, mean1+4*std1);
    x = linspace(x_min, x_max, 500);
    
    % 高斯分佈
    G0 = (1/(std0*sqrt(2*pi))) * exp(-0.5*((x-mean0)/std0).^2);
    G1 = (1/(std1*sqrt(2*pi))) * exp(-0.5*((x-mean1)/std1).^2);
    
    % 加權混合
    Gmix = (weight0*G0 + weight1*G1) / (weight0 + weight1);
    
    % 繪圖
    subplot(2,3,band);
    plot(x, G0, 'b', 'LineWidth',1.5); hold on;
    plot(x, G1, 'r', 'LineWidth',1.5);
    plot(x, Gmix, 'k--', 'LineWidth',1.5);
    title(['Subband ' num2str(band)]);
    legend('G0','G1','G_{mix}');
    xlabel('Value'); ylabel('Probability Density');
end

2)結果

WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#webrtc_32

2.人聲高斯模型分佈

和計算噪聲高斯模型分佈一樣,只需要將matlab中代碼替換即可,這裏就不貼出代碼,直接看結果:

WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#webrtc_33

三、VAD人聲判斷

在上一篇文章webrtc之語音活動上——VAD能量檢測原理以及源碼詳解中,我們已經知道了:

  • 檢測模式的區別
  • 幀長劃分方法
  • 6個非等寬頻帶的能量
  • 頻帶劃分的意義

1.高斯分佈計算

1)公式

源碼中關於PDF計算使用Q格式運算以保證精度,其計算公式為:
WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#音視頻_34
該接口的計算方式少了一個WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_高斯混合模型_35,並且這裏同樣將WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#音視頻_36運算簡化,因此它是一個相對值(比例),而縮放後的 PDF是用於相對比較,不是嚴格的概率密度,不會影響後續決策與更新。

2)對應代碼

接口名為WebRtcVad_GaussianProbability,以下是參數和返回值:

  • input:頻帶能量,以Q4格式保存
  • mean:噪聲/人聲的高斯均值,Q7格式
  • std:噪聲/人聲的高斯方差,Q7格式
  • deltaWebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#webrtc_37,用於後續更新模型,Q11格式
  • 返回值:概率密度,Q20格式

2.人聲判定策略

1)判斷機制

VAD判斷人聲是雙重機制:

  • 局部判定: 捕捉某個子帶特別強烈的語音
  • 全局判定:要求整體證據夠強
  • 最終決策:兩者取 OR,既保證靈敏度,又保證魯棒性

2)重要閾值確認

在設置模式WebRtcVad_set_mode_core接口裏,確定

  • individual:單個頻帶人聲閾值範圍,用於局部判定
  • total:整體人聲閾值閾值範圍,用於整體

而對於不同幀長再進一步確定閾值,這裏是在GmmProbability真正進行決策的地方進行確定。而對於:

  • 短幀10ms:由於信息少,則使用高閾值以避免誤判
  • 中等20ms:閾值低,放寬一些
  • 長幀30ms:本身信息穩定,為了避免太寬鬆 又設高

除此之外,在進行全局判定時,不同頻帶的權重存儲在kSpectrumWeight中。

WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#信號處理_38


值得注意的是,這些閾值的由來是根據大規模實驗 + ROC 曲線分析 + 主觀聽感測試 手工調出來的經驗值。

3)整體流程

WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#音視頻_39

第一步:計算噪聲/人聲概率分佈

同一段頻帶語音根據噪聲/人聲的高斯參數,分別進行計算主分量/補充分量的概率分佈,再根據權值計算真正的概率密度,代碼如下:

for (channel = 0; channel < kNumChannels; channel++) {
      h0_test = 0;
      h1_test = 0;
      for (k = 0; k < kNumGaussians; k++) {
        gaussian = channel + k * kNumChannels;
        tmp1_s32 = WebRtcVad_GaussianProbability(features[channel],
                                                 self->noise_means[gaussian],
                                                 self->noise_stds[gaussian],
                                                 &deltaN[gaussian]);
        noise_probability[k] = kNoiseDataWeights[gaussian] * tmp1_s32;
        h0_test += noise_probability[k];  // Q27
        tmp1_s32 = WebRtcVad_GaussianProbability(features[channel],
                                                 self->speech_means[gaussian],
                                                 self->speech_stds[gaussian],
                                                 &deltaS[gaussian]);
        speech_probability[k] = kSpeechDataWeights[gaussian] * tmp1_s32;
        h1_test += speech_probability[k];  // Q27
      }
第二步:噪聲/人聲比較

判斷單個頻帶是否為人聲使用的是似然對數比,也就是WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_高斯混合模型_40,這麼做的好處是:

  • 避免概率數值太小引起數值問題
  • LLR 的加和性質,可以進行多個頻帶綜合判定

值得注意的是:這近似在數幀平均下通常偏差較小,但對極端 / 較小樣本會產生偏差,所以後面通過局部+全局、hangover機制與模型更新來抵消誤判

這裏的移位操作是為了簡化WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#音視頻_36運算,用最高位表示為整數部分,用整數位的差值來近似WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#webrtc_42,核心代碼為:

shifts_h0 = WebRtcSpl_NormW32(h0_test);
      shifts_h1 = WebRtcSpl_NormW32(h1_test);
      if (h0_test == 0) {
        shifts_h0 = 31;
      }
      if (h1_test == 0) {
        shifts_h1 = 31;
      }
      log_likelihood_ratio = shifts_h0 - shifts_h1;
第三步:判斷單個閾值

如果單個頻帶的信號人聲強烈,將會直接認定為語音,值得注意的是即使判斷為人聲了,還是會接着計算其他頻帶,這是為了更新模型。核心代碼為:

if ((log_likelihood_ratio * 4) > individualTest) {
        vadflag = 1;
      }
第四步:全局判定

這裏分為兩部分

  • 根據權重加合
  • 整體閾值判斷

其核心代碼如下:

sum_log_likelihood_ratios +=
          (int32_t) (log_likelihood_ratio * kSpectrumWeight[channel]);
  .......
 vadflag |= (sum_log_likelihood_ratios >= totalTest);

四、模型更新

在進行模型更新時,需要考慮到平滑處理,所以這裏運用到大量的平滑濾波的思想,見文章語音信號處理三十一——常用的時域/頻域平滑濾波。

1.計算條件概率

首先獲取各個頻帶噪聲/人聲在主分量和補充分量的條件概率,分別存儲在ngprvecsgprvec中。

如果WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#音視頻_43,那麼默認該數值必然是主分量特徵,以下是噪聲的條件概率核心代碼,人聲類似:

h0 = (int16_t) (h0_test >> 12);  // Q15
      if (h0 > 0) {
        tmp1_s32 = (noise_probability[0] & 0xFFFFF000) << 2;  // Q29
        ngprvec[channel] = (int16_t) WebRtcSpl_DivW32W16(tmp1_s32, h0);  // Q14
        ngprvec[channel + kNumChannels] = 16384 - ngprvec[channel];
      } else {
        ngprvec[channel] = 16384;
      }

2.獲取噪聲基線和均值

1)噪聲基線的作用

WebRTC VAD 和很多能量型 VAD 都是基於一個基本假設:在連續音頻信號中,語音的能量通常高於環境噪聲的能量。

而噪聲基的含義是指:環境噪聲在短時間或一段時間內的平均能量水平或特徵值參考。

2)確定噪聲基線

噪聲基線的獲取是一種中值濾波的思想。在接口WebRtcVad_FindMinimum中,它的大致流程為:

輸入: feature_value, channel, self

┌─► [1] 更新歷史值的年齡
│      ├─ 遍歷16個存儲的最小值
│      ├─ age < 100 → age++
│      └─ age == 100 → 移除該值,數組前移,最後位置填充大數
│
└─► [2] 判斷是否插入新值
       ├─ feature_value 比數組中的某些值小
       │     └─ 找到插入位置 position
       │          └─ 從尾部往後移,插入新值,age=1
       └─ 否則丟棄(説明它不是最小16個之一)

┌─► [3] 計算當前中位數
│      ├─ frame_counter > 2 → 取 smallest_values[2] (第3小)
│      └─ frame_counter <= 2 → 取 smallest_values[0] (最小值)
│
└─► [4] 平滑更新 mean_value[channel]
       ├─ 如果 current_median < mean_value → α = 0.2 (快速下降)
       └─ 否則 α = 0.99 (緩慢上升)
       └─ mean_value[channel] ← α * mean_value[channel] + (1-α) * current_median

[5] 返回 mean_value[channel]

這裏可以對照代碼進行理解。

3)獲取噪聲均值

實現主要在WeightedAverage接口內,該接口的使用方法為:

  • 輸入主分量和補充分量的均值
  • 輸入對應的權重
  • 輸出計算後的均值

3.更新噪聲均值

1)短時更新

WebRTC 裏不可能每次都存很多幀再做完整的EM,並且GMM更新中webrtc希望帶權,所以這裏採用的是梯度遞推進行更新,公式為:
WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#音視頻_44

  • WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#信號處理_45:權係數
  • WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#信號處理_46:當前頻帶當前分量的條件概率

由於篇幅原因,這裏不做推導了,可以看看別的文章,或者可以私信博主

這裏加權是並不希望噪聲平均變化過快。對應代碼為:

static const int16_t kNoiseUpdateConst = 655; // Q15
nmk2 = nmk;
if (!vadflag) {
   delt = (int16_t)((ngprvec[gaussian] * deltaN[gaussian]) >> 11);
   nmk2 = nmk + (int16_t)((delt * kNoiseUpdateConst) >> 22);
}

2)平滑修正

首先會用噪聲基線減去頻帶噪聲均值得到差值,再根據差值進行指數加權移動平均的思想,和當前幀的噪聲均值相加為下一幀的噪聲均值,對應代碼:

ndelt = (feature_minimum << 4) - tmp1_s16;
nmk3 = nmk2 + (int16_t)((ndelt * kBackEta) >> 9);

值得注意的是:平滑修正不受vadflag影響

3)邊界約束

即強制噪聲均值不能小於某個最小值,和防止噪聲均值過大,避免它被語音能量無限拉昇。代碼如下:

tmp_s16 = (int16_t) ((k + 5) << 7);
        if (nmk3 < tmp_s16) {
          nmk3 = tmp_s16;
        }
        tmp_s16 = (int16_t) ((72 + k - channel) << 7);
        if (nmk3 > tmp_s16) {
          nmk3 = tmp_s16;
        }

4)策略意義

對於nmk2會受到WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#算法_47的影響,而WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#算法_47並不是固定常數,而是隨着當前幀的條件概率變動,一旦語音能量進入,更新方向就會發生錯誤。此時平滑修正的意義就體現出來:

  • 短時更新可能受到語音污染,可能偏離真實噪聲基線
  • 如果當前幀判定為非噪聲,那麼均值會被kBackEta慢慢收斂回長期基線

4.更新人聲均值

和更新噪聲均值一樣的梯度更新計算方法,只是區別是:

  • 不需要進行長期拉回,因為人聲是特徵動態明顯,不需要噪聲基線作為參考
  • 噪聲的限幅控制是為了保證模型穩定,避免漂移到語音區,而人聲限幅是跟隨語音分佈變化,防止過度漂移,所以限幅範圍不同
  • 人聲均值向上取整而不是向下取整smk2 = smk + ((tmp_s16 + 1) >> 1);

5.更新人聲方差

1)更新公式

和上文一樣,這裏採用的是遞推更新:
WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#webrtc_49

2)代碼解析

tmp_s16 = ((smk + 4) >> 3);
          tmp_s16 = features[channel] - tmp_s16;  
          tmp1_s32 = (deltaS[gaussian] * tmp_s16) >> 3;
          tmp2_s32 = tmp1_s32 - 4096;
          tmp_s16 = sgprvec[gaussian] >> 2;
          tmp1_s32 = tmp_s16 * tmp2_s32;
          tmp2_s32 = tmp1_s32 >> 4; 
          if (tmp2_s32 > 0) {
            tmp_s16 = (int16_t) WebRtcSpl_DivW32W16(tmp2_s32, ssk * 10);
          } else {
            tmp_s16 = (int16_t) WebRtcSpl_DivW32W16(-tmp2_s32, ssk * 10);
            tmp_s16 = -tmp_s16;
          }
          tmp_s16 += 128;  // Rounding.
          ssk += (tmp_s16 >> 8);

其中:

  • WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#信號處理_15:來自於sgprvec
  • WebRTC VAD流程解析【摘自“語音算法組”公眾號-Ryuk】_#信號處理_45:為0.025,代碼中體現在WebRtcSpl_DivW32W16產生了0.1的分母,sgprvec[gaussian] >> 2產生了4的分母
  • tmp_s16 = ((smk + 4) >> 3):這裏的加4原因是為了保證右移3位是向上取整保存精度

3)邊界約束

方差值不能小於384

static const int16_t kMinStd = 384;
if (ssk < kMinStd) {
            ssk = kMinStd;
          }

6.更新噪聲方差

和更新人聲方差一樣,區別在於更新步長的大小是根據偏移量決定的,不過範圍大致在0.015~0.02 之間。

7.模型分離

為了防止speech GMM 模型和 noise GMM 模型的均值太接近而粘在一起,需要最終結合兩者進行決策。

1)計算全局均值

noise_global_mean  = WeightedAverage(&self->noise_means[channel], 0, &kNoiseDataWeights[channel]);
speech_global_mean = WeightedAverage(&self->speech_means[channel], 0, &kSpeechDataWeights[channel]);
  • 對每個 channel,把各個高斯分量的均值做加權平均。
  • 得到一個 全局 noise 平均值 和一個 全局 speech 平均值。

2)計算差值

diff = (int16_t)(speech_global_mean >> 9) - (int16_t)(noise_global_mean >> 9);
  • speech 全局均值 − noise 全局均值。
  • 如果差值太小,説明兩個模型過於接近

3)如果模型太近,強行拉開

if (diff < kMinimumDifference[channel]) {
   tmp_s16 = kMinimumDifference[channel] - diff;

   // tmp1_s16 ≈ 0.8 * (缺口)
   // tmp2_s16 ≈ 0.2 * (缺口)

   // 把 speech 模型整體往上推一點
   speech_global_mean = WeightedAverage(&self->speech_means[channel],
                                        tmp1_s16, &kSpeechDataWeights[channel]);

   // 把 noise 模型整體往下拉一點
   noise_global_mean  = WeightedAverage(&self->noise_means[channel],
                                       -tmp2_s16, &kNoiseDataWeights[channel]);
}
  • 如果 gap 太小,就把 speech 往上推 80%,noise 往下拉 20%,強制保持至少 kMinimumDifference 的區分度。
  • 這樣避免兩個模型均值重疊,保持判決的魯棒性。

4)限制模型漂移範圍

if (speech_global_mean >> 7 > kMaximumSpeech[channel]) {
   // speech 上限
   ...
}
if (noise_global_mean >> 7 > kMaximumNoise[channel]) {
   // noise 上限
   ...
}

作用是給 speech 和 noise 均值設上界,避免模型被極端值推得太遠。

五、hangover 機制

1.策略作用

WebRTC VAD 的hangover 機制,用來在語音剛結束時 延長一小段時間繼續判為語音,避免抖動。

2.整體流程

檢測到語音 → num_of_speech++
     ├─ 如果 num_of_speech <= kMaxSpeechFrames → over_hang = overhead1
     └─ 如果 num_of_speech >  kMaxSpeechFrames → over_hang = overhead2

檢測到非語音
     ├─ 如果 over_hang > 0 → 繼續輸出語音, over_hang--
     └─ 如果 over_hang = 0 → 輸出靜音

其中:

  • overhead1:給短語音的尾巴加一點點餘量。
  • overhead2:給長語音的尾巴加更長的餘量。
  • num_of_speech:用來區分當前語音段的“長/短”。
  • over_hang:用來平滑語音和靜音的切換,防止“斷斷續續”

3.對應代碼

1)非語音處理

if (!vadflag) {
    if (self->over_hang > 0) {
      vadflag = 2 + self->over_hang;
      self->over_hang--;
    }
    self->num_of_speech = 0;
}
  • 但 hangover 計數器 over_hang > 0:説明前面剛有語音 → 進入“延長語音”階段
  • 把 vadflag 設置為 2 + self->over_hang(這裏 2 是特殊標記,表明這不是直接檢測出的語音,而是 hangover 延長出來的語音)
  • over_hang--:計數器遞減
  • 否則就徹底當作 silence
  • num_of_speech = 0:清零語音幀計數器

2)語音

else {
    self->num_of_speech++;
    if (self->num_of_speech > kMaxSpeechFrames) {
      self->num_of_speech = kMaxSpeechFrames;
      self->over_hang = overhead2;
    } else {
      self->over_hang = overhead1;
    }
}
  • num_of_speech++:累計連續語音幀數
  • 如果超過 kMaxSpeechFrames(最大語音幀數限制):
  • num_of_speech 固定到上限
  • over_hang = overhead2(長的 hangover 時間)
  • 否則:over_hang = overhead1(短的 hangover 時間)

總結

WebRTC VAD 展示了一個典型的“經典信號處理 + 工程優化”方案:

  • 在特徵層面,採用子帶能量的對數刻畫,使得語音與噪聲分佈更接近高斯;
  • 在模型層面,使用固定參數的雙高斯混合模型,結合局部和全局判決,提高魯棒性;
  • 在實現層面,大量利用 Q 格式、移位近似、預計算表,保證了低算力環境下的實時性;
  • 在動態性上,通過逐幀更新和邊界約束,使模型能逐漸適應環境變化。

當然,這種基於 GMM 的方法也有侷限:在強噪聲、非平穩噪聲環境下可能誤判,且閾值調優高度依賴經驗。隨着算力提升,DNN/RNN 基的 VAD 在魯棒性上表現更優,但代價是更高的複雜度與延遲。