在前面的文章中已经能够利用 SDL 去播放 YUV 视频文件了,接下来要通过 SDL 去播放 PCM 音频文件。
SDL 播放音频文件有两种方法,可以理解成 推(push)
和拉(pull)
两种模式。
推
就是我们主动向设备缓冲区填充 Buffer ,而 拉
就是由设备拉取 Buffer 填充到缓冲区。
在一些开发模型中,如果数据传递能够抽象成流
的形式,那么肯定就会有推
和拉
两种模式。
本篇文章主要是讲解 SDL 以推的形式播放音频文件。
PCM 文件素材准备
首先还是得准备素材,做音视频相关实验就是这么麻烦~~
找一个 mp3 文件,使用 FFmpeg 命令将它转换成 pcm 文件,方便的话可以直接使用代码仓库提供的 mp3 文件。
不像在视频播放中准备素材那样简单,音频文件对于参数的信息要求多一点。首先要使用 ffmpeg 查看 mp3 文件的一些信息,比如采样率、声道数等。
ffmpeg -i file_name.mp3
得到如图所示的信息,可以看到 mp3 文件采样率是 44100 Hz,双声道,再使用 FFmpeg 转换时要用到上面的信息。
ffmpeg -i file_name.mp3 -acodec pcm_s16le -f s16le -ac 2 -ar 44100 file_name.pcm
其中:
- -acodec pcm_s16le
- 指定编码器
- -f s16le
- 指定文件格式,是大端模式还是小端模式
- -ac 2
- 指定通道数,2 代表双通道
- -ar 44100
- 指定采样率,这里是 44100 Hz
在转换时要根据原文件的采样率和声道数进行转换,否则转换后的 pcm 文件播放声音不对了。
ffplay -ar 44100 -channels 2 -f s16le -i file_name.pcm
通过上面的命令可以验证转换是否正确,还是要注意声道数和采样率的设置,如果没问题的话,说明 PCM 文件素材就准备完毕,可以进行代码实践了。
代码实践
首先要通过 SDL_OpenAudioDevice
方法打开一个音频设备。
SDL_OpenAudioDevice(constchar
*device,
int iscapture,
const SDL_AudioSpec * desired,
SDL_AudioSpec *obtained,
int allowed_changes);
其中结构体 SDL_AudioSpec
指定了一系列音频相关的参数,具体如下:
typedefstruct SDL_AudioSpec
{
int freq; /**< DSP frequency -- samples per second */
SDL_AudioFormat format; /**< Audio data format */
Uint8 channels; /**< Number of channels: 1 mono, 2 stereo */
Uint8 silence; /**< Audio buffer silence value (calculated) */
Uint16 samples; /**< Audio buffer size in sample FRAMES (total samples divided by channel count) */
Uint16 padding; /**< Necessary for some compile environments */
Uint32 size; /**< Audio buffer size in bytes (calculated) */
SDL_AudioCallback callback; /**< Callback that feeds the audio device (NULL to use SDL_QueueAudio()). */
void *userdata; /**< Userdata passed to callback (ignored for NULL callbacks). */
} SDL_AudioSpec;
这些参数和音频是息息相关的,比如采样率、声道、音频数据格式、采样个数等,后面会专门去介绍它们。
SDL_OpenAudioDevice
方法有两个参数 desired
和 obtained
都是 SDL_AudioSpec
类型的。
这里的意思是我们传入 desired
指定的音频参数,但不一定是 SDL 支持的,所以 SDL 会返回一个它支持的参数信息放在 obtained
里面。
不过为了简单就先把它写死好了,但即使写死了有些信息还是要和你的 PCM 文件对应上才行,比如 freg
采样率和 channels
通道数等。
SDL_AudioSpec audioSpec;
audioSpec.freq = 44100;
audioSpec.format = AUDIO_S16SYS;
audioSpec.channels = 2;
audioSpec.silence = 0;
audioSpec.samples = 1024;
// 因为是推模式,所以这里为 nullptr
audioSpec.callback = nullptr;
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;
}
注意到 SDL_AudioSpec
有个参数 callback
设置为了 nullptr 。这个回调是为了在 拉
模式中从回调取数据的,因为这里暂时用不到就写成了 nullptr ,下一篇文章就会用到了。
这样就打开了音频设备,返回一个文件 Id,如果结果小于 2 说明打开失败了。
接下来通过 SDL_PauseAudioDevice
方法去播放或者暂停音乐。
SDL_PauseAudioDevice(SDL_AudioDeviceID dev,
int pause_on);
SDL_AudioDeviceID
参数就是上面返回的文件 Id,pause_on
为 0 的话代表播放,1 代表暂停。
最后就要开始主动向设备缓冲区填充 Buffer 了。
就向 SDL 播放 YUV 视频那样,要从 PCM 文件中读取一块 Buffer ,然后通过 SDL_QueueAudio
方法进行填充。
int bufferSize = 4096;
char* buffer = (char *)malloc(bufferSize);
// 省略中间代码
num = fread(buffer,1,bufferSize,pFile);
if (num){
// 填充
SDL_QueueAudio(deviceId,buffer,bufferSize);
}
如上代码,首先定义了缓冲区的大小 4096,然后 fread 方法读取这么大的内容,最后把它填充进去。
此时运行程序,就会听到和原来 mp3 文件一样的声音了。
不过这里有要注意的地方,并不是填充了一下 Buffer 就马上会有声音播放出来的,要多填充一些才会有声音播放。
另外,当播放声音时,必须要让程序不能退出,因为音频播放并不是一个阻塞当前主线程的方法,填充完数据就不管了的话,是听不到声音的。要么加个 SDL_Delay 方法要么就把 SDL_QueueAudio
方法放在接受消息队列信息的循环中,我采用的就是后者。
上面已经实现了 推
的模式去播放,接下来看看 拉
的模式如何实现。
还是用同样的 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 播放音频的两种方式,后续会主要以 拉
的模式进行开发。
本文具体代码见仓库:
https://github.com/glumes/av-beginner
本篇文章对应的提交 tag
为 av-beginner-004
,可切换至对应源码查看。
能力有限,文中有不对之处,欢迎加我微信 ezglumes 进行交流~~
技术交流,欢迎加我微信:ezglumes ,拉你入技术交流群。
本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/yinshipin/18665.html