VAD实现-读取语音数据、数据预处理、算法计算流程与框架

转载于:https://blog.csdn.net/lv_xinmy/article/details/9092159

              https://blog.csdn.net/lv_xinmy/article/details/9093163

              https://blog.csdn.net/lv_xinmy/article/details/9095555

作者:lv_xinmy


一,什么是VAD

VAD,也就是语音端点检测技术,是Voice Activity Detection的缩写。这个技术的主要任务是从带有噪声的语音中准确的定位出语音的开始和结束点,因为语音中含有很长的静音,也就是把静音和实际语音分离开来,因为是语音数据的原始处理,所以VAD是语音信号处理过程的关键技术之一。它的好坏,直接影响成败,由于技术本身的特殊性,所以在涉及语音信号处理的领域,端点检测技术的应用非常广泛。语音识别系统在识别或者声学模型训练阶段所遇到的第一个技术就是端点检测,把静音和噪声作为干扰信号从原始数据中去除,并且端点检测对于语音识别系统的性能至关重要。


二,VAD的作用

现在流行的语音识别系统大部分,或者是相当一部分都是基于统计和训练的原理所构建的系统,因此对数据来源和训练环境都是很敏感的。在识别的过程中,经常存在实际语音因背景噪声的干扰而与训练失配的情况,实际这也是造成语音识别系统鲁棒性差的一个根本原因(另一个主要的是无法处理非预期的输入),从而导致识别错误,性能下降。哪怕是两段内容上是完全一致的语音信号,可能由于语速不一样,所以语音信号的时间也不相同,音素之间的时间间隙也就不一样,对于时变而非平稳的语音信号来说,其特征就完全不相同了。有音素之间的间隙,也有静音和语音本身的间隙,为了对数据从时间上进行相对的校准,语音端点检测技术就应运而生了,因此端点检测技术可以决定这种校准的相对精度,使得同一内容的特征更趋于相同,当然,一般情况下是不可能完全相同的。大量研究表明,如果环境是安静的环境,没有太多背景噪声,此时语音识别系统的主要错误来源于端点检测技术不精确。

但在实际应用中,不可能没有背景噪声,另外由于麦克风的录制和信号增益也会带来噪声,所以语音识别系统的错误是由多方面影响的,至少包括:端点检测、特征提取、语音模型、声学模型、解码器等多个方面。

假定读取的语音数据是WAV文件格式的数据。

三,WAV数据头结构定义

[cpp]  view plain   copy
  1. typedef struct{  
  2.    char FLAG[4];            // 'RIFF'资源交换文件标识  
  3.    int resource_length;//从下个字节开始,到资源结尾的总长度。  
  4.   
  5.    char WAVE_FLAG[4];            // 'WAVE' //WAVE文件标识  
  6.   
  7.    char format_flag[4];            // 'fmt '//波形格式标识  
  8.    int pcm_header_len;   // varies...过滤字节  
  9.    short int format_tag;//格式种类,值为1时,表示数据为线性PCM编码  
  10.    short int channel_count;      // 单声道为1,双声道为2.  
  11.    int sample_rate;//采样频率  
  12.    int bytespersec;//波形数据传输速率  
  13.    short int block_align;// 字节对齐  
  14.    short int bits_per_samples;//样本数据在存储时所使用的位数。  
  15. } wav_header;  

上面是WAV文件格式的头定义 ,数据定义如下

[cpp]  view plain   copy
  1. typedef struct{  
  2.    char data_flag[4];            // 数据标识符,'data' or 'fact'  
  3.    int length;//采样数据长度  
  4. }data_header;  
在该结构之后,就是采样数据了。

下面读取文件代码:

