天天看點

使用ffmpeg解碼音頻sdl(pull)播放

作者:音視訊流媒體技術

自定義播放器系列

第一章 視訊渲染

第二章 音頻(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 等等)有需要的可以點選加群免費領取~

使用ffmpeg解碼音頻sdl(pull)播放

四、完整代碼

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部落格