树莓派3 ROS语音包开发之类封装(上)

来源:互联网 发布:大作家写作软件 编辑:程序博客网 时间:2024/05/29 11:50

 近来奔波考驾照,着实耗费了好多时间。过程艰辛,但还是顺利闯关啊!想想最近开发的这个语音包,也是少不了苦头,希望还是能有同样的结果吧!俗话说嘛:好事多磨。老司机也不是天生的对吧!接下来进入正题:
 
 在前面的几篇博客中介绍了最简单的语音信号采集,端点检测的一些办法并开发简单代码。如果想回去看看,直接点击:音频采集 端点检测
 
 这里需要着重记录一下“坑”,请看下面代码片段:

snd_pcm_readi(snd_pcm_t *pcm,   //回话语句斌,类似fd         void *buffer,           //帧缓存地址        snd_pcm_uframes_t size //缓存个数        );

 看到上面的代码中的注意了吗?这个ALSA API的读取声卡数据的函数,之前认识上有误区,现在纠正一下,size 指的是每次往声卡读回来的数据帧个数。早早意识到这点,那么就不会在音频快进,采样频率改变中来回调试bug了。博主最近清醒了,不要在迷糊的时候编写代码。
 
 记录了小bug,由于之前都是用C语言编写的,代码显得乱,博主是有强迫症的人,琢磨着用C++对函数封装吧,就装了个ROS IDE,开发起来顺心多了。话说回来,如何用面向对象的思想对这些函数进行归类和封装呢?博主C++功底不太靠谱。不过还是能够整出满意的对象。
 
 如何处理划分? 按博主的思维来想,类比做工程生产线感觉靠谱。至于为何要这么麻烦,博主觉得模块化编程很有魅力,说不定以后自己也能用到是不?且看这流程图:
 生产线
 
 如果您有更好的想法,希望您可以分享一下大家学习学习。先从音频设备开始封装吧。首先规划好类成员和行为;博主是这样考虑的, 看看代码如何定义:

音频设备类设计

/* * 音频设备 * */class AudioDevice{public:    AudioDevice();    ~AudioDevice();    //配置参数    void deviceConfig(const char* name,    unsigned int rate,char channle,    char sample,enum _MODE mode,    int  buf_size    );    //生效配置    void deviceInit(AudioDevice& device);    //参数获取    void setDeviceBufferSize(AudioDevice& device,int size);    short* getDeviceBufferPtr(AudioDevice& device);    int getDeviceBufferSize(AudioDevice& device);    char getDeviceMode(AudioDevice& device);    _snd_pcm* getDeviceHandler();    char getDeviceChannle();    char getDeviceSample();    unsigned int getDeviceRate();    //主要行为 录音    void readDataFormPCM(AudioDevice& recorder);    short* readDataFormPCM();    short* readDataFormPCM(int size);    //主要行为 播音    void writeDataToPCM(AudioDevice& player);protected:private:    const char* _name;    enum _MODE _mode;               //录音或播放    _snd_pcm* _handle;              //回话语句柄    unsigned int  _rate;            //采样频率    char _channle;                  //通道数    char _sample;                   //样本精度    //缓存区    int _size;          //每个缓存共字节数,即一个short 为两个字节    short* _buffer;};
  • 音频设备的行为:往声卡读数据和写数据、设备配置
  • 属性:通道数、样本长度、频率等

好了,现在已经规划好音频类行为和属性,接下去就是怎么实现的,具体实现给出主要的行为函数:

/** 读取声卡数据*/short* AudioDevice::readDataFormPCM(int size){    int err = 0;    if ((err = snd_pcm_readi (_handle,_buffer,size))            != size)    {        fprintf (stderr, "read from audio interface failed (%s)\n",        snd_strerror (err));        exit (1);    }    return this->_buffer;}
/** 填充声卡数据*/void AudioDevice::writeDataToPCM(AudioDevice& player){    int err = 0;    if ((err = snd_pcm_writei (player._handle,    player._buffer,player._size))!= player._size)    {    fprintf (stderr, "write to audio interface failed (%s)\n",        snd_strerror (err));        exit (1);    }}

至于设备配置,直接将之前写的代码拷贝过来修改修改就好了!

信号检测器:

  之前探讨了简单的音频检测,现在按一开始的想法,可以很快设计出想要的类模型了,当然,这个类感觉功能太弱了点,不过没关系,能用就好。
  

class AudioDetector {public:    AudioDetector();    AudioDetector(const char* file);    AudioDetector(float ms);    virtual ~AudioDetector();    /*主要行为函数*/    void setWindowLengthAuto(AudioDevice& recorder);    void setStableTime(float ms);    void setBackgroundSampleFile(const char* path);    void setBackgroundNoise(float noise);    void setBackgroundLevel(float);    void setUpThreshold(float);    void setDownThreshold(float);    void setAdjustment(float);    void setForgetFactor(float);    int getWindowLength();    float getPreFramesEngery();    float getBackgroudNoise();    float getBackgroudNoiseLevelBySampleFile();    float getBackgroudNoiseLevelBySampleFile_C_Stream();    float getBackgroundLevel();    bool getStartPoint();    bool getEndPoint();    /*端点检测*/    void endpointDetection(AudioDevice &recorder);    /*主要行为函数 端点检测后写入文件*/    void endpointDetectionAndSaveData(    AudioDevice &recorder,     //注意使用引用,不然会导致    AudioFileProducer& productor //段错误、double free等问题    );protected:    /*函数功能:获取每个窗口帧能量*/    float getEnergyPerWindow();    /*函数功能:短时过零率符号函数*/    int sgn(short* sample);    /*计算短时过零率(每个窗口的短时过零点率)*/    float shortTimeCrossZeroRate();private:    float _bace_energy;         //起始能量 ;在样本中初始化    float _bace_level;          //起始能量水平线 在样本中初始化    const char* _background_sample_file;//背景噪声样本    const char* _tmp_file;          //暂时保存数据路径    float _stable_time;             //平稳时间    int _window_length;         //窗口长度,即帧缓存大小    short*  _window_buffer;     //窗口缓存    int _size;                  //每个缓存的字节数    float _frames_energy;           //窗口帧能量(短时能量)    float _frames_zero_cross_rate;  //窗口帧过零率(短时过零率)    float _background_noise;        //背景噪声    float _energy_level;            //能量水平线    float _up_threshold;            //上门限    float _down_threshold;          //下门限    float _adjustment;              //自适应调节因子    float _forget_factor;           //遗忘因子    bool _is_startpoint;            //前端点标志    bool _is_endpoint;              //后端点标志};

同样,这里贴出主要的行为函数实现过程:

/* 函数功能:*  根据录音器配置与稳定时间自动设置最佳窗口长度并分配窗口缓存空间*/void AudioDetector::setWindowLengthAuto(AudioDevice& recorder){    float  bufsize = (float)_stable_time/1000*            recorder.getDeviceRate()*            recorder.getDeviceChannle()*            recorder.getDeviceSample();    //取出每个缓存的字节数,与录音设备一样    this->_size = recorder.getDeviceBufferSize(recorder);    switch(this->_size)        {            case 1:                bufsize = bufsize / 8;                break;            case 2:                bufsize = bufsize / 16;                break;            case 4:                bufsize = bufsize / 32;                break;            default:                bufsize = bufsize / 8;                break;        }        this->_window_length  = bufsize;        this->_window_buffer = new short[_window_length];}
/* * 函数功能:在端点检测的同时将数据保存到文件对象中 * */static bool flag = false;void AudioDetector::endpointDetectionAndSaveData(             AudioDevice &recorder,            AudioFileProducer& productor            ){    float current_energy = 0.0f;    for(unsigned int i=0; i<this->_window_length;i++)    {        *(this->_window_buffer+i) =*        recorder.readDataFormPCM(1);    }    //检测到起始端点后开始录音    if (flag)          productor.copyDataToFile(_window_buffer,          _window_length*_size          );    //窗口能量统计    current_energy = this->getEnergyPerWindow();    //调整能量水平 初始能量水平来至样本文件    _energy_level = ((_energy_level * _forget_factor) +                     current_energy) / (_forget_factor + 1);    //调整背景噪声 初始噪声来至样本文件    if (current_energy < _background_noise)        _background_noise = current_energy;    else                                        _background_noise += (current_energy -             _background_noise) * _adjustment;    //调整能量水平    if (_energy_level < _background_noise)         _energy_level = _background_noise;    //起始端点判定    if((_energy_level - _background_noise > _up_threshold))        {          _is_startpoint = true;          _is_endpoint = false;          flag = true;          cout<<"Is start point!"<<endl;        }      //后端点检测  当前能量值-初始能量值的范围内波动即为      if ((current_energy -_bace_energy <=       _down_threshold )&& _is_startpoint)       {          _is_endpoint = true;          _is_startpoint = false;          flag = false;          cout<<"Is end point! "<<endl;;        }}

文件生产器:

 搞定两个了,现在要来确定如何设计生产者这号人物了。它扮演哪种角色,有什么技能,这也要琢磨琢磨。咱想,它不就是把通过物料检测部门(检测器)中通过检测的数据拿过来,顺便在物料上(音频数据)上加头加尾等简单的修修补补的工作嘛?想清楚了这就好办了。来看看生产线上的工作人员如何包装的。
 

class AudioFileProducer {public:    AudioFileProducer();    AudioFileProducer(const char* path);    virtual ~AudioFileProducer();    /*将物料装盒子*/    void audioFileInit();    void setFileMode(std::ios_base::openmode  mode);    /*文件播放测试*/    void audioPlaying_C_Stream(AudioDevice& player);    void audioPlaying_C_File(AudioDevice& player);    int audioRecording(AudioDevice& recorder);    uint32_t getNoHeadSize();    uint32_t  getTotalSize();    /*主要工作*/    void addWavHeadToFile(AudioDevice& recorder);    void addWavHeadToFile_C_File(AudioDevice& recorder);    /*拷贝数据到指定文件*/    void copyDataToFile(short* src, int size);    void copyDataToFile(long* src, int size);protected:private:    struct WavHeader* _file_head;    const char *_file_name;                     //文件绝对路径    std:: ios_base::openmode  _mode;                            uint32_t _no_head_size;             //没加头文件大小    uint32_t _total_size;               //文件总大小};

 看起来简单多了吧。它很闲,生产线上就拿数据,包装数据…对于具体行为如何,这也给出主要的代码片段:
 

void AudioFileProducer:: addWavHeadToFile(AudioDevice& recorder){    fstream fs(_file_name);    /* 回到文件头,重新更新音频文件大小 */   fs.seekg(ios::beg);    /*填充文件头*/    //RIFF WAVE Chunk   _file_head->riff_id = ID_RIFF;           //固定格式   _file_head->riff_sz = _no_head_size + 36;               _file_head->riff_fmt = ID_WAVE;    //Format Chunk   _file_head->fmt_id = ID_FMT;   _file_head->fmt_sz = 16;     //一般为16,如果18要附加信息   _file_head->audio_format = FORMAT_PCM;//编码方式:一般为1   _file_head->num_channels = recorder.getDeviceChannle();         _file_head->sample_rate =recorder.getDeviceRate();          _file_head->bits_per_sample = recorder.getDeviceSample();                       _file_head->byte_rate = _file_head->sample_rate * _file_head->num_channels * _file_head->bits_per_sample / 8 ;           //每秒所需的字节数  采样频率×通道数×样本长度   _file_head->block_align =    _file_head->num_channels *   _file_head->bits_per_sample / 8;                             //Data Chunk   _file_head->data_id = ID_DATA;   _file_head->data_sz = _no_head_size;   _total_size = _no_head_size+44;   fs.write((char*)_file_head,sizeof(struct WavHeader));   fs.close();}

主要的还是在数据如何获取吧,不过挺简单的..

void AudioFileProducer::copyDataToFile(short* src,int size){    //创建流对象,以追加模式打开流文件    this->setFileMode(ios::in|ios::out                        |ios::binary|ios::app);    fstream fs(_file_name,_mode);    if(!fs)        cout<<"open error"<<endl;    //拷贝数据    fs.write((char*)src,size);    //更新文件大小    _no_head_size+=size;    //关闭流    fs.close();}

 如果不对生产的文件进行检查,那么到这里也就完工了。但是质量每保证,老板可不干。每个产品出厂前定要过产检这关吧。但如何对文件进行检测呢?因为一直开着录音机,如果突然间有比较强烈的噪声出现,足以让机器误以为是人的语音出现,所以得想个方法来处理突然间的噪声干扰。能否在时间上?总能量?或者有更好的方法来验证?这得着下篇博客来探讨一下…

1 0
原创粉丝点击