FFmpeg開發筆記(七):ffmpeg解碼音頻保存為PCM並使用軟件播放


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

 

前言

  本篇解碼音頻,包括從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,本想做動態分布,結果踩坑.

解決

  在最前面開辟認為的最大緩存空間,如下:
  在這里插入圖片描述

 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM