接上篇 SDL 播放 PCM 音频文件,已经实现了 推
的模式去播放,接下来看看 拉
的模式如何实现。
PCM 文件素材准备
前面的文章中已经准备好了相关素材,这里就不重复了,还是用同样的 PCM 文件作为这次实验素材。
代码实践
首先还是要通过 SDL_OpenAudioDevice
方法打开一个音频设备。
SDL_AudioSpec audioSpec;
audioSpec.freq = 44100;
audioSpec.format = AUDIO_S16SYS;
audioSpec.channels = 2;
audioSpec.silence = 0;
audioSpec.samples = 1024;
// 拉的模式,这里要传一个函数
audioSpec.callback = fill_audio;
SDL_AudioDeviceID deviceId;
if ((deviceId = SDL_OpenAudioDevice(nullptr, 0, &audioSpec, nullptr, SDL_AUDIO_ALLOW_ANY_CHANGE)) < 2) {
cout << "open audio device failed " << endl;
return -1;
}
不同的是,这里 callback
参数不能是 nullptr 了,要传一个函数指针。这个函数在 拉
模式下会不断回调,从而将音频数据填充给设备缓冲区。
函数声明如下:
typedef void (SDLCALL * SDL_AudioCallback) (
// 传用户自定义的数据
void *userdata,
// 指向要填充给设备缓冲区的音频数据Buffer的指针
Uint8 * stream,
// 音频数据Buffer的长度
int len);
参数 stream
是个指针类型,它指向要填充给设备缓冲区的音频数据 Buffer ,而 len 就是 Buffer 的长度。userdata
是我们自定义的数据,需要的时候可以用到。
在这个函数中我们要做的就是将读取的 PCM 音频数据传给 stream
指向的 Buffer ,而且还不能超出 len 的长度,如果超出了截断一下,下次回调时传剩下的部分。
因此就有了如下的实现:
// 读取出 pcm 数据长度
static Uint32 audio_len;
// 读取出的音频数据 Buffer
static Uint8 *audio_pos;
// 函数实现
void fill_audio(void *udata, Uint8 *stream, int len) {
SDL_memset(stream, 0, len);
if (audio_len == 0) {
return;
}
// 数据大小不能超过 len
len = len > audio_len ? audio_len : len;
// 将 stream 和 audio_pos 进行混合播放
// SDL_MixAudio(stream, audio_pos, len, SDL_MIX_MAXVOLUME);
// 单独播放 audio_pos,也就是解码出来的音频数据
memcpy(stream, audio_pos, len);
audio_pos += len;
audio_len -= len;
if (audio_len <= 0){
// 读取完了,通知继续读取数据
notifyGetAudioFrame();
}
}
首先将 stream
数据清空。然后比较读出的 pcm 数据长度 audio_len
和 len
的大小,保证数据大小不超过 len
的要求。
在播放时,也就是给 stream
写数据时有两种方式。一种是直接 memcpy
将音频数据 audio_pos
拷贝到 Buffer 上就好了。另一种是通过 SDL_MixAudio
方法。
SDL_MixAudio
方法顾名思义就是混音了,将 stream
和音频数据 audio_pos
混合播放,由于一开始就将 stream
数据清空为 0 了,所以看似混音,实际上和直接播放音频数据效果一致的。
最后,如果读出的 pcm 数据长度大于 len
,那说明数据还没有全部填充完,下一次回调把剩下的填充到缓冲区,同时移动相应的指针位置。
如果小于,就得通知继续读取数据了,这里自定义了一个事件去通知应用读取音频数据。
// 自定义事件,通知读取音频数据
void notifyGetAudioFrame(){
SDL_Event sdlEvent;
sdlEvent.type = SDL_EVENT_BUFFER_END;
SDL_PushEvent(&sdlEvent);
}
// 在程序事件循环中去响应事件,读取音频 Buffer
while (!bQuit) {
while (SDL_PollEvent(&windowEvent)) {
switch (windowEvent.type) {
case SDL_EVENT_BUFFER_END:
// 读取音频数据
if (fread(buffer, 1, bufferSize, pFile)) {
data_count += bufferSize;
audio_chunk = reinterpret_cast<Uint8 *>(buffer);
audio_len = bufferSize;
audio_pos = audio_chunk;
}
default:
break;
}
}
}
在事件的消息循环中进行响应,读取音频 Buffer 。如果读取的到的长度等于 0 了,也可以通过 fseek
方法将指针 seek 到 0,循环读取。
最后运行一下程序,就会播放出和原来 mp3 一样的音乐了。
总结
以上就是音视频基础学习连载的 008
篇。
通过两篇文章讲解了 SDL 播放音频的两种方式,后续会主要以 拉
的模式进行开发。
本文具体代码见仓库:
本篇文章对应的提交 tag
为 av-beginner-004
,可切换至对应源码查看。
能力有限,文中有不对之处,欢迎加我微信 ezglumes 进行交流~~
知识星球
公众号音视频开发进阶对应的知识星球,一个编程开发领域的专业圈子,贩卖知识和技巧! ※ 入群须知:了解该星球能提供的价值和帮助,在提问时务必阐述好背景,附带相关的信息。 iOS 用户可以加我微信 ezglumes 邀请你进星球,有疑问也可以加我微信咨询。 ※ 星球内容: 基础教程: 在知识星球连载的干货教程,可以在专栏中找到,随着时间的推移,教程也会越来越多: - 音视频基础概念 - WebRTC 入门教程及源码实践 - 播放器教程及源码实践 - OpenGL 和特效开发教程 - Vulkan 入门教程 部分内容可以在博客 https://glumes.com 中检索到,后面会在星球里持续更新. 干货分享: 涵盖了移动开发和音视频工程领域的绝大部分,从项目实战角度出发,提升能力,包括但不限于以下领域: - Android/iOS 移动开发 - Camera 开发 - 短视频编辑 SDK 项目实践 - 在线直播和推流 - WebRTC 开发 - 播放器基础和提高 - OpenGL 图像渲染及特效开发 - C++ 基础和提高 - FFmpeg 使用和分析 - 干货资源和书籍分享 不止于技术方面的,各种 IT 新闻、茶余饭后、生活趣事也欢迎大家分享!!! 技术答疑解惑: 针对上述基础教程和干货分享的答疑,另外还有音视频和 IT 开发中的各种交流讨论。 - 基础知识点答疑 - 工业项目实践答疑 - 问题排查思路分析 一个 BUG 排查很久,不如来星球里提个问题,效率提升百倍。 求职和面试辅导: 一站式职场服务,每份工作都值得用心对待!!! - 面试题和面试经验分享 - 简历修改和模拟面试 - 大厂内推和信息同步 - 职场经验分享 - 职业规划和发展分析 ※ 星主和合伙人介绍 星主是公众号音视频开发进阶的作者,也是网站 https://glumes.com 的作者,曾参与过抖音、剪映等头部音视频 APP 底层 SDK 的开发。 合伙人也是在头条、快手从事音视频架构师的职位,具有多年的音视频开发经验,能力圈覆盖了音视频的绝大多数领域,资深音视频从业人员为你保驾护航。
微信公众号
扫描下面的二维码关注我的微信公众号《音视频开发进阶》,推送更多精彩内容!
添加我的微信 ezglumes 拉你入音视频与图形图像技术群一起交流学习~

原创文章,转载请注明来源: 【音视频连载-008】基础学习篇-SDL 播放 PCM 音频文件(下)