文档章节

支持无设备补静音和热插拔的PortAudio录音的封装

龍禳
 龍禳
发布于 2017/07/11 10:05
字数 1288
阅读 21
收藏 0

参见前一篇https://my.oschina.net/jinzei/blog/1305843

上代码. 警告:以下代码未经过测试,如果放到您的软件出现问题风险自担!

头文件

#ifndef PORTAUDIORECORDER_H
#define PORTAUDIORECORDER_H

//通过PortAudio录音的类.
//这个类特别之处是维护设备热插拔和热更替,提供缺数据(比如蓝牙话筒关话筒)解决方案
//当开始后,没有录音设备,将提供最多5分钟的静音录制,如果及时接入设备,能够正常录音;
//如果不提供设备5分钟后则整体放弃.
//如果设备中途断开,则持续提供静音录制.力保时间同步性.

#include "portaudio/portaudio.h"
#include <QTimer>
#include <QElapsedTimer>

class ISinkForPA
{
public:
    enum ErrorType{
        ETStreamOpen,    //流打开,代表正常工作.
        ETError,         //发生中断,可能是设备断连,可以重插或等待
        ETNoDevice,
    };

    //常规数据处理
    virtual bool tPaHandle(const void *input,unsigned long frameCount)=0;
    //宣布放弃处理, 设计为一段时间都没有设备或不正常.
    virtual void tPaNotice(ErrorType et)=0; //发生故障,time是遗失的时间.如果要继续需要补相应时间的空白.
    //需要补足0数据
    virtual void tPaNeedPad(double time)=0; //需要补上这么多时间的空数据,否则时间长度不对.(蓝牙话筒关话筒的时候出现!)
};

//这个类没有做成单例,但是按单例来实现.
class PortAudioRecord
{
public:
    PortAudioRecord(ISinkForPA *cb);
    ~PortAudioRecord();
    void set(int sampleRate, int framePerBuffer);
    int defaultInputDevice();
    //除非初始化失败,否则都是成功(哪怕没设备)
    bool start();
    void stop();

public:
    static int PACallback(
            const void *input, void *output,
            unsigned long frameCount,
            const PaStreamCallbackTimeInfo* timeInfo,
            PaStreamCallbackFlags statusFlags,
            void *userData );
    int dataCallBack(
            const void *input,
            unsigned long frameCount,
            const PaStreamCallbackTimeInfo* timeInfo,
            PaStreamCallbackFlags statusFlags);
protected:
    bool openStream();
    void closeStream();
    void checkDataFeed();
private:
    ISinkForPA *cb_;
    PaStream *paStream_;
    QElapsedTimer etimer_;
    QTimer chktimer_;
    qint64 feedTime_; //已经喂饱数据的时间,以etimer_参照.
    double lastDacTime_;  //最后一次提供数据的portaudio时间.注意起始时间是不确定的.(小于0代表未知.)
    int deviceIdx_;
    int sampleRate_;
    int framePerBuffer_;
    volatile bool haltFlag_; //pa录制异常的标记.
};

#endif // PORTAUDIORECORDER_H

cpp文件

#include "PortAudioRecord.h"
#include <QDebug>

//说明:input是从设备读到的数据,字节长度是frameCount*sizeof(SAMPLE)
//如果要echo,需要在初始化的时候指定好设备,把input拷到output即可.
//#define NODEVICEGIVEUP (5*60*1000) //无设备放弃时间.

//定义pa的sample类型为int16,这个可以配合webrtc模块
#define PA_SAMPLE_TYPE paInt16
//对应的sample单位是short,占2字节.
typedef short SAMPLE;

class PaInitMana
{
public:
    PaInitMana():initOK(false){}
    bool set()  { if(!initOK) initOK = (paNoError == Pa_Initialize()); return initOK;  }
    bool reset(){ if(initOK) unset(); return set();  }
    void unset(){ if(initOK) Pa_Terminate();  initOK = false; }
    operator bool(){ return initOK; }
private:
    bool initOK;
} gPaInit;


