Guide for working with OpenAL in C ++. Part 1: play sound

Your game needs sound! You probably already used OpenGL for drawing on the screen. You figured out its API, and so you turned to OpenAL because the name seems familiar.

Well, the good news is that OpenAL also has a very familiar API. It was originally designed to simulate the OpenGL specification API. That is why I chose it among the many sound systems for games; in addition, it is cross-platform.

In this article, I will talk in detail about what code is needed to use OpenAL in a game written in C ++. We will discuss sounds, music and sound positioning in 3D space with code examples.

History of OpenAL


I will try to be brief. As mentioned above, it was intentionally designed as an imitation of the OpenGL API, and there is a reason for this. This is a convenient API that is known to many, and if the graphics are one side of the game engine, then the sound should be different. Initially, OpenAL was supposed to be open-source, but then something happened ...

People are not so interested in sound as graphics, so Creative eventually made OpenAL its property, and the reference implementation is now proprietary and not free. But! The OpenAL specification is still an “open” standard, that is, it is published .

From time to time, specifications are amended, but not many. The sound does not change as fast as the graphics, because there is no particular need for this.

The open specification allowed other people to create an open-source implementation of the specification. One such implementation is OpenAL Soft , and frankly, it makes no sense to look for any others. This is the implementation that I will use, and I recommend that you use it as well.

She is cross-platform. It is implemented quite curiously - in fact, inside the library uses other sound APIs present in your system. On Windows, it uses DirectSound , on Unix, OSS . Thanks to this, she was able to become cross-platform; in essence, this is a big name for the wrapper API.

You may be worried about the speed of this API. But don't worry. This is the same sound, and it does not create a large load, so it does not require the large optimizations required by the graphics API.

But enough story, let's move on to technology.

What do you need to write code in OpenAL?


You need to build OpenAL Soft in the toolchain of your choice. This is a very simple process that you can follow in accordance with the instructions in the Source Install section . I've never had any problems with this, but if you have any difficulties, write a comment under the original article or write to the OpenAL Soft mailing list .

Next, you will need some sound files and a way to download them. Loading audio data into buffers and subtle details of various audio formats are outside the scope of this article, but you can read about downloading and streaming Ogg / Vorbis files . Downloading WAV files is very simple, there are already hundreds of articles on the Internet about this.

The task of finding audio files you have to decide for yourself. There are many noises and explosions on the Internet that you can download. If you have a rumor, then you can try to write your own chiptune music [ translation on Habré].

Also, keep the Programmers Guide from OpenALSoft handy . This documentation is much better pdf with "official" specialization.

That, in fact, is all. We will assume that you already know how to write code, use the IDE and toolchain.

OpenAL API Overview


As I said several times, it is similar to the OpenGL API. The similarity lies in the fact that it is based on states and you interact with descriptors / identifiers, and not with the objects themselves directly.

There are discrepancies between the API conventions in OpenGL and OpenAL, but they are not significant. In OpenGL, you need to make special OS calls to generate a rendering context. These challenges are different for different OSs and are not really part of the OpenGL specification. In OpenAL, everything is different - the context creation functions are part of the specification and are the same regardless of the operating system.

When interacting with the API, there are three main types of objects that you interact with. Listeners(“Listeners”) is the location of the “ears” located in 3D space (there is always only one listener). Sources (“sources”) are “speakers” that make sound, again in 3D space. Listener and sources can be moved in space and depending on this, what you hear through the speakers in the game changes.

The last objects are buffers . They store samples of sounds that sources will play for listeners.

There are also modes that the game uses to change the way audio is processed through OpenAL.

Sources


As mentioned above, these objects are sources of sounds. They can be set position and direction, and they are associated with a buffer of playback audio data.

Listener


The only set of "ears" in the game. What the listener hears is reproduced through the speakers of the computer. He also has a position.

Buffers


In OpenGL, their equivalent is Texture2D. In essence, this is the audio data that source reproduces.

Data types


To be able to support cross-platform code, OpenAL performs a certain sequence of actions and defines some data types. In fact, it follows OpenGL so precisely that we can even directly convert OpenAL types to OpenGL types. The table below lists them and their equivalents.

