FFmpeg開發筆記(八):ffmpeg解碼音訊並使用SDL同步音訊播放

紅胖子(AAA紅模仿)發表於2020-09-30

若該文為原創文章,未經允許不得轉載
原博主部落格地址:https://blog.csdn.net/qq21497936
原博主部落格導航:https://blog.csdn.net/qq21497936/article/details/102478062
本文章部落格地址:https://blog.csdn.net/qq21497936/article/details/108828879
各位讀者,知識無窮而人力有窮,要麼改需求,要麼找專業人士,要麼自己研究

紅胖子(紅模仿)的博文大全:開發技術集合(包含Qt實用技術、樹莓派、三維、OpenCV、OpenGL、ffmpeg、OSG、微控制器、軟硬結合等等)持續更新中…(點選傳送門)

FFmpeg和SDL開發專欄(點選傳送門)

上一篇:《FFmpeg開發筆記(七):ffmpeg解碼音訊儲存為PCM並使用軟體播放
下一篇:敬請期待


前言

  ffmpeg解碼音訊之後進行播放,本篇使用SDL播放ffmpeg解碼音訊流轉碼後的pcm。


FFmpeg解碼音訊

  FFmpeg解碼音訊的基本流程請參照:《FFmpeg開發筆記(七):ffmpeg解碼音訊儲存為PCM並使用軟體播放


SDL播放音訊

  SDL播放音訊的基本流程請參照:《SDL開發筆記(二):音訊基礎介紹、使用SDL播放音訊


ffmpeg音訊同步

  ffmpeg同步包含音訊、視訊、字幕等等,此處描述的同步是音訊的同步。

基本流程

  在這裡插入圖片描述

同步關鍵點

  不改變播放速度的前提下,音訊的播放相對容易,本文章暫時未涉及到音視訊雙軌或多軌同步。
  解碼音訊後,時間間隔還是計算一下,主要是控制解碼的間隔,避免解碼過快導致快取區溢位導致異常。
  解碼音訊進行重取樣之後可以得到指定取樣率、聲道、資料型別的固定引數,使用SDL用固定引數開啟音訊,將解碼的資料扔向快取區即可。因為解碼的時候其資料量與取樣率是對應的,播放的時候也是扔入對應的資料量,所以再不改變音訊取樣率的前提下,我們是可以偷懶不做音訊同步的。
  壓入資料快取區,可以根據播放的回撥函式之後資料快取區的大小進行同步解碼壓入音訊,但是音訊與視訊不同,音訊卡頓的話對音訊播放的效果將會大打折扣,導致音訊根本無法被順利的播放,非常影響使用者體驗,實測需要保留一倍以上的預載入的音訊緩衝區,否則等需要的是再載入就已經晚了。
  音訊更為複雜的操作涉及到倍速播放、音調改變等等,後續文章會有相應的文章說明。


ffmpeg音訊同步相關結構體詳解

AVCodecContext

  該結構體是編碼上下文資訊,對檔案流進行探測後,就能得到檔案流的具體相關資訊了,關於編解碼的相關資訊就在此檔案結構中。
  與同步視訊顯示相關的變數在此詳解一下,其他的可以自行去看ffmpeg原始碼上對於結構體AVCodecContext的定義。

struct AVCodecContext {
    AVMediaType codec_type;         // 編碼器的型別,如視訊、音訊、字幕等等
    AVCodec *codec;                // 使用的編碼器
    enum AVSampleFormat sample_fmt; // 音訊取樣點的資料格式
    int sample_rate;                // 每秒的取樣率
    int channels;                   // 音訊通道資料量
    uint64_t channel_layout;        // 通道佈局 
} AVCodecContext;

SwrContext

  重取樣的結構體,最關鍵的是幾個引數,輸入的取樣頻率、通道佈局、資料格式,輸出的取樣頻率、通道佈局、資料格式。
