OpenAudio 与 OpenAudioDevice 简介
使用 SDL 库可以快捷实现 PCM 音频文件的播放功能。以播放 44100Hz,双声道,S16 格式的音频数据为例:
SDL_Init(SDL_INIT_TIMER|SDL_INIT_AUDIO); // 初始化
SDL_AudioSpec audio_spec;
audio_spec.format = AUDIO_S16SYS; // 设置格式
audio_spec.freq = 44100; // 设置频率
audio_spec.channels = 2; // 设置声道数
audio_spec.callback = fill_audio_data; // 设置回调函数
audio_spec.samples = 1024; // 设置缓冲区采样数量
audio_spec.userdata = nullptr;
SDL_OpenAudio(&audio_spec); // 打开音频设备
SDL_PauseAudio(0); // 开始播放
// 其他逻辑:如等待播放结束,清理数据,退出等。
其中回调函数 fill_audio_data
实现如下:
size_t cursor = 0; // 已播放的字节数
std::vector<uint8_t> pcm_data; // 设需播放的pcm数据都存放在 pcm_data 中。
bool end_flag = false;
void fill_audio_data(void *userdata, Uint8 *stream, int len) {
SDL_memset(stream, 0, len); // 先清理缓冲区,避免影响后续数据
// 检查剩余数据是否可填满缓冲区,若不能则只填充一部分。
if (len > pcm_data.size() - cursor) {
len = pcm_data.size() - cursor;
}
if (len == 0) {
// 无数据可播了,那就不填数据了。
// 顺便更新下结束标志,以便主线程处理。
end_flag = true;
return;
}
// 填充数据
SDL_MixAudio(stream, &pcm_data[cursor], len, SDL_MIX_MAXVOLUME);
// 更新已播放字节数
cursor += len;
}
以上代码就是播放PCM功能的主要部分啦。但 SDL_OpenAudio
只能打开一个音频设备,对于同时需要多个音频设备的场景,需用 SDL_OpenAudioDevice
。
SDL_OpenAudioDevice
的函数签名如下:
SDL_AudioDeviceID
SDL_OpenAudioDevice(const char *device, int iscapture,
const SDL_AudioSpec * desired,
SDL_AudioSpec * obtained,
int allowed_changes)
一般使用方式如下:
SDL_AudioSpec audio_spec;
audio_spec.format = AUDIO_S16SYS; // 设置格式
audio_spec.freq = 44100; // 设置频率
audio_spec.channels = 2; // 设置声道数
audio_spec.callback = fill_audio_data; // 设置回调函数
audio_spec.samples = 1024; // 设置缓冲区采样数量
audio_spec.userdata = nullptr;
// SDL_OpenAudio(&audio_spec); // 打开音频设备
auto device_id = SDL_OpenAudioDevice(nullptr, 0, &audio_spec, nullptr, 0);
// SDL_PauseAudio(0); // 开始播放
SDL_PauseAudioDevice(device_id, 0); // 控制 device_id 管理的设备开始播放
问题描述
升级之后发现不能播放了。现象是:
- 有返回值的函数的返回值都正常。如:
SDL_OpenAudioDevice
的返回值device_id
为2
。SDL_Init
的返回值为 0。
- 回调函数
fill_audio_data
有调用。 - 没有声音。。。
解决方案
度娘给的方案
度娘给了几个解决方案,尝试玩之后还是没的声音。。
1. SDL_Init
不要加 SDL_INIT_VIDEO
SDL_Init
不要加 SDL_INIT_VIDEO
。即按如下方式调用:
SDL_Init(SDL_INIT_TIMER|SDL_INIT_AUDIO)
2. OpenAudioDevice
的 allowed_changes
参数设为 SDL_AUDIO_ALLOW_ANY_CHANGE
我的发现
浏览了 SDL_OpenAudio
和 SDL_OpenAudioDevice
的源码,没发现端倪 ,其实也没看懂多少 。不过发现了一个函数 SDL_SetError
,在很多错误返回的地方都会调用该函数记录原因。
于是,我抱着试一试的心态,在回调函数fill_audio_data
中加了一行日志:
void fill_audio_data(void *userdata, Uint8 *stream, int len) {
std::cerr << SDL_GetError() << std::endl;
...
}
然后就得到了内容为 Invalid audio device ID
的错误日志。通过这行日志在源码中找到了 get_audio_device
函数:
static SDL_AudioDevice *
get_audio_device(SDL_AudioDeviceID id)
{
id--;
if ((id >= SDL_arraysize(open_devices)) || (open_devices[id] == NULL)) {
SDL_SetError("Invalid audio device ID");
return NULL;
}
return open_devices[id];
}
然后使用 lldb 调试,打印调用栈。发现是在 SDL_audio.c:1787 行调进来的,id = 0。这就不太对啦,理论上 id = 2 (和 SDL_OpenAudioDevice 的返回值)才对。
继续阅读 SDL_audio.c:1787 的代码。发现里面写死了 id = 1,基本猜到 SDL_MixAudio
是和 SDL_OpenAudio
配套使用得了。
1783 void
1784 SDL_MixAudio(Uint8 * dst, const Uint8 * src, Uint32 len, int volume)
1785 {
1786 /* Mix the user-level audio format */
1787 SDL_AudioDevice *device = get_audio_device(1);
1788 if (device != NULL) {
1789 SDL_MixAudioFormat(dst, src, device->callbackspec.format, len, volume);
1790 }
1791 }
尝试改造回调函数,用 SDL_MixAudioFormat
替换 SDL_MixAudio
。
size_t cursor = 0; // 已播放的字节数
std::vector<uint8_t> pcm_data; // 设需播放的pcm数据都存放在 pcm_data 中。
bool end_flag = false;
void fill_audio_data(void *userdata, Uint8 *stream, int len) {
SDL_memset(stream, 0, len); // 先清理缓冲区,避免影响后续数据
// 检查剩余数据是否可填满缓冲区,若不能则只填充一部分。
if (len > pcm_data.size() - cursor) {
len = pcm_data.size() - cursor;
}
if (len == 0) {
// 无数据可播了,那就不填数据了。
// 顺便更新下结束标志,以便主线程处理。
end_flag = true;
return;
}
// 填充数据
SDL_MixAudioFormat(stream,
&pcm_data[cursor], audio_spec.format, len,
SDL_MIX_MAXVOLUME);
// 更新已播放字节数
cursor += len;
}
如上,将 SDL_MixAudio
替换为 SDL_MixAudioFormat
后就能正常播放声音了。