十分钟学会如何开发一个音频播放器(ffmpeg3.2+sdl2.0)

版权声明:来自XP_online转载请注明 https://blog.csdn.net/XP_online/article/details/90582107

前言

这套教程是使用ffmpeg3.2+SDL2.0开发的。这两个版本跟之前版本的函数有了很大的改变,但基本的原理还是一致的。在阅读时请注意自身使用的版本。本篇的源码已提交在github上:https://github.com/XP-online/audio-player

媒体播放器的原理

媒体播放器的播放原理很简单:一个媒体文件文件如mp4,mp3等内部储存着几个AV流,一般包含视频流,音频流有的还有字幕流等。而每个流里都是有若干个包(packet)组成的。包中储存的就是最重要的信息“帧”(frame),帧中储存的数据就是我们需要的视频或音频等的原始数据。
不过包(packet)内的信息被编码过了,所以播放器需要找到这些包并解编码出每一帧,将这些帧中的数据或传给操作系统播放出来(如音频播放就是通过操作系统播放的)或者按照我们自己的方式处理(如视频信息我们可以在获得每一帧的图像信息后,通过任何我们想要的方式显示)。

创建一个音频播放器的步骤

本篇我们先说一下创建一个音频播放器的步骤。在这里我们有必要在强调一下音频播放的原理即:找到音频流 —— 读取音频流中的包
—— 解编码包并获取音频帧 —— 将音频帧的数据给操作系统让操作系统将音频播放出来。那么我们的具体操作步骤如下所示:

  1. 读取AV文件格式信息和音频或视频流的索引( avformat_open_inputavformat_find_stream_info )。
  2. 找到解码器,并设置解码器参数( avcodec_find_decoderavcodec_parameters_to_contextavcodec_open2 )以及sdl的重采样相关参数。
  3. 循环调用 av_read_frame 不断从音频流里读取packet。
  4. 使用 avcodec_send_packetavcodec_receive_frame 相配合不断地将packet送入解码器,并从解码器中读取解码后的frame。
  5. 对解码后的frame的采样率进行转换( swr_convert )。
  6. 在sdl的回调中将设置好的数据放入系统指定的地址中。系统将根据传入的数据播放声音( sdl_audio_callback )。

源码分析

在这里的代码主要是为了便于理解音频播放的原理。设计时的代码逻辑,变量位置,类型也是基于这一个目的设计的。大家完全可以在看懂了之后按照自己的方式设计代码逻辑。

一、定义一些基本的参数

这里定义一些全局变量。每个变量的意义后面有注释,具体的用法下文会提到。

#define MAX_AUDIO_FRAME_SIZE 192000 // 1 second of 48khz 32bit audio  

//swr
struct SwrContext* au_convert_ctx; // 重采样上下文
int out_buffer_size; // 重采样后的缓冲区
uint8_t* out_buffer; // sdl调用音频数据的缓冲区

//audio decode
int au_stream_index = -1; // 音频流在文件中的位置
AVFormatContext* pFormatCtx = nullptr; // 文件上下文
AVCodecParameters* audioCodecParameter; // 音频解码器参数
AVCodecContext* audioCodecCtx = nullptr; // 音频解码器上下文
AVCodec* audioCodec = nullptr; // 音频解码器

// sdl
static Uint32 audio_len; // 音频数据缓冲区中未读数据剩余的长度
static Uint8* audio_pos; // 音频缓冲区中读取的位置
SDL_AudioSpec wanted_spec; // sdl播放音频的参数

二、解析文件信息

首先我们需要获取基本的文件信息( avformat_open_input ),和文件中的流信息( avformat_find_stream_info )。有了这些信息我们才可以去创建配置ffmpeg的音频解码器。

	//初始化ffmpeg的组件
	av_register_all();

	//读取文件头的文件基本信息到pFormateCtx中
	pFormatCtx = avformat_alloc_context();
	if (avformat_open_input(&pFormatCtx, filePath, nullptr, nullptr) != 0) {
		printf_s("avformat_open_input failed\n");
		system("pause");
		return -1;
	}
	// 在文件中找到文件中的音频流或视频流等“流”信息
	if (avformat_find_stream_info(pFormatCtx, nullptr) < 0) {
		//异常处理...
	}
	// 找到音频流的位置
	for (unsigned i = 0; i < pFormatCtx->nb_streams; ++i) {
		if (AVMEDIA_TYPE_AUDIO == pFormatCtx->streams[i]->codecpar->codec_type) {
			au_stream_index = i;
			continue;
		}
	}
	if (-1 == au_stream_index) {
		//异常处理...
	}

