WebRTC之VAD算法

VAD(Voice Activity Detection)算法的作用是检测语音,在远场语音交互场景中,VAD面临着两个难题:

  1. 可以成功检测到最低能量的语音(灵敏度)。
  2. 如何在多噪环境下成功检测(漏检率和虚检率)。
    漏检反应的是原本是语音但是没有检测出来,而虚检率反应的是不是语音信号而被检测成语音信号的概率。相对而言漏检是不可接受的,而虚检可以通过后端的ASR和NLP算法进一步过滤,但是虚检会带来系统资源利用率上升,随之系统的功耗和发热会进一步增加,而这会上升为可移动和随声携带设备的一个难题。
    本文基于WebRTC的AEC算法,WebRTC的VAD模型采用了高斯模型,这一模型应用极其广泛。

高斯分布

高斯分布又称为正态分布(Normal distribution/Gaussian distribution)。
若随机变量X服从一个数学期望为μ,标准差为σ^2的高斯分布,则:
X~N(μ,σ^2)
其概率密度函数为:
f(x)=1/(√2π σ) e(-〖(x-u)〗2/(2σ^2 ))
高斯在webRTC中的使用:
f(x_k |Z,r_k)=1/√2π e^(-(x_k-u_z )2/(2σ2 ))
x_k是选取的特征向量,webRTC中指x_k是六个子带的能量,r_k是均值u_z和方差σ的参数结合,这两个参数决定了高斯分布的概率。Z=0情况是计算噪声的概率,Z=1是计算是语音的概率。

WebRTC算法流程

1.设置VAD激进模式

共四种模式,用数字0~3来区分,激进程度与数值大小正相关。
0: Normal,1:low Bitrate, 2:Aggressive;3:Very Aggressive
这些激进模式是和以下参数是息息相关的。

<comman_audio/vad/vad_core.c>
// Mode 0, Quality.
static const int16_t kOverHangMax1Q[3] = { 8, 4, 3 };
static const int16_t kOverHangMax2Q[3] = { 14, 7, 5 };
static const int16_t kLocalThresholdQ[3] = { 24, 21, 24 };
static const int16_t kGlobalThresholdQ[3] = { 57, 48, 57 };
// Mode 1, Low bitrate.
static const int16_t kOverHangMax1LBR[3] = { 8, 4, 3 };
static const int16_t kOverHangMax2LBR[3] = { 14, 7, 5 };
static const int16_t kLocalThresholdLBR[3] = { 37, 32, 37 };
static const int16_t kGlobalThresholdLBR[3] = { 100, 80, 100 };
// Mode 2, Aggressive.
static const int16_t kOverHangMax1AGG[3] = { 6, 3, 2 };
static const int16_t kOverHangMax2AGG[3] = { 9, 5, 3 };
static const int16_t kLocalThresholdAGG[3] = { 82, 78, 82 };
static const int16_t kGlobalThresholdAGG[3] = { 285, 260, 285 };
// Mode 3, Very aggressive.
static const int16_t kOverHangMax1VAG[3] = { 6, 3, 2 };
static const int16_t kOverHangMax2VAG[3] = { 9, 5, 3 };
static const int16_t kLocalThresholdVAG[3] = { 94, 94, 94 };
static const int16_t kGlobalThresholdVAG[3] = { 1100, 1050, 1100 };

它们在计算高斯模型概率时用到。

2帧长设置

A) 共有三种帧长可以用到,分别是80/10ms,160/20ms,240/30ms。
B) 其它采样率的48k,32k,24k,16k会重采样到8k来计算VAD。
之所以选择上述三种帧长度,是因为语音信号是短时平稳信号,其在10ms30ms之间可看成平稳信号,高斯马尔科夫等比较的信号处理方法基于的前提是信号是平稳的,在10ms30ms,平稳信号处理方法是可以使用的。

3 高斯模型中特征向量选取

在WebRTC的VAD算法中用到了聚类的思想,只有两个类,一个类是语音,一个类是噪声,对每帧信号都求其是语音和噪声的概率,根据概率进行聚类,当然为了避免一帧带来的误差也有一个统计量判决在算法里,那么问题来了,选择什么样的特征作为高斯分布的输入呢?这关系到聚类结果的准确性,也即VAD性能,毋庸置疑,既然VAD目的是区分噪声和语音,那么噪声信号和语音信号这两种信号它们的什么特征相差最大呢?选择特征相差比较大自然能得到比较好的区分度。
众所周知,信号的处理分类主要有时域,频域和空域,从空域上看,webRTC的VAD是基于单麦克的,噪声和语音没有空间区分度的概念,在多麦克风场景,确实基于多麦克风的VAD算法,从时域上看,而者都是时变信号,且短时信号变化率比较小,所以推算来推算去只有频域的区分度可能是比较好的。

image.png

汽车噪声频谱

image.png

粉红噪声频谱

image.png

白噪声频谱

image.png

语音声谱