[cpp]  view plain   copy
  1. /** 
  2. * 
  3. * 传入参数:WAV文件名(IN),数据(OUT),采样频率(OUT) 
  4. * 参数返回:返回采样样本数 
  5.  
  6. **/  
  7. int read_wave_file(char *filename, short ** v_data, int *sampleRate){  
  8.     FILE *fp = fopen(filename, "rb");  
  9.     if (fp == NULL) {  
  10.         fprintf(stderr, "open wav file %s error\n", wfn);  
  11.         exit(-1);  
  12.     }  
  13.     char buf[20];  
  14.     wav_header *wav = new wav_header;  
  15.     data_header *data = new data_header;  
  16.   
  17.     //读取WAV文件头  
  18.     if (fread((void *) wav, sizeof (wav_header), 1, fp) != 1){  
  19.         fprintf(stderr, "cant read wav header\n");  
  20.         exit(-1);  
  21.     }  
  22.   
  23.     for(int i=0;i<4;i++)   
  24.         buf[i] = wav->FLAG[i];  
  25.     buf[4] = 0;  
  26.     if(strcmp(buf,"RIFF")!=0){  
  27.         fprintf(stderr, "%s bad RIFF format\n", buf);  
  28.         exit(-1);  
  29.     }  
  30.   
  31.     for(int i=0;i<4;i++)   
  32.         buf[i] = wav->WAVE_FLAG[i];  
  33.     buf[4] = 0;  
  34.     if(strcmp(buf,"WAVE")!=0){  
  35.         fprintf(stderr, "%s bad WAVE format\n", buf);  
  36.         exit(-1);  
  37.     }  
  38.   
  39.     for(int i=0;i<3;i++)   
  40.         buf[i] = wav->format_flag[i];  
  41.     buf[3] = 0;  
  42.     if(strcmp(buf,"fmt")!=0){  
  43.         fprintf(stderr, "%s bad fmt format\n", buf);  
  44.         exit(-1);  
  45.     }  
  46.   
  47.     //要求数据为线性PCM编码  
  48.     if(wav->format_tag!=1){  
  49.         fprintf(stderr, "bad wav format tag\n");  
  50.         exit(-1);  
  51.     }  
  52.   
  53.     if(wav->bits_per_samples != 16){  
  54.         fprintf(stderr, "bad wav bits per sample\n");  
  55.         exit(-1);  
  56.     }  
  57.   
  58.     if (sampleRate != NULL)  
  59.         *sampleRate = wav->sample_rate;//需要返回采样频率  
  60.   
  61.     //读取数据,如果不是数据块就跳过,直到"data"块被找到  
  62.     int skip = 1;  
  63.     while(skip!=0){  
  64.   
  65.         if(skip>10){  
  66.             fprintf(stderr, "too many chunks\n");  
  67.             exit(-1);  
  68.         }  
  69.   
  70.         //读取数据头  
  71.         if (fread((void *)data,sizeof(data_header),(size_t)1,fp) != 1){  
  72.             fprintf(stderr, "cant read chunk\n");  
  73.             exit(-1); }  
  74.   
  75.             // check chunk type  
  76.         //读取数据标识  
  77.         for(int i=0;i<4;i++)   
  78.             buf[i] = data->data_flag[i];  
  79.         buf[4] = 0;  
  80.   
  81.         if(strcmp(buf,"data")==0)   
  82.             break;  
  83.   
  84.         //跳过当前非数据块  
  85.             skip++;  
  86.             fseek(fp,data->length,SEEK_CUR);  
  87.     }  
  88.   
  89.     // 现在可以读取数据了,因为前面已经找到数据块  
  90.     int wbuff_len = data->length;  
  91.     char * wbuff = new char [wbuff_len];  
  92.     if(wbuff==NULL){  
  93.         fprintf(stderr, "alloc memory failed\n");  
  94.         exit(-1);  
  95.     }  
  96.     // 计算采样数。数据采样存储总长度/(每个采样的存储占用字节)  
  97.     int nsample = data->length/(wav->bits_per_samples / 8);  
  98.   
  99.     //读取信号数据  
  100.     if (fread((void *)wbuff,wbuff_len,(size_t)1,fp) != 1){  
  101.         fprintf(stderr, "cant read wbuff\n");  
  102.         exit(-1);  
  103.     }  
  104.   
  105.     // 将数据作为引用参数传回调用  
  106.     *v_data = (short *) wbuff;  
  107.   
  108.     fclose(fp);  
  109.   
  110.     //返回采样数  
  111.     return nsample;  
  112. }

在用VAD算法确定静音和语音数据的开始和起止点之前,需要对语音数据进行处理,然后再计算语音数据的开始和起止点,这个过程称为数据的预处理,有些VAD算法是基于短时能量和过零率实现的,并不进行预处理操作,但实验表明,对数据进行预处理之后的效果要比不进行预处理的效果好。这里,对数据进行去除直流和加窗两个预处理。

四、去除直流

至少有这几个理由,要求我们去除直流。

  1. 直流,是频域的慢变成分,一般都是因为信号采集过程中仪器所致。
  2. 如果需要进行频谱分析,那么因为直流的频谱泄漏,会严重影响低频谱的分析。
  3. 直流一般不随时间变化,不反应信号的趋性。
去除直接代码如下:
[cpp]  view plain   copy
  1. float  new_last_data ,last_data ; // 上次计算的去除直流之后和之前的值。  
  2. float *remove_dc(short *data,int frame_size){  
  3.   
  4.     float in0 =0.0 ,of0 = 0.0 ;  
  5.   
  6.     float *tf = new float[frame_size];  
  7.     for (int i=0; i < frame_size; i++){  
  8.   
  9.         tf[i] = in0 = (float)(data[i]);  
  10.   
  11.         tf[i] = in0 - last_data + 0.9997 * new_last_data;  // 这里所采用的去除直流公式:s_new(n) = s_old(n)-s_old(n-1)+0.9997*s_new(n-1)  
  12.         last_data = in0;  
  13.         new_last_data = of0 = tf[i];  
  14.     }  
  15.     return tf;  
  16. }  

五、加窗
加窗的作用就更明显了,见 语音信号的加窗处理
[cpp]  view plain   copy
  1. float *hamming(short *data,int frame_size){  
  2.   
  3.     float *tf = new float[frame_size];  
  4.   
  5.     float a = 6.28318530717959 / (frame_size - 1); // 2 * PI = 6.28318530717959  
  6.   
  7.     for (int i=0; i< frame_size;i++){  
  8.         tf[i] = (float)(data[i]);  
  9.         tf[i] *= (0.54 - 0.46 * cos(a*i)); //ω(n) =0.54-0.46cos[2πn/(N-1)]  
  10.     }  
  11.     return tf;  
  12. }  

六、在实现VAD算法之前,先给出在这里VAD算法的实现流程和算法框架。

调用关系依次是 detect_wav -> detect_frame->process_vad->energy_detect,energy_detect比较复杂,所以暂时不在这里,这只是表示出基本的计算流程和框架

[cpp]  view plain   copy
  1. int frame_size = 256 ;   
  2. int sample_rate = 8000 ;//  采样频率  
  3. int frame_step =  80;  
  4. int msec_per_frame  = (int) 1000 * frame_size/sample_rate; // 一帧占据多少毫秒  
  5. int silence_filter_len = 30 ; //由语音进入静音之前的过渡区域,这是一个灰度区域  
  6. int speech_filter_len = 30 ; // 由静音进入语音前的过渡区域  
  7.   
  8. int number_of_silence = 0; // 当前处理的静音长度。  
  9. int number_of_speech = 0;  // 当前处理的语音长度。  
  10. int total_of_silence = 0;  //状态切换之前的静音长度  
  11. int total_of_speech = 0;   //状态切换之前的语音长度  
  12.   
  13. int silence_count = 0; //在当前处理时,合法的数据被接受,可以正式计数  
  14. int speech_count = 0;  
  15.   
  16. int frame_count = 0;  

上面是申明的一些全局变量。

[cpp]  view plain   copy
  1. /** 
  2. * 
  3. * data:整个WAV文件的数据。nsample:WAV文件的采样数。这两个值都是可以从返回的 
  4. * 
  5. **/  
  6. int detect_wav(short *data,int nsample){  
  7.     int state = state_SILENCE ;  //初始状态:静音  
  8.     int index = 0,offset = 0;  
  9.     int cst = 0; // 当前状态时间,暂时以帧表示   
  10.     //index会返回当前是处理的第几帧,这和第几个采样点是不一样的  
  11.     int ret = detect_frame(data,&index); // 从第一个帧开始处理,配置每一帧256个采样点,ret返回当前帧的状态,静音、结束、语音等。  
  12.   
  13.     while(ret != state_OVER){  // 直到最后一帧,也就是处理结束。  
  14.         if(ret != state_WAIT){  //是否返回静音或者语音段  
  15.             if(ret  != st ){ // 返回状态和上一状态不相同  
  16.                 int k ;  
  17.                 if(ret == state_SILENCE){  
  18.                     k = index * frame_step -  silence_filter_len * frame_step ;//过渡区域计为本次的静音。  
  19.                     if(k <= cst){ //  
  20.                         ret = st;  
  21.                         continue;  
  22.                     }  
  23.                     //输出静音段  
  24.                     printf("%3.2f %3.2f Speech\n",cst/(float)sample_rate,k/(float)sample_rate);  
  25.                 }else{  
  26.                     k = index * frame_step - speech_filter_len  * frame_step;//之前的过渡区域计为本次的语音段  
  27.                     if(k <= cst){  
  28.                         ret = st;  
  29.                         continue;  
  30.                     }  
  31.                     printf("%3.2f %3.2f Silence\n", cst/(float)sample_rate, ((k/(float)sample_rate)>0?(k/(float)sample_rate):0.00));  
  32.                 }  
  33.                 cst = k;   //保留上次计算的K 值。  
  34.                 st = ret ; //保留上次状态值  
  35.             }  
  36.         }  
  37.         // 准备处理下一帧  
  38.         offset += frame_step ; // 移动帧的起始位置,可以由此计算帧的重叠  
  39.         if(offset <= nsample - frame_size){  
  40.             ret = detect_frame(data+offset,&index);  
  41.         }else{  
  42.             ret = detect_frame(NULL,&index);  
  43.         }  
  44.     }  
  45.         // 这里需要输出最后的段  
  46.        if (st == state_SPEEACH){  
  47.                 printf("%3.2f %3.2f SPeech\n", cst/(float)sample_rate, nSample/(float)sample_rate);  
  48.        }else{  
  49.                 printf("%3.2f %3.2f Silence\n", cst/(float)sample_rate, nSample/(float)sample_rate);  
  50.         }  
  51.         return 0;  
  52. }  