三、创建解码器,配置音频参数

在正式的读取文件中的音频包之前,我们先要创建对应的解码器以及配置音频的参数。这一部分较细节较多,稍有不慎都可能导致音频的声音不正确。整个流程大致可分为:从音频流中读取原始音频参数——通过音频参数创建配置解码器——根据自身机器的音频输出方式配置重采样器——配置sdl音频播放参数。我在这里创建了一个函数专门用来处理这些问题。

// 初始化编码器,重采样器所需的各项参数
int init_audio_parameters() {
	// 获取音频解码器参数
	audioCodecParameter = pFormatCtx->streams[au_stream_index]->codecpar;
	// 获取音频解码器
	audioCodec = avcodec_find_decoder(audioCodecParameter->codec_id);
	if (audioCodec == nullptr) {
		printf_s("audio avcodec_find_decoder failed.\n");
		return -1;
	}
	// 获取解码器上下文
	audioCodecCtx = avcodec_alloc_context3(audioCodec);
	if (avcodec_parameters_to_context(audioCodecCtx, audioCodecParameter) < 0) {
		printf_s("audio avcodec_parameters_to_context failed\n");
		return -1;
	}
	// 根据上下文配置音频解码器
	avcodec_open2(audioCodecCtx, audioCodec, nullptr);
	// -------------------设置重采样相关参数-------------------------//
	uint64_t out_channel_layout = AV_CH_LAYOUT_STEREO; // 双声道输出
	int out_channels = av_get_channel_layout_nb_channels(out_channel_layout);

	AVSampleFormat out_sample_fmt = AV_SAMPLE_FMT_S16; // 输出的音频格式
	int out_sample_rate = 44100; // 采样率
	int64_t in_channel_layout = av_get_default_channel_layout(audioCodecCtx->channels); //输入通道数
	audioCodecCtx->channel_layout = in_channel_layout;
	au_convert_ctx = swr_alloc(); // 初始化重采样结构体
	au_convert_ctx = swr_alloc_set_opts(au_convert_ctx, out_channel_layout, out_sample_fmt, out_sample_rate,
		in_channel_layout, audioCodecCtx->sample_fmt, audioCodecCtx->sample_rate, 0, nullptr); //配置重采样率
	swr_init(au_convert_ctx); // 初始化重采样率

	int out_nb_samples = audioCodecCtx->frame_size;
	// 计算出重采样后需要的buffer大小,后期储存转换后的音频数据时用
	out_buffer_size = av_samples_get_buffer_size(NULL, out_channels, out_nb_samples, out_sample_fmt, 1);
	out_buffer = (uint8_t*)av_malloc(MAX_AUDIO_FRAME_SIZE * 2);
	// -------------------设置 SDL播放音频时的参数 ---------------------------//
	wanted_spec.freq = out_sample_rate;//44100;
	wanted_spec.format = AUDIO_S16SYS;
	wanted_spec.channels = out_channels;
	wanted_spec.silence = 0;
	wanted_spec.samples = out_nb_samples;
	wanted_spec.callback = sdl_audio_callback; //sdl系统会掉。上面有说明
	wanted_spec.userdata = nullptr; // 回调时想带进去的参数

	// SDL打开音频播放设备
	if (SDL_OpenAudio(&wanted_spec, NULL) < 0) {
		printf_s("can't open audio.\n");
		return -1;
	}
	// 暂停/播放音频,参数为0播放音频,非0暂停音频
	SDL_PauseAudio(0);
	return 0;
}

这里wanted_spec.callback = sdl_audio_callback;设置的回调函即为sdl播放音频时不断获取音频数据的回调函数。下文中会对此做专门的说明。这里只需要知道sdl通过这个函数来获取所需的音频数据进行播放的即可。
可以看到在设置重采样这一部分的参数类型非常多。我对这些参数类型所表示意义也不是很了解。欢迎多来沟通。

四、开始读取音频包(AVPacket)

现在终于可以开始读取音频包了!从文件中读取音频包非常简单,只需要循环调用 av_read_frame 即可,他将会把读到的包存入到作为参数传入的AVPacket中,之后在将其解包既可以得到我们想要的AVFrame(帧),帧里储存的即为原始的音频数据。

