在C ++中使用OpenAL的指南。第1部分:播放声音

您的游戏需要声音!您可能已经使用OpenGL在屏幕上绘图。您已经弄清了它的API,因此您选择了OpenAL,因为该名称看起来很熟悉。

好消息是,OpenAL也有一个非常熟悉的API。它最初旨在模拟OpenGL规范API。这就是为什么我在众多游戏声音系统中选择它的原因。另外,它是跨平台的。

在本文中,我将详细讨论在用C ++编写的游戏中使用OpenAL需要哪些代码。我们将通过代码示例讨论3D空间中的声音,音乐和声音定位。

OpenAL的历史


我会尽量简短。如上所述,它是故意设计为模仿OpenGL API的,这是有原因的。这是许多人都知道的便捷API,如果图形是游戏引擎的一侧,则声音应该有所不同。最初,OpenAL应该是开源的,但是后来发生了一些事情……

人们对声音的兴趣不及图形,因此Creative最终将OpenAL赋予了它的财产,并且参考实现现在专有的而不是免费的。但!OpenAL 规范仍然是“开放”标准,即已发布

有时会修改规格,但数量不多。声音的变化速度不如图形变化快,因为没有特别的需要。

开放规范允许其他人创建规范的开源实现。OpenAL Soft就是这样一种实现,坦率地说,寻找其他任何实现都是没有意义的。这是我将要使用的实现,并且我建议您也使用它。

她是跨平台的。它的实现非常奇怪-实际上,库内部使用系统中存在的其他声音API。在Windows上,它使用DirectSound,在Unix上,使用OSS因此,她得以跨平台。本质上,这是包装API的大名。

您可能会担心此API的速度。但是不用担心。这是相同的声音,不会产生很大的负载,因此不需要图形API进行的大优化。

但是,有了足够的故事,让我们继续探讨技术。

您需要在OpenAL中编写什么代码?


您需要在您选择的工具链中构建OpenAL Soft。这是一个非常简单的过程,您可以按照“源代码安装”部分中的说明进行操作。我从来没有遇到任何问题,但是如果遇到任何困难,请在原始文章下写评论,或写到OpenAL Soft邮件列表

接下来,您将需要一些声音文件以及下载它们的方法。将音频数据加载到缓冲区中以及各种音频格式的细微细节不在本文的讨论范围之内,但是您可以阅读有关下载和传输Ogg / Vorbis文件的信息。下载WAV文件非常简单,Internet上已经有数百篇关于此的文章。

查找音频文件的任务必须自己决定。您可以下载Internet上的许多噪音和爆炸声。如果有谣言,那么您可以尝试编写自己的Chiptune音乐 [ Habré上的翻译 ]。

另外,请妥善保存《 OpenALSoft程序员指南》该文档是具有“官方”专业知识的pdf更好的文档。

实际上,仅此而已。我们将假定您已经知道如何编写代码,使用IDE和工具链。

OpenAL API概述


正如我多次说过的,它类似于OpenGL API。相似之处在于它基于状态,并且您与描述符/标识符交互,而不是直接与对象本身交互。

OpenGL和OpenAL中的API约定之间存在差异,但是它们并不重要。在OpenGL中,您需要进行特殊的OS调用以生成渲染上下文。这些挑战针对不同的操作系统而有所不同,并且并不是OpenGL规范的真正组成部分。在OpenAL中,一切都不同-上下文创建功能是规范的一部分,并且与操作系统无关,它们是相同的。

与API进行交互时,可以与三种主要类型的对象进行交互。听众(“听众”)是3D空间中“耳朵”的位置(始终只有一个听众)。(“源”)是再次在3D空间中发出声音的“扬声器”。侦听器和信号源可以在空间中移动,并且根据此情况,通过游戏中的扬声器所听到的声音也会发生变化。

最后的对象是缓冲区它们存储声音的样本,这些声音将为听众播放。

也有模式,该游戏的用途改变音频通过OpenAL的处理方式。

资料来源


如上所述,这些对象是声音的来源。可以设置它们的位置和方向,并将它们与回放音频数据的缓冲区关联。

听众


游戏中唯一的一组“耳朵”。收听者听到的内容通过计算机的扬声器复制。他也有职位。

缓冲液


在OpenGL中,等效项是Texture2D。本质上,这是源再现的音频数据。

资料类型