此結構是不透明的。這意味著如果要設定選項,必須使用API,無法直接將屬性當作結構體變數進行設定。

  • 屬性“out_ch_layout”:輸入的通道佈局,需要通過通道轉換函式轉換成通道佈局列舉,其通道數與通道佈局列舉的值是不同的,
  • 屬性“out_sample_fmt”:輸入的取樣點資料格式,解碼流的資料格式即可。
  • 屬性“out_sample_rate”:輸入的取樣頻率,解碼流的取樣頻率。
  • 屬性“in_ch_layout”:輸出的通道佈局。
  • 屬性“in_sample_fmt”:輸出的取樣點資料格式。
      在這裡插入圖片描述
  • 屬性“in_sample_rate”:輸出的取樣頻率。

Demo原始碼

void FFmpegManager::testDecodeAudioPlay()
{
    QString fileName = "E:/testFile2/1.mp3";        // 輸入解碼的檔案
    QFile file("D:/1.pcm");

    AVFormatContext *pAVFormatContext = 0;          // ffmpeg的全域性上下文,所有ffmpeg操作都需要
    AVStream *pAVStream = 0;                        // ffmpeg流資訊
    AVCodecContext *pAVCodecContext = 0;            // ffmpeg編碼上下文
    AVCodec *pAVCodec = 0;                          // ffmpeg編碼器
    AVPacket *pAVPacket = 0;                        // ffmpag單幀資料包
    AVFrame *pAVFrame = 0;                          // ffmpeg單幀快取
    SwrContext *pSwrContext = 0;                    // ffmpeg音訊轉碼

    SDL_AudioSpec sdlAudioSpec;                     // sdk音訊結構體,用於開啟音訊播放器

    int ret = 0;                                    // 函式執行結果
    int audioIndex = -1;                            // 音訊流所在的序號
    int numBytes = 0;                               // 音訊取樣點位元組數
    uint8_t * outData[8] = {0};                        // 音訊快取區(不帶P的)
    int dstNbSamples = 0;                           // 解碼目標的取樣率

    int outChannel = 0;                             // 重取樣後輸出的通道
    AVSampleFormat outFormat = AV_SAMPLE_FMT_NONE;  // 重取樣後輸出的格式
    int outSampleRate = 0;                          // 重取樣後輸出的取樣率

    pAVFormatContext = avformat_alloc_context();    // 分配
    pAVPacket = av_packet_alloc();                  // 分配
    pAVFrame = av_frame_alloc();                    // 分配

    if(!pAVFormatContext || !pAVPacket || !pAVFrame)
    {
        LOG << "Failed to alloc";
        goto END;
    }

    // 步驟一:註冊所有容器和編解碼器(也可以只註冊一類,如註冊容器、註冊編碼器等)
    av_register_all();

    // 步驟二:開啟檔案(ffmpeg成功則返回0)
    LOG << "檔案:" << fileName << ",是否存在:" << QFile::exists(fileName);
    ret = avformat_open_input(&pAVFormatContext, fileName.toUtf8().data(), 0, 0);
    if(ret)
    {
        LOG << "Failed";
        goto END;
    }

    // 步驟三:探測流媒體資訊
    ret = avformat_find_stream_info(pAVFormatContext, 0);
    if(ret < 0)
    {
        LOG << "Failed to avformat_find_stream_info(pAVCodecContext, 0)";
        goto END;
    }

    // 步驟四:提取流資訊,提取視訊資訊
    for(int index = 0; index < pAVFormatContext->nb_streams; index++)
    {
        pAVCodecContext = pAVFormatContext->streams[index]->codec;
        pAVStream = pAVFormatContext->streams[index];
        switch (pAVCodecContext->codec_type)
        {
        case AVMEDIA_TYPE_UNKNOWN:
            LOG << "流序號:" << index << "型別為:" << "AVMEDIA_TYPE_UNKNOWN";
            break;
        case AVMEDIA_TYPE_VIDEO:
            LOG << "流序號:" << index << "型別為:" << "AVMEDIA_TYPE_VIDEO";
            break;
        case AVMEDIA_TYPE_AUDIO:
            LOG << "流序號:" << index << "型別為:" << "AVMEDIA_TYPE_AUDIO";
            audioIndex = index;
            break;
        case AVMEDIA_TYPE_DATA:
            LOG << "流序號:" << index << "型別為:" << "AVMEDIA_TYPE_DATA";
            break;
        case AVMEDIA_TYPE_SUBTITLE:
            LOG << "流序號:" << index << "型別為:" << "AVMEDIA_TYPE_SUBTITLE";
            break;
        case AVMEDIA_TYPE_ATTACHMENT:
            LOG << "流序號:" << index << "型別為:" << "AVMEDIA_TYPE_ATTACHMENT";
            break;
        case AVMEDIA_TYPE_NB:
            LOG << "流序號:" << index << "型別為:" << "AVMEDIA_TYPE_NB";
            break;
        default:
            break;
        }
        // 已經找打視訊品流
        if(audioIndex != -1)
        {
            break;
        }
    }
    if(audioIndex == -1 || !pAVCodecContext)
    {
        LOG << "Failed to find video stream";
        goto END;
    }

    // 步驟五:對找到的音訊流尋解碼器
    pAVCodec = avcodec_find_decoder(pAVCodecContext->codec_id);
    if(!pAVCodec)
    {
        LOG << "Fialed to avcodec_find_decoder(pAVCodecContext->codec_id):"
            << pAVCodecContext->codec_id;
        goto END;
    }

    // 步驟六:開啟解碼器
    ret = avcodec_open2(pAVCodecContext, pAVCodec, NULL);
    if(ret)
    {
        LOG << "Failed to avcodec_open2(pAVCodecContext, pAVCodec, pAVDictionary)";
        goto END;
    }

    // 列印
    LOG << "解碼器名稱:" <<pAVCodec->name << endl
        << "通道數:" << pAVCodecContext->channels << endl
        << "通道佈局:" << av_get_default_channel_layout(pAVCodecContext->channels) << endl
        << "取樣率:" << pAVCodecContext->sample_rate << endl
        << "取樣格式:" << pAVCodecContext->sample_fmt;

    outChannel = 2;
    outSampleRate = 44100;
    outFormat = AV_SAMPLE_FMT_S16;

    // 步驟七:獲取音訊轉碼器並設定取樣引數初始化
    pSwrContext = swr_alloc_set_opts(0,                                 // 輸入為空,則會分配
                                     av_get_default_channel_layout(outChannel),
                                     outFormat,                         // 輸出的取樣頻率
                                     outSampleRate,                     // 輸出的格式
                                     av_get_default_channel_layout(pAVCodecContext->channels),
                                     pAVCodecContext->sample_fmt,       // 輸入的格式
                                     pAVCodecContext->sample_rate,      // 輸入的取樣率
                                     0,
                                     0);
    ret = swr_init(pSwrContext);
    if(ret < 0)
    {
        LOG << "Failed to swr_init(pSwrContext);";
        goto END;
    }
    // 最大快取區,1152個取樣樣本,16位元組,支援最長8個通道
    outData[0] = (uint8_t *)av_malloc(1152 * 2 * 8);

    ret = SDL_Init(SDL_INIT_AUDIO);

    // SDL步驟一:初始化音訊子系統
    ret = SDL_Init(SDL_INIT_AUDIO);
    if(ret)
    {
        LOG << "Failed";
        return;
    }
    // SDL步驟二:開啟音訊裝置
    sdlAudioSpec.freq = outSampleRate;
    sdlAudioSpec.format = AUDIO_S16LSB;
    sdlAudioSpec.channels = outChannel;
    sdlAudioSpec.silence = 0;
    sdlAudioSpec.samples = 1024;
    sdlAudioSpec.callback = callBack_fillAudioData;
    sdlAudioSpec.userdata = 0;

    ret = SDL_OpenAudio(&sdlAudioSpec, 0);
    if(ret)
    {
        LOG << "Failed";
        return;
    }

    SDL_PauseAudio(0);

    _audioBuffer = (uint8_t *)malloc(102400);
    file.open(QIODevice::WriteOnly | QIODevice::Truncate);
    // 步驟八:讀取一幀資料的資料包
    while(av_read_frame(pAVFormatContext, pAVPacket) >= 0)
    {
        if(pAVPacket->stream_index == audioIndex)
        {
            // 步驟九:將封裝包發往解碼器
            ret = avcodec_send_packet(pAVCodecContext, pAVPacket);
            if(ret)
            {
                LOG << "Failed to avcodec_send_packet(pAVCodecContext, pAVPacket) ,ret =" << ret;
                break;
            }
            // 步驟十:從解碼器迴圈拿取資料幀
            while(!avcodec_receive_frame(pAVCodecContext, pAVFrame))
            {
                // nb_samples並不是每個包都相同,遇見過第一個包為47,第二個包開始為1152的
//                LOG << pAVFrame->nb_samples;
                // 步驟十一:獲取每個取樣點的位元組大小
                numBytes = av_get_bytes_per_sample(outFormat);
                // 步驟十二:修改取樣率引數後,需要重新獲取取樣點的樣本個數
                dstNbSamples = av_rescale_rnd(pAVFrame->nb_samples,
                                              outSampleRate,
                                              pAVCodecContext->sample_rate,
                                              AV_ROUND_ZERO);
                // 步驟十三:重取樣
                swr_convert(pSwrContext,
                            outData,
                            dstNbSamples,
                            (const uint8_t **)pAVFrame->data,
                            pAVFrame->nb_samples);
                // 第一次顯示
                static bool show = true;
                if(show)
                {
                    LOG << numBytes << pAVFrame->nb_samples << "to" << dstNbSamples;
                    show = false;
                }
                // 快取區大小,小於一次回撥獲取的4097就得提前新增,否則聲音會開盾
                while(_audioLen > 4096 * 1)
//                while(_audioLen > 4096 * 0)
                {
                    SDL_Delay(1);
                }
                _mutex.lock();
                memcpy(_audioBuffer + _audioLen, outData[0], numBytes * dstNbSamples * outChannel);
                file.write((const char *)outData[0], numBytes * dstNbSamples * outChannel);
                _audioLen += numBytes * dstNbSamples * outChannel;
                _mutex.unlock();
            }
            av_free_packet(pAVPacket);
        }
    }
END:
    file.close();
    LOG << "釋放回收資源";
    SDL_CloseAudio();
    SDL_Quit();
    if(outData[0])
    {
        av_free(outData[0]);
        outData[0] = 0;
        LOG << "av_free(outData)";
    }
    if(pSwrContext)
    {
        swr_free(&pSwrContext);
        pSwrContext = 0;
    }
    if(pAVFrame)
    {
        av_frame_free(&pAVFrame);
        pAVFrame = 0;
        LOG << "av_frame_free(pAVFrame)";
    }
    if(pAVPacket)
    {
        av_free_packet(pAVPacket);
        pAVPacket = 0;
        LOG << "av_free_packet(pAVPacket)";
    }
    if(pAVCodecContext)
    {
        avcodec_close(pAVCodecContext);
        pAVCodecContext = 0;
        LOG << "avcodec_close(pAVCodecContext);";
    }
    if(pAVFormatContext)
    {
        avformat_close_input(&pAVFormatContext);
        avformat_free_context(pAVFormatContext);
        pAVFormatContext = 0;
        LOG << "avformat_free_context(pAVFormatContext)";
    }
}

void FFmpegManager::callBack_fillAudioData(void *userdata, uint8_t *stream, int len)
{
    SDL_memset(stream, 0, len);

    _mutex.lock();
    if(_audioLen == 0)
    {
        _mutex.unlock();
        return;
    }
    LOG << _audioLen << len;
    len = (len > _audioLen ? _audioLen : len);
    SDL_MixAudio(stream, _audioBuffer, len, SDL_MIX_MAXVOLUME);
    _audioLen -= len;
    memmove(_audioBuffer, _audioBuffer + len, _audioLen);

    _mutex.unlock();

    // 每次載入4096
//    LOG << _audioLen << len;
}

工程模板v1.4.0

  對應工程模板v1.4.0:增加解碼音訊轉碼使用SDL播放


上一篇:《FFmpeg開發筆記(七):ffmpeg解碼音訊儲存為PCM並使用軟體播放
下一篇:敬請期待


原博主部落格地址:https://blog.csdn.net/qq21497936
原博主部落格導航:https://blog.csdn.net/qq21497936/article/details/102478062
本文章部落格地址:https://blog.csdn.net/qq21497936/article/details/108828879

相關文章