树莓派3 ROS语音包开发之端点检测(短时能量)

来源:互联网 发布:全境封锁优化设置 编辑:程序博客网 时间:2024/06/14 05:31

树莓派3 ROS语音包开发之端点检测(短时能量)

  前篇博文介绍了如何获取音频信号并给出相应的解决方法。如果您还没了解,建议戳下面链接:

http://blog.csdn.net/u013494117/article/details/52269463

  接下来把问题转向另个方面,估计你已经想到了,没错,就是端点检测。先来看看概念:

端点检测

  一般而言,端点检测定义为从连续音频信号中检测出实际语音片段的起始点和终止点,从而提取出有效的语音片段,排除噪声等其他非语音信号的干扰,为后续语音处理系统提供可靠的语音信号;同时,语音端点检测去除了不必要的非语音片段,减少了后续语音处理系统的计算压力,有利于提高系统的响应速度。

端点检测常见方法

时域分析

  1. 短时能量:音信号的能量随着时间变化比较明显,一般清音部分的能量比浊音的能量小的多,所以在区分清音和浊音,有声段和无声段的应用中效果比较明显。

  2. 短时过零率:表示一帧语音中语音信号波形穿过横轴(零电平)的次数。它可以用来区分清音和浊音,这是因为语音信号中的高频段有高的过零率,低频段过零率较低

  3. 自相关函数:
  4. 基频:

频域分析

  1. LPC:
  2. 倒谱距离:
  3. 频率方差:
  4. 谱熵

倒频域分析

 …

注:由于计算量比较小,而频域分析的方法计算量较大;本文的运行平台是树莓派这种嵌入式设备,本身不具备太强大的运算能力,所以对于频域分析不做深入探讨,当然,如果想尝试,可以试一下使用树莓派的GPU单元做快速计算;接下来只采用时域分析中的短时能量、过零率来进行下面实验。

语音信号处理术语解析

  1. 加窗分帧:这很容易理解,即使用一个窗口将音频信号分成一段一段的,可以连续分段,但一般推荐使用交叠分段方法,这是为了使帧与帧之间平滑过度。

  2. 帧移:前一帧与后一帧交叠部分,一般取 0~1/2

  3. 加窗函数:对语音处理来说,短时分析的方法是有效的解决途径。就是用一个长度有限的窗序列(w(m)}截取一段语音信号来进行分析,并让这个窗滑动以便分析任一时刻附近的信号,即加窗函数。常见的窗函数有:矩形窗和汉明窗,当然窗口函数很多,有兴趣的可谷歌一下…

分帧图解

简明了解可查阅该博客:

http://www.programgo.com/article/210254535/;jsessionid=5EF689B9717AA2AFB3B8751AA6C9369D

关键说明

  语音可粗略分为清音和浊音两大类:浊音在时域上呈现出明显的周期性,在频域上出现共振峰,而且能量大部分集中在较低频段内。清音段相对于很大一类噪声没有明显的时域和频域特征,类似于白噪声。
  
  一般情况下,语音信号是一种典型的非平稳信号,但是我们认为人说话是突然的,即:语音信号可假定为短时平稳的(10-30ms);因此在对语音信号进行分析时,需要将语音信号以30ms为一段分为若干帧来进行分析,则两帧起始点之间的间隔10ms

  看下图可以很容易看出短时能量和过零率具有互补性,这是我们接下来编程实现的重点。
  短时能量图

  短时过零率图

以上的相关概念已经解析七七八八了,如果还有什么不懂或疑问,谷歌和百度是不错的选择。貌似我们丢了最核心的东西,对的,说了概念,但没说实现啊!要实现先得看看数学解释才行。还是那句话,多点套路,少走弯路!  

处理过程

  1. 预处理:通常包括分帧和预滤波等。
    1.1 分帧是指将语音信号分段(称为语音帧,各帧通常是有交叠的)
    1.2 预滤波一般是指采用高通滤波器滤除低频噪声
  2. 能量检测
  3. 端点判决:是指采用一种判决准则(如门限判决或模式分类等)来区分语音帧与非
    语音帧;
  4. 后处理:是指对上述判决结果进行间断检测,得到最终的语音端点判决结果。

代码实现

