在上篇文章 基于 FFmpeg 的跨平台视频播放器简明教程(五):使用 SDL 播放视频 中,我们使用 FFmpeg + SDL 来播放视频画面,但仅仅只是画面。今天,我们将讨论如何使用 FFmpeg + SDL 同时播放画面和声音。
本文参考文章来自 An ffmpeg and SDL Tutorial - Tutorial 03: Playing Sound 。这个系列对新手较为友好,但 2015 后就不再更新了,以至于文章中的 ffmpeg api 已经被弃用了。幸运的是,有人对该教程的代码进行重写,使用了较新的 api,你可以在 rambodrahmani/ffmpeg-video-player 找到这些代码。
本文的代码在 ffmpeg_video_player_tutorial-my_tutorial03。
–
数字音频是一种将声音信号转换为可以被计算机存储和处理的格式的技术。顾名思义,数字音频指的是以数字形式表示的音频。在录制数字音频时,首先需要对原始的模拟声音信号进行采样,即在连续的声音波形上定期取样。然后,对每个采样点进行量化,即将其幅度映射到一个有限数量的离散数值集合上。这样,我们就得到了一系列数字值,它们可以在计算机系统中进行存储、编辑和播放。关于数字音频更多内容,请参考 数字音频基础-从PCM说起。
在 基于 FFmpeg 的跨平台视频播放器简明教程(二):基础知识和解封装(demux)中,提到了一个容器通常同时包含视频流和音频流。当需要音频数据时,可以从音频流中读取AVPacket,然后进行音频解码,得到AVFrame。
FFmpeg的音频解码器支持包括MP3、AAC、FLAC、WAV等多种音频文件格式。解码音频后,数据将存储在AVFrame->data中。要正确使用这些解码后的数据,首先需要了解它们是以何种采样格式(Sample Format)存储的。
在 FFmpeg 中可以使用 AVSampleFormat 枚举类型来确定音频数据的采样格式。常见的采样格式有:S16、S32、FLT、DBL、U8等。当你了解了音频数据的采样格式,就可以对其进行消费,如将音频数据写入音频缓冲区,然后送到音频解码器或音频混合器中进行混音处理,最终实现音频的播放。在 基于 FFMPEG 的音频编解码(二):音频解码 中一文中,有更多关于 FFmpeg 采样格式的介绍,请阅读,此处不再赘述。
计算机操作系统是如何播放音频的?计算机操作系统播放音频主要分为以下两部分:
音频线程:操作系统会启动一个专门负责处理音频数据的音频线程,这个线程的主要任务是将音频数据传递给音频硬件(例如声卡、扬声器)。音频线程在后台运行,它会定期将缓冲区中的音频数据发送给硬件,确保音频播放的连续性和稳定性。
回调函数:回调函数是音频线程与具体的音频播放应用之间的桥梁。音频线程通过调用回调函数从应用程序中获取待播放的音频数据。这些数据通常需要经过解码、重采样等处理,以适应音频硬件的播放要求。回调函数负责按照预设的格式提供音频数据,例如,对音量进行调整,将音频数据调整为硬件可识别的采样率、比特深度等。
在实际的音频播放过程中,音频线程周期性地调用回调函数,从应用程序中获取新的音频数据,然后发送给音频硬件进行播放。回调函数则需要在每次调用时读取解码后的音频数据并进行处理,以满足音频线程的需求。这样的设计模式使得音频播放能够与应用程序逻辑分离,方便开发者对音频播放过程进行控制,提高音频播放的响应速度与灵活性。同时,这种框架也有利于操作系统统一管理音频资源,保证多个音频应用之间的协同与兼容。
SDL 同样采用回调函数的形式来播放音频,具体步骤包括:
if (SDL_Init(SDL_INIT_AUDIO) < 0) {
// 错误处理逻辑
}
SDL_AudioSpec wanted_spec, obtained_spec;
memset(&wanted_spec, 0, sizeof(wanted_spec));
wanted_spec.freq = 44100;
wanted_spec.format = AUDIO_S16SYS;
wanted_spec.channels = 2;
wanted_spec.samples = 1024;
wanted_spec.callback = audio_callback; // 回调函数
wanted_specs.userdata = user_data; // 设置回调函数中的 userdata
SDL_AudioDeviceID audio_dev;
audio_dev = SDL_OpenAudioDevice(NULL, 0, &wanted_spec, &obtained_spec, SDL_AUDIO_ALLOW_ANY_CHANGE);
if (audio_dev == 0) {
// 错误处理逻辑
}
void audio_callback(void *userdata, Uint8 *stream, int len) {
// 用户实现:将音频数据填充到stream中,长度为len
// userdata 则是你自定义的数据,可以将其转换成对应的指针
}
SDL_PauseAudioDevice(audio_dev, 0);
SDL_CloseAudioDevice(audio_dev);
SDL_Quit();
在不同的视频文件中,音频流可能采用不同的采样格式,而系统支持的采样格式通常是固定的。例如,在 SDL 中,可以使用 SDL_OpenAudioDevice 函数来打开音频设备并设置所需的音频参数,其中 SDL_AudioSpec 结构体中的 format 字段表示所需的播放采样格式。
因此,要想实现通用的音频播放器,需要对不同的采样格式进行转换。这个过程被称为音频重采样,可以将音频数据从一个采样格式转换为另一个采样格式。在重采样过程中需要注意保持音频质量、减少失真和降噪等。
一般来说,重采样的核心思想是通过插值或者截断的方式改变采样率,达到从一种采样格式转换为另一种采样格式的目的。在实现过程中,可以使用各种算法,例如线性插值、卷积或者傅里叶变换等来实现重采样。
因此,在构建音频播放器时,需要在播放前对音频数据进行重采样,以保证其与系统所支持的播放采样格式相同,从而实现播放效果。
与图像转换类似,FFmpeg 中也提供了音频重采样的工具: libswresample。使用的步骤包括:
SwrContext
重采样上下文av_opt_set_int
配置上下文变量,通常包括如下信息:av_opt_set_int(swr, "in_channel_count", in_num_channels, 0);
av_opt_set_int(swr, "out_channel_count", out_num_channels, 0);
av_opt_set_int(swr, "in_channel_layout", in_channel_layout, 0);
av_opt_set_int(swr, "out_channel_layout", out_channel_layout, 0);
av_opt_set_int(swr, "in_sample_rate", in_sample_rate, 0);
av_opt_set_int(swr, "out_sample_rate", out_sample_rate, 0);
av_opt_set_sample_fmt(swr, "in_sample_fmt", in_sample_format, 0);
av_opt_set_sample_fmt(swr, "out_sample_fmt", out_sample_format, 0);
swr_init(swr)
初始化上下午swr_convert
进行重采样在我们的教程中封装了 FFmpegAudioResampler 进行音频重采样,具体实现请参看源码。
基于 FFmpeg 的跨平台视频播放器简明教程(五):使用 SDL 播放视频 中,我们已经知晓如何使用 FFmpeg + SDL 进行视频画面的播放。基于这份代码,我们添加上播放音频的逻辑。
整体框架如上图,其中:
这个框架基本包含了播放器中所有基本要素,后续播放器音画同步、seek 等功能都基于此框架实现。
具体代码实现在 ffmpeg_video_player_tutorial-my_tutorial03 中,详细的代码说明就此略过,如有不清楚的地方可以直接评论。
本文首先介绍了计算机音频的基本概念,并对 SDL 中播放音频的流程进行了说明;接着,介绍了 FFmpeg 中音频重采样的功能;最后,结合上述知识点和之前视频播放的功能,对视频播放器整体框架做了介绍,并给出了具体代码实现。