FFmpeg開發筆記(七):ffmpeg解碼音訊儲存為PCM並使用軟體播放

2020-09-28 11:00:47

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

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

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

上一篇:《FFmpeg開發筆記(六):ffmpeg解碼視訊並使用SDL同步時間顯示播放
下一篇:敬請期待


前言

  本篇解碼音訊,包括從mp3等檔案中抽取音訊流的pcm,從視訊檔中抽取音訊流的pcm。
  本文章篇幅相對較長,碼字作圖不易,請各位讀者且行且珍惜。


音訊基礎知識

  音訊的幾個關鍵因素請檢視:《SDL開發筆記(二):音訊基礎介紹、使用SDL播放音訊


Demo

  匯入原始檔案,設定好資料型別、聲道、取樣率
  在這裡插入圖片描述

  在這裡插入圖片描述
  在這裡插入圖片描述

軟體下載地址

  CSDN:https://download.csdn.net/download/qq21497936/12888731
  QQ群:1047134658(點選「檔案」搜尋「audacity」,群內與博文同步更新)


FFmpeg解碼音訊

ffmpeg解碼音訊流程

  ffmpeg解碼音訊轉碼基本流程如下:
  在這裡插入圖片描述

步驟一:註冊:

  使用ffmpeg對應的庫,都需要進行註冊,可以註冊子項也可以註冊全部。

步驟二:開啟檔案:

  開啟檔案,根據檔名資訊獲取對應的ffmpeg全域性上下文。

步驟三:探測流資訊:

  一定要探測流資訊,拿到流編碼的編碼格式,不探測流資訊則其流編碼器拿到的編碼型別可能為空,後續進行資料轉換的時候就無法知曉原始格式,導致錯誤。

步驟四:查詢對應的解碼器

  依據流的格式查詢解碼器,軟解碼還是硬解碼是在此處決定的,但是特別注意是否支援硬體,需要自己查詢原生的硬體解碼器對應的標識,並查詢其是否支援。普遍操作是,列舉支援檔案字尾解碼的所有解碼器進行查詢,查詢到了就是可以硬解了(此處,不做過多的討論,對應硬解碼後續會有文章進行進一步研究)。
  (注意:解碼時查詢解碼器,編碼時查詢編碼器,兩者函數不同,不要弄錯了,否則後續能開啟但是資料是錯的)

步驟五:開啟解碼器

  開啟獲取到的解碼器。

步驟六:申請重取樣結構體

  此處特別注意,基本上解碼的資料都是pcm格式,pcm格式也分很多種,若8位元整形,無符號8為整形,32位元浮點,帶P和不帶P的,不帶P的資料真儲存為LRLRLRLR,帶P的為LLLLRRRR,還有單通道、雙連結和多通道,通道又涉及到了聲道的定位列舉,所以pcm原始資料也多種多樣,對齊進行重弄取樣使其輸出的pcm格式引數特點一致。

步驟七:重取樣初始化

  重取樣結構體設定好後,需要設定生效。

步驟八:解封裝獲取其中一個封包。

  封包是封裝在容器中的一個封包。獲取不到資料則跳轉「步驟十四

步驟九:分組封包送往解碼器

  拿取封裝的一個packet後,判斷packet資料的型別進行送往解碼器解碼。

步驟十:從解碼器快取中獲取解碼後的資料

  一個包可能存在多組資料,老的api獲取的是第一個,新的api分開後,可以迴圈獲取,直至獲取不到跳轉「步驟十三

步驟十一:樣本點重取樣

  使用衝殘陽函數結合轉換結構體對編碼的資料進行轉換,拿到重取樣後的音訊原始資料。

步驟十二:自行處理

  拿到了原始資料自行處理。
  迴圈解碼跳轉「步驟八」,若步驟八獲取不到資料則執行「步驟十四

步驟十三:釋放QAVPacket

  此處要單獨列出是因為,其實很多網上和開發者的程式碼:
  在進入迴圈解碼前進行了av_new_packet,迴圈中未av_free_packet,造成記憶體溢位;
  在進入迴圈解碼前進行了av_new_packet,迴圈中進行av_free_pakcet,那麼一次new對應無數次free,在編碼器上是不符合前後一一對應規範的。
  檢視原始碼,其實可以發現av_read_frame時,自動進行了av_new_packet(),那麼其實對於packet,只需要進行一次av_packet_alloc()即可,解碼完後av_free_packet。
  執行完後,返回執行「步驟八:獲取一幀packet」,一次迴圈結束。

步驟十四:釋放衝重取樣結構體

  全部解碼完成後,按照申請順序,反向依次進行對應資源的釋放。

步驟十五:關閉解碼/編碼器

  關閉之前開啟的解碼/編碼器。