Type openalType openalcType openglC ++ TypedefDescription
ALbooleanALCbooleanGLbooleanstd::int8_t8-bit boolean value
ALbyteALCbyteGLbytestd::int8_t8-bit integer value of the additional code with a sign
ALubyteALCubyteGLubytestd::uint8_t8-bit unsigned integer value
ALcharALCcharGLcharcharsymbol
ALshortALCshortGLshortstd::int16_t16-bit signed integer value
ALushortALCushortGLushortstd::uint16_t16-
ALintALCintGLintstd::int32_t32-
ALuintALCuintGLuintstd::uint32_t32-
ALsizeiALCsizeiGLsizeistd::int32_t32-
ALenumALCenumGLenumstd::uint32_t32-
ALfloatALCfloatGLfloatfloat32- IEEE 754
ALdoubleALCdoubleGLdoubledouble64- IEEE 754
ALvoidALCvoidGLvoidvoid

OpenAL


There is an article on how to simplify OpenAL error recognition , but for the sake of completeness, I will repeat it here. There are two types of OpenAL API calls: regular and contextual.

Context calls starting with alcare similar to OpenGL win32 calls to get the rendering context or their counterparts on Linux. Sound is a simple enough thing for all operating systems to have the same calls. Ordinary calls begin with al. To get errors in contextual calls, we call alcGetError; in the case of regular calls, we call alGetError. They return either a value ALCenumor a value ALenumthat simply lists possible errors.

Now we will consider only one case, but in everything else they are almost the same. Let's take the usual challenges al. First, create a preprocessor macro to do the boring job of passing details:

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

Theoretically, your compiler may not support __FILE__either __LINE__, but, to be honest, I would be surprised if that turned out to be so. __VA_ARGS__denotes a variable number of arguments that can be passed to this macro.

Next, we implement a function that manually receives the last error reported and displays a clear value to the standard error stream.

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;
}

There are not many possible mistakes. The explanations I wrote in the code are the only information you will receive about these errors, but the specification explains why a particular function may return a specific error.

Then we implement two different template functions that will wrap all of our OpenGL calls.

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);
}

There are two of them, because the first is used for OpenAL functions that return void, and the second is used when the function returns a non-empty value. If you are not very familiar with metaprogramming templates in C ++, then take a look at the parts of the c code std::enable_if. They determine which of these template functions are implemented by the compiler for each function call.

And now the same for calls 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);
}

The biggest change is the inclusion device, which all calls use alc, as well as the corresponding use of style errors ALCenumand ALC_. They look very similar, and for a very long time small changes from alto alcbadly damaged my code and understanding, so I just continued reading right on top of it c.

That's all. Typically, an OpenAL call in C ++ looks like one of the following options:

/* 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 */
}

But now we can do it like this:

/* 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 */
}

It may seem strange to you, but it’s more convenient for me. Of course, you can choose a different structure.

Download .wav files


You can either download them yourself, or use the library. Here is an open-source implementation of loading .wav files . I'm crazy, so I do it myself:

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;
}

I will not explain the code, because this is not entirely in the subject of our article; but it is very obvious if you read it in parallel with the specification of the WAV file .

Initialization and Destruction


First we need to initialize OpenAL, and then, like any good programmer, finish it when we finish working with it. It is used during initialization ALCdevice(note that this ALCis not AL ), which essentially represents something on your computer for playing background music and uses it ALCcontext.

ALCdevicesimilar to choosing a graphics card. on which your OpenGL game will render. ALCcontextsimilar to the rendering context you want to create (unique to the operating system) for OpenGL.

Alcdevice


An OpenAL Device is what the sound is output through, whether it's a sound card or a chip, but theoretically it can be a lot of different things. Similar to how standard output iostreamcan be a printer instead of a screen, a device can be a file or even a data stream.

However, for programming games, it will be a sound device, and usually we want it to be a standard sound output device in the system.

To get a list of devices available in the system, you can request them with this function:

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;
}

This is actually just a wrapper around a wrapper around a call alcGetString. The return value is a pointer to a list of strings separated by a value nulland ending with two values null. Here, the wrapper simply turns it into a vector convenient for us.

Fortunately, we do not need to do this! In the general case, as I suspect, most games can simply output sound to the device by default, whatever it may be. I rarely see the options for changing the audio device through which you want to output sound. Therefore, to initialize the OpenAL Device, we use a call alcOpenDevice. This call is slightly different from everything else, because it does not specify the error state that can be obtained through alcGetError, so we call it like a normal function:

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

