SDL2学习:二、音频播放流程及相关api(SDL音频播放的两种模式)
SDL音频播放两种模式
SDL 播放音频文件有两种方法,可以理解成 推(push) 和 拉(pull) 两种模式。
推 就是我们主动向设备缓冲区填充 Buffer ,而 拉 就是由设备拉取 Buffer 填充到缓冲区。
两种方式优缺点对比:
- 官方推荐使用推送模式
- 推送延迟较大(推荐前几帧抛弃,待系统稳定时再播放)
- 推送没有办法精确的进行音视频同步。
- 推送并不是一来数据就播放。
- 拉去模式:回调函数和主函数会同时操作缓冲区,可能会发生冲突
SDL音频播放模式一(回调函数——拉取模式)
- 目前最常用的一种方式
1、初始化
- 1)初始化SDL
- 2)根据参数(SDL_AudioSpec)打开音频设备
2、循环播放数据
- 1)播放音频数据
- 2)延时等待播放完成
【拉取模式具体流程api和结构体】
【一、初始化】SDL_Init()
使用SDL_Init()初始化SDL。该函数可以确定希望激活的子系统。SDL_Init()函数原型如下:
int SDLCALL SDL_Init(Uint32 flags)
其中,flags可以取下列值:
- SDL_INIT_TIMER:定时器
- SDL_INIT_AUDIO:音频
- SDL_INIT_VIDEO:视频
- SDL_INIT_JOYSTICK:摇杆
- SDL_INIT_HAPTIC:触摸屏
- SDL_INIT_GAMECONTROLLER:游戏控制器
- SDL_INIT_EVENTS:事件
- SDL_INIT_NOPARACHUTE:不捕获关键信号(这个不理解)
- SDL_INIT_EVERYTHING:包含上述所有选项
有关SDL_Init()有一点需要注意:初始化的时候尽量做到“够用就好”,而不要用SDL_INIT_EVERYTHING。因为有些情况下使用SDL_INIT_EVERYTHING会出现一些不可预知的问题。例如,在MFC应用程序中播放纯音频,如果初始化SDL的时候使用SDL_INIT_EVERYTHING,那么就会出现听不到声音的情况。后来发现,去掉了SDL_INIT_VIDEO之后,问题才得以解决。
【二、配置音频参数 并 打开音频设备】SDL_OpenAudio()
使用SDL_OpenAudio()打开音频设备。该函数需要传入一个SDL_AudioSpec的结构体。DL_OpenAudio()的原型如下。
【函数原型】
int SDLCALL SDL_OpenAudio(SDL_AudioSpec * desired,
SDL_AudioSpec * obtained);
【参数分析】
- desired:期望的参数。
- obtained:实际音频设备的参数,一般情况下设置为NULL即可。
SDL_AudioSpec结构体
typedef struct 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 samples (power of 2) */
Uint16 padding; /**< Necessary for some compile environments */
Uint32 size; /**< Audio buffer size in bytes (calculated) */
SDL_AudioCallback callback;
void *userdata;
} SDL_AudioSpec;
其中包含了关于音频各种参数:
freq:音频数据的采样率。常用的有48000,44100等。
format:音频数据的格式。举例几种格式:
AUDIO_U16SYS:Unsigned 16-bit samples
AUDIO_S16SYS:Signed 16-bit samples
AUDIO_S32SYS:32-bit integer samples
AUDIO_F32SYS:32-bit floating point samples
channels:声道数。例如单声道取值为1,立体声取值为2。
silence:设置静音的值。
samples:音频缓冲区中的采样个数,要求必须是2的n次方。
padding:考虑到兼容性的一个参数。
size:音频缓冲区的大小,以字节为单位。
callback:填充音频缓冲区的回调函数。
userdata:用户自定义的数据。
在这里记录一下填充音频缓冲区的回调函数的作用。当音频设备需要更多数据的时候会调用该回调函数。回调函数的格式要求如下。
void (SDLCALL * SDL_AudioCallback) (void *userdata, Uint8 * stream,
int len);
回调函数的参数含义如下所示。
userdata:SDL_AudioSpec结构中的用户自定义数据,一般情况下可以不用。
stream:该指针指向需要填充的音频缓冲区。
len:音频缓冲区的大小(以字节为单位)。
在回调函数中可以使用SDL_MixAudio()完成混音等工作。众所周知SDL2和SDL1.x关于视频方面的API差别很大。但是SDL2和SDL1.x关于音频方面的API是一模一样的。唯独在回调函数中,SDL2有一个地方和SDL1.x不一样:SDL2中必须首先使用SDL_memset()将stream中的数据设置为0。
【三、循环播放数据】
【1)播放音频数据】
使用SDL_PauseAudio()可以播放音频数据。SDL_PauseAudio()的原型如下。
void SDLCALL SDL_PauseAudio(int pause_on)
当pause_on设置为0的时候即可开始播放音频数据。设置为1的时候,将会播放静音的值
【2)延时等待播放完成】
这一步就是延时等待音频播放完毕了。使用像SDL_Delay()这样的延时函数即可
SDL音频播放模式二(推送模式)
【一、初始化】SDL_Init() 与模式一相同
使用SDL_Init()初始化SDL。该函数可以确定希望激活的子系统。SDL_Init()函数原型如下:
int SDLCALL SDL_Init(Uint32 flags)
【二、设置音频参数并打开音频设备】与模式一不同
1、设置播放音频参数
使用和模式一相同的结构体
示例:
/* 音频初始化 */
SDL_AudioSpec wanted_spec;
wanted_spec.freq = audFormat->sampleRate;
wanted_spec.format = AUDIO_S16SYS;
wanted_spec.channels = audFormat->channelNum;
wanted_spec.silence = 0; // 设置静音的值
wanted_spec.samples = audFormat->frameLength;
wanted_spec.callback = nullptr; // 推送模式播放音频
wanted_spec.userdata = NULL;
2、打开音频设备
使用SDL_OpenAudioDevice 方法打开一个音频设备。
函数声明:
SDL_AudioDeviceID SDL_OpenAudioDevice(
const char *device,
int iscapture,
const SDL_AudioSpec *desired,
SDL_AudioSpec *obtained,
int allowed_changes);
参数说明:
- *device: 设备名称,可以设为NULL,打开默认设备
- iscapture: 0表示播放,非0表示录制
- *desired: desired 和 obtained 都是 SDL_AudioSpec 类型的。
- *obtained: 我们传入 desired 指定的音频参数,但不一定是 SDL 支持的,所以 SDL 会返回一个它支持的参数信息放在 obtained 里面。
- allowed_changes:
返回值:
- 返回一个有效的设备 ID,成功时 > 0,失败时返回 0; 调用 SDL_GetError() 获取更多信息。
- 为了与 SDL 1.2 兼容,这将永远不会返回 1,因为 SDL 为旧版 SDL_OpenAudio() 函数保留了该 ID。
- 所以成功返回值不小于2。
完整示例如下:
SDL_AudioSpec audioSpec;
audioSpec.freq = 44100;
audioSpec.format = AUDIO_S16SYS;
audioSpec.channels = 2;
audioSpec.silence = 0;
audioSpec.samples = 1024;
audioSpec.callback = nullptr; // 因为是推模式,所以这里为 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;
}
【3、播放或暂停】
使用 SDL_PauseAudioDevice 方法去播放或者暂停音乐
函数声明:
void SDL_PauseAudioDevice(SDL_AudioDeviceID dev,
int pause_on);
参数说明:
- dev: a device opened by SDL_OpenAudioDevice()
- pause_on: non-zero to pause, 0 to unpause
【4、主动向设备缓冲区填充 Buffer 】关键
使用 SDL_QueueAudio 向设备缓冲区填充Buffer,理论上填充之后处理buffer不会影响音频播放
SDL2 不支持平面音频。在对音频进行排队之前,您需要将平面音频格式重新采样为非平面音频格式
函数声明:
int SDL_QueueAudio(SDL_AudioDeviceID dev, const void *data, Uint32 len);
参数说明:
- dev: 前面打开的设备id
- *data: 数据排队到设备供以后播放
- len: 数据字节数,不是采样点数
返回值:
- 成功时返回 0,失败时返回负错误代码
示例:
int bufferSize = 4096;
char* buffer = (char *)malloc(bufferSize);
// 省略中间代码
num = fread(buffer,1,bufferSize,pFile);
if (num){
// 填充
SDL_QueueAudio(deviceId,buffer,bufferSize);
}
不过这里有要注意的地方,并不是填充了一下 Buffer 就马上会有声音播放出来的,要多填充一些才会有声音播放。
另外,当播放声音时,必须要让程序不能退出,因为音频播放并不是一个阻塞当前主线程的方法,填充完数据就不管了的话,是听不到声音的。要么加个 SDL_Delay 方法要么就把 SDL_QueueAudio 方法放在接受消息队列信息的循环中,我采用的就是后者。
【5、设备缓冲队列相关操作】
【SDL_ClearQueuedAudio】
函数功能:
- 丢弃所有等待发送到硬件的排队音频数据
函数声明:
void SDL_ClearQueuedAudio(SDL_AudioDeviceID dev);
参数说明:
- dev: 设备ID
【SDL_GetQueuedAudioSize】
函数功能:
- 获取正在排队的音频字节数
函数声明:
Uint32 SDL_GetQueuedAudioSize(SDL_AudioDeviceID dev);
参数说明:
- 设备ID
返回值:
- Returns the number of bytes (not samples!) of queued audio.
【SDL_DequeueAudio】
函数功能:
- 在非回调设备上取消更多音频排队。
函数声明:
Uint32 SDL_DequeueAudio(SDL_AudioDeviceID dev, void *data, Uint32 len);
参数说明:
- dev: 取消音频排队的设备 ID
- *data: 指向应将音频数据复制到何处的指针
- len: (数据)指向的字节数
返回值:
- 返回已取消排队的字节数,该字节数可能小于请求的字节数
如果您希望在非回调播放设备上对音频进行排队以进行输出,则需要改用SDL_QueueAudio()。如果SDL_DequeueAudio() 与播放设备一起使用,它将始终返回 0。
【6、暂停和关闭操作】
【关闭SDL,采用SDL_CloseAudioDevice】
函数说明:
void SDL_CloseAudioDevice(SDL_AudioDeviceID dev);
【其他api】
void SDL_LockAudioDevice(SDL_AudioDeviceID dev);
void SDL_UnlockAudioDevice(SDL_AudioDeviceID dev);