PortAudioRecord::PortAudioRecord(ISinkForPA *cb)
    : cb_(NULL)
    , paStream_(NULL)
    , feedTime_(0)
    , lastDacTime_(-1.0) //小于0代表未知.
    , deviceIdx_(paNoDevice)
    , sampleRate_    (32000)
    , framePerBuffer_(6400)
    , haltFlag_(false)
{
    Q_ASSERT(cb);
    cb_ = cb;

    chktimer_.setInterval(500);
    chktimer_.setSingleShot(false);
    QObject::connect(&chktimer_,&QTimer::timeout,[=](){
        //检查数据缺失, 或者没设备导致的数据缺失.
        checkDataFeed();
    });
    chktimer_.start();
}

PortAudioRecord::~PortAudioRecord()
{
    this->stop();
}

void PortAudioRecord::set(int sampleRate, int framePerBuffer)
{
    sampleRate_ = sampleRate;
    framePerBuffer_ = framePerBuffer;
}

bool PortAudioRecord::start()
{
    qInfo()<<"Pa_start";
    etimer_.start();
    if(!gPaInit.set())return false;
    lastDacTime_ = -1.0;
    Q_ASSERT(deviceIdx_==paNoDevice);
    deviceIdx_ = defaultInputDevice();
    if(deviceIdx_ != paNoDevice)
    {
        PaError err;
        if(openStream()==false){
            paStream_ = NULL;
            deviceIdx_ = paNoDevice;
            goto start_end;
        }

        err = Pa_StartStream( paStream_ );
        qInfo()<<"Pa_StartStream"<<Pa_GetErrorText(err);
        if(err != paNoError){
            Pa_CloseStream(paStream_);
            paStream_ = NULL;
            deviceIdx_ = paNoDevice;
            goto start_end;
        }
        haltFlag_ = false;
        qInfo()<<"Pa_startStream1 succ!";
        cb_->tPaNotice(ISinkForPA::ETStreamOpen);
        return true;
    }

start_end:
    if(deviceIdx_ == paNoDevice)
    {
        cb_->tPaNotice(ISinkForPA::ETNoDevice);
        gPaInit.unset();
    }
    return true;
}
void PortAudioRecord::stop()
{
    qInfo()<<"Pa_stop";
    chktimer_.stop();

    if(paStream_){
        closeStream();
    }

    gPaInit.unset();
}


int PortAudioRecord::dataCallBack(
        const void *input,
        unsigned long frameCount,
        const PaStreamCallbackTimeInfo *timeInfo,
        PaStreamCallbackFlags statusFlags)
{
    if(statusFlags==0)
    {
        lastDacTime_ = timeInfo->currentTime;
        bool rt = cb_->tPaHandle(input,frameCount);
        feedTime_ = etimer_.elapsed();
        return rt?paContinue:paComplete;
    }
    else
    {
        qInfo()<<"PACallback statusFlags:"<<statusFlags<<"Abort!";
        haltFlag_ = true;
        return paAbort;
    }
}

bool PortAudioRecord::openStream()
{
    Q_ASSERT(gPaInit);
    if(!gPaInit) return false;
    PaError err;

    PaStreamParameters inputDev;
    inputDev.device = deviceIdx_; //Pa_GetDefaultInputDevice();
    inputDev.channelCount = 1;
    inputDev.sampleFormat = PA_SAMPLE_TYPE;
    inputDev.suggestedLatency = 1;
    inputDev.hostApiSpecificStreamInfo = NULL;

    PaStreamParameters outputDev;
    outputDev.device = Pa_GetDefaultOutputDevice(); //paNoDevice;  //Pa_GetDefaultOutputDevice();
    outputDev.channelCount = 1;
    outputDev.sampleFormat = PA_SAMPLE_TYPE;
    outputDev.suggestedLatency = 1;
    outputDev.hostApiSpecificStreamInfo = NULL;

    err = Pa_OpenStream(
                &paStream_,
                &inputDev,
                &outputDev,
                sampleRate_,
                framePerBuffer_, /* frames per buffer */
                paDitherOff,    /* paDitherOff, // flags */
                PortAudioRecord::PACallback,
                this);

    qInfo()<<"openStream"<<Pa_GetErrorText(err);
    return (paNoError==err);
}