If you have listed the devices as shown above, and you want the user to select one of them, then you need to transfer its name to alcOpenDeviceinstead nullptr. Sending nullptrorders to open the device by default . The return value is either the corresponding device, or nullptrif an error occurs.

Depending on whether you have completed the enumeration or not, an error may stop the program on tracks. No device = No OpenAL; no OpenAL = no sound; no sound = no game.

The last thing we do when closing a program is to finish it correctly.

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

At this stage, if completion was not possible, then this is no longer important to us. Before closing the device, we must close all created contexts, however, in my experience, this call also completes the context. But we will do it right. If you complete everything before making a call alcCloseDevice, then there should be no errors, and if for some reason they have arisen, then you can not do anything about it.

You may have noticed that calls from alcCallsend two copies of the device. It happened because of how the template function works - one is needed for error checking, and the second is used as a function parameter.

Theoretically, I can improve the template function so that it passes the first parameter for error checking and still sends it to the function; but I'm lazy to do it. I will leave this as your homework.

Our ALCcontext


The second part of initialization is context. As before, it is similar to the rendering context from OpenGL. There can be several contexts in one program and we can switch between them, but we will not need this. Each context has its own listener and sources , and they cannot be passed between contexts.

Perhaps this is useful in sound processing software. However, for games in 99.9% of cases, only one context is enough.

Creating a new context is very simple:

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

We need to communicate for what ALCdevicewe want to create a context; we can also pass an optional null-terminated list of keys and values ALCint, which are attributes with which the context should be created.

Honestly, I don’t even know in what situation attribute passing comes in handy. Your game will run on a regular computer with the usual sound features. Attributes have default values, depending on the computer, so this is not particularly important. But in case you still need it:

Attribute nameDescription
ALC_FREQUENCYMixing frequency to the output buffer, measured in Hz
ALC_REFRESHUpdate intervals, measured in Hz
ALC_SYNC0or 1indicate whether it should be a synchronous or asynchronous context
ALC_MONO_SOURCESA value that helps tell you how many sources you will use that require the ability to process monaural audio data. It does not limit the maximum amount, it just allows you to be more effective when you know this in advance.
ALC_STEREO_SOURCESThe same, but for stereo data.

If you get errors, then most likely this is because the attributes you want are impossible or you cannot create another context for the supported device; this will result in an error ALC_INVALID_VALUE. If you pass an invalid device, you will get an error ALC_INVALID_DEVICE, but, of course, we are already checking this error.

Creating context is not enough. We still need to make it current - it looks like a Windows OpenGL Rendering Context, right? It is the same.

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 */
}

It is necessary to make the context current for any further operations with the context (or with sources and listeners in it). The operation will return trueor false, the only possible error value transmitted alcGetErroris that ALC_INVALID_CONTEXTwhich is clear from the name.

Finishing with context, i.e. when exiting the program, it is necessary that the context is no longer current, and then destroy it.

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

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

The only possible error from alcDestroyContextis the same as that of alcMakeContextCurrent- ALC_INVALID_CONTEXT; if you do everything right, then you won’t get it, but if you do, then nothing can be done about it.

Why check for errors with which nothing can be done?

Because I want the messages about them to at least appear in the stream of errors, which for us does. alcCallSuppose it never gives us errors, but it will be useful to know that such an error occurs on someone else's computer. Thanks to this, we can study the problem, and possibly report a bug to the OpenAL Soft developers .

Play our first sound


Well, enough of all this, let's play the sound. To begin with, we obviously need a sound file. For example, this one, from a game I will ever finish .


I am the protector of this system!

So, open the IDE and use the following code. Remember to plug in OpenAL Soft and add the file upload code and error checking code shown above.

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;
}

Compiling! We compose! Launch! I am the prrrootector of this system . If you do not hear the sound, then check everything again. If something is written in the console window, then this should be the standard output of the error stream, and it is important. Our error reporting functions should tell us the line of source code that generated the error. If you find an

error, read the Programmers Guide and the specification to understand the conditions under which this error can be generated by a function. This will help you figure it out. If it does not succeed, then leave a comment under the original article, and I will try to help.

Download RIFF WAVE data


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;
}

This refers to the wave boot code. The important thing is that we receive data, either as a pointer, or collected in a vector: the number of channels, sampling rate and the number of bits per sample.