AVPacket packet;
	AVFrame* pFrame = NULL;
	// 开始读取文件中编码后的音频数据,并将读到的数据储存在
	while (av_read_frame(pFormatCtx, &packet) >= 0)
	{
		if (packet.stream_index == au_stream_index)
		{
			if (!pFrame)
			{
				if (!(pFrame = av_frame_alloc()))
				{
					printf_s("Could not allocate audio frame\n");
					system("pause");
					exit(1);
				}
			}
			if (packet.size) {
				// 对读取到的pkt解码,并将数据传递给音频数据缓冲区
				decode_audio_packet(audioCodecCtx, &packet, pFrame);
			}

			av_frame_unref(pFrame);
			av_packet_unref(&packet);
		}
	}

这里我将解码部分的代码单独拿了出来,以使得整体结构较为清晰。

// 将读取到的一个音频pkt解码成avframe。avframe中的数据就是原始的音频数据
void decode_audio_packet(AVCodecContext * code_context, AVPacket * pkt, AVFrame * frame)
{
	int i, ch;
	int ret, data_size;
	// ffmpeg3.2版本后推荐使用的方式,将一个pkt发送给解码器。之后在avcodec_receive_frame中取出解码后的avframe
	ret = avcodec_send_packet(code_context, pkt);
	if (ret < 0)
	{
		printf_s("Error submitting the packet to the decoder\n");
		system("pause");
		exit(1);
	}
	// 不断尝试取出音频数据,直到无法再取出
	while (ret >= 0)
	{
		ret = avcodec_receive_frame(code_context, frame); // 前文已经介绍,在此处取出原始音频数据

		if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) //该帧目前无法解出,需要再发送一个pkt
			return;
		else if (ret < 0)
		{
			printf_s("Error during decoding\n");
			system("pause");
			exit(1);
		}
		// 将音频的采样率转换成本机能播出的采样率
		swr_convert(au_convert_ctx, &out_buffer, out_buffer_size,
			(const uint8_t * *)frame->data, code_context->frame_size);

		while (audio_len > 0) // 在此处等待sdl_audio_callback将之前传递的音频数据播放完再向其中发送新的数据
			SDL_Delay(1);

		// 将读取到的数据存入音频缓冲区 
		audio_len = out_buffer_size; // 记录音频数据的长度
		audio_pos = (Uint8*)out_buffer;
	}
}

可以看到最后我们将音频数据放入了音频缓冲区( out_buffer ),这里缓冲区是我之前在 init_audio_parameters 中最后注册的 sdl_audio_callback 函数获取音频数据的数据源。每当系统需要音频数据就会调用我们 sdl_audio_callback 函数从这里取出数据,如果缓冲区的数据全部被读取完,则将刚解码完的音频数据重新放入缓冲区。不断重复这个过程直到音频播放完毕。

五、不断地给音频回调“喂食”( sdl_audio_callback

最后的最后,系统会根据采样率自动控制音频的播放速率。因此我们只需不断地给系统提供数据即可。下面让我们来完成系统不断调用的 sdl_audio_callback 回调函数。

// sdl配置中的系统播放音频的回调。
// udata:我们自己设置的参数,
// stream:系统读取音频数据的buffer由我们在这个函数中把音频数据拷贝到这个buffer中,
// len:系统希望读取的长度(可以比这个小,但不能给多)
void sdl_audio_callback(void* udata, Uint8* stream, int len)
{
	//SDL 2.0之后的函数。很像memset在这里用来清空指定内存  
	SDL_memset(stream, 0, len);
	if (audio_len == 0)
		return;
	len = ((Uint32)len > audio_len ? audio_len : len); //比较剩余未读取的音频数据的长度和所需要的长度。尽最大可能的给予其音频数据

	SDL_MixAudio(stream, audio_pos, len, SDL_MIX_MAXVOLUME); //SDL_MixAudio的作用和memcpy类似,这里将audio_pos的数据传递给stream

	//audio_pos是记录out_buffer(存放我们读取音频数据的缓冲区)当前读取的位置
	//audio_len是记录out_buffer剩余未读数据的长度
	audio_pos += len; //audio_pos前进到新的位置
	audio_len -= len; //audio_len的长度做相应的减少
}

这里有三个参数,

  • udata:使我们希望在回调中调用的数据。通常是自定义的变量。在本例中不需要,所以没有处理。
  • stream:系统给出的缓存区。需要我们来填充,系统来调用。
  • len:stream的大小。我们可以传入的数据比这个小,但不能比这个数值大。不然会产生溢出。

在最后对我们设置音频数据缓冲区( out_buffer )的剩余大小和读取位置坐了计算:
audio_pos是用来记录我们音频数据的缓冲区当前读取的位置,它随着每次回调不断前进。
audio_len 是用来记录缓存区未读取的长度。它随着每次回调不断减少。减少零时重新给缓冲区赋值。

展开阅读全文

没有更多推荐了,返回首页