步驟十六:關閉上下文

  關閉檔案上下文後,要對之前申請的變數按照申請的順序,依次釋放。


ffmpeg解碼音訊相關變數

  與視訊解碼通用變數請參照博文《FFmpeg開發筆記(四):ffmpeg解碼的基本流程詳解》中的「ffmpeg解碼相關變數」。

SwrContext

  重取樣的結構體,最關鍵的是幾個引數,輸入的取樣頻率、通道佈局、資料格式,輸出的取樣頻率、通道佈局、資料格式。

ffmpeg解碼音訊流程相關函數原型

  與視訊解碼通用函數原型請參照博文《FFmpeg開發筆記(四):ffmpeg解碼的基本流程詳解》中的「ffmpeg解碼相關函數原型」。

swr_alloc_set_opts

struct SwrContext *swr_alloc_set_opts(struct SwrContext *s,
                                      int64_t out_ch_layout,
                                      enum AVSampleFormat out_sample_fmt,
                                      int out_sample_rate,
                                      int64_t  in_ch_layout,
                                      enum AVSampleFormat in_sample_fmt,
                                      int  in_sample_rate,
                                      int log_offset,
                                      void *log_ctx);

  分配並設定重取樣的結構體上下文。

  • 引數一:輸入需要設定的重取樣結構體,如果為空,則會由此函數內部進行分配。
  • 引數二:輸出的通道佈局(轉換後的)
  • 引數三:輸出的樣本格式(轉換後的)
      在這裡插入圖片描述
      帶P和不帶P,關係到了AVFrame中的data的資料排列,不帶P,則是LRLRLRLRLR排列,帶P則是LLLLLRRRRR排列,若是雙連結則帶P則意味著data[0]全是L,data[1]全是R(注意:這是取樣點不是位元組),PCM播放器播放的檔案需要的是LRLRLRLR的。
  • 引數四:輸出的取樣率(轉換後的)
  • 引數五:輸入的通道佈局(轉換前的)
  • 引數六:輸入的樣本格式(轉換前的)
  • 引數七:輸入的取樣率(轉換前的)
  • 引數八:紀錄檔等級,忽略直接0
  • 引數九:紀錄檔,忽略直接0

swr_init

int swr_init(struct SwrContext *s);

  初始化取樣器,使取樣器生效。

swr_free

void swr_free(struct SwrContext **s);

  釋放給定的SwrContext並將指標設定為NULL。


ffmpeg3之後的新解碼api解碼函數原型

avcodec_send_packet:ffmpeg3新增解碼傳送封包給解碼器

int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);

  將原始分組資料傳送給解碼器。
  在內部,此呼叫將複製相關的AVCodeContext欄位,這些欄位可以影響每個封包的解碼,並在實際解碼封包時應用這些欄位。(例如AVCodeContext.skip_frame,這可能會指示解碼器丟棄使用此函數傳送的封包所包含的幀。)
  這個函數可以理解為ffmpeg為多執行緒準備的,將解碼資料框包送入編碼器理解為一個執行緒,將從編碼器獲取解碼後的資料理解為一個執行緒。

  • 引數一:編解碼器上下文
  • 引數二:avpkt輸入的AVPacket。通常,這將是一個單一的視訊幀,或幾個完整的音訊幀。封包的所有權歸呼叫者所有,解碼器不會寫入封包。解碼器可以建立對分組資料的參照(如果分組沒有被參照計數,則複製它)。與舊的API不同,封包總是被完全消耗掉,如果它包含多個幀(例如某些音訊編解碼器),則需要在傳送新封包之前多次呼叫avcodec_receive_frame()。它可以是NULL(或者資料設定為NULL且大小設定為0的AVPacket);在這種情況下,它被認為是一個重新整理包,它發出流結束的訊號。傳送第一個重新整理包將返回成功。後續的是不必要的,將返回AVERROR ou EOF。如果解碼器仍有幀緩衝,它將在傳送重新整理包後返回它們。

avcodec_receive_frame:ffmpeg3新增解碼從解碼器獲取解碼後的幀

int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);

  從解碼器返回解碼輸出資料。這個函數可以理解為ffmpeg為多執行緒準備的,將解碼資料框包送入編碼器理解為一個執行緒,將從編碼器獲取解碼後的資料理解為一個執行緒。

  • 引數一:編解碼器上下文
  • 引數二:這將被設定為參考計數的視訊或音訊解碼器分配的幀(取決於解碼器型別)。請注意,函數在執行任何其他操作之前總是呼叫av_frame_unref(frame),自己釋放frame,只有最後一幀不釋放。

Demo原始碼

解碼音訊不帶重取樣版本v1.3.0