Buffer generation


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

It probably looks familiar to you if you ever generated texture data buffers in OpenGL. In essence, we generate a buffer and pretend that it will exist only in the sound card. In fact, it will most likely be stored in ordinary RAM, but the OpenAL specification abstracts all these operations.

So, the value ALuintis a handle to our buffer. Remember that buffer is essentially sound data in the sound card's memory. We no longer have direct access to this data, since we took it from the program (from regular RAM) and moved it to a sound card / chip, etc. OpenGL works similarly, moving texture data from RAM to VRAM.

This descriptorgenerates alGenBuffers. It has a couple of possible error values, the most important of which is AL_OUT_OF_MEMORY, which means that we can no longer add sound data to the sound card. You will not get this error if, for example, you use a single buffer, but you need to consider this if you are creating an engine .

Determine the format of audio data


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;
}

Sound data works like this: there are several channels and there is a bit size per sample . Data consists of many samples .

To determine the number of samples in the audio data, we do the following:

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

What can be conveniently converted to calculating the duration of audio data:

std::size_t duration = numberOfSamples / sampleRate;

But while we do not need to know neither numberOfSamples, nor duration, however, it is important to know how all these pieces of information are used.

Back to format- we need to tell OpenAL the audio data format. That seems obvious, right? Similar to how we fill the OpenGL texture buffer, saying that the data is in a BGRA sequence and is composed of 8-bit values, we need to do the same in OpenAL.

To tell OpenAL how to interpret the data pointed to by the pointer that we will pass later, we need to define the data format. Under the format , it is meant as understood by OpenAL. There are only four possible meanings. There are two possible values ​​for the number of channels: one for mono, two for stereo.

In addition to the number of channels, we have the number of bits per sample. It is equal to or 8, or 16, and is essentially sound quality.

So using the values ​​of the channels and bits per sample, which the wave load function has informed us about, we can determine which one to ALenumuse for the future parameter format.

Buffer filling


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

With this, everything should be simple. We load into the OpenAL Buffer, which the descriptor points to buffer; the data pointed to by ptr soundData.data()in the size of sizethe specified sampleRate. We will also inform OpenAL the format of this data through the parameter format.

In the end, we simply delete the data that the wave loader received. Why? Because we have already copied them to the sound card. We do not need to store them in two places and spend precious resources. If the sound card loses data, then we will simply download it from the disk again and we won’t need to copy it to the CPU or someone else.

Source setting


Recall that OpenAL is essentially a listener that listens to sounds made by one or more sources . Well, now is the time to create a sound source.

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);

Honestly, it’s not necessary to set some of these parameters, because the default values ​​are quite suitable for us. But this shows us some aspects with which you can experiment and see what they do (you can even act cunningly and change them over time).

First we generate source - remember, this is again a handle to something inside the OpenAL API. We set pitch (tone) so that it does not change, gain (volume) is made equal to the original value of the audio data, the position and speed are reset; we do not loop the sound, because otherwise our program will never end, and indicate the buffer.

Remember that different sources can use the same buffer. For example, enemies shooting a player from different places can play the same shot sound, so we don’t need many copies of the sound data, but only a few places in 3D space from which the sound is made.

Play sound


alCall(alSourcePlay, source);

ALint state = AL_PLAYING;

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

First we need to start playing source. Simply call alSourcePlay.

Then we create a value to store the current state of the AL_SOURCE_STATEsource and update it endlessly. When it is no longer equal, AL_PLAYINGwe can continue. You can change the state to AL_STOPPEDwhen it finishes making sound from the buffer (or when an error occurs). If you set the value for looping true, the sound will play forever.

Then we can change the source buffer and play another sound. Or replay the same sound, etc. Just set the buffer, use alSourcePlayit, and maybe alSourceStop, if necessary. In the following articles we will consider this in more detail.

Cleaning


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

Since we simply play the audio data once and exit, we will delete the previously created source and buffer.

The rest of the code is understandable without explanation.

Where to go next?


Knowing everything described in this article, you can already create a small game! Try to create Pong or some other classic game , for them more is not required.

But remember! These buffers are only suitable for short sounds, most likely for a few seconds. If you need music or voice acting, you will need to stream audio to OpenAL. We will talk about this in one of the following parts of a series of tutorials.

All Articles