void PortAudioRecord::closeStream()
{
    Q_ASSERT(gPaInit);
    if(!gPaInit) return ;
    if(paStream_){
        PaError err;
        err = Pa_CloseStream( paStream_ );
        qDebug()<<"Pa_CloseStream:"<<Pa_GetErrorText(err);
        paStream_ = NULL;
    }
    deviceIdx_ = paNoDevice;
}

int PortAudioRecord::PACallback(
        const void *input,
        void *output,
        unsigned long frameCount,
        const PaStreamCallbackTimeInfo *timeInfo,
        PaStreamCallbackFlags statusFlags,
        void *userData)
{
    Q_UNUSED(output);
    PortAudioRecord *media=(PortAudioRecord*)userData;
    return media->dataCallBack(input,frameCount,timeInfo,statusFlags);
}

void PortAudioRecord::checkDataFeed()
{
    if(haltFlag_)
    {
        if(paStream_){
            PaError err;
            err = Pa_CloseStream( paStream_ );
            qDebug()<<"Pa_CloseStream:"<<Pa_GetErrorText(err);
            paStream_ = NULL;
        }
        deviceIdx_ = paNoDevice;
        lastDacTime_ = -1.0;
        haltFlag_ = false;
        Q_ASSERT(gPaInit);
        gPaInit.unset();
        cb_->tPaNotice(ISinkForPA::ETError);
    }

    if(paStream_ == NULL)
    {   //枚举设备,处理热插拔.
        if(gPaInit.set())
        {
            int didx = defaultInputDevice();
            if(didx != deviceIdx_)
            {
                qInfo()<<"Pa_device changed!"<<deviceIdx_<<"->"<<didx;
                closeStream();
                deviceIdx_ = didx;

                if(openStream()==false){
                    paStream_ = NULL;
                    deviceIdx_ = paNoDevice;
                }
                else
                {
                    PaError err;
                    err = Pa_StartStream( paStream_ );
                    qInfo()<<"StartStream"<<Pa_GetErrorText(err);
                    if(err != paNoError){
                        Pa_CloseStream(paStream_);
                        paStream_ = NULL;
                        deviceIdx_ = paNoDevice;
                    }
                    else
                    {
                        cb_->tPaNotice(ISinkForPA::ETStreamOpen);
                        qInfo()<<"Pa_startStream2 succ!";
                    }
                }
            }
            if(paStream_==NULL)
            {//如果不成功,继续释放Pa
                gPaInit.unset();
            }
        }
    }

    qint64 tnow = etimer_.elapsed();
    //检查数据喂养情况,如果有较大出入,补静音.
    qint64 feedGap = tnow-feedTime_;
    if(feedGap>0 && feedGap > 1000)
    {
        if(deviceIdx_==paNoDevice)
        { //如果没有录音设备,直接补静音
//            if(lastDacTime_ >0 && tnow > NODEVICEGIVEUP)
//            {  //太久了,放弃本次录音. 只针对从来没录过音的情况.
//                cb_->tPaNotice(ISinkForPA::ETHalt);
//            }
//            else
            {
                cb_->tPaNeedPad((double)(feedGap)/1000);
                feedTime_ = tnow;
            }
        }
        else
        {
            if(lastDacTime_<=0)
            {   //如果没有来过有效数据,补静音
                cb_->tPaNeedPad((double)(feedGap)/1000);
                feedTime_ = tnow;
            }
            else
            {   //来过有效数据,补到当前时间之前.
                Q_ASSERT(paStream_);
                double unit = (double)framePerBuffer_/sampleRate_;
                double dnow = Pa_GetStreamTime(paStream_);
                qDebug()<<"dnow"<<dnow<<"lastDacTime_"<<lastDacTime_;
                double gap = dnow-lastDacTime_-unit;
                Q_ASSERT(gap>0);
                Q_ASSERT(gap*1000<feedGap);
                if(gap>0){
                    cb_->tPaNeedPad(gap);
                    lastDacTime_ += gap;  //dnow-unit;
                    feedTime_ += qint64(gap*1000);
                }
            }
        }
    }
}