从以上四个图中,可以看到从频谱来看噪声和语音,它们的频谱差异还是比较大,且以一个个波峰和波谷的形式呈现。
WebRTC正式基于这一假设,将频谱分成了6个子带。它们是:
80Hz250Hz,250Hz500Hz,500Hz1K,1K2K,2K3K,3K4K。
可以看到以1KHz为分界,向下500HZ,250Hz以及170HZ三个段,向上也有三个段,每个段是1KHz,这一频段涵盖了语音中绝大部分的信号能量,且能量越大的子带的区分度越细致。
我国交流电标准是220V~50Hz,电源50Hz的干扰会混入麦克风采集到的数据中且物理震动也会带来影响,所以取了80Hz以上的信号。

高通滤波器设计

// High pass filtering, with a cut-off frequency at 80 Hz, if the |data_in| is
// sampled at 500 Hz.
//
// - data_in      [i]   : Input audio data sampled at 500 Hz.
// - data_length  [i]   : Length of input and output data.
// - filter_state [i/o] : State of the filter.
// - data_out     [o]   : Output audio data in the frequency interval
//                        80 - 250 Hz.
static void HighPassFilter(const int16_t* data_in, size_t data_length,
                           int16_t* filter_state, int16_t* data_out) {
  size_t i;
  const int16_t* in_ptr = data_in;
  int16_t* out_ptr = data_out;
  int32_t tmp32 = 0;

  // The sum of the absolute values of the impulse response:
  // The zero/pole-filter has a max amplification of a single sample of: 1.4546
  // Impulse response: 0.4047 -0.6179 -0.0266  0.1993  0.1035  -0.0194
  // The all-zero section has a max amplification of a single sample of: 1.6189
  // Impulse response: 0.4047 -0.8094  0.4047  0       0        0
  // The all-pole section has a max amplification of a single sample of: 1.9931
  // Impulse response: 1.0000  0.4734 -0.1189 -0.2187 -0.0627   0.04532

  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];
  }
}

新版的定义在 common_audio\vad\vad_filterbank.c

// High pass filtering, with a cut-off frequency at 80 Hz, if the |data_in| is
// sampled at 500 Hz.
//
// - data_in      [i]   : Input audio data sampled at 500 Hz.
// - data_length  [i]   : Length of input and output data.
// - filter_state [i/o] : State of the filter.
// - data_out     [o]   : Output audio data in the frequency interval
//                        80 - 250 Hz.
static void HighPassFilter(const int16_t* data_in, int data_length,
                           int16_t* filter_state, int16_t* data_out) {
  int i;
  const int16_t* in_ptr = data_in;
  int16_t* out_ptr = data_out;
  int32_t tmp32 = 0;


  // The sum of the absolute values of the impulse response:
  // The zero/pole-filter has a max amplification of a single sample of: 1.4546
  // Impulse response: 0.4047 -0.6179 -0.0266  0.1993  0.1035  -0.0194
  // The all-zero section has a max amplification of a single sample of: 1.6189
  // Impulse response: 0.4047 -0.8094  0.4047  0       0        0
  // The all-pole section has a max amplification of a single sample of: 1.9931
  // Impulse response: 1.0000  0.4734 -0.1189 -0.2187 -0.0627   0.04532

  for (i = 0; i < data_length; i++) {
    // All-zero section (filter coefficients in Q14).
    tmp32 = WEBRTC_SPL_MUL_16_16(kHpZeroCoefs[0], *in_ptr);
    tmp32 += WEBRTC_SPL_MUL_16_16(kHpZeroCoefs[1], filter_state[0]);
    tmp32 += WEBRTC_SPL_MUL_16_16(kHpZeroCoefs[2], filter_state[1]);
    filter_state[1] = filter_state[0];
    filter_state[0] = *in_ptr++;

    // All-pole section (filter coefficients in Q14).
    tmp32 -= WEBRTC_SPL_MUL_16_16(kHpPoleCoefs[1], filter_state[2]);
    tmp32 -= WEBRTC_SPL_MUL_16_16(kHpPoleCoefs[2], filter_state[3]);
    filter_state[3] = filter_state[2];
    filter_state[2] = (int16_t) (tmp32 >> 14);
    *out_ptr++ = filter_state[2];
  }
}

对应的调用入口在modules\audio_processing\VoiceDetectionImpl.cc 中,通过WebRtcVad_Process(common_audio\vad\Webrtc_vad.c int WebRtcVad_Process(VadInst* handle, int fs, const int16_t* audio_frame,
int frame_length)) 最终会调用到vad检测;