为了能够支持跨平台代码,OpenAL执行一定的动作序列并定义一些数据类型。实际上,它非常精确地遵循OpenGL,因此我们甚至可以直接将OpenAL类型转换为OpenGL类型。下表列出了它们及其等效项。

输入类型键入openalc键入openglC ++ Typedef描述
ALbooleanALCbooleanGLbooleanstd::int8_t8位布尔值
ALbyteALCbyteGLbytestd::int8_t带符号附加代码的 8位整数值
ALubyteALCubyteGLubytestd::uint8_t8位无符号整数值
ALcharALCcharGLcharchar符号
ALshortALCshortGLshortstd::int16_t16位有符号整数值
ALushortALCushortGLushortstd::uint16_t16-
ALintALCintGLintstd::int32_t32-
ALuintALCuintGLuintstd::uint32_t32-
ALsizeiALCsizeiGLsizeistd::int32_t32-
ALenumALCenumGLenumstd::uint32_t32-
ALfloatALCfloatGLfloatfloat32- IEEE 754
ALdoubleALCdoubleGLdoubledouble64- IEEE 754
ALvoidALCvoidGLvoidvoid

OpenAL


有一篇有关如何简化OpenAL错误识别的文章,但是出于完整性考虑,我将在这里重复。有两种类型的OpenAL API调用:常规和上下文。

以上下文开头的调用alc类似于OpenGL win32调用,以获取渲染上下文或在Linux上的对应上下文。声音对于所有操作系统都具有相同的调用来说已经足够简单了。普通电话以开头al。要在上下文调用中出错,请致电alcGetError;对于常规通话,我们致电alGetError。它们返回一个值ALCenum或一个ALenum简单列出可能的错误的值

现在,我们将只考虑一种情况,但在其他所有情况下,它们几乎都是相同的。让我们来应对通常的挑战al首先,创建一个预处理器宏来完成传递细节的工作:

#define alCall(function, ...) alCallImpl(__FILE__, __LINE__, function, __VA_ARGS__)

从理论上讲,你的编译器可能不支持__FILE____LINE__,但是,说实话,我会感到惊讶,如果这被证明是如此。__VA_ARGS__表示可以传递给此宏的可变数量的参数。

接下来,我们实现一个函数,该函数手动接收上次报告的错误并为标准错误流显示一个清晰的值。

bool check_al_errors(const std::string& filename, const std::uint_fast32_t line)
{
    ALenum error = alGetError();
    if(error != AL_NO_ERROR)
    {
        std::cerr << "***ERROR*** (" << filename << ": " << line << ")\n" ;
        switch(error)
        {
        case AL_INVALID_NAME:
            std::cerr << "AL_INVALID_NAME: a bad name (ID) was passed to an OpenAL function";
            break;
        case AL_INVALID_ENUM:
            std::cerr << "AL_INVALID_ENUM: an invalid enum value was passed to an OpenAL function";
            break;
        case AL_INVALID_VALUE:
            std::cerr << "AL_INVALID_VALUE: an invalid value was passed to an OpenAL function";
            break;
        case AL_INVALID_OPERATION:
            std::cerr << "AL_INVALID_OPERATION: the requested operation is not valid";
            break;
        case AL_OUT_OF_MEMORY:
            std::cerr << "AL_OUT_OF_MEMORY: the requested operation resulted in OpenAL running out of memory";
            break;
        default:
            std::cerr << "UNKNOWN AL ERROR: " << error;
        }
        std::cerr << std::endl;
        return false;
    }
    return true;
}

没有太多可能的错误。我在代码中编写的解释是您将获得的有关这些错误的唯一信息,但是规范说明了为什么特定功能可能返回特定错误的原因。

然后,我们实现两个不同的模板函数,这些函数将包装所有的OpenGL调用。

template<typename alFunction, typename... Params>
auto alCallImpl(const char* filename, 
                const std::uint_fast32_t line, 
                alFunction function, 
                Params... params)
->typename std::enable_if_t<!std::is_same_v<void,decltype(function(params...))>,decltype(function(params...))>
{
    auto ret = function(std::forward<Params>(params)...);
    check_al_errors(filename,line);
    return ret;
}

template<typename alcFunction, typename... Params>
auto alcCallImpl(const char* filename, 
                 const std::uint_fast32_t line, 
                 alcFunction function, 
                 ALCdevice* device, 
                 Params... params)
->typename std::enable_if_t<std::is_same_v<void,decltype(function(params...))>,bool>
{
    function(std::forward<Params>(params)...);
    return check_alc_errors(filename,line,device);
}

