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

参见前一篇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

转载于:https://my.oschina.net/jinzei/blog/1329761

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值