一、簡介
接著上節(jié)的音頻解碼,使用SDL播放音頻。
通過上節(jié)程序運(yùn)行打印發(fā)現(xiàn)這些音頻信息明顯不符合SDL的,所以我們需要進(jìn)行重采樣
二、音頻重采樣
這里我們可以參考之前的《12_采樣格式&音頻重采樣》來實(shí)現(xiàn)現(xiàn)在的重采樣。
2.1 引入頭文件
extern "C" {
#include <libswresample/swresample.h>
}
還需要在pro
文件中引入swresample
庫
LIBS += -L $${FFMPEG_HOME}/lib \
-lavcodec \
-lavformat \
-lavutil \
-lswresample
2.2 定義重采樣相關(guān)屬性
/******** 音頻相關(guān) ********/
typedef struct {
int sampleRate;
AVSampleFormat sampleFmt;
int chLayout;
int chs;
int bytesPerSampleFrame;
} AudioSwrSpec;
/** 音頻重采樣上下文 */
SwrContext *_aSwrCtx = nullptr;
/** 音頻重采樣輸入\輸出參數(shù) */
AudioSwrSpec _aSwrInSpec, _aSwrOutSpec;
/** 音頻重采樣輸入\輸出frame */
AVFrame *_aSwrInFrame = nullptr, *_aSwrOutFrame = nullptr;
/** 音頻重采樣輸出PCM的索引(從哪個位置開始取出PCM數(shù)據(jù)填充到SDL的音頻緩沖區(qū)) */
int _aSwrOutIdx = 0;
/** 音頻重采樣輸出PCM的大小 */
int _aSwrOutSize = 0;
/** 初始化音頻重采樣 */
int initSwr();
2.3初始化重采樣
int VideoPlayer::initSwr() {
// 重采樣輸入?yún)?shù)
_aSwrInSpec.sampleFmt = _aDecodeCtx->sample_fmt;
_aSwrInSpec.sampleRate = _aDecodeCtx->sample_rate;
_aSwrInSpec.chLayout = _aDecodeCtx->channel_layout;
_aSwrInSpec.chs = _aDecodeCtx->channels;
// 重采樣輸出參數(shù)
_aSwrOutSpec.sampleFmt = AV_SAMPLE_FMT_S16;
_aSwrOutSpec.sampleRate = 44100;
_aSwrOutSpec.chLayout = AV_CH_LAYOUT_STEREO;
_aSwrOutSpec.chs = av_get_channel_layout_nb_channels(_aSwrOutSpec.chLayout);
_aSwrOutSpec.bytesPerSampleFrame = _aSwrOutSpec.chs
* av_get_bytes_per_sample(_aSwrOutSpec.sampleFmt);
// 創(chuàng)建重采樣上下文
_aSwrCtx = swr_alloc_set_opts(nullptr,
// 輸出參數(shù)
_aSwrOutSpec.chLayout,
_aSwrOutSpec.sampleFmt,
_aSwrOutSpec.sampleRate,
// 輸入?yún)?shù)
_aSwrInSpec.chLayout,
_aSwrInSpec.sampleFmt,
_aSwrInSpec.sampleRate,
0, nullptr);
if (!_aSwrCtx) {
qDebug() << "swr_alloc_set_opts error";
return -1;
}
// 初始化重采樣上下文
int ret = swr_init(_aSwrCtx);
RET(swr_init);
// 初始化重采樣的輸入frame
_aSwrInFrame = av_frame_alloc();
if (!_aSwrInFrame) {
qDebug() << "av_frame_alloc error";
return -1;
}
// 初始化重采樣的輸出frame
_aSwrOutFrame = av_frame_alloc();
if (!_aSwrOutFrame) {
qDebug() << "av_frame_alloc error";
return -1;
}
return 0;
}
在initAudioInfo
方法中調(diào)用initSwr
方法
int VideoPlayer::initAudioInfo() {
int ret = initDecoder(&_aDecodeCtx,&_aStream,AVMEDIA_TYPE_AUDIO);
RET(initDecoder);
// 初始化音頻重采樣
ret = initSwr();
RET(initSwr);
// 初始化SDL
ret = initSDL();
RET(initSDL);
return 0;
}
2.4 重采樣
上面進(jìn)行了重采樣的初始化后,現(xiàn)在我們可以在解碼出來的PCM進(jìn)行重采樣
int VideoPlayer::decodeAudio(){
......
// 重采樣輸出的樣本數(shù)
int outSamples = av_rescale_rnd(_aSwrOutSpec.sampleRate,
_aSwrInFrame->nb_samples,
_aSwrInSpec.sampleRate, AV_ROUND_UP);
// 由于解碼出來的PCM。跟SDL要求的PCM格式可能不一致,需要進(jìn)行重采樣
ret = swr_convert(_aSwrCtx,
_aSwrOutFrame->data,
outSamples,
(const uint8_t **) _aSwrInFrame->data,
_aSwrInFrame->nb_samples);
RET(swr_convert);
return ret * _aSwrOutSpec.bytesPerSampleFrame;
}
swr_convert
函數(shù)的參數(shù)解釋:
int swr_convert(struct SwrContext *s, uint8_t **out, int out_count,
const uint8_t **in , int in_count);
參數(shù)1:重采樣上下文
-
參數(shù)2:輸出到什么地方,這里我們希望輸出到
_aSwrOutFrame->data
,到時候可以直接通過_aSwrOutFrame->data[0]
拿到它指向的PCM數(shù)據(jù),如果是Planar格式data[0]
指向第一個聲道,data[1]
指向下一個聲道,但是這里最終重采樣出來的數(shù)據(jù)是非Planar,是s16的所以這里可以直接通data[0]
拿到PCM數(shù)據(jù)
Planar格式和非Planar格式 參數(shù)3:希望輸出多少個樣本,
outSamples = outSampleRate * inSamples / inSampleRate
,可以直接使用ffmpeg的av_rescale_rnd
函數(shù)得到。參數(shù)4:輸入數(shù)據(jù),可以使用
_aSwrInFrame->data
參數(shù)5:重采樣輸入數(shù)據(jù)里面包含多少個樣本,可以使用
_aSwrInFrame->nb_samples
,這個值不一定是固定的,返回值:真正轉(zhuǎn)換成功的樣本,也就是每一個聲道的樣本數(shù)。
如果此時你運(yùn)行代碼會出現(xiàn)內(nèi)存錯誤,這是因?yàn)橹夭蓸訒r,_aSwrOutFrame->data
的data[0]
未分配空間,所以需要在初始化重采樣的地方給data[0]
分配空間
// 初始化重采樣的輸出frame的data[0]空間
ret = av_samples_alloc(_aSwrOutFrame->data,
_aSwrOutFrame->linesize,
_aSwrOutSpec.chs,
4096, _aSwrOutSpec.sampleFmt, 1);
RET(av_samples_alloc);
三、SDL播放
上面實(shí)現(xiàn)了重采樣,那么現(xiàn)在我們需要把重采樣的數(shù)據(jù)填充到回調(diào)函數(shù)sdlAudioCallback
的stream
里面。
void VideoPlayer::sdlAudioCallback(Uint8 *stream, int len){
// 清零(靜音)
SDL_memset(stream, 0, len);
// len:SDL音頻緩沖區(qū)剩余的大?。ㄟ€未填充的大小)
while (len > 0) {
if (_state == Stopped) break;
// 說明當(dāng)前PCM的數(shù)據(jù)已經(jīng)全部拷貝到SDL的音頻緩沖區(qū)了
// 需要解碼下一個pkt,獲取新的PCM數(shù)據(jù)
if (_aSwrOutIdx >= _aSwrOutSize) {
// 全新PCM的大小
_aSwrOutSize = decodeAudio();
// 索引清0
_aSwrOutIdx = 0;
// 沒有解碼出PCM數(shù)據(jù),那就靜音處理
if (_aSwrOutSize <= 0) {
// 假定PCM的大小
_aSwrOutSize = 1024;
// 給PCM填充0(靜音)
memset(_aSwrOutFrame->data[0], 0, _aSwrOutSize);
}
}
// 本次需要填充到stream中的PCM數(shù)據(jù)大小
int fillLen = _aSwrOutSize - _aSwrOutIdx;
fillLen = std::min(fillLen, len);
// 填充SDL緩沖區(qū)
SDL_MixAudio(stream,
_aSwrOutFrame->data[0] + _aSwrOutIdx,
fillLen, SDL_MIX_MAXVOLUME);
// 移動偏移量
len -= fillLen;
stream += fillLen;
_aSwrOutIdx += fillLen;
}
}
SDL_MixAudio
函數(shù)解釋:
extern DECLSPEC void SDLCALL SDL_MixAudio(Uint8 * dst, const Uint8 * src,
Uint32 len, int volume);
- 參數(shù)1:填充的目的地
- 參數(shù)2:數(shù)據(jù)的源頭,就是PCM從那個地方開始
- 參數(shù)3:填充數(shù)據(jù)的長度,需要填充多少數(shù)據(jù)
- 參數(shù)4:音量大小
也就是把src這個位置開始的多少個數(shù)據(jù)len填入到dst里面去
各個字段解釋:
_aSwrOutSize
:表面這次重采樣PCm的大小
fillLen = _aSwrOutSize - _aSwrOutIdx
:需要填充到stream
中的PCM數(shù)據(jù)大小,減去_aSwrOutIdx
主要是用于一次采樣PCM大小大于了stream
的緩沖區(qū)。
len -= fillLen
:SDL音頻緩沖區(qū)剩余的大?。ㄟ€未填充的大?。?br>
stream += fillLen
:跳過剛剛已經(jīng)填充的大小
_aSwrOutIdx += fillLen
:跳過剛剛已經(jīng)填充的大小
如果_aSwrOutIdx >= _aSwrOutSize
說明PCM所有內(nèi)容都已經(jīng)拷貝到stream里面了,此時的PCM數(shù)據(jù)已經(jīng)沒有利用價值了,這個時候就得解碼下一個pkt
,獲取新的PCM數(shù)據(jù),此時_aSwrOutIdx
就需要清零。如果沒有解碼出PCM數(shù)據(jù),那就靜音處理(_aSwrOutSize = 1024
是經(jīng)驗(yàn)值)。
四、停止功能
首先需要修改videoplayer.cpp
中play
方法,在讀取文件時判斷當(dāng)前狀態(tài)釋放時停止?fàn)顟B(tài)。
void VideoPlayer::play() {
if (_state == Playing) return;
// 狀態(tài)可能是:暫停、停止、正常完畢
if(_state == Stopped){
// 開始線程:讀取文件
std::thread([this](){
readFile();
}).detach();// detach 等到readFile方法執(zhí)行完,這個線程就會銷毀
setState(Playing);
}
}
在videoplayer.h
新增釋放資源的方法
/** 釋放資源 */
void free();
void freeAudio();
void freeVideo();
在videoplayer.cpp
中釋放公共的一些資源
void VideoPlayer::free(){
avformat_close_input(&_fmtCtx);
freeAudio();
freeVideo();
}
現(xiàn)在主要是釋放音頻相關(guān)的資源
void VideoPlayer::freeAudio(){
_aSwrOutIdx = 0;
_aSwrOutSize =0;
clearAudioPktList();
avcodec_free_context(&_aDecodeCtx);
swr_free(&_aSwrCtx);
av_frame_free(&_aSwrInFrame);
if(_aSwrOutFrame){
av_freep(&_aSwrOutFrame->data[0]);// 因手動創(chuàng)建了data[0]的空間
av_frame_free(&_aSwrOutFrame);
}
// 停止播放
SDL_PauseAudio(1);
SDL_CloseAudio();
}
在解碼音頻的方法decodeAudio
中,還需要判斷狀態(tài)釋放為停止?fàn)顟B(tài),因?yàn)?,一?zhí)行此方法就加鎖了,就會再次阻塞等待,等到后終于可以拿到鎖了,但是在我們等待期間有可能就被我們關(guān)掉了,此時就會出現(xiàn)問題,因此這里還需要在判斷一下狀態(tài)_state == Stopped
int VideoPlayer::decodeAudio(){
// 加鎖
_aMutex->lock();
if (_aPktList->empty() || _state == Stopped) {
_aMutex->unlock();
return 0;
}
......
}
在videoplayer.cpp
的讀取文件的while
循環(huán)中也要判斷釋放為停止?fàn)顟B(tài)
while (true) {
if(_state == Stopped) break;
AVPacket pkt;
ret = av_read_frame(_fmtCtx,&pkt);
if ( ret == 0) {
if (pkt.stream_index == _aStream->index) { // 讀取到的是音頻數(shù)據(jù)
addAudioPkt(pkt);
}else if(pkt.stream_index == _vStream->index){// 讀取到的是視頻數(shù)據(jù)
addVideoPkt(pkt);
}
}else{
continue;
}
}
我們之前分裝好的END
的宏函數(shù)最后是goto
去是釋放資源,現(xiàn)在我們直接調(diào)用free
方法就可以了
#define END(func) \
if (ret < 0) { \
ERROR_BUF; \
qDebug() << #func << "error" << errbuf; \
setState(Stopped); \
emit playFailed(this); \
free(); \
return; \
}
// 初始化音頻信息
bool hasAudio = initAudioInfo() >= 0;
// 初始化視頻信息
bool hasVideo = initVideoInfo() >= 0;
if (!hasAudio && !hasVideo) {
emit playFailed(this);
free();
return;
}
五、處理讀完音頻包的情況
while (_state != Stopped) {
AVPacket pkt;
ret = av_read_frame(_fmtCtx, &pkt);
if (ret == 0) {
if (pkt.stream_index == _aStream->index) { // 讀取到的是音頻數(shù)據(jù)
addAudioPkt(pkt);
} else if (pkt.stream_index == _vStream->index) { // 讀取到的是視頻數(shù)據(jù)
addVideoPkt(pkt);
}
} else if (ret == AVERROR_EOF) { // 讀到了文件的尾部
qDebug() << "已經(jīng)讀取到文件尾部";
break;
} else {
ERROR_BUF;
qDebug() << "av_read_frame error" << errbuf;
continue;
}
}