其中有两个,因为第一个用于返回的OpenAL函数,void第二个用于该函数返回非空值。如果您不太熟悉C ++中的元编程模板,那么请看一下c代码的各个部分std::enable_if它们确定编译器为每个函数调用实现这些模板函数中的哪个。

现在通话也一样alc

#define alcCall(function, device, ...) alcCallImpl(__FILE__, __LINE__, function, device, __VA_ARGS__)

bool check_alc_errors(const std::string& filename, const std::uint_fast32_t line, ALCdevice* device)
{
    ALCenum error = alcGetError(device);
    if(error != ALC_NO_ERROR)
    {
        std::cerr << "***ERROR*** (" << filename << ": " << line << ")\n" ;
        switch(error)
        {
        case ALC_INVALID_VALUE:
            std::cerr << "ALC_INVALID_VALUE: an invalid value was passed to an OpenAL function";
            break;
        case ALC_INVALID_DEVICE:
            std::cerr << "ALC_INVALID_DEVICE: a bad device was passed to an OpenAL function";
            break;
        case ALC_INVALID_CONTEXT:
            std::cerr << "ALC_INVALID_CONTEXT: a bad context was passed to an OpenAL function";
            break;
        case ALC_INVALID_ENUM:
            std::cerr << "ALC_INVALID_ENUM: an unknown enum value was passed to an OpenAL function";
            break;
        case ALC_OUT_OF_MEMORY:
            std::cerr << "ALC_OUT_OF_MEMORY: an unknown enum value was passed to an OpenAL function";
            break;
        default:
            std::cerr << "UNKNOWN ALC ERROR: " << error;
        }
        std::cerr << std::endl;
        return false;
    }
    return true;
}

template<typename alcFunction, typename... Params>
auto alcCallImpl(const char* filename, 
                 const std::uint_fast32_t line, 
                 alcFunction function, 
                 ALCdevice* device, 
                 Params... params)
->typename std::enable_if_t<std::is_same_v<void,decltype(function(params...))>,bool>
{
    function(std::forward<Params>(params)...);
    return check_alc_errors(filename,line,device);
}

template<typename alcFunction, typename ReturnType, typename... Params>
auto alcCallImpl(const char* filename,
                 const std::uint_fast32_t line,
                 alcFunction function,
                 ReturnType& returnValue,
                 ALCdevice* device, 
                 Params... params)
->typename std::enable_if_t<!std::is_same_v<void,decltype(function(params...))>,bool>
{
    returnValue = function(std::forward<Params>(params)...);
    return check_alc_errors(filename,line,device);
}

最大的变化是device所有调用都使用的包含alc以及样式错误ALCenumALC_它们看起来非常相似,并且在很长一段时间内,从很小的更改alalc严重破坏了我的代码和理解,因此我只是继续在它上面继续阅读c

就这样。通常,C ++中的OpenAL调用看起来像以下选项之一:

/* example #1 */
alGenSources(1, &source);
ALenum error = alGetError();
if(error != AL_NO_ERROR)
{
    /* handle different possibilities */
}

/* example #2 */
alcCaptureStart(&device);
ALCenum error = alcGetError();
if(error != ALC_NO_ERROR)
{
    /* handle different possibilities */
}

/* example #3 */
const ALchar* sz = alGetString(param);
ALenum error = alGetError();
if(error != AL_NO_ERROR)
{
    /* handle different possibilities */
}

/* example #4 */
const ALCchar* sz = alcGetString(&device, param);
ALCenum error = alcGetError();
if(error != ALC_NO_ERROR)
{
    /* handle different possibilities */
}

但是现在我们可以这样做:

/* example #1 */
if(!alCall(alGenSources, 1, &source))
{
    /* error occurred */
}

/* example #2 */
if(!alcCall(alcCaptureStart, &device))
{
    /* error occurred */
}

/* example #3 */
const ALchar* sz;
if(!alCall(alGetString, sz, param))
{
    /* error occurred */
}

/* example #4 */
const ALCchar* sz;
if(!alcCall(alcGetString, sz, &device, param))
{
    /* error occurred */
}

这对您来说似乎很奇怪,但是对我来说更方便。当然,您可以选择其他结构。

下载.wav文件


您可以自己下载它们,也可以使用库。这是加载.wav文件的开源实现我疯了,所以我自己做:

std::int32_t convert_to_int(char* buffer, std::size_t len)
{
    std::int32_t a = 0;
    if(std::endian::native == std::endian::little)
        std::memcpy(&a, buffer, len);
    else
        for(std::size_t i = 0; i < len; ++i)
            reinterpret_cast<char*>(&a)[3 - i] = buffer[i];
    return a;
}

bool load_wav_file_header(std::ifstream& file,
                          std::uint8_t& channels,
                          std::int32_t& sampleRate,
                          std::uint8_t& bitsPerSample,
                          ALsizei& size)
{
    char buffer[4];
    if(!file.is_open())
        return false;

    // the RIFF
    if(!file.read(buffer, 4))
    {
        std::cerr << "ERROR: could not read RIFF" << std::endl;
        return false;
    }
    if(std::strncmp(buffer, "RIFF", 4) != 0)
    {
        std::cerr << "ERROR: file is not a valid WAVE file (header doesn't begin with RIFF)" << std::endl;
        return false;
    }

    // the size of the file
    if(!file.read(buffer, 4))
    {
        std::cerr << "ERROR: could not read size of file" << std::endl;
        return false;
    }

    // the WAVE
    if(!file.read(buffer, 4))
    {
        std::cerr << "ERROR: could not read WAVE" << std::endl;
        return false;
    }
    if(std::strncmp(buffer, "WAVE", 4) != 0)
    {
        std::cerr << "ERROR: file is not a valid WAVE file (header doesn't contain WAVE)" << std::endl;
        return false;
    }

    // "fmt/0"
    if(!file.read(buffer, 4))
    {
        std::cerr << "ERROR: could not read fmt/0" << std::endl;
        return false;
    }

    // this is always 16, the size of the fmt data chunk
    if(!file.read(buffer, 4))
    {
        std::cerr << "ERROR: could not read the 16" << std::endl;
        return false;
    }

    // PCM should be 1?
    if(!file.read(buffer, 2))
    {
        std::cerr << "ERROR: could not read PCM" << std::endl;
        return false;
    }

    // the number of channels
    if(!file.read(buffer, 2))
    {
        std::cerr << "ERROR: could not read number of channels" << std::endl;
        return false;
    }
    channels = convert_to_int(buffer, 2);

    // sample rate
    if(!file.read(buffer, 4))
    {
        std::cerr << "ERROR: could not read sample rate" << std::endl;
        return false;
    }
    sampleRate = convert_to_int(buffer, 4);

    // (sampleRate * bitsPerSample * channels) / 8
    if(!file.read(buffer, 4))
    {
        std::cerr << "ERROR: could not read (sampleRate * bitsPerSample * channels) / 8" << std::endl;
        return false;
    }

    // ?? dafaq
    if(!file.read(buffer, 2))
    {
        std::cerr << "ERROR: could not read dafaq" << std::endl;
        return false;
    }

    // bitsPerSample
    if(!file.read(buffer, 2))
    {
        std::cerr << "ERROR: could not read bits per sample" << std::endl;
        return false;
    }
    bitsPerSample = convert_to_int(buffer, 2);

    // data chunk header "data"
    if(!file.read(buffer, 4))
    {
        std::cerr << "ERROR: could not read data chunk header" << std::endl;
        return false;
    }
    if(std::strncmp(buffer, "data", 4) != 0)
    {
        std::cerr << "ERROR: file is not a valid WAVE file (doesn't have 'data' tag)" << std::endl;
        return false;
    }

    // size of data
    if(!file.read(buffer, 4))
    {
        std::cerr << "ERROR: could not read data size" << std::endl;
        return false;
    }
    size = convert_to_int(buffer, 4);

    /* cannot be at the end of file */
    if(file.eof())
    {
        std::cerr << "ERROR: reached EOF on the file" << std::endl;
        return false;
    }
    if(file.fail())
    {
        std::cerr << "ERROR: fail state set on the file" << std::endl;
        return false;
    }

    return true;
}

char* load_wav(const std::string& filename,
               std::uint8_t& channels,
               std::int32_t& sampleRate,
               std::uint8_t& bitsPerSample,
               ALsizei& size)
{
    std::ifstream in(filename, std::ios::binary);
    if(!in.is_open())
    {
        std::cerr << "ERROR: Could not open \"" << filename << "\"" << std::endl;
        return nullptr;
    }
    if(!load_wav_file_header(in, channels, sampleRate, bitsPerSample, size))
    {
        std::cerr << "ERROR: Could not load wav header of \"" << filename << "\"" << std::endl;
        return nullptr;
    }

    char* data = new char[size];

    in.read(data, size);

    return data;
}