int PortAudioRecord::defaultInputDevice()
{
    //注意,经过试验,如果不调用Pa_Terminate,Pa_GetDefaultInputDevice的结果不会变化.
    //如果没有调用Pa_Terminate,Pa_Initialize也不能刷新DefaultInputDevice.
    if(!gPaInit) return paNoDevice;
    return Pa_GetDefaultInputDevice();
}

 

讨论加q群20487942

© 著作权归作者所有

共有 人打赏支持
龍禳
粉丝 2
博文 13
码字总数 14283
作品 0
深圳
程序员
PortAudio编程入门 V19

PortAudio是什么? PortAudio是一个免费、跨平台、开源的音频I/O库。看到I/O可能就想到了文件,但是PortAudio操作的I/O不是文件,而是音频设备。它能够简化C/C++的音频程序的设计实现,能够运...

开源中国驻成都办事处
2012/09/06
0
4
借用PortAudio采集和播放音频,实现一个双路混音器(转)

转自:http://www.cnblogs.com/haibindev/archive/2011/12/07/2277366.html 混音,顾名思义,就是把多个音源混合的过程,是一个很常见的应用。这两天我也做了一个双路混音器,当然,我没有做...

元谷
2013/12/09
0
0
kaldi DNN在线解码 aishell为例

在kaldi 的工具集里有好几个程序可以用于在线识别。这些程序都位在src/onlinebin文件夹里,他们是由src/online文件夹里的文件编译而成(你现在可以用make ext 命令进行编译)。这些程序大多还需...

SpeechAi
08/04
0
0
为什么说物联网是 Linux 的未来?

导读 Linux 操作系统无疑在物联网中扮演着关键性的角色,除此之外它也很可能成为公众关注的焦点。随着 Canonical 重新致力于盈利和新兴技术,我们中的一些人不知不觉的在思考 Linux 的未来走...

linux-tao
2017/10/18
0
0
Facebook Paper使用的第三方库

第三方库名 简介 链接 ACE code editor https://github.com/ajaxorg/ace Appirater 用户评分组件 https://github.com/arashpayan/appirater Reachability 网络连通测试 https://github.com/t......

hejunbinlan
2015/08/18
0
0

没有更多内容

加载失败,请刷新页面

加载更多

awk命令扩展使用操作

awk 中使用外部shell变量 示例1 [root@centos01 t1022]# A=888[root@centos01 t1022]# echo "" | awk -v GET_A=$A '{print GET_A}'888[root@centos01 t1022]# echo "aaaaaaaaaaaaa" | aw......

野雪球
18分钟前
4
0
深入解析MySQL视图VIEW

Q:什么是视图?视图是干什么用的? A:视图(view)是一种虚拟存在的表,是一个逻辑表,本身并不包含数据。作为一个select语句保存在数据字典中的。   通过视图,可以展现基表的部分数据;...

IT--小哥
今天
5
0
虚拟机学习之二:垃圾收集器和内存分配策略

1.对象是否可回收 1.1引用计数算法 引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时候计数器值为0的对象就是不可能...

贾峰uk
今天
6
0
smart-doc功能使用介绍

smart-doc从8月份底开始开源发布到目前为止已经迭代了几个版本。在这里非常感谢那些敢于用smart-doc去做尝试并积极提出建议的社区用户。因此决定在本博客中重要说明下smart-doc的功能,包括使...

上官胡闹
昨天
12
0
JavaEE——Junit

声明:本栏目所使用的素材都是凯哥学堂VIP学员所写,学员有权匿名,对文章有最终解释权;凯哥学堂旨在促进VIP学员互相学习的基础上公开笔记。 Junit Junit又名单元测试,Junit是用来测试Jav...

凯哥学堂
昨天
9
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部