void FFmpegManager::testDecodeAudio()
{
    QString fileName = "test/1.avi";
//    QString fileName = "test/1.mp4";
//    QString fileName = "E:/testFile2/1.mp3";
    QString outFileName = "E:/1.pcm";
    // ffmpeg相關變數預先定義與分配
    AVFormatContext *pAVFormatContext = 0;          // ffmpeg的全域性上下文,所有ffmpeg操作都需要
    AVCodecContext *pAVCodecContext = 0;            // ffmpeg編碼上下文
    AVCodec *pAVCodec = 0;                          // ffmpeg編碼器
    AVPacket *pAVPacket = 0;                        // ffmpag單幀封包
    AVFrame *pAVFrame = 0;                          // ffmpeg單幀快取
    QFile file(outFileName);                        // Qt檔案操作

    int ret = 0;                                    // 函數執行結果
    int audioIndex = -1;                            // 音訊流所在的序號
    int numBytes = 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(), pAVInputFormat, 0);
    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;
    }
    LOG << "視訊檔包含流資訊的數量:" << pAVFormatContext->nb_streams;

    // 步驟四:提取流資訊,提取視訊資訊
    for(int index = 0; index < pAVFormatContext->nb_streams; index++)
    {
        pAVCodecContext = pAVFormatContext->streams[index]->codec;
        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;
    }
#if 0
    pAVCodecContext = avcodec_alloc_context3(pAVCodec);
    // 填充CodecContext資訊
    if (avcodec_parameters_to_context(pAVCodecContext,
                                      pAVFormatContext->streams[audioIndex]->codecpar) < 0)
    {
        printf("Failed to copy codec parameters to decoder context!\n");
        goto END;
    }
#endif

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

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

    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))
            {
//                for(int index = 0; index < pAVFrame->linesize[0]; index++)
//                {
                    // 入坑一;位元組交錯錯誤,單條音軌是好的,雙軌存入檔案,使用pcm的軟體播放,則預設是LRLRLRLR的方式(取樣點交錯)
//                    file.write((const char *)(pAVFrame->data[0] + index), 1);
//                    file.write((const char *)(pAVFrame->data[1] + index), 1);
//                }
                // 入坑一;位元組交錯錯誤,單條音軌是好的,雙軌存入檔案,使用pcm的軟體播放,則預設是LRLRLRLR的方式(取樣點交錯)
//                file.write((const char *)(pAVFrame->data[0], pAVFrame->linesize[0]);
//                file.write((const char *)(pAVFrame->data[1], pAVFrame->linesize[0]);
                // 輸出為2, S16P格式是2位元組
                numBytes = av_get_bytes_per_sample(pAVCodecContext->sample_fmt);
//                LOG << "numBytes =" << numBytes;
                /*
                    P表示Planar(平面),其資料格式排列方式為 (特別記住,該處是以點nb_samples取樣點來交錯,不是以位元組交錯):
                    LLLLLLRRRRRRLLLLLLRRRRRRLLLLLLRRRRRRL...(每個LLLLLLRRRRRR為一個音訊幀)
                    而不帶P的資料格式(即交錯排列)排列方式為:
                    LRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRL...(每個LR為一個音訊樣本)
                */
                // 使用命令列提取pcm ffmpeg.exe -i 1.mp3 -f s16le -ar 44100 -ac 2 -acodec pcm_s16le D:/2.pcm
                for (int index = 0; index < pAVFrame->nb_samples; index++)
                {
                    for (int channel = 0; channel < pAVCodecContext->channels; channel++)  // 交錯的方式寫入, 大部分float的格式輸出
                    {
                        file.write((char *)pAVFrame->data[channel] + numBytes * index, numBytes);
                    }
                }
                av_free_packet(pAVPacket);
            }
        }
    }
    file.close();
END:
    LOG << "釋放回收資源";
    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)";
    }
}

解碼音訊重取樣版本v1.3.1

void FFmpegManager::testDecodeAudioForPcm()
{
//    QString fileName = "test/1.avi";
    QString fileName = "E:/testFile/3.mp4";
//    QString fileName = "E:/testFile2/1.mp3";

    QString outFileName = "D:/1.pcm";

    AVFormatContext *pAVFormatContext = 0;          // ffmpeg的全域性上下文,所有ffmpeg操作都需要
    AVCodecContext *pAVCodecContext = 0;            // ffmpeg編碼上下文
    AVCodec *pAVCodec = 0;                          // ffmpeg編碼器
    AVPacket *pAVPacket = 0;                        // ffmpag單幀封包
    AVFrame *pAVFrame = 0;                          // ffmpeg單幀快取
    SwrContext *pSwrContext = 0;                    // ffmpeg音訊轉碼
    QFile file(outFileName);                        // Qt檔案操作

    int ret = 0;                                    // 函數執行結果
    int audioIndex = -1;                            // 音訊流所在的序號
    int numBytes = 0;
    uint8_t * outData[2] = {0};
    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(), pAVInputFormat, 0);
    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;
    }
    LOG << "視訊檔包含流資訊的數量:" << pAVFormatContext->nb_streams;

    // 步驟四:提取流資訊,提取視訊資訊
    for(int index = 0; index < pAVFormatContext->nb_streams; index++)
    {
        pAVCodecContext = pAVFormatContext->streams[index]->codec;
        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;
    }
