系列文章目录

  1. 基于 FFmpeg 的跨平台视频播放器简明教程(一):FFMPEG + Conan 环境集成
  2. 基于 FFmpeg 的跨平台视频播放器简明教程(二):基础知识和解封装(demux)
  3. 基于 FFmpeg 的跨平台视频播放器简明教程(三):视频解码
  4. 基于 FFmpeg 的跨平台视频播放器简明教程(四):像素格式与格式转换
  5. 基于 FFmpeg 的跨平台视频播放器简明教程(五):使用 SDL 播放视频


前言

在上篇文章 基于 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 中的音频

基于 FFmpeg 的跨平台视频播放器简明教程(二):基础知识和解封装(demux)中,提到了一个容器通常同时包含视频流和音频流。当需要音频数据时,可以从音频流中读取AVPacket,然后进行音频解码,得到AVFrame。

FFmpeg的音频解码器支持包括MP3、AAC、FLAC、WAV等多种音频文件格式。解码音频后,数据将存储在AVFrame->data中。要正确使用这些解码后的数据,首先需要了解它们是以何种采样格式(Sample Format)存储的。

在 FFmpeg 中可以使用 AVSampleFormat 枚举类型来确定音频数据的采样格式。常见的采样格式有:S16、S32、FLT、DBL、U8等。当你了解了音频数据的采样格式,就可以对其进行消费,如将音频数据写入音频缓冲区,然后送到音频解码器或音频混合器中进行混音处理,最终实现音频的播放。在 基于 FFMPEG 的音频编解码(二):音频解码 中一文中,有更多关于 FFmpeg 采样格式的介绍,请阅读,此处不再赘述。

音频播放

计算机操作系统是如何播放音频的?计算机操作系统播放音频主要分为以下两部分:

  1. 音频线程:操作系统会启动一个专门负责处理音频数据的音频线程,这个线程的主要任务是将音频数据传递给音频硬件(例如声卡、扬声器)。音频线程在后台运行,它会定期将缓冲区中的音频数据发送给硬件,确保音频播放的连续性和稳定性。

  2. 回调函数:回调函数是音频线程与具体的音频播放应用之间的桥梁。音频线程通过调用回调函数从应用程序中获取待播放的音频数据。这些数据通常需要经过解码、重采样等处理,以适应音频硬件的播放要求。回调函数负责按照预设的格式提供音频数据,例如,对音量进行调整,将音频数据调整为硬件可识别的采样率、比特深度等。

在实际的音频播放过程中,音频线程周期性地调用回调函数,从应用程序中获取新的音频数据,然后发送给音频硬件进行播放。回调函数则需要在每次调用时读取解码后的音频数据并进行处理,以满足音频线程的需求。这样的设计模式使得音频播放能够与应用程序逻辑分离,方便开发者对音频播放过程进行控制,提高音频播放的响应速度与灵活性。同时,这种框架也有利于操作系统统一管理音频资源,保证多个音频应用之间的协同与兼容。

SDL 中播放音频

SDL 同样采用回调函数的形式来播放音频,具体步骤包括:

  1. 初始化SDL 首先需要对SDL进行初始化,以便能够使用它的音频功能。可以通过调用SDL_Init函数来实现,需要传入的参数为SDL_INIT_AUDIO。
if (SDL_Init(SDL_INIT_AUDIO) < 0) {
    // 错误处理逻辑
}
  1. 设置音频规格 接下来需要定义音频的规格,包括采样率、样本格式等。这可以通过填充SDL_AudioSpec结构来完成。
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
  1. 打开音频设备 使用SDL_OpenAudioDevice函数打开音频设备,并传入上一步定义的音频规格。这个函数将返回一个设备ID,以便于后续操作。
SDL_AudioDeviceID audio_dev;
audio_dev = SDL_OpenAudioDevice(NULL, 0, &wanted_spec, &obtained_spec, SDL_AUDIO_ALLOW_ANY_CHANGE);
if (audio_dev == 0) {
    // 错误处理逻辑
}
  1. 实现回调函数 当音频设备需要更多的音频数据时,SDL会回调audio_callback()函数。需要为其提供一个播放位置、缓冲区等信息。
