不要退出QwQ, 我可以表演一下当猫娘喵。
U2B和B站都已经被OpenAI教程占领了,现在搜索OpenAL它会认为你拼错了……不管怎样,这是一个OpenAL的中文教程。
其实更像是笔记
OpenAL是做什么的
据我所知,是一个3D音频库,现在常见的一个开源实现是OpenAL Soft。
一些概念
想象你现在正在一个大剧院里,大剧院当然有很多人。舞台上有演员,台下有观众,那么:
- 演员:Source
声音从哪里来?它可以拥有一个位置和一个方向,表明它正在向哪个方向播放音频。 - 观众:Listener
你的耳朵在哪里?是一个位置。 - 音频内容:Buffer
正在放什么?这个放着你的音频。
数据类型
和OpenGL非常相似。
| OpenAL | OpenALC | OpenGL | C++ |
|---|---|---|---|
| ALboolean | ALCboolean | GLboolean | std::int8_t |
| ALbyte | ALCbyte | GLbyte | std::int8_t |
| ALubyte | ALCubyte | GLubyte | std::uint8_t |
| ALchar | ALCchar | GLchar | char |
| ALshort | ALCshort | GLshort | std::int16_t |
| ALushort | ALCushort | GLushort | std::uint16_t |
| ALint | ALCint | GLint | std::int32_t |
| ALuint | ALCuint | GLuint | std::uint32_t |
| ALsizei | ALCsizei | GLsizei | std::int32_t |
| ALenum | ALCenum | GLenum | std::uint32_t |
| ALfloat | ALCfloat | GLfloat | float |
| ALdouble | ALCdouble | GLdouble | double |
| ALvoid | ALCvoid | GLvoid | void |
所以OpenALC是什么?
它是Open Audio Library Context(直译:开源音频库上下文)。它管你用什么东西播放音频,比如你的扬声器或者是耳机。而OpenAL不管这些。
关于加载音频
OpenAL不提供这样的功能(就像OpenGL不提供图像加载一样),你需要自己想其他办法。加载wav可以用这个,ogg可以用这个或者libogg和libvorbis。
开始吧
首先你需要打开音频设备:
#include <AL/al.h>#include <AL/alc.h>
ALCdevice *device = alcOpenDevice(nullptr);if (device == nullptr) { // 打开失败}nullptr 或者 NULL 参数表示使用系统的默认音频设备。
和OpenGL一样,你需要一个上下文。不过OpenAL的更简单一些:
ALCcontext *context = alcCreateContext(device, nullptr);if (context == nullptr) { // 失败}关于 alcCreateContext 的第二个参数,它是用于指定属性的列表:
const ALCint attrib_list[] = { ALC_FREQUENCY, 44100, ALC_REFRESH, 60, ALC_SYNC, ALC_FALSE, 0 // 你需要一个0来终止列表};ALCcontext *context = alcCreateContext(device, attrib_list);if (context == nullptr) { // 失败}下面是一些属性:
| 名称 | 描述 |
|---|---|
| ALC_FREQUENCY | 输出Buffer的频率,以赫兹(Hz)为单位(例如44100) |
| ALC_REFRESH | 刷新间隔,以赫兹(Hz)为单位 |
| ALC_SYNC | 如果为真,上下文将会是同步的(而非异步的),通常我们用 ALC_FALSE |
| ALC_MONO_SOURCES | 分配的单声道音源数量 |
| ALC_STEREO_SOURCES | 和上面一样,只不过是立体声 |
注意:这些只是请求,实际可能不是你指定的那样。
将上下文设置成当前上下文:
if (!alcMakeContextCurrent(context)) { // 失败}当然,我们用完了得释放资源,在你的程序最后加上:
alcDestroyContext(context);alcCloseDevice(device);准备Buffer
首先我们得告诉OpenAL我们需要一个Buffer,生成Buffer我们需要用到函数alGenBuffers:
ALuint buffer;alGenBuffers(1, &buffer);和OpenGL一样,第一个参数是我们要生成的数量,这里我们只要一个,所以我们填1;第二个参数是一个指向ID的指针,所以我们把buffer变量取地址传进去。
错误处理
如果你想要知道哪里发生了错误……使用 alGetError ,和OpenGL一样,它会返回一个整数错误码,0表示没有错误。其他常见的值如下:
| 标识 | 描述 | 数值 |
|---|---|---|
| AL_NO_ERROR | (0)没有错误 | 0 |
| AL_INVALID_NAME | 传进去的ID不合法 | 0xA001(40961) |
| AL_INVALID_ENUM | 传进去了一个不合法的枚举 | 0xA002(40962) |
| AL_INVALID_VALUE | 值不合法 | 0xA003(40963) |
| AL_INVALID_OEPRATION | 请求的操作不合法(比如错误的上下文状态) | 0xA004(40964) |
| AL_OUT_OF_MEMORY | 正如其名,(声卡)内存不足 | 0xA005(40965) |
注意:当它被调用的时候,它会清除所有错误标记。换言之,如果返回了一个错误,你无法确定那是唯一的一个标记。
加载音频
这需要用音频库,我这里用 stb_vorbis.c :
// 在另一个头文件里写 stb_vorbis.h#define STB_VORBIS_HEADER_ONLY#include "stb_vorbis.c"
// 你的main.cpp#include "stb_vorbis.h"
int length;int channels;int sample_rate;int error;// 加载的ogg文件路径std::string file_name = "./test.ogg";stb_vorbis *vorbis = stb_vorbis_open_filename(file_name.c_str(), &error, nullptr);if (vorbis == nullptr) { std::cerr << "打开OGG文件失败: " << file_name << '\n'; return -1;}stb_vorbis_info info = stb_vorbis_get_info(vorbis);channels = info.channels;sample_rate = info.sample_rate;// 总样本数unsigned int total_samples = stb_vorbis_stream_length_in_samples(vorbis);length = total_samples * info.channels;std::vector<short> data(length);// 解码实际数据int samples_decoded = stb_vorbis_get_samples_short_interleaved( vorbis, info.channels, data.data(), length);if ((unsigned int)samples_decoded < total_samples) { std::cerr << "Warning: did not decode all samples\n";}stb_vorbis_close(vorbis);我们暂时只需要其中的channels数量和每sample位数,而stb_vorbis解码为16位PCM。
为什么vector的元素类型是short:因为16位数字占两个字节,而在大多数平台上,short类型是两字节的。
检查音频数据的格式
接下来你需要检查音频数据的格式,告诉OpenAL如何处理你的数据:
// 上面已经提到了,在不进行特别修改的情况下,// stb_vorbis只能解码为16位PCM,所以我这里直接写成常量const int bits_per_sample = 16;
ALenum format;if (channels == 1 && bits_per_sample == 8) { format = AL_FORMAT_MONO8;} else if (channels == 1 && bits_per_sample == 16) { format = AL_FORMAT_MONO16;} else if (channels == 2 && bits_per_sample == 8) { format = AL_FORMAT_STEREO8;} else if (channels == 2 && bits_per_sample == 16) { format = AL_FORMAT_STEREO16;} else { // 未知格式}声音数据是这样的:我们有一个或者多个通道(一个勉强的解释是:你的耳朵数量大于1),而每个sample有很多位,数据由sample组成。
以及,为了满足你的好奇心。英语中mono表示单声道,stereo表示立体声。
总sample数的计算
事实上,如果你只是跟着我提供的这个例子做的话,你不需要算这两个东西。直接跳到上传数据,填充Buffer就可以了。
int number_of_samples = data_size / (number_of_channels * (bits_per_sample / 8));总时长的计算
size_t duration = number_of_samples / sample_rate;上传数据,填充Buffer
现在我们终于可以上传数据了:
alBufferData(buffer, format, data.data(), data.size() * sizeof(short), sample_rate);我们还需要一个Source
别忘了OpenAL是一个3D音频库,所以我们还需要一个喇叭。
// 生成SourceALuint source;alGenSources(1, &source);// 设置属性alSourcef(source, AL_PITCH, 1);alSourcef(source, AL_GAIN, 1.0f);alSource3f(source, AL_POSITION, 0, 0, 0);alSource3f(source, AL_VELOCITY, 0, 0, 0);alSourcei(source, AL_LOOPING, AL_FALSE);alSourcei(source, AL_BUFFER, buffer);你可以看到一些东西,比如我们的位置( AL_POSITION ),和喇叭的朝向( AL_VELOCITY )。
解释一下其余东西:
AL_LOOPING:它表示是否循环播放,这里我们不需要,所以设置假。AL_GAIN:一个更常见的名字是音量。这里我们设置1.0。AL_BUFFER:这很明显了,不解释它了。
播放声音
alSourcePlay(source);ALint state = AL_PLAYING;while (state == AL_PLAYING) { alGetSourcei(source, AL_SOURCE_STATE, &state);}我们调用 alSourcePlayer 开始播放Source。然后我们准备一个用来存储 AL_SOURCE_STATE 的东西。然后在循环中更新它的值。这样就可以等待它播放完成(变成 AL_STOPPED )。
清理
我们已经讲过关闭设备和上下文了。我们还有一个Source和Buffer需要清理:
alDeleteSource(1, &source);alDeleteBuffers(1, &buffer);WARNING尽量不要尝试放歌曲。这些代码只是把音频整个解码到内存中再播放。这样对于较长的文件(比如完整的歌曲)是有问题的,一个是加载时间长。第二个是占内存太大了(实测5分钟音乐能占120MB左右的内存)。后面我们会介绍其他方法来播放歌曲。
部分信息可能已经过时