int VoiceDetectionImpl::ProcessCaptureAudio(AudioBuffer* audio) {
  if (!is_component_enabled()) {
    return apm_->kNoError;
  }

  if (using_external_vad_) {
    using_external_vad_ = false;
    return apm_->kNoError;
  }
  assert(audio->samples_per_split_channel() <= 160);

  // TODO(ajm): concatenate data in frame buffer here.

  int vad_ret = WebRtcVad_Process(static_cast<Handle*>(handle(0)),
                                  apm_->proc_split_sample_rate_hz(),
                                  audio->mixed_low_pass_data(),
                                  frame_size_samples_);

github地址:https://github.com/starmier/webrtc-1/blob/master/webrtc/modules/audio_processing/audio_processing_impl.cc

感兴趣的小伙伴可以去研读一下😄;

WebRTC在设计该滤波器上还是很有技巧的,技巧有二:

  1. 定点数计算,指两个方面,一是滤波系数量化,而是计算过程的定点化,高斯模型计算也使用了这一技巧。
  2. 舍入技巧,减少运算量。
    下面就来看看,这些技巧是如何使用的,首先根据代码的注释可以看出,
    全零点和全极点脉冲响应的实际上是浮点数,它们脉冲响应分别是:
    0.4047 -0.8094 0.4047 0 0 0
    1.0000 0.4734 -0.1189 -0.2187 -0.0627 0.04532
    所以可见应该是六阶方程,但是超过3阶后,零点全零,极点数值较小,这时适当增大第三个数值,达到减少计算次数的目的。

量化是按照2的十四次方进行定点化。这是因为最差情况下,零极点的放大倍数不超过两倍,所以16位数可以表示的下来。其零极点绘图如下:

image.png

对这两个图的解释就忽略了,能够看懂上述代码和两张图的意义,就可以更改滤波器的特性了,对不要相位信息的,采用IIR比FIR达到相同的增益平坦度需要的阶数要少。关于高通滤波器的设计还有疑问的可以留言共同交流。频响如下:

image.png

WebRtcVad_CalculateFeatures函数计算每个子带的能量。能量结果存放在features数组里,然后调用GmmProbability计算概率。

int WebRtcVad_CalcVad8khz(VadInstT* inst, const int16_t* speech_frame,
                          size_t frame_length) {
    int16_t feature_vector[kNumChannels], total_power;

    // Get power in the bands
    total_power = WebRtcVad_CalculateFeatures(inst, speech_frame, frame_length,
                                              feature_vector);

    // Make a VAD
    inst->vad = GmmProbability(inst, feature_vector, total_power, frame_length);

    return inst->vad;
}

计算流程

高斯模型有两个参数H0和H1,它们分表示的是噪声和语音,判决测试使用LRT(likelihood ratio test)。分为全局和局部两种情况。

image.png

a)高斯概率计算采用的高斯公式如下:

image.png

c)对数似然比,分为全局和局部,全局是六个子带之加权之和,而局部是指每一个子带则是局部,所以语音判决会先判断子带,子带判断没有时会判断全局,只要有一方过了,就算有语音,公式表达如下:

image.png

后记:

和判决准则相关的参数在vad_core.c文件,他们是:

// Spectrum Weighting
static const int16_t kSpectrumWeight[kNumChannels] = { 6, 8, 10, 12, 14, 16 };
static const int16_t kNoiseUpdateConst = 655; // Q15
static const int16_t kSpeechUpdateConst = 6554; // Q15
static const int16_t kBackEta = 154; // Q8
// Minimum difference between the two models, Q5
static const int16_t kMinimumDifference[kNumChannels] = {
    544, 544, 576, 576, 576, 576 };
// Upper limit of mean value for speech model, Q7
static const int16_t kMaximumSpeech[kNumChannels] = {
    11392, 11392, 11520, 11520, 11520, 11520 };
// Minimum value for mean value
static const int16_t kMinimumMean[kNumGaussians] = { 640, 768 };
// Upper limit of mean value for noise model, Q7
static const int16_t kMaximumNoise[kNumChannels] = {
    9216, 9088, 8960, 8832, 8704, 8576 };
// Start values for the Gaussian models, Q7
// Weights for the two Gaussians for the six channels (noise)
static const int16_t kNoiseDataWeights[kTableSize] = {
    34, 62, 72, 66, 53, 25, 94, 66, 56, 62, 75, 103 };
// Weights for the two Gaussians for the six channels (speech)
static const int16_t kSpeechDataWeights[kTableSize] = {
    48, 82, 45, 87, 50, 47, 80, 46, 83, 41, 78, 81 };
// Means for the two Gaussians for the six channels (noise)
static const int16_t kNoiseDataMeans[kTableSize] = {
    6738, 4892, 7065, 6715, 6771, 3369, 7646, 3863, 7820, 7266, 5020, 4362 };
// Means for the two Gaussians for the six channels (speech)
static const int16_t kSpeechDataMeans[kTableSize] = {
    8306, 10085, 10078, 11823, 11843, 6309, 9473, 9571, 10879, 7581, 8180, 7483
};
// Stds for the two Gaussians for the six channels (noise)
static const int16_t kNoiseDataStds[kTableSize] = {
    378, 1064, 493, 582, 688, 593, 474, 697, 475, 688, 421, 455 };
// Stds for the two Gaussians for the six channels (speech)
static const int16_t kSpeechDataStds[kTableSize] = {
    555, 505, 567, 524, 585, 1231, 509, 828, 492, 1540, 1079, 850 };

原文链接:https://blog.csdn.net/shichaog/article/details/52399354

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,875评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,569评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,475评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,459评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,537评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,563评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,580评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,326评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,773评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,086评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,252评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,921评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,566评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,190评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,435评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,129评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,125评论 2 352

推荐阅读更多精彩内容