• 11.QT-ffmpeg+QAudioOutput实现音频播放器


    1.前言
         由于QAudioOutput支持的输入数据必须是原始数据,所以播放mp3,WAV,AAC等格式文件,需要解封装后才能支持播放.
         而在QT中,提供了QMediaPlayer类可以支持解封装,但是该类的解码协议都是基于平台的,如果平台自身无法播放,那么QMediaPlayer也无法播放.有兴趣的朋友可以去试试.
         所以接下来,我们使用ffmpeg+QAudioOutput来实现一个简单的音频播放器.
     
    在此之前,需要学习:
     
    2.界面展示
    因为业余爱好,只是简单实现了大部分功能,支持播放、暂停、恢复、换歌、播放进度调节,如下图所示:
     
    3.效果展示
     
    下载链接(已经把ffmpeg移植好了,可以直接编译):https://download.csdn.net/download/qq_37997682/12834268
     
    4.代码流程
    首先创建一个playthread线程类,然后在线程中,不断解数据,重采样,并输入到QAudioOutput的缓冲区进行播放.以及处理界面发来的命令
    然后创建一个Widget界面类,通过用户操作,向playthread线程类发送控制命令.然后在playthread线程类中处理命令,命令有以下这些:
     
    4.1 playthread线程类
    在playthread线程类中,最核心的函数是runPlay(),该函数就是在不断的不断解数据,重采样,并输入到QAudioOutput的缓冲区进行播放.
    playtherad.cpp如下所示:
    #include "playthread.h"
    
    playthread::playthread()
    {
        audio=NULL;
        type = control_none;
    }
    
    bool playthread::initAudio(int SampleRate)
    {
        QAudioFormat format;
    
        if(audio!=NULL)
            return true;
    
        format.setSampleRate(SampleRate);     //设置采样率
        format.setChannelCount(2);        //设置通道数
        format.setSampleSize(16);        //样本数据16位
        format.setCodec("audio/pcm");        //播出格式为pcm格式
        format.setByteOrder(QAudioFormat::LittleEndian);  //默认小端模式
        format.setSampleType(QAudioFormat::UnSignedInt);    //无符号整形数
    
        QAudioDeviceInfo info(QAudioDeviceInfo::defaultOutputDevice());    //选择默认输出设备
    
    //    foreach(int count,info.supportedChannelCounts())
    //    {
    //        qDebug()<<"输出设备支持的通道数:"<<count;
    //    }
    
    //    foreach(int count,info.supportedSampleRates())
    //    {
    //        qDebug()<<"输出设备支持的采样率:"<<count;
    //    }
    
    //    foreach(int count,info.supportedSampleSizes())
    //    {
    //        qDebug()<<"输出设备支持的样本数据位数:"<<count;
    //    }
    
        if (!info.isFormatSupported(format))
        {
            qDebug()<<"输出设备不支持该格式,不能播放音频";
            return false;
        }
    
        audio = new QAudioOutput(format, this);
    
        audio->setBufferSize(100000);
    
        return true;
    }
    
    void playthread::play(QString filePath)
    {
        this->filePath = filePath;
        type = control_play;
    
        if(!this->isRunning())
        {
             this->start();
        }
    }
    
    void playthread::stop()
    {
    
        if(this->isRunning())
        {
            type = control_stop;
        }
    
    }
    void playthread::pause()
    {
    
        if(this->isRunning())
        {
            type = control_pause;
        }
    
    }
    
    void playthread::resume()
    {
        if(this->isRunning())
        {
            type = control_resume;
        }
    }
    
    
    void playthread::seek(int value)
    {
    
        if(this->isRunning())
        {
            seekMs = value;
            type = control_seek;
        }
    }
    
    void playthread::debugErr(QString prefix, int err)  //根据错误编号获取错误信息并打印
    {
        char errbuf[512]={0};
    
        av_strerror(err,errbuf,sizeof(errbuf));
    
        qDebug()<<prefix<<":"<<errbuf;
    
        emit ERROR(prefix+":"+errbuf);
    }
    
    
    
    bool playthread::runIsBreak()      //处理控制,判断是否需要停止
    {
    
        bool ret = false;
        //处理播放暂停
        if(type == control_pause)
        {
            while(type == control_pause)
            {
                 audio->suspend();
                 msleep(500);
            }
    
            if(type == control_resume)
            {
                 audio->resume();
            }
        }
    
        if(type == control_play)    //重新播放
        {
            ret = true;
            if(audio->state()== QAudio::ActiveState)
                audio->stop();
        }
    
        if(type == control_stop)    //停止
        {
             ret = true;
             if(audio->state()== QAudio::ActiveState)
                 audio->stop();
        } 
        return ret;
    }
    
    void playthread::runPlay()
    {
        int ret;
    
        int destMs,currentMs;
    
        if(audio==NULL)
        {
            emit ERROR("输出设备不支持该格式,不能播放音频");
            return ;
        }
        //初始化网络库 (可以打开rtsp rtmp http 协议的流媒体视频)
        avformat_network_init();
        AVFormatContext *pFmtCtx=NULL;
        ret = avformat_open_input(&pFmtCtx, this->filePath.toLocal8Bit().data(),NULL, NULL) ;  //打开音视频文件并创建AVFormatContext结构体以及初始化.
        if (ret!= 0)
        {
            debugErr("avformat_open_input",ret);
            return ;
        }
        ret = avformat_find_stream_info(pFmtCtx, NULL);   //初始化流信息
        if (ret!= 0)
        {
            debugErr("avformat_find_stream_info",ret);
            return ;
        }
    
        int audioindex=-1;
    
        audioindex = av_find_best_stream(pFmtCtx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
    
        qDebug()<<"audioindex:"<<audioindex;
    
        AVCodec *acodec = avcodec_find_decoder(pFmtCtx->streams[audioindex]->codecpar->codec_id);//获取codec
    
        AVCodecContext *acodecCtx = avcodec_alloc_context3(acodec); //构造AVCodecContext ,并将vcodec填入AVCodecContext中
        avcodec_parameters_to_context(acodecCtx, pFmtCtx->streams[audioindex]->codecpar); //初始化AVCodecContext
    
        ret = avcodec_open2(acodecCtx, NULL,NULL);  //打开解码器,由于之前调用avcodec_alloc_context3(vcodec)初始化了vc,那么codec(第2个参数)可以填NULL
        if (ret!= 0)
        {
            debugErr("avcodec_open2",ret);
            return ;
        }
        SwrContext *swrctx =NULL;
        swrctx=swr_alloc_set_opts(swrctx, av_get_default_channel_layout(2),AV_SAMPLE_FMT_S16,44100,
                                    acodecCtx->channel_layout, acodecCtx->sample_fmt,acodecCtx->sample_rate, NULL,NULL);
        swr_init(swrctx);
    
        destMs = av_q2d(pFmtCtx->streams[audioindex]->time_base)*1000*pFmtCtx->streams[audioindex]->duration;
        qDebug()<<"码率:"<<acodecCtx->bit_rate;
        qDebug()<<"格式:"<<acodecCtx->sample_fmt;
        qDebug()<<"通道:"<<acodecCtx->channels;
        qDebug()<<"采样率:"<<acodecCtx->sample_rate;
        qDebug()<<"时长:"<<destMs;
        qDebug()<<"解码器:"<<acodec->name;
    
        AVPacket * packet =av_packet_alloc();
        AVFrame *frame =av_frame_alloc();
    
        audio->stop();
        QIODevice*io = audio->start();
    
        while(1)
        {
    
    
            if(runIsBreak())
                break;
    
            if(type == control_seek)
            {
                av_seek_frame(pFmtCtx, audioindex, seekMs/(double)1000/av_q2d(pFmtCtx->streams[audioindex]->time_base),AVSEEK_FLAG_BACKWARD);
                type = control_none;
                emit seekOk();
            }
    
            ret = av_read_frame(pFmtCtx, packet);
            if (ret!= 0)
            {
                debugErr("av_read_frame",ret);
                emit duration(destMs,destMs);
                break ;
            }
    
            //解码一帧数据
            ret = avcodec_send_packet(acodecCtx, packet);
            av_packet_unref(packet);
    
            if (ret != 0)
            {
                debugErr("avcodec_send_packet",ret);
                continue ;
            }
    
            if(packet->stream_index==audioindex)
            {
                while( avcodec_receive_frame(acodecCtx, frame) == 0)
                {
    
                    if(runIsBreak())
                        break;
                    uint8_t *data[2] = { 0 };
                    int byteCnt=frame->nb_samples * 2 * 2;
    
                    unsigned char *pcm = new uint8_t[byteCnt];     //frame->nb_samples*2*2表示分配样本数据量*两通道*每通道2字节大小
    
                    data[0] = pcm;  //输出格式为AV_SAMPLE_FMT_S16(packet类型),所以转换后的LR两通道都存在data[0]中
    
                    ret = swr_convert(swrctx,
                                      data, frame->nb_samples,        //输出
                                     (const uint8_t**)frame->data,frame->nb_samples );    //输入
    
    
                    //将重采样后的data数据发送到输出设备,进行播放
                    while (audio->bytesFree() < byteCnt)
                    {
                        if(runIsBreak())
                            break;
                        msleep(10);
                    }
    
                    if(!runIsBreak())
                     io->write((const char *)pcm,byteCnt);
    
                    currentMs = av_q2d(pFmtCtx->streams[audioindex]->time_base)*1000*frame->pts;
                    //qDebug()<<"时长:"<<destMs<<currentMs;
                    emit duration(currentMs,destMs);        
    
                    delete[] pcm;
                }
            }
    
    
        }
    
    
        //释放内存
        av_frame_free(&frame);
        av_packet_free(&packet);
        swr_free(&swrctx);
        avcodec_free_context(&acodecCtx);
        avformat_close_input(&pFmtCtx);
     
    }
    
    void playthread::run()
    {
    
        if(!initAudio(44100))
        {
            emit ERROR("输出设备不支持该格式,不能播放音频");
        }
    
        while(1)
        {
    
            switch(type)
            {
                case control_none: msleep(100);    break;
                case control_play : type=control_none;runPlay();  break;    //播放
                default: type=control_none;   break;
            }
        }
    
    }


    4.2 widget界面类

    而在界面中要处理的就很简单,widget.cpp如下所示:

    #include "widget.h"
    #include "ui_widget.h"
    #include <QDebug>
    
    
    Widget::Widget(QWidget *parent)
        : QWidget(parent)
        , ui(new Ui::Widget)
    {
        ui->setupUi(this);
    
        this->setAcceptDrops(true);
    
        thread = new playthread();
    
        connect(thread,SIGNAL(duration(int,int)),this,SLOT(onDuration(int,int)));
    
        connect(thread,SIGNAL(seekOk()),this,SLOT(onSeekOk()));
    
    
        thread->start();
    
        sliderSeeking =false;
    }
    
    
    
    Widget::~Widget()
    {
        delete ui;
    
    
        thread->stop();
    }
    
    
    void Widget::onSeekOk()
    {
        sliderSeeking=false;
    }
    
    void Widget::onDuration(int currentMs,int destMs)      //时长
    {
        static int currentMs1=-1,destMs1=-1;
    
        if(currentMs1==currentMs&&destMs1==destMs)
        {
            return;
        }
    
        currentMs1 = currentMs;
        destMs1   =  destMs;
    
        qDebug()<<"onDuration:"<<currentMs<<destMs<<sliderSeeking;
    
        QString currentTime = QString("%1:%2:%3").arg(currentMs1/360000%60,2,10,QChar('0')).arg(currentMs1/6000%60,2,10,QChar('0')).arg(currentMs1/1000%60,2,10,QChar('0'));
    
        QString destTime = QString("%1:%2:%3").arg(destMs1/360000%60,2,10,QChar('0')).arg(destMs1/6000%60,2,10,QChar('0')).arg(destMs1/1000%60,2,10,QChar('0'));
    
    
        ui->label_duration->setText(currentTime+"/"+destTime);
    
    
    
        if(!sliderSeeking) //未滑动
        {
            ui->slider->setMaximum(destMs);
            ui->slider->setValue(currentMs);
        }
    
    }
    
    void Widget::dragEnterEvent(QDragEnterEvent *event)
    {
          if(event->mimeData()->hasUrls())      //判断拖的类型
          {
                event->acceptProposedAction();
          }
          else
          {
                event->ignore();
          }
    }
    
    void Widget::dropEvent(QDropEvent *event)
    {
        if(event->mimeData()->hasUrls())        //判断放的类型
        {
    
            QList<QUrl> List = event->mimeData()->urls();
    
            if(List.length()!=0)
            {
              ui->line_audioPath->setText(List[0].toLocalFile());
            }
    
        }
        else
        {
              event->ignore();
        }
    }
    
    
    void Widget::on_btn_start_clicked()
    {
    
        sliderSeeking=false;
    
        thread->play(ui->line_audioPath->text());
    
    }
    
    
    void Widget::on_btn_stop_clicked()
    {
        thread->stop();
    }
    
    void Widget::on_btn_pause_clicked()
    {
        thread->pause();
    }
    
    void Widget::on_btn_resume_clicked()
    {
       thread->resume();
    }
    
    
    void Widget::on_slider_sliderPressed()
    {
        sliderSeeking=true;
    }
    
    void Widget::on_slider_sliderReleased()
    {
    
        thread->seek(ui->slider->value());
    
    }
     
     
     
     
     
     

  • 相关阅读:
    wpf 资源
    九宫格扩展,输入一个奇数,得到横竖斜相加相等结果
    安装IIS的convlog.exe问题
    Windows API
    使用快捷键弹出新对话框
    数据结构笔试(转)
    运算符重载
    宽字节与多字节转换
    C++之乱七八糟<真正的随笔>
    MFC调用ActiveX
  • 原文地址:https://www.cnblogs.com/lifexy/p/13648442.html
Copyright © 2020-2023  润新知