自定義播放器系列
第一章 視訊渲染
第二章 音頻(push)播放
第三章 音頻(pull)播放(本章)
第四章 實作時鐘同步
第五章 實作通用時鐘同步
第六章 實作播放器
前言
我們上一章講了,ffmpeg解碼sdl push的方式播放音頻,調用流程簡單,但是實作起來還是有點難度的。接下來講的就是使用pull的方式播放音頻,pull的方式即是使用回調的方式播放音頻,在打開SDL音頻裝置的時候傳入一個回調方法,SDL内部會按照一定頻率回調這個方法,我們在回調方法中往音頻緩沖寫資料就能夠播放聲音了。
一、ffmpeg解碼
ffmpeg解碼部分與《使用ffmpeg解碼音頻sdl(push)播放》一緻,這裡就不再贅述。
二、sdl播放
1、初始化sdl
使用sdl前需要在最開始初始化sdl,全局隻需要初始化一次即可。
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
printf("Could not initialize SDL - %s\n", SDL_GetError());
return -1;
}
2、打開音頻裝置
建議使用SDL_OpenAudioDevice打開裝置,使用SDL_OpenAudio的話samples設定可能不生效,比較不靈活一點。pull的方式播放其實就是采用回調的方式播放,我們給下面代碼的wanted_spec.callback指派即可。
SDL_AudioSpec wanted_spec, spec;
int audioId = 0;
//打開裝置
wanted_spec.channels = av_get_channel_layout_nb_channels(pCodecCtx->channel_layout);
wanted_spec.freq = pCodecCtx->sample_rate;
wanted_spec.format = AUDIO_F32SYS;
wanted_spec.silence = 0;
wanted_spec.samples = FFMAX(512, 2 << av_log2(wanted_spec.freq / 30));
//注冊回調方法
wanted_spec.callback = audioCallback;
wanted_spec.userdata = NULL;
audioId = SDL_OpenAudioDevice(NULL, 0, &wanted_spec, &spec, 1);
if (audioId < 2)
{
printf("Open audio device error!\n");
goto end;
}
//開啟播放
SDL_PauseAudioDevice(audioId, 0);
3、播放(pull)
我們采用pull的方式播放,即注冊回調方法,sdl會按照一定的頻率觸發回調,我們隻需往回調參數的緩存指針寫入音頻資料即可。
//音頻裝置讀取資料回調方法
void audioCallback(void* userdata, Uint8* stream, int len) {
//TODO:從音頻隊列中讀取資料到裝置緩沖
};
4、銷毀資源
使用完成後需要銷毀資源,如下所示,SDL_Quit并不是必要的,通常是程式退出才需要調用,這個時候調不調已經無所謂了。
if (audioId >= 2)
{
SDL_PauseAudioDevice(audioId, 1);
SDL_CloseAudioDevice(audioId);
}
SDL_Quit();
三、使用AVAudioFifo
由于是采用回調的方式播放,必然需要一個隊列往裡面寫入解碼的資料,音頻裝置回調時再将資料讀出。我們直接使用ffmpeg提供的音頻隊列即可。
1、初始化
AVAudioFifo是基于采樣數的,是以初始化的時候需要設定音頻格式以及隊列長度采樣數。直接使用音頻裝置的參數,隊列長度要稍微長一點。
//使用音頻隊列
AVAudioFifo *fifo = av_audio_fifo_alloc(forceFormat, spec.channels, spec.samples * 10);
2、寫入資料
我們需要解碼處往隊列裡寫入資料
//解碼後的音頻資料
uint8_t* data;
//音頻資料的采樣數
size_t samples;
if (av_audio_fifo_space(fifo) >= samples)
{
av_audio_fifo_write(fifo, (void**)&data, samples);
}
3、讀取資料
在音頻裝置回調中讀取隊列中的音頻資料
void audioCallback(void* userdata, Uint8* stream, int len) {
//從音頻隊列中讀取資料到裝置緩沖
av_audio_fifo_read(fifo, (void**)&stream, spec.samples);
};
4、銷毀資源
if (fifo)
{
av_audio_fifo_free(fifo);
}
相關學習資料推薦,點選下方連結免費報名,先碼住不迷路~】
【免費分享】音視訊學習資料包、大廠面試題、技術視訊和學習路線圖,資料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以點選加群免費領取~
四、完整代碼
1、代碼
将上述代碼合并起來形成一個完整的音頻解碼播放流程:
示例的sdk版本:ffmpeg 4.3、sdl2
windows、linux都可以正常運作
#include <stdio.h>
#include <SDL.h>
#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"
#include "libswscale/swscale.h"
#include "libswresample/swresample.h"
#include "libavutil/audio_fifo.h"
#undef main
AVAudioFifo* fifo = NULL;
SDL_AudioSpec wanted_spec, spec;
SDL_mutex* mtx = NULL;
static void audioCallback(void* userdata, Uint8* stream, int len) {
//由于AVAudioFifo非線程安全,且是子線程觸發此回調,是以需要加鎖
SDL_LockMutex(mtx);
//讀取隊列中的音頻資料
av_audio_fifo_read(fifo, (void**)&stream, spec.samples);
SDL_UnlockMutex(mtx);
};
int main(int argc, char** argv) {
const char* input = "test_music.wav";
enum AVSampleFormat forceFormat;
AVFormatContext* pFormatCtx = NULL;
AVCodecContext* pCodecCtx = NULL;
const AVCodec* pCodec = NULL;
AVDictionary* opts = NULL;
AVPacket packet;
AVFrame* pFrame = NULL;
struct SwrContext* swr_ctx = NULL;
uint8_t* outBuffer = NULL;
int audioindex = -1;
int exitFlag = 0;
int isLoop = 1;
double framerate;
int audioId = 0;
memset(&packet, 0, sizeof(AVPacket));
//初始化SDL
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
printf("Could not initialize SDL - %s\n", SDL_GetError());
return -1;
}
//打開輸入流
if (avformat_open_input(&pFormatCtx, input, NULL, NULL) != 0) {
printf("Couldn't open input stream.\n");
goto end;
}
//查找輸入流資訊
if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
printf("Couldn't find stream information.\n");
goto end;
}
//擷取音頻流
for (unsigned i = 0; i < pFormatCtx->nb_streams; i++)
if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
audioindex = i;
break;
}
if (audioindex == -1) {
printf("Didn't find a audio stream.\n");
goto end;
}
//建立解碼上下文
pCodecCtx = avcodec_alloc_context3(NULL);
if (pCodecCtx == NULL)
{
printf("Could not allocate AVCodecContext\n");
goto end;
}
//擷取解碼器
if (avcodec_parameters_to_context(pCodecCtx, pFormatCtx->streams[audioindex]->codecpar) < 0)
{
printf("Could not init AVCodecContext\n");
goto end;
}
pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
if (pCodec == NULL) {
printf("Codec not found.\n");
goto end;
}
//使用多線程解碼
if (!av_dict_get(opts, "threads", NULL, 0))
av_dict_set(&opts, "threads", "auto", 0);
//打開解碼器
if (avcodec_open2(pCodecCtx, pCodec, &opts) < 0) {
printf("Could not open codec.\n");
goto end;
}
if (pCodecCtx->sample_fmt == AV_SAMPLE_FMT_NONE)
{
printf("Unknown sample foramt.\n");
goto end;
}
if (pCodecCtx->sample_rate <= 0 || pFormatCtx->streams[audioindex]->codecpar->channels <= 0)
{
printf("Invalid sample rate or channel count!\n");
goto end;
}
//打開裝置
wanted_spec.channels = pFormatCtx->streams[audioindex]->codecpar->channels;
wanted_spec.freq = pCodecCtx->sample_rate;
wanted_spec.format = AUDIO_F32SYS;
wanted_spec.silence = 0;
wanted_spec.samples = FFMAX(512, 2 << av_log2(wanted_spec.freq / 30));
wanted_spec.callback = audioCallback;
wanted_spec.userdata = NULL;
audioId = SDL_OpenAudioDevice(NULL, 0, &wanted_spec, &spec, 1);
if (audioId < 2)
{
printf("Open audio device error!\n");
goto end;
}
switch (spec.format)
{
case AUDIO_S16SYS:
forceFormat = AV_SAMPLE_FMT_S16;
break;
case AUDIO_S32SYS:
forceFormat = AV_SAMPLE_FMT_S32;
break;
case AUDIO_F32SYS:
forceFormat = AV_SAMPLE_FMT_FLT;
break;
default:
printf("audio device format was not surported!\n");
goto end;
break;
}
pFrame = av_frame_alloc();
if (!pFrame)
{
printf("alloc frame failed!\n");
goto end;
}
//使用音頻隊列
fifo = av_audio_fifo_alloc(forceFormat, spec.channels, spec.samples * 10);
if (!fifo)
{
printf("alloc fifo failed!\n");
goto end;
}
mtx = SDL_CreateMutex();
if (!mtx)
{
printf("alloc mutex failed!\n");
goto end;
}
SDL_PauseAudioDevice(audioId, 0);
start:
while (!exitFlag)
{
//讀取包
int gotPacket = av_read_frame(pFormatCtx, &packet) == 0;
if (!gotPacket || packet.stream_index == audioindex)
//!gotPacket:未擷取到packet需要将解碼器的緩存flush,是以還需要進一次解碼流程。
{
//發送包
if (avcodec_send_packet(pCodecCtx, &packet) < 0)
{
printf("Decode error.\n");
av_packet_unref(&packet);
goto end;
}
//接收解碼的幀
while (avcodec_receive_frame(pCodecCtx, pFrame) == 0) {
uint8_t* data;
size_t dataSize;
size_t samples;
if (forceFormat != pCodecCtx->sample_fmt || spec.freq != pFrame->sample_rate || spec.channels != pFrame->channels)
//重采樣
{
//計算輸入采樣數
int out_count = (int64_t)pFrame->nb_samples * spec.freq / pFrame->sample_rate + 256;
//計算輸出資料大小
int out_size = av_samples_get_buffer_size(NULL, spec.channels, out_count, forceFormat, 0);
//輸入資料指針
const uint8_t** in = (const uint8_t**)pFrame->extended_data;
//輸出緩沖區指針
uint8_t** out = &outBuffer;
int len2 = 0;
if (out_size < 0) {
av_log(NULL, AV_LOG_ERROR, "av_samples_get_buffer_size() failed\n");
goto end;
}
if (!swr_ctx)
//初始化重采樣對象
{
swr_ctx = swr_alloc_set_opts(NULL, av_get_default_channel_layout(spec.channels), forceFormat, spec.freq, av_get_default_channel_layout(pFormatCtx->streams[audioindex]->codecpar->channels) , pCodecCtx->sample_fmt, pCodecCtx->sample_rate, 0, NULL);
if (!swr_ctx || swr_init(swr_ctx) < 0) {
av_log(NULL, AV_LOG_ERROR, "swr_alloc_set_opts() failed\n");
goto end;
}
}
if (!outBuffer)
//申請輸出緩沖區
{
outBuffer = (uint8_t*)av_mallocz(out_size);
}
//執行重采樣
len2 = swr_convert(swr_ctx, out, out_count, in, pFrame->nb_samples);
if (len2 < 0) {
av_log(NULL, AV_LOG_ERROR, "swr_convert() failed\n");
goto end;
}
//取得輸出資料
data = outBuffer;
//輸出資料長度
dataSize = av_samples_get_buffer_size(0, spec.channels, len2, forceFormat, 1);
samples = len2;
}
else
{
data = pFrame->data[0];
dataSize = av_samples_get_buffer_size(pFrame->linesize, pFrame->channels, pFrame->nb_samples, forceFormat, 0);
samples = pFrame->nb_samples;
}
//寫入隊列
while (1) {
SDL_LockMutex(mtx);
if (av_audio_fifo_space(fifo) >= samples)
{
av_audio_fifo_write(fifo, (void**)&data, samples);
SDL_UnlockMutex(mtx);
break;
}
SDL_UnlockMutex(mtx);
//隊列可用空間不足則延時等待
SDL_Delay((dataSize) * 1000.0 / (spec.freq * av_get_bytes_per_sample(forceFormat) * spec.channels) - 1);
}
}
}
av_packet_unref(&packet);
if (!gotPacket)
{
//循環播放時flush出緩存幀後需要調用此方法才能重新解碼。
avcodec_flush_buffers(pCodecCtx);
break;
}
}
if (!exitFlag)
{
if (isLoop)
{
//定位到起點
if (avformat_seek_file(pFormatCtx, -1, 0, 0, 0, AVSEEK_FLAG_FRAME) >= 0)
{
goto start;
}
}
}
end:
//銷毀資源
if (fifo)
{
av_audio_fifo_free(fifo);
}
if (audioId >= 2)
{
SDL_PauseAudioDevice(audioId, 1);
SDL_CloseAudioDevice(audioId);
}
if (mtx)
{
SDL_DestroyMutex(mtx);
}
if (pFrame)
{
if (pFrame->format != -1)
{
av_frame_unref(pFrame);
}
av_frame_free(&pFrame);
}
if (packet.data)
{
av_packet_unref(&packet);
}
if (pCodecCtx)
{
avcodec_close(pCodecCtx);
avcodec_free_context(&pCodecCtx);
}
if (pFormatCtx)
avformat_close_input(&pFormatCtx);
if (pFormatCtx)
avformat_free_context(pFormatCtx);
swr_free(&swr_ctx);
av_dict_free(&opts);
if (outBuffer)
av_free(outBuffer);
SDL_Quit();
return 0;
}
總結
以上就是今天要講的内容,使用pull的方式播發音頻比push簡單很多,而且更加靈活可以繼續實作更複雜的功能比如多路音頻合并,使用push則難以實作。我們唯一要注意的就是保證線程安全,對隊列的讀寫也非常簡單,長度控制直接通過延時就可以做到。
原文 使用ffmpeg解碼音頻sdl(pull)播放_CodeOfCC的部落格-CSDN部落格