若該文為原創文章,未經允許不得轉載
原博主部落格地址: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開發筆記(六):ffmpeg解碼視訊並使用SDL同步時間顯示播放》
下一篇:敬請期待
本篇解碼音訊,包括從mp3等檔案中抽取音訊流的pcm,從視訊檔中抽取音訊流的pcm。
本文章篇幅相對較長,碼字作圖不易,請各位讀者且行且珍惜。
音訊的幾個關鍵因素請檢視:《SDL開發筆記(二):音訊基礎介紹、使用SDL播放音訊》
匯入原始檔案,設定好資料型別、聲道、取樣率
CSDN:https://download.csdn.net/download/qq21497936/12888731
QQ群:1047134658(點選「檔案」搜尋「audacity」,群內與博文同步更新)
ffmpeg解碼音訊轉碼基本流程如下:
使用ffmpeg對應的庫,都需要進行註冊,可以註冊子項也可以註冊全部。
開啟檔案,根據檔名資訊獲取對應的ffmpeg全域性上下文。
一定要探測流資訊,拿到流編碼的編碼格式,不探測流資訊則其流編碼器拿到的編碼型別可能為空,後續進行資料轉換的時候就無法知曉原始格式,導致錯誤。
依據流的格式查詢解碼器,軟解碼還是硬解碼是在此處決定的,但是特別注意是否支援硬體,需要自己查詢原生的硬體解碼器對應的標識,並查詢其是否支援。普遍操作是,列舉支援檔案字尾解碼的所有解碼器進行查詢,查詢到了就是可以硬解了(此處,不做過多的討論,對應硬解碼後續會有文章進行進一步研究)。
(注意:解碼時查詢解碼器,編碼時查詢編碼器,兩者函數不同,不要弄錯了,否則後續能開啟但是資料是錯的)
開啟獲取到的解碼器。
此處特別注意,基本上解碼的資料都是pcm格式,pcm格式也分很多種,若8位元整形,無符號8為整形,32位元浮點,帶P和不帶P的,不帶P的資料真儲存為LRLRLRLR,帶P的為LLLLRRRR,還有單通道、雙連結和多通道,通道又涉及到了聲道的定位列舉,所以pcm原始資料也多種多樣,對齊進行重弄取樣使其輸出的pcm格式引數特點一致。
重取樣結構體設定好後,需要設定生效。
封包是封裝在容器中的一個封包。獲取不到資料則跳轉「步驟十四」
拿取封裝的一個packet後,判斷packet資料的型別進行送往解碼器解碼。
一個包可能存在多組資料,老的api獲取的是第一個,新的api分開後,可以迴圈獲取,直至獲取不到跳轉「步驟十三」
使用衝殘陽函數結合轉換結構體對編碼的資料進行轉換,拿到重取樣後的音訊原始資料。
拿到了原始資料自行處理。
迴圈解碼跳轉「步驟八」,若步驟八獲取不到資料則執行「步驟十四」
此處要單獨列出是因為,其實很多網上和開發者的程式碼:
在進入迴圈解碼前進行了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開發筆記(四):ffmpeg解碼的基本流程詳解》中的「ffmpeg解碼相關函數原型」。
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);
分配並設定重取樣的結構體上下文。
int swr_init(struct SwrContext *s);
初始化取樣器,使取樣器生效。
void swr_free(struct SwrContext **s);
釋放給定的SwrContext並將指標設定為NULL。
int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);
將原始分組資料傳送給解碼器。
在內部,此呼叫將複製相關的AVCodeContext欄位,這些欄位可以影響每個封包的解碼,並在實際解碼封包時應用這些欄位。(例如AVCodeContext.skip_frame,這可能會指示解碼器丟棄使用此函數傳送的封包所包含的幀。)
這個函數可以理解為ffmpeg為多執行緒準備的,將解碼資料框包送入編碼器理解為一個執行緒,將從編碼器獲取解碼後的資料理解為一個執行緒。
int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);
從解碼器返回解碼輸出資料。這個函數可以理解為ffmpeg為多執行緒準備的,將解碼資料框包送入編碼器理解為一個執行緒,將從編碼器獲取解碼後的資料理解為一個執行緒。
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)";
}
}
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:增加解碼音訊裸存pcmDemo
對應工程模板v1.3.1:增加解碼音訊重取樣存pcmDemo
存檔案存錯了,入坑一;位元組交錯錯誤,單條音軌是好的,雙軌存入檔案,使用pcm的軟體播放,則預設是LRLRLRLR的方式(取樣點交錯)。
分析音訊檔如下:
通道佈局與通道資料的列舉值是不同的,需要轉換
重取樣之後,取樣率不同了,那麼對應的時間分片的封包是相同的,那麼很明顯,取樣率低了,則資料應該減少,時間是一樣長的,問題就處在轉換函數需要計算一次取樣率變了之後的實際取樣點,關係到其輸出的音訊取樣點資料,否則長了還好說,短了的話,存入更多就是錯誤資料,自然就出現聲音不對。
解碼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