上面的函数会对所有的数据进行循环调用,每调用一帧都会调用detect_frame进行处理:

[cpp]  view plain   copy
  1. /** 
  2. * 
  3. * 处理一帧数据,就是从data开始的frame_size个采样。state表示当前状态 
  4. * 返回当前帧的处理后的状态 
  5. * 
  6. **/  
  7. int detect_frame(short *data,int *index,int state){  
  8.     short *pcm_data = data ;  
  9.     int silence_flag = process_vad(pcm_data,index); //处理具体的数据,并返回当前帧的处理结果  
  10.   
  11.     //结束  
  12.     if(silence_flag == state_OVER || silence_flag == state_WAIT )  
  13.         return silence_flag ;  
  14.     if(silence_flag == state_SILENCE){ // 静音部分段  
  15.         number_of_silence  ++ ;  
  16.         total_of_silence  ++ ;  
  17.         number_of_speech = 0;  
  18.         if(number_of_silence > 5){ // 五个以上的静音帧,就相当于50ms  
  19.             speech_count = 0; // 开始静音统计,语音为0;  
  20.         }  
  21.   
  22.         // 如果之前已经是静音,没有问题,这里处理如果之前是语音的情况。  
  23.         if(state == state_SPEECH )  
  24.             silence_count ++ ;  
  25.         /** 
  26.         * 这里的处理是为了使得如果静音段过长,就是超过了300ms。此时状态切换 
  27.         **/  
  28.         if((state == state_SPEECH) && (silence_count > silence_filter_len )){  
  29.             state = ((total_of_speech > total_of_silence) ? state_SPEECH : state_SILENCE) ;  
  30.             number_of_silence = number_of_speech =total_of_silence = total_of_speech = 0;  
  31.         }  
  32.     }else{  
  33.         total_of_speech ++ ;  
  34.         number_of_speech ++ ;  
  35.         number_of_silence = 0;  
  36.         if(number_of_speech > 5){  
  37.             silence_count = 0;  
  38.         }  
  39.         if(state == state_SILENCE )  
  40.             speech_count ++;  
  41.         if((state == state_SILENCE ) && ( speech_count > speech_filter_len)){  
  42.             state = ((total_of_speech > total_of_silence) ? state_SPEECH : state_SILENCE) ;  
  43.             number_of_silence = number_of_speech =total_of_silence = total_of_speech = 0;  
  44.         }  
  45.     }  
  46.       
  47.     frame_count ++;   
  48.     return state;  
  49. }  
上面的detect_frame其实是一个后处理函数,就是返回当前帧的可能状态之后,进行状态变更和计数的,其数据处理的操作为:

[cpp]  view plain   copy
  1. int process_vad(short *data,int *index){  
  2.     if(data == NULL)  
  3.         return state_OVER;  
  4.     //数据去除直流。  
  5.     //数据加窗 并返回total_rms   
  6.     energy_detect(total_rms,flag); // 根据均方差计算当前帧的状态,并从flag返回,该参数为引用参数。  
  7.     *index = *index++ ;// 这里是简化,有可能多帧处理。  
  8.     return flag == 0 ? state_SPEECH : state_SILENCE ;  
  9. }  
现在就剩下最关键的实现了,就是energy_detect,基于能量的端点检测,这里采用track energy的方法。



你可能感兴趣的:(VAD)