1. 预处理之分帧
  以上相关概念指出,语音在10-30ms具有短时平稳特性,折中取20ms,那么我们可以依据音频采集的硬件设置来推算出20ms需要取多少帧。

新建 signal_process.cpp

/*音频信息*/typedef struct {    int     sample_rate;    char    channle;    char    sample_length;}AUDIO_INFO;//初始化音频信息 返回结构体指针AUDIO_INFO* audioInfoInit(int rate,char channle,char sample_l){    AUDIO_INFO* info = (AUDIO_INFO*)malloc(sizeof(AUDIO_INFO));    info->sample_rate = rate;    info->channle = channle;    info->sample_length = sample_l;    return info;}//释放结构指针void audioInfoFree(AUDIO_INFO* info){    free(info);}/*作者:hntea_hong函数功能:获取最佳分帧数参数说明:    ms: 短时取值(10~30ms)    audio_info: 音频信息    buff_size:缓冲区字节字节数返回值:预分配缓存大小*/int getPerFrames(char ms,AUDIO_INFO* audio_info,int buf_size){    float  bufsize = 0;    AUDIO_INFO* info ;    info = audio_info;    bufsize = (float)ms/1000 *\    info->sample_rate * info->channle * info->sample_length;    switch(buf_size)    {        case 1:             bufsize = bufsize / 8;            break;        case 2:             bufsize = bufsize / 16;            break;        case 4:             bufsize = bufsize / 32;            break;        default:            bufsize = bufsize / 8;              break;    }    return bufsize;}/* 主函数调用测试*/void getPerFramesTest(){    int bufsize=0;    AUDIO_INFO* info =  audioInfoInit(8000,1,16);    bufsize  = getPerFrames(20,info,sizeof(short));    printf("You should use %d of buffer to install frames!\n",bufsize);    audioInfoFree(info);}

获取到帧缓存大小之后,接着讨论一下在语音信号预处理即加窗后的语音信号定义如下:

sw(n)=s(n)w(n)

其中 
sw(n)w(n)  n:

比较常用的窗口函数分别为:

矩形窗:

