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

    xiaoxiao2025-08-06  7

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

    前言媒体播放器的原理创建一个音频播放器的步骤源码分析一、定义一些基本的参数二、解析文件信息三、创建解码器,配置音频参数四、开始读取音频包(AVPacket)五、不断地给音频回调“喂食”( *sdl_audio_callback* )

    前言

    这套教程是使用ffmpeg3.2+SDL2.0开发的。这两个版本跟之前版本的函数有了很大的改变,但基本的原理还是一致的。在阅读时请注意自身使用的版本。本篇的源码已提交在github上: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 是用来记录缓存区未读取的长度。它随着每次回调不断减少。减少零时重新给缓冲区赋值。

    最新回复(0)