void audio_callback(void *userdata, Uint8 *stream, int len) {
    // 用户实现:将音频数据填充到stream中,长度为len
    // userdata 则是你自定义的数据,可以将其转换成对应的指针
}
  1. 开始播放 通过SDL_PauseAudioDevice函数启动音频设备,开始播放音频。
SDL_PauseAudioDevice(audio_dev, 0);
  1. 事件处理与播放控制 在实际应用中,需要处理不同的事件,如暂停、恢复等。具体处理方式可依据具体需求来实现。
  2. 关闭音频设备与释放资源 当音频播放结束或应用退出时,需要关闭音频设备并释放相关资源。
SDL_CloseAudioDevice(audio_dev);
SDL_Quit();

FFmpeg 音频重采样

在不同的视频文件中,音频流可能采用不同的采样格式,而系统支持的采样格式通常是固定的。例如,在 SDL 中,可以使用 SDL_OpenAudioDevice 函数来打开音频设备并设置所需的音频参数,其中 SDL_AudioSpec 结构体中的 format 字段表示所需的播放采样格式。

因此,要想实现通用的音频播放器,需要对不同的采样格式进行转换。这个过程被称为音频重采样,可以将音频数据从一个采样格式转换为另一个采样格式。在重采样过程中需要注意保持音频质量、减少失真和降噪等。

一般来说,重采样的核心思想是通过插值或者截断的方式改变采样率,达到从一种采样格式转换为另一种采样格式的目的。在实现过程中,可以使用各种算法,例如线性插值、卷积或者傅里叶变换等来实现重采样。

因此,在构建音频播放器时,需要在播放前对音频数据进行重采样,以保证其与系统所支持的播放采样格式相同,从而实现播放效果。

与图像转换类似,FFmpeg 中也提供了音频重采样的工具: libswresample。使用的步骤包括:

  1. 使用 swr_alloc 创建 SwrContext 重采样上下文
  2. 通过 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);
  1. 使用 swr_init(swr) 初始化上下午
  2. 初始化上下文成功后,即可调用 swr_convert 进行重采样

在我们的教程中封装了 FFmpegAudioResampler 进行音频重采样,具体实现请参看源码。

FFmpeg 解码,SDL 播放视频与音频

基于 FFmpeg 的跨平台视频播放器简明教程(五):使用 SDL 播放视频 中,我们已经知晓如何使用 FFmpeg + SDL 进行视频画面的播放。基于这份代码,我们添加上播放音频的逻辑。
在这里插入图片描述

整体框架如上图,其中:

  1. Demuxer 解封装,负责将音频流和视频流分开,将不同流的 Packet 送到不同的 Packet Queue 中。
  2. Codec 解码器,分为音频解码器和视频解码器,负责从 Packet Queue 读取 Packet,然后解码成 Frame,并将 Frame 送到不同的 Frame Queue 中。
  3. 主线程中,从 Video Frame Queue 中读取 Frame,经过 Image Convert 进行像素格式转换,通过 SDL Render 最终渲染至屏幕中。
  4. 音频线程中,通过 SDL 回调函数去 Audio Frame Queue 中读取 Frame,经过 Audio Resampler 进行重采样,最终通过喇叭播放出声音。

这个框架基本包含了播放器中所有基本要素,后续播放器音画同步、seek 等功能都基于此框架实现。

具体代码实现在 ffmpeg_video_player_tutorial-my_tutorial03 中,详细的代码说明就此略过,如有不清楚的地方可以直接评论。

总结

本文首先介绍了计算机音频的基本概念,并对 SDL 中播放音频的流程进行了说明;接着,介绍了 FFmpeg 中音频重采样的功能;最后,结合上述知识点和之前视频播放的功能,对视频播放器整体框架做了介绍,并给出了具体代码实现。

参考

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