#if 0
    pAVCodecContext = avcodec_alloc_context3(pAVCodec);
    // 填充CodecContext資訊
    if (avcodec_parameters_to_context(pAVCodecContext,
                                      pAVFormatContext->streams[audioIndex]->codecpar) < 0)
    {
        printf("Failed to copy codec parameters to decoder context!\n");
        goto END;
    }
#endif

    // 步驟六:開啟解碼器
    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;
#if 1
    outChannel = 2;
    outSampleRate = 44100;
    outFormat = AV_SAMPLE_FMT_S16P;
#endif
#if 0
    outChannel = 2;
    outSampleRate = 48000;
    outFormat = AV_SAMPLE_FMT_FLTP;
#endif
    LOG << "to" << endl
        << "通道數:" << outChannel << endl
        << "通道佈局:" << av_get_default_channel_layout(outChannel) << endl
        << "取樣率:" << outSampleRate << endl
        << "取樣格式:" << outFormat;
    // 步驟七:獲取音訊轉碼器並設定取樣引數初始化
    // 入坑二:通道佈局與通道資料的列舉值是不同的,需要轉換
    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;
    }

    file.open(QIODevice::WriteOnly | QIODevice::Truncate);

    outData[0] = (uint8_t *)av_malloc(1152 * 8);
    outData[1] = (uint8_t *)av_malloc(1152 * 8);

    // 步驟七:讀取一幀資料的封包
    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;
                }
                // 步驟十四:使用LRLRLRLRLRL(取樣點為單位,取樣點有幾個位元組,交替儲存到檔案,可使用pcm播放器播放)
                for (int index = 0; index < dstNbSamples; index++)
                {
                    for (int channel = 0; channel < pAVCodecContext->channels; channel++)  // 交錯的方式寫入, 大部分float的格式輸出
                    {
                        //  用於原始檔案jinxin跟對比
//                        file.write((char *)pAVFrame->data[channel] + numBytes * index, numBytes);
                        file.write((char *)outData[channel] + numBytes * index, numBytes);
                    }
                }
                av_free_packet(pAVPacket);
            }
        }
    }
    file.close();
END:
    LOG << "釋放回收資源";
    if(outData[0] && outData[1])
    {
        av_free(outData[0]);
        av_free(outData[1]);
        outData[0] = 0;
        outData[1] = 0;
        LOG << "av_free(outData[0])";
        LOG << "av_free(outData[1])";
    }
    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)";
    }
}

工程模板v1.3.0、v1.3.1

  對應工程模板v1.3.0:增加解碼音訊裸存pcmDemo
  對應工程模板v1.3.1:增加解碼音訊重取樣存pcmDemo


入坑

入坑一:v1.3.0輸出的pcm檔案音訊播放聲音變了

原因

  存檔案存錯了,入坑一;位元組交錯錯誤,單條音軌是好的,雙軌存入檔案,使用pcm的軟體播放,則預設是LRLRLRLR的方式(取樣點交錯)。
分析音訊檔如下:
  在這裡插入圖片描述
  在這裡插入圖片描述

解決

  在這裡插入圖片描述

入坑二:v1.3.1輸出的pcm檔案音訊播放聲音過快

原因

  通道佈局與通道資料的列舉值是不同的,需要轉換

解決

  在這裡插入圖片描述

入坑三:v1.3.1輸出的pcm檔案音訊降低取樣率出現滴答的聲音

原因

  重取樣之後,取樣率不同了,那麼對應的時間分片的封包是相同的,那麼很明顯,取樣率低了,則資料應該減少,時間是一樣長的,問題就處在轉換函數需要計算一次取樣率變了之後的實際取樣點,關係到其輸出的音訊取樣點資料,否則長了還好說,短了的話,存入更多就是錯誤資料,自然就出現聲音不對。

解決

  在這裡插入圖片描述

入坑四:v1.3.1輸出的pcm檔案較短

原因

  解碼mp4封裝時,獲取到的第一個AVFrame的nb_samples不同,第一幀尾32,本想做動態分佈,結果踩坑.

解決

  在最前面開闢認為的最大快取空間,如下:
  在這裡插入圖片描述


上一篇:《FFmpeg開發筆記(六):ffmpeg解碼視訊並使用SDL同步時間顯示播放
下一篇:敬請期待


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