我不会解释代码,因为这不完全是本文的主题。但是如果与WAV文件规范并行阅读的话很明显

初始化与销毁


首先,我们需要初始化OpenAL,然后像任何优秀的程序员一样,在完成使用后完成它。它初始化期间使用ALCdevice(注意,这ALC不是 AL),基本上代表了你的计算机上播放背景音乐,并用它的东西ALCcontext

ALCdevice类似于选择图形卡。OpenGL游戏将在其上渲染。ALCcontext类似于您要为OpenGL创建(以操作系统唯一的方式)的渲染上下文。

Alcdevice


无论声音是声卡还是芯片,OpenAL设备都是通过声音输出的,但是从理论上讲,它可以有很多不同的东西。与标准输出iostream可以是打印机而不是屏幕一样,设备可以是文件,甚至是数据流。

但是,对于编程游戏,它将是声音设备,通常我们希望它成为系统中的标准声音输出设备。

要获取系统中可用设备的列表,可以使用以下功能请求它们:

bool get_available_devices(std::vector<std::string>& devicesVec, ALCdevice* device)
{
    const ALCchar* devices;
    if(!alcCall(alcGetString, devices, device, nullptr, ALC_DEVICE_SPECIFIER))
        return false;

    const char* ptr = devices;

    devicesVec.clear();

    do
    {
        devicesVec.push_back(std::string(ptr));
        ptr += devicesVec.back().size() + 1;
    }
    while(*(ptr + 1) != '\0');

    return true;
}

实际上,这只是调用周围的包装器alcGetString。返回值是指向由一个值分隔null并以两个值结尾的字符串列表的指针null。在这里,包装程序将其简单地转换为对我们方便的向量。

幸运的是,我们不需要这样做!我怀疑在一般情况下,默认情况下,大多数游戏都可以简单地将声音输出到设备,无论它是什么。我很少看到用于更改要通过其输出声音的音频设备的选项。因此,要初始化OpenAL设备,我们使用call alcOpenDevice。此调用与其他调用略有不同,因为它没有指定可通过获取的错误状态alcGetError,因此我们将其称为普通函数:

ALCdevice* openALDevice = alcOpenDevice(nullptr);
if(!openALDevice)
{
    /* fail */
}

如果您已经列出了如上所示的设备,并且希望用户选择其中之一,则需要将其名称alcOpenDevice改为nullptr。发送nullptr订单以默认打开设备。返回值是相应的设备,或者nullptr发生错误。

根据您是否已完成枚举,错误可能会使程序停止运行。没有设备=没有OpenAL;没有OpenAL =没有声音;没有声音=没有游戏。

关闭程序时,我们要做的最后一件事就是正确完成它。

ALCboolean closed;
if(!alcCall(alcCloseDevice, closed, openALDevice, openALDevice))
{
    /* do we care? */
}

在这个阶段,如果不可能完成,那么这对我们就不再重要。在关闭设备之前,我们必须关闭所有创建的上下文,但是根据我的经验,此调用也可以完成上下文。但是我们会做对的。如果您在拨打电话之前完成了所有操作alcCloseDevice,则应该没有错误,并且由于某种原因而出现了错误,那么您将无能为力。

您可能已经注意到,来自的呼叫alcCall发送了该设备的两个副本。发生这种情况的原因是模板函数的工作方式-一种用于错误检查,另一种用作函数参数。

从理论上讲,我可以改进模板函数,以便它传递第一个参数以进行错误检查,并仍然将其发送给函数;但我很懒。我将把它留作功课。

我们的ALC上下文


初始化的第二部分是上下文。和以前一样,它类似于OpenGL的渲染上下文。一个程序中可以有多个上下文,我们可以在它们之间切换,但是我们不需要。每个上下文都有自己的侦听器,并且不能在上下文之间传递它们。

也许这在声音处理软件中很有用。但是,对于99.9%的游戏而言,仅一种情况就足够了。

创建一个新的上下文非常简单:

ALCcontext* openALContext;
if(!alcCall(alcCreateContext, openALContext, openALDevice, openALDevice, nullptr) || !openALContext)
{
    std::cerr << "ERROR: Could not create audio context" << std::endl;
    /* probably exit program */
}

我们需要ALCdevice就想要创建上下文的内容进行沟通我们还可以传递一个可选的零尾键和值列表,这些键和值ALCint是应使用上下文创建的属性。

