文档章节

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

龍禳
 龍禳
发布于 2017/07/11 10:05
字数 1288
阅读 16
收藏 0
点赞 0
评论 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
深圳
程序员
AVAudioSessionCategory说明

#pragma mark -- Values for the category property -- /* Use this category for background sounds such as rain, car engine noise, etc. Mixes with other music. */AVF_EXPORT NSString......

OSMaker_- ⋅ 2016/04/08 ⋅ 0

WebRTC源码架构浅析

WebRTC源码架构浅析 Google 在2010年花了6千8百万美元收购了大名鼎鼎的 Global IP Sound/Solutions (GIPS) 公司, 得到了它的 VoIP 相关技术的专利和软件. 第二年, Google就把这些软件开源了,...

ideawu ⋅ 2013/08/14 ⋅ 2

PortAudio编程入门 V19

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

开源中国驻成都办事处 ⋅ 2012/09/06 ⋅ 4

AVFoundation开发秘籍笔记-02播放和录制音频

一、音频会话 AVAudioSession 音频会话在应用程序和操作系统之间扮演着中间人的角色,提供一种简单实用的方法是OS得知应用程序应该如何与iOS音频环境进行交互。 有框架引入。每个iOS应用程序...

竹与豆 ⋅ 05/25 ⋅ 0

借用PortAudio采集和播放音频,实现一个双路混音器(转)

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

元谷 ⋅ 2013/12/09 ⋅ 0

VirtualBox虚拟机使用麦克风

如果你用的是VirtualBox,虚拟机声卡设置选AC97声卡。 要使用麦克风 1.把虚拟机的播放设备主音量中“线路输入”的静音去掉。 2.录音控制选项中,不能选择麦克风,而应该选择“线路输入”。 ...

博泉科技-谌侃 ⋅ 2012/12/03 ⋅ 0

小程序 音频API采坑完全手册

最近公司上线一个类似小打卡的一个小程序,基于WEPY开发。 其他都功能点都还好,录音跟音频播放的功能点踩各种莫名其妙的坑,社区也有不少人在提问,特写此文祭天 产品大概的UI如下图 录音功能...

MarsDes ⋅ 05/11 ⋅ 0

基于ASIO驱动的多通道音频播放控制

@欧阳左至 你好,想跟你请教个问题: 看到您写的关于PortAudio入门编程博客,想必您对这方面比较了解。 我的情况是这样:目前我们有一块基于ASIO驱动的多通道声卡,想利用这个硬件设备来播放...

lsgTHU ⋅ 2012/09/13 ⋅ 3

Cool edit pro 2.1简体中文版下载

Cool edit pro 2.1汉化破解版是一款非常经典且实用的音频制作编辑软件,它的用户群体十分广大。Cool edit pro在音乐的编辑上有着几起强大的功能,从音调、音准以及音效等各个方面的全方位编辑...

y15067805290 ⋅ 04/12 ⋅ 0

USB-0-基础概念

1.USB特性 不同设备类型,统一接口 支持热插拔 灵活的供电方式 多级速率支持 2.分类 OHCI主要为非PC系统上以及带有SiShe ALi芯片组的PC主板上的USB芯片 UHCI大多为Intel和Via主板上的USB控制...

CoderDock ⋅ 01/07 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

Confluence 6 从其他备份中恢复数据

一般来说,Confluence 数据库可以从 Administration Console 或者 Confluence Setup Wizard 中进行恢复。 如果你在恢复压缩的 XML 备份的时候遇到了问题,你还是可以对整个站点进行恢复的,如...

honeymose ⋅ 昨天 ⋅ 0

myeclipse10 快速搭建spring boot开发环境(入门)

1.创建一个maven的web项目 注意上面标红的部分记得选上 2.创建的maven目录结构,有缺失的目录可以自己建立目录补充 补充后 这时候一个maven的web项目创建完成 3.配置pom.xml配置文件 <proje...

小海bug ⋅ 昨天 ⋅ 0

nginx.conf

=========================================================================== nginx.conf =========================================================================== user nobody; #......

A__17 ⋅ 昨天 ⋅ 0

645. Set Mismatch - LeetCode

Question 645. Set Mismatch Solution 思路: 遍历每个数字,然后将其应该出现的位置上的数字变为其相反数,这样如果我们再变为其相反数之前已经成负数了,说明该数字是重复数,将其将入结果r...

yysue ⋅ 昨天 ⋅ 0

Python这么强?红包杀手、消息撤回也可以无视,手机App辅助!

论述 标题也许有点不好理解,其实就是一款利用Python实现的可以监控微信APP内的红包与消息撤回的助手。不得不说,这确实是一款大家钟意的神器。 消息撤回是一件很让人恶心的事,毕竟人都是有...

Python燕大侠 ⋅ 昨天 ⋅ 0

压缩打包介绍、gzip压缩工具、bzip2压缩工具、xz压缩工具

压缩打包介绍 压缩的好处不仅能节省磁盘空间而且在传输的时候节省传输时间和网络带宽 windows系统下文件带有 .rar .zip .7z 后缀的就是压缩文件 linux系统下则是 .zip, .gz, .bz2, .xz, ...

黄昏残影 ⋅ 昨天 ⋅ 0

观察者模式

1.利用java原生类进行操作 package observer;import java.util.Observable;import java.util.Observer;/** * @author shadow * @Date 2016年8月12日下午7:29:31 * @Fun 观察目标 **/......

Cobbage ⋅ 昨天 ⋅ 0

Ubuntu打印服务器配置

参考:https://blog.csdn.net/gsls200808/article/details/50950586 https://blog.csdn.net/jiay2/article/details/80252369 https://wiki.gentoo.org/wiki/HPLIP 由于媳妇儿要大量打印资料,......

大熊猫 ⋅ 昨天 ⋅ 0

面试的角度诠释Java工程师(二)

原文出处: locality 续言: 相信每一位简书的作者,都会有我这样的思考:怎么写好一篇文章?或者怎么写好一篇技术类的文章?我就先说说我的感悟吧,写文章其实和写程序是一样的。为什么我会...

颖伙虫 ⋅ 昨天 ⋅ 0

github中SSH的Key

https://help.github.com/articles/connecting-to-github-with-ssh/ https://help.github.com/articles/testing-your-ssh-connection/ https://help.github.com/articles/adding-a-new-ssh-k......

whoisliang ⋅ 昨天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部