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

* 前言 <https://blog.csdn.net/XP_online/article/details/90582107#_1>
* 媒体播放器的原理 <https://blog.csdn.net/XP_online/article/details/90582107#_3>
* 创建一个音频播放器的步骤 <https://blog.csdn.net/XP_online/article/details/90582107#_6>
* 源码分析 <https://blog.csdn.net/XP_online/article/details/90582107#_14>
* 一、定义一些基本的参数 <https://blog.csdn.net/XP_online/article/details/90582107#_16>
* 二、解析文件信息 <https://blog.csdn.net/XP_online/article/details/90582107#_38>
* 三、创建解码器,配置音频参数
<https://blog.csdn.net/XP_online/article/details/90582107#_66>
* 四、开始读取音频包(AVPacket)
<https://blog.csdn.net/XP_online/article/details/90582107#AVPacket_126>
* 五、不断地给音频回调“喂食”( *sdl_audio_callback* )
<https://blog.csdn.net/XP_online/article/details/90582107#_sdl_audio_callback__199>


<>前言


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

<>媒体播放器的原理


媒体播放器的播放原理很简单:一个媒体文件文件如mp4,mp3等内部储存着几个AV流,一般包含视频流,音频流有的还有字幕流等。而每个流里都是有若干个包(packet)组成的。包中储存的就是最重要的信息“帧”(frame),帧中储存的数据就是我们需要的视频或音频等的原始数据。

不过包(packet)内的信息被编码过了,所以播放器需要找到这些包并解编码出每一帧,将这些帧中的数据或传给操作系统播放出来(如音频播放就是通过操作系统播放的)或者按照我们自己的方式处理(如视频信息我们可以在获得每一帧的图像信息后,通过任何我们想要的方式显示)。

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

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

* 读取AV文件格式信息和音频或视频流的索引( avformat_open_input ,avformat_find_stream_info )。
* 找到解码器,并设置解码器参数( avcodec_find_decoder ,avcodec_parameters_to_context ,
avcodec_open2 )以及sdl的重采样相关参数。
* 循环调用 av_read_frame 不断从音频流里读取packet。
* 使用 avcodec_send_packet 和 avcodec_receive_frame
相配合不断地将packet送入解码器,并从解码器中读取解码后的frame。
* 对解码后的frame的采样率进行转换( swr_convert )。
* 在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 是用来记录缓存区未读取的长度。它随着每次回调不断减少。减少零时重新给缓冲区赋值。

友情链接
KaDraw流程图
API参考文档
OK工具箱
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:ixiaoyang8@qq.com
QQ群:637538335
关注微信