老实说,我什至不知道在什么情况下属性传递会派上用场。您的游戏将在具有常规声音功能的常规计算机上运行。属性具有默认值,具体取决于计算机,因此这并不是特别重要。但是如果您仍然需要它:

属性名称描述
ALC_FREQUENCY输出缓冲器的混频,以Hz为单位
ALC_REFRESH更新间隔,以Hz为单位
ALC_SYNC01指出它应该是同步还是异步上下文
ALC_MONO_SOURCES该值有助于告诉您将使用多少个源,这些源需要能够处理单声道音频数据。它不限制最大数量,而只是让您在事先知道这一点时更加有效。
ALC_STEREO_SOURCES相同,但用于立体声数据。

如果收到错误,则很可能是因为所需的属性不可用,或者无法为受支持的设备创建其他上下文;这是因为错误。这将导致错误ALC_INVALID_VALUE如果您传递的设备无效,则会出现错误ALC_INVALID_DEVICE,但是,当然,我们已经在检查此错误。

创建上下文还不够。我们仍然需要使其保持最新状态-看起来像Windows OpenGL渲染上下文,对吗?这是相同的。

ALCboolean contextMadeCurrent = false;
if(!alcCall(alcMakeContextCurrent, contextMadeCurrent, openALDevice, openALContext)
   || contextMadeCurrent != ALC_TRUE)
{
    std::cerr << "ERROR: Could not make audio context current" << std::endl;
    /* probably exit or give up on having sound */
}

对于上下文(或其中的源和侦听器)进行任何进一步的操作,都必须使上下文成为当前上下文。该操作将返回truefalse下,发送的唯一可能的误差值alcGetError是,ALC_INVALID_CONTEXT这是从名称清楚。

完成上下文,即 退出程序时,有必要使上下文不再是最新的,然后销毁它。

if(!alcCall(alcMakeContextCurrent, contextMadeCurrent, openALDevice, nullptr))
{
    /* what can you do? */
}

if(!alcCall(alcDestroyContext, openALDevice, openALContext))
{
    /* not much you can do */
}

from的唯一可能错误与- alcDestroyContext相同如果您做对了所有事情,那么您将一事无成,但如果您做得正确,那么将一事无成。alcMakeContextCurrentALC_INVALID_CONTEXT

为什么要检查无法解决的错误?

因为我希望有关它们的消息至少出现在错误流中(对我们来说确实如此),alcCall假设它永远不会给我们带来错误,但是知道这样的错误发生在其他人的计算机上将很有用。因此,我们可以研究问题,并有可能向OpenAL Soft开发人员报告错误

播放我们的第一个声音


好了,所有这些就足够了,让我们播放声音。首先,我们显然需要一个声音文件。例如,这是我将永远完成游戏


我是这个系统的保护者!

因此,打开IDE并使用以下代码。切记插入OpenAL Soft并添加上面显示的文件上传代码和错误检查代码。

