若該文為原創文章,未經允許不得轉載
原博主博客地址: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
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/150400.html
標籤:其他