w(n)={1,0,0nN1else

汉明窗:

w(n)={0.540.46cos(23.14n/(N1)), 0,0nN1else

  
短时能量:

En=m=m=+[x(m)w(nm)]2=m=n(N1)n[x(m)w(nm)]2

其中:
N:   x(m):   w(m):

根据上方定义,为编程方便,暂时使用矩形窗口来实现分帧,且帧移为零。后续如果没有达到满意的实验效果再来添加处理方法不迟。为计算方便,我们使用对数形式减少数值的大小,统一取对数如下所示:
El=logEn=logi=0nx2i

经过上面的推导过程,我们使用 El 表达算式来计算短时能量值,这里给出代码片段

/*函数功能:    使用矩形窗口函数进行信号预处理参数说明:        audioFramePtr: 音频帧        win_len: 滑动窗口长度返回值:        每段帧的能量值*/float energyPerSampleUseRectangle(short* audio_frame_ptr,int win_len){    float energy = 0.2f; //保留小数点后两位      short sample;      for (unsigned long i = 0; i<win_len; i++)      {          sample = *(audio_frame_ptr + i);          energy += sample * sample;      }      energy = (float)log(energy);      return energy;  }

现在我们就可以来测试一下效果了,不过在这之前,我们还应该编写一个测试函数来打印能量值,这样能有一个直观的认识,上代码:

/***************************************************函数功能:打印音频文件每一段的帧能量参数说明:file_wav:wav格式的音频文件名(绝对路径)返回值:无***************************************************/void energyPrintf(const char* file_wav){    size_t size = 0;    float energy = 0;    AUDIO_INFO* info = NULL;     int  segment = 0;    int frames = 0;    int i=0;    info = audioInfoInit(8000,1,16);    frames = getPerFrames(20,info,sizeof(short));    short audioFrame[frames];    FILE *stream = fopen(file_wav, "r+");    if(stream == NULL)    {        printf("open file err!\n");        exit(1);    }    //跳过wav文件头    size = fread(audioFrame, sizeof(short),44,stream);    bzero(audioFrame,44);    printf("energy is:\n");    while(size = fread(audioFrame, sizeof(short),frames,stream))    {                energy = energyPerSampleUseRectangle(audioFrame,frames);         printf("%0.2f    ",energy);        i++;        if(i==8){            i=0;            printf("\n");         }         segment++;    }    printf("\nTotil segment is %d\n",segment);    fclose(stream);    audioInfoFree(info);}

  这里给出博主自己录制的噪声文件波形图…

录制的噪声wav音频文件

  如何录制音频在上个博客已经说明,不过没指出如实生成wav文件格式,其实很简单,只要加入一个音频头,即wav的格式文件头即可,具体做法如下,这里还是给出代码,对于音频文件头不太了解的,可自行百度或者谷歌详细了解,这里不做过多说明。

//wav 文件头数据结构#define ID_RIFF     0x46464952#define ID_WAVE     0x45564157#define ID_FMT      0x20746d66#define ID_DATA     0x61746164#define FORMAT_PCM  1struct wav_header {    /* RIFF WAVE Chunk */    uint32_t riff_id;       /*固定字符串 RIFF*/    uint32_t riff_sz;       /**/    uint32_t riff_fmt;    /* Format Chunk */    uint32_t fmt_id;    uint32_t fmt_sz;    uint16_t audio_format;    uint16_t num_channels;    uint32_t sample_rate;    uint32_t byte_rate;     /* sample_rate * num_channels * bps / 8 */    uint16_t block_align;   /* num_channels * bps / 8 */    uint16_t bits_per_sample;    /* Data Chunk */    uint32_t data_id;    uint32_t data_sz;};typedef struct wav_header WAV_HEADER_T;

  有了文件头,只要在录制的音频文件的头部插入相关数据即可

int add_wav_head(int fd, WAV_HEADER_T* hdr,u32 totle_size){    /* 回到文件头,重新更新音频文件大小 */    lseek(fd, 0, SEEK_SET);         /*填充文件头*/    //RIFF WAVE Chunk    hdr->riff_id = ID_RIFF;                 //固定格式    hdr->riff_sz = totle_size + 36;         //Filelength=totle_size + 44 - 8        hdr->riff_fmt = ID_WAVE;    //Format Chunk    hdr->fmt_id = ID_FMT;    hdr->fmt_sz = 16;                       //一般为16,如果18要附加信息    hdr->audio_format = FORMAT_PCM;         //编码方式:一般为1    hdr->num_channels = 1;                  //声道数    hdr->sample_rate = 8000;//16000;        //采样频率      hdr->bits_per_sample = 16;              //样本长度    hdr->byte_rate = hdr->sample_rate *\ hdr->num_channels * hdr->bits_per_sample / 8 ;         //每秒所需的字节数  采样频率×通道数×样本长度    hdr->block_align = hdr->num_channels * hdr->bits_per_sample / 8;                            //数据块对齐单位(每个采样需要的字节数)    //Data Chunk    hdr->data_id = ID_DATA;    hdr->data_sz = totle_size;          if (write(fd, hdr, sizeof(WAV_HEADER_T)) != sizeof(WAV_HEADER_T)) {        fprintf(stderr, "arec: cannot write header\n");        return -1;    }    fprintf(stderr,"arec: %d ch, %ld hz, %d bit, %s\n",            hdr->num_channels, hdr->sample_rate, hdr->bits_per_sample,            hdr->audio_format == FORMAT_PCM ? "PCM" : "unknown");        return fd;}

  好了,至此我们自己能生成wav文件啦,赶紧测试一下,然后用一下命令来播放你的音频文件啦!或者自己再写一个播发程序,只要把数据不断扔给声卡就完事了。

play xxx.wav

  我们刚刚测试了能量值,看看是不是波动大多在16-18之间?你嗅到关键的点了没?这里说明一下,不同环境下可能不同,博主在办公室,周围比较安静,或许你就不是这么一回事了,注意自己调整。现在我们知道背景噪声的大概范围,接下来就要进入最关键的一点了,那就是端点检测参数的调整了。

能量显示

  到底如何编写程序来实现端点检测,想必看到这里已经有头绪了,没错,咱应该设置阈值,这是最容易想到的,也是最容易实现的。不过在这之前我们还漏了一块,即短时过零率。这到底如何实现?咱们下篇博客再来介绍。休息…

1 0