int main()
{
    ALCdevice* openALDevice = alcOpenDevice(nullptr);
    if(!openALDevice)
        return 0;

    ALCcontext* openALContext;
    if(!alcCall(alcCreateContext, openALContext, openALDevice, openALDevice, nullptr) || !openALContext)
    {
        std::cerr << "ERROR: Could not create audio context" << std::endl;
        return 0;
    }
    ALCboolean contextMadeCurrent = false;
    if(!alcCall(alcMakeContextCurrent, contextMadeCurrent, openALDevice, openALContext)
       || contextMadeCurrent != ALC_TRUE)
    {
        std::cerr << "ERROR: Could not make audio context current" << std::endl;
        return 0;
    }

    std::uint8_t channels;
    std::int32_t sampleRate;
    std::uint8_t bitsPerSample;
    std::vector<char> soundData;
    if(!load_wav("iamtheprotectorofthissystem.wav", channels, sampleRate, bitsPerSample, soundData))
    {
        std::cerr << "ERROR: Could not load wav" << std::endl;
        return 0;
    }

    ALuint buffer;
    alCall(alGenBuffers, 1, &buffer);

    ALenum format;
    if(channels == 1 && bitsPerSample == 8)
        format = AL_FORMAT_MONO8;
    else if(channels == 1 && bitsPerSample == 16)
        format = AL_FORMAT_MONO16;
    else if(channels == 2 && bitsPerSample == 8)
        format = AL_FORMAT_STEREO8;
    else if(channels == 2 && bitsPerSample == 16)
        format = AL_FORMAT_STEREO16;
    else
    {
        std::cerr
            << "ERROR: unrecognised wave format: "
            << channels << " channels, "
            << bitsPerSample << " bps" << std::endl;
        return 0;
    }

    alCall(alBufferData, buffer, format, soundData.data(), soundData.size(), sampleRate);
    soundData.clear(); // erase the sound in RAM

    ALuint source;
    alCall(alGenSources, 1, &source);
    alCall(alSourcef, source, AL_PITCH, 1);
    alCall(alSourcef, source, AL_GAIN, 1.0f);
    alCall(alSource3f, source, AL_POSITION, 0, 0, 0);
    alCall(alSource3f, source, AL_VELOCITY, 0, 0, 0);
    alCall(alSourcei, source, AL_LOOPING, AL_FALSE);
    alCall(alSourcei, source, AL_BUFFER, buffer);

    alCall(alSourcePlay, source);

    ALint state = AL_PLAYING;

    while(state == AL_PLAYING)
    {
        alCall(alGetSourcei, source, AL_SOURCE_STATE, &state);
    }

    alCall(alDeleteSources, 1, &source);
    alCall(alDeleteBuffers, 1, &buffer);

    alcCall(alcMakeContextCurrent, contextMadeCurrent, openALDevice, nullptr);
    alcCall(alcDestroyContext, openALDevice, openALContext);

    ALCboolean closed;
    alcCall(alcCloseDevice, closed, openALDevice, openALDevice);

    return 0;
}

编译中!我们组成!发射!我是这个系统的负责人如果听不到声音,请再次检查所有内容。如果在控制台窗口中写了一些内容,那么这应该是错误流的标准输出,这一点很重要。我们的错误报告功能应该告诉我们产生错误的源代码行。如果发现

错误,请阅读《程序员指南》规范,以了解函数可在何种条件下生成此错误。这将帮助您解决问题。如果未成功,请在原始文章下发表评论,我将尽力提供帮助。

下载RIFF WAVE数据


std::uint8_t channels;
std::int32_t sampleRate;
std::uint8_t bitsPerSample;
std::vector<char> soundData;
if(!load_wav("iamtheprotectorofthissystem.wav", channels, sampleRate, bitsPerSample, soundData))
{
    std::cerr << "ERROR: Could not load wav" << std::endl;
    return 0;
}

这是指wave引导代码。重要的是,我们以指针形式或以向量形式接收数据:通道数,采样率和每个样本的位数。

缓冲液产生


ALuint buffer;
alCall(alGenBuffers, 1, &buffer);

如果您曾经在OpenGL中生成纹理数据缓冲区,则可能看起来很熟悉。本质上,我们生成一个缓冲区并假装它仅存在于声卡中。实际上,它很可能存储在普通RAM中,但是OpenAL规范抽象了所有这些操作。

因此,该值ALuint缓冲区句柄。请记住,缓冲区本质上是声卡内存中的声音数据。我们不再直接访问此数据,因为我们从程序(从常规RAM)中获取了数据并将其移至声卡/芯片等。 OpenGL的工作原理类似,将纹理数据从RAM移到VRAM。

这个描述符产生alGenBuffers它有几个可能的错误值,其中最重要的是AL_OUT_OF_MEMORY,这意味着我们不再可以向声卡添加声音数据。例如,如果使用单个缓冲区,则不会出现错误,但是如果要创建engine,则需要考虑此错误

确定音频数据的格式


ALenum format;

if(channels == 1 && bitsPerSample == 8)
    format = AL_FORMAT_MONO8;
else if(channels == 1 && bitsPerSample == 16)
    format = AL_FORMAT_MONO16;
else if(channels == 2 && bitsPerSample == 8)
    format = AL_FORMAT_STEREO8;
else if(channels == 2 && bitsPerSample == 16)
    format = AL_FORMAT_STEREO16;
else
{
    std::cerr
        << "ERROR: unrecognised wave format: "
        << channels << " channels, "
        << bitsPerSample << " bps" << std::endl;
    return 0;
}

声音数据的工作方式如下:有多个通道每个样本都有一个大小数据由许多样本组成

为了确定音频数据中样本数量,我们执行以下操作:

std::int_fast32_t numberOfSamples = dataSize / (numberOfChannels * (bitsPerSample / 8));

什么可以方便地转换为计算音频数据持续时间

std::size_t duration = numberOfSamples / sampleRate;

但是,尽管我们既不需要知道numberOfSamples,也不需要知道duration,但是了解如何使用所有这些信息很重要。

返回format-我们需要告诉OpenAL音频数据格式。这看起来很明显,对吧?类似于填充OpenGL纹理缓冲区的方式,即数据是按BGRA序列并由8位值组成的,我们需要在OpenAL中执行相同的操作。

为了告诉OpenAL如何解释我们稍后将传递的指针所指向的数据,我们需要定义数据格式。在格式下,它意味着OpenAL可以理解。只有四种可能的含义。声道数量有两个可能的值:一个代表单声道,两个代表立体声。

除了通道数,我们还有每个样本的位数。它等于或816,本质上是声音质量。

因此,使用波浪负载功能已告知我们的每个样本的通道和比特值,我们可以确定要ALenum使用哪个参数作为将来的参数format

缓冲液填充


alCall(alBufferData, buffer, format, soundData.data(), soundData.size(), sampleRate);
soundData.clear(); // erase the sound in RAM

这样,一切都应该很简单。我们加载到OpenAL缓冲区,描述符 指向该缓冲区buffer由ptr指向的数据的指定soundData.data()大小我们还将通过参数将这些数据的格式告知OpenAL 最后,我们仅删除波加载程序接收到的数据。为什么?因为我们已经将它们复制到了声卡。我们不需要将它们存储在两个地方并花费宝贵的资源。如果声卡丢失了数据,那么我们只需再次从磁盘下载它,而无需将其复制到CPU或其他人。sizesampleRateformat



来源设定


回想一下,OpenAL本质上是一个侦听器,它侦听一个或多个发出的声音。好了,现在是创建声源的时候了。

ALuint source;
alCall(alGenSources, 1, &source);
alCall(alSourcef, source, AL_PITCH, 1);
alCall(alSourcef, source, AL_GAIN, 1.0f);
alCall(alSource3f, source, AL_POSITION, 0, 0, 0);
alCall(alSource3f, source, AL_VELOCITY, 0, 0, 0);
alCall(alSourcei, source, AL_LOOPING, AL_FALSE);
alCall(alSourcei, source, AL_BUFFER, buffer);

老实说,由于这些默认值非常适合我们,因此无需设置其中一些参数。但这向我们展示了您可以尝试并查看其工作方式的某些方面(您甚至可以狡猾地采取行动,并随着时间的推移而改变它们)。

首先,我们生成源代码-请记住,这也是OpenAL API内部内容的句柄。我们将音调(音调)设置为不改变,使增益(音量)等于音频数据的原始值,并重置位置和速度;我们不会循环播放声音,因为否则我们的程序将永远不会结束,并指示缓冲区。

请记住,不同的源可以使用相同的缓冲区。例如,从不同地方射击玩家的敌人可以播放相同的射击声音,因此我们不需要太多的声音数据副本,而只需要在3D空间中的几个地方即可产生声音。

播放声音


alCall(alSourcePlay, source);

ALint state = AL_PLAYING;

while(state == AL_PLAYING)
{
    alCall(alGetSourcei, source, AL_SOURCE_STATE, &state);
}

首先,我们需要开始播放源代码。只需致电alSourcePlay

然后,我们创建一个值来存储AL_SOURCE_STATE的当前状态并不断更新它。当它不再相等时,AL_PLAYING我们可以继续。您可以将状态更改为AL_STOPPED当它完成从缓冲区发出声音时(或发生错误时)。如果您设置looping的值true,声音将永远播放。

然后,我们可以更改源缓冲区并播放另一种声音。或重播相同的声音等。只需设置缓冲区,使用alSourcePlay它,alSourceStop必要时使用在以下文章中,我们将更详细地考虑这一点。

清洁用品


alCall(alDeleteSources, 1, &source);
alCall(alDeleteBuffers, 1, &buffer);

由于我们只播放一次音频数据然后退出,因此我们将删除先前创建的源和缓冲区。

其余代码无需解释即可理解。

接下来要去哪里?


了解本文介绍的所有内容之后,您就可以创建一个小游戏!尝试创建Pong其他经典游戏,因为不需要它们。

但要记住!这些缓冲区仅适用于短声音,最有可能持续几秒钟。如果您需要音乐或声音表演,则需要将音频流传输到OpenAL。我们将在一系列教程的以下部分中讨论这一点。

All Articles