The Complete Guide to OpenAL with C++ – Part 1: Playing a Sound

Title of the article: The Complete Guide to OpenAL with C++: Playing a Sound

Your game needs audio! You’ve already utilised OpenGL to get things drawn to the screen. The API worked for you, you understand how it works, and so you look to OpenAL because the name seems familiar.

Well good news, OpenAL has a very familiar API as well. It was originally designed to intentionally mimic the API of OpenGL. That’s the reason I chose it over all the other game audio options; that and it’s cross-platform.

In this article I will take you through every piece of code you will need to utilize OpenAL in your C++ game. Sounds, music, and positioning of the audio in 3D space will all be discussed and code provided. If there’s anything missing from the guide, let me know in the comments and I will address it in an update.

History of OpenAL

I’ll keep this short. As I said, it was originally designed to mimic OpenGL’s API, and for good reason. It was and is a nice API that people were familiar with and if Graphics are one side of the game-engine-coin then Audio must be the other. Originally, OpenAL was supposed to be open-source, but some things happened…

Basically, people simply don’t care about Audio as much as Graphics, and so eventually Creative got a hold of OpenAL and the reference implementation is now proprietary and no longer free. But! The specification for OpenAL is still an “open” standard, meaning that it’s published.

There’s occassionally some updates made to the specification, but not many. Audio simply doesn’t change at the same rate as graphics, so there’s not much need.

The open specification gives way for other, kinder, better-looking people, to make open-source implementations of the specification. OpenAL Soft is one such open-source implementation, and to be honest there’s not much point in trying to find another implementation. This is the implementation that I’m going to be using, and I recommend it to be the one you use.

It is cross-platform. The way it’s implmented is a bit curious; essentially, under the hood, the library is using other audio APIs that are available on your system. Under Windows, it’s using DirectSound, under Unix it’s using OSS. This is how it can be cross-platform, basically a glorified API wrapper.

You might then be concerned about the speed of this API. Don’t be. It’s audio, there’s not a lot happening, it doesn’t need the massive optimisations that graphics APIs need.

Enough history, let’s get into the tech.

What do I need to code with OpenAL?

You’ll need to build OpenAL Soft with your toolchain of choice. It’s a very straightforward process which you can follow here under Source Install. I’ve never had a problem, but if you do comment below and I’ll try and help or message the OpenAL Soft mailing list.

The next thing you’ll need is some audio files and a way to load them. Loading audio data into buffers and the finer points of different audio formats is a little beyond the scope of this article, but you can read about loading and streaming Ogg/Vorbis files here. Loading WAV files is super straight-forward, there’s like 10,000 articles about that on the world wide web already.

Finding audio files is a problem I can’t solve for you. There’s plenty of free beeps and bloops you can download off the internet. If you’ve got a bit of an ear, you can have a go at writing your own chip-tune music as well.

Finally, keep the Programmers Guide from OpenALSoft open. It’s a better reference than the “official” specification .pdf.

That’s it really; I’ll assume you already know how to code, use your IDE and toolchain.

Overview of the OpenAL API

As mentioned several times, it’s similar to the OpenGL API. It’s similar in the sense that it’s state-based and you interact with handles/identifiers, rather than objects directly.

There are some differences in the API convention between OpenGL and OpenAL, but they’re minor. In OpenGL you need to make Operating System specific calls to generate a rendering context. These calls are different for different Operating Systems and aren’t really a part of the OpenGL specification. Not true for OpenAL, the context creating functions are a part of the specification and are the same regardless of the Operating System.

When interacting with the API there are 3 main objects you’re interacting with. Listeners are where the “ears” are, and they are positioned in 3D space (there is only ever one listener). Sources are the “speakers”, emmitting the sound, again in 3D space. The listener and sources can be moved around in space and the results that you hear through your games speakers changes accordingly.

The final objects are buffers. These hold the samples of audio that the sources are going to be playing for the listeners.

There’s also modes that your game uses to change the way OpenAL will process the audio.

Sources

As mentioned, these are where the audio is coming from. They can have a position and a direction, and are associated with a buffer of audio data to play.

Listener

The one set of ears for your game. What the listener hears is what comes out of your speakers. It has a position.

Buffers

An OpenGL Texture2D would be the collorary in OpenGL. It’s basically the audio data that the source is going to play.

Data Types

To support cross-platform code, OpenAL follows in the footsteps and defines some data types. In fact, it follows so closely that you can even map the OpenAL types to OpenGL types directly. The table below lists them all and their equivalencies.

OpenAL TypeOpenALC TypeOpenGL TypeC++ TypedefDescription
ALbooleanALCbooleanGLbooleanstd::int8_t8-bit boolean value
ALbyteALCbyteGLbytestd::int8_tsigned 8-bit 2’s-complement integer
ALubyteALCubyteGLubytestd::uint8_tunsigned 8-bit integer
ALcharALCcharGLcharcharcharacter
ALshortALCshortGLshortstd::int16_tsigned 16-bit 2’s-complement integer
ALushortALCushortGLushortstd::uint16_tunsigned 16-bit integer
ALintALCintGLintstd::int32_tsigned 32-bit 2’s-complement integer
ALuintALCuintGLuintstd::uint32_tunsigned 32-bit integer
ALsizeiALCsizeiGLsizeistd::int32_tnon-negative 32-bit binary integer size
ALenumALCenumGLenumstd::uint32_tenumerated 32-bit value
ALfloatALCfloatGLfloatfloat32-bit IEEE 754 floating-point
ALdoubleALCdoubleGLdoubledouble64-bit IEEE 754 floating-point
ALvoidALCvoidGLvoidvoidvoid value

Detecting OpenAL Errors

There’s an article here on making OpenAL error detection easier, but I’ll reiterate it for the sake of a complete guide. There are two types of OpenAL API calls, there’s the normal ones and the context ones.

Context calls start with alc are are like the OpenGL win32 calls to get a rendering context, or the equivalent in Linux. Audio is simple enough that all operating systems have the same calls. The regular calls start with al. To retrieve errors in for context calls, we call alcGetError; for normal calls we call alGetError. These return either an ALCenum or ALenum value, which just enumerate the possible errors.

We’ll just cover one case right now, but it’s otherwise virtually the same. Let’s do the normal al calls. First we create a preprocessor macro to handle the grunt work of forwarding details:


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

Now, technically, your compiler might not support __FILE__ or __LINE__, but TBH I would be surprised if it didn’t. __VA_ARGS__ is for a variatic/variable number of arguments that may be passed to this macro.

Next we’ll implement a function that manually retrieves the last error reported and output to the standard error stream a meaningful value.

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’s not that many possible errors. The explanations I’ve written in the code are the only info you can get on these errors, but the specification does explain why a particular function might return a particular error.

Next we’ll 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 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...))>, bool> { function(std::forward<Params>(params)...); return check_al_errors(filename, line); }

The reason there is two is because the first one is used for OpenAL functions that return void, whereas the second is used when it returns a non-void value. If you’re not too familiar with template meta-programming in C++, pay attention to the std::enable_if parts in the above code. They are what determine which of these template functions is actually implemented by the compiler for every function call.

Here’s the same stuff for the alc calls now:

#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 of device which all alc calls use, and there’s also the appropriate use of ALCenum and ALC_ style errors. They look really, really similar, and for the longest time the slight change with the al to alc really plagued my code and understanding, I just kept reading right over that c.

That’s it. Normally, an OpenAL call in C++ would look like one of these:

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

This could be a bit weird to you, but I find it far easier to use. You of course could design things differently.

Loading .wav Files

You can either load them yourself or use a library. Here’s an open-source implementation for loading .wav files. I do this myself, because I’m crazy like that:

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

You might notice that I’m not 100% across loading .wav files :). I won’t take you through this code because it’s a little outside the scope of what we’re doing; but it’s super obvious if you read it alongside the WAV file specification.

Initialisation and Destruction

You need to first initialise OpenAL and then like any good programmer, you’ll shut it down when you’re finished with it. Initialisation makes use of an ALCdevice (note it’s ALC not AL) which basically represents something on your computer to play back music, and makes use of an ALCcontext.

The ALCdevice is like choosing which graphics card your OpenGL game is going to render on. The ALCcontext is like the rendering context you need to create (in an Operating System unique way) for OpenGL.

The ALCdevice

The OpenAL Device is usually something like your sound output, either a sound-card or a chip, but technically it could be many different things. Just in the same way that the standard iostream output could be a printer instead of a screen, a device could be a file, or even a stream of data.

Anyway, for our purposes of game programming, it’s going to be a sound device, and typically, we want it to be the systems default sound output device.

To retrieve a list of devices available on the system, you can query 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 really just a wrapper around a wrapper around a call to alcGetString. The returned value is a pointer to a list of strings separated by a null value and ending with two null values. The wrapper here just puts it into a handy vector for us.

Thankfully, you don’t need to do this! In general, I suspect most games can just output to the default device, whatever it is. I’ve not often seen options to change the audio device I want to output to. So, to initialise the OpenAL Device, you use a call to alcOpenDevice. This call is a little different to everything else, because it doesn’t set error state that can be retrieved via alcGetError, so we call it like a normal function:

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

If you’ve enumerated the devices as above, and want the user to select one, you pass that name into alcOpenDevice instead of a nullptr. Sending nullptr says open the default device. The return value is either the device in question, or if it’s nullptr, then there was an error.

Depending on whether you’re enumerating or not, an error could just stop you right in your tracks. No device = no OpenAL; no OpenAL = no sound; no sound = no game.

The last thing we do when exiting the program is shut-down gracefully.

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

At this point, if shutting down fails… I mean what do I care? You are supposed to close any created contexts before closing the device, though in my experience this call will shut down the context as well. But, we will do it properly. Assuming you’ve shut everything down before making the alcCloseDevice call, there shouldn’t be any errors, and if for some reason there is, you won’t be able to do anything about it.

You might have noticed that the calls with alcCall are sending two copies of the device. This is because the way the template function works is it needs one specifically for the error checks, and then the second is used as a parameter to the function.

Technically, I could improve the template function to take the first parameter to pass to the error checking, and still forward it to the function; but I’m lazy. I’ll leave that as an exercise for you.

Your ALCcontext

The second part of initialisation is a context. As before, it’s like a rendering context from OpenGL. You can have multiple contexts for the same program, and switch between them, but you never will. Each context has it’s own listener and sources, and they can’t be passed between them.

Maybe it’s useful in audio processing software. For our games though, we’ll just have the one 99.9% of the time.

Creating a new context is super 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 say which ALCdevice we want to create the context for; and we can also optionally send a zero-terminated list of ALCint keys and values that are attributes we want the context to be created with.

In all honesty, I’m not sure when you would want to provide attributes. Your game is going to run on a regular computer with regular sound capabilities. The values for the attributes have defaults depending on the computer, so it’s really not that big a deal. But just in case you care:

Attribute NameDescription
ALC_FREQUENCYFrequency for mixing in the output buffer, measured in Hz
ALC_REFRESHRefresh intervals, measured in Hz
ALC_SYNC0 or 1 indicating whether it should be a sychronous or asynchronous context
ALC_MONO_SOURCESA hint value that gives an idea of how many sources you’ll use that will need to be able to handle mono sound data. It doesn’t restict the number you can use, just makes it’s more efficient if you know ahead of time.
ALC_STEREO_SOURCESSame as above, but for stereo data.

If you get any errors, it’s most likely because the attributes you wanted aren’t possible or you can’t create another context for the supplied device; these will give you error ALC_INVALID_VALUE. If you provide an invalid device you’ll get ALC_INVALID_DEVICE, but of course you error checked that already.

Creating a context isn’t enough. We also need to make it current… sounds familiar to a Windows OpenGL Rendering Context, right? Same thing.

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

Making the context current is needed to do any further operating on the context (or sources or listeners within it). It’s either going to return true or false, the only possible error value reported by alcGetError is ALC_INVALID_CONTEXT which is fairly self-explanatory.

When you’re done with the context, i.e., program is exiting, you need to stop the context being 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 alcDestroyContext is the same as with alcMakeContextCurrent; ALC_INVALID_CONTEXT which assuming you’re doing everything right, you shouldn’t get, and even if you do, there’s not much you can do about it.

Why check for errors you can’t do anything about?
Because I like to have it at least reported in the error stream, which is what our fancy alcCall will do for us. Let’s say it never errors for us, but it would be good to know if an error like this occurs on someone elses computer. That way we can investigate, and maybe report a bug to the OpenAL Soft developers.

Playing your first sound

Enough of all this, let’s play a sound. First, you’re going to need a sound file to play, obviously. Here’s one from a game that I will one day finish.

I am the protector of this system!

So, open up your IDE and use the following code. Don’t forget to link against OpenAL Soft and include your wav file loading code and our error checking code 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; }

Compile! Link! Execute! I am the prrrootector of this system. If you don’t hear any audio, check over everything. If you get something written to the console window, then it must be standard error stream output, and it will be important. Our error reporting functions should tell you the line in your source code that the error was generated from.

With that error, go ahead and check the Programmers Guide and the specification to understand under what conditions that error can be generated by that function. That should help you figure things out. If not, leave a comment below and I’ll help you out.

Load the 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 is particular to your wave loading code. The important thing is we get the data, either a pointer to it or collected in a vector; the number of channels, sample rate, and bits per sample.

Generate the Buffer

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

This probably looks simliar to you if you’ve ever generated texture data buffers in OpenGL. Basically we generate the buffer and we pretend it’s going to exist only on the sound card. In all reality, it’s likely going to be stored in normal everyday RAM, but the way the OpenAL specification works is that it abstracts that away from us.

So the ALuint value is a handle to our buffer. Remember, a buffer is basically the sound data in the sound-card’s memory. We don’t have direct access to that data anymore, because we’ve taken it away from our program (from normal RAM), and moved it onto the soundcard/soundchip etc. This is the same way OpenGL works, moving the texture data from RAM into VRAM.

alGenBuffers is generating that handle for us. It has a couple possible error values, the big one being AL_OUT_OF_MEMORY which means we can’t add more sound data to the sound card. You won’t get that error with a single buffer in our example, but it’s something you need to watch out for if you’re building an engine.

Determine the Format of the sound 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; }

The way sound data works is: there’s a number of channels and there’s a number of bits per sample. The data is made up of several samples.

To determine the number of samples in your audio data you:

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

Which neatly folds into calculating the duration of the sound data with:

std::size_t duration = numberOfSamples / sampleRate;

But right now, we don’t need to know the numberOfSamples or the duration, but it’s important to know how all of these bits of information are used.

Back to the format, we need to tell OpenAL the format of the sound data. That should be obvious right? In the same manner that when we populate a texture buffer in OpenGL, we need to tell it that the data is in BGRA order and that it’s made up of 8 bit values; we need to do similar things for OpenAL.

To inform OpenAL how to interpret the data pointed to by the pointer we’ll give it later, we need to determine the format of that data. The format meaning, the way OpenAL can understand it. There’s only 4 possible values. We’ve got two possible values for the number of channels, mono has one, stereo has two.

I really assume I don’t need to tell you the difference between mono and stereo right? Maybe some younger people won’t realise that audio used to always be mono… google it, kids.

As well as the number of channels, we also have a number of bits per sample. Either 8 or 16 which is basically the quality of the sound.

So by using the channels and bits per sample reported to us by our wave loading function, we can determine which ALenum to use for the upcoming format parameter.

Fill the buffer

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

This should be simple. We’re going to load, into the OpenAL Buffer pointed to by the handle, buffer; the data pointed to by the soundData.data() ptr, for the specified size with a specific sampleRate. We’re also telling OpenAL the format of that data through the format parameter.

The last bit there is we’re just erasing the data that our wave loader got for it. You know why? Because we just copied it over to the sound-card. We don’t need to keep it in two places at once and use vital resources. If the sound card lost the data, we’d just load it from disk again, we don’t need a copy here for the CPU or anything.

Setup the Source

Remember that OpenAL is basically a listener listening to the sounds being emitted by one or more sources? Well, now we need to create our 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);

To be honest, we don’t need to define some of these parameters of our source because they default nicely. But this should show some of the things that you can play with to hear what they do (you can be clever and change them over time).

We generate the source first, remember, it’s again a handle to something within the OpenAL API. We set the pitch to just be no change, gain (volume) to the original value of the sound data, position and velocity set to zeros; we do not loop, becasuse otherwise our program will never end, and we specify the buffer.

Remember, multiple sources can use the same buffer. For example, a whole bunch of criminals shooting at the player are in different positions, so they can all play the same gunshot sound, we don’t need multiple copies of the sound data, just multiple places where it’s being emitted from in our 3D space.

Play the sound

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

First we start the source off playing. Simple call to alSourcePlay.

Next we create a value to store the current AL_SOURCE_STATE of the source, and update it endlessly. When it is no longer AL_PLAYING we can continue. It will change to AL_STOPPED when it’s finished emitting all the sound from the buffer (or there’s an error or something). If we set looping to true it would keep playing forever.

You can then change the buffer of the source and play a different sound. Or replay the same sound etc. Just set the buffer, use alSourcePlay and possibly alSourceStop if you need. We’ll get into more depth on that stuff in a later article.

Cleanup

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

Because we’re just playing the sound data once and then exiting, we’ll delete the source and buffer that we created before.

The rest of the code is self explanatory.

What’s Next?

With everything I’ve given you here, it’s enough to make a small game! Try making Pong, or one of the other classics, they really don’t need much more than this.

But be warned! These buffers are only good for short sounds, maybe a few seconds in length. If you want music, or voice-overs, you’re going to need to stream your audio data into OpenAL. That’s what we’ll be covering in part 2 of this series, how to stream audio files.

If you found this helpful, please let me know in the comments below; link to the article, tweet it, whatever, spread the word.

7 thoughts on “The Complete Guide to OpenAL with C++ – Part 1: Playing a Sound”

  1. Ok so I ve managed to build by cmake the soft openal But I am absolutely have no idea how to link my project in visual studio 2019 to it. Can you outline steps more specifically please?

    1. Hi Mckl, I don’t personally use Visual Studio so I can only give some general help. You’re going to want to google how to include .dll libraries in your Visual Studio Project/Solution. Once the library is built, I assume you built in CMake using the Visual Studio toolchain, the process of including it in your project is the same as including any other library, there’s nothing OpenAL specific at that point.

  2. Ok so I ve copied and pasted you code and I am getting two type of errors:
    Severity Code Description Project File Line Suppression State
    Error (active) E0304 no instance of function template “alCallImpl” matches the argument list OpenAlTestCode C:\Users\mstar\source\repos\OpenAlTestCode\OpenAlTestCode\main.cpp 507
    so each allcall function is underlined as error.
    And also if (!load_wav(“iamtheprotectorofthissystem.wav”, channels, sampleRate, bitsPerSample, soundData)) soundData Error underlined and it says:
    Severity Code Description Project File Line Suppression State
    Error (active) E0434 a reference of type “ALsizei &” (not const-qualified) cannot be initialized with a value of type “std::vector<char, std::allocator>” OpenAlTestCode C:\Users\mstar\source\repos\OpenAlTestCode\OpenAlTestCode\main.cpp 500

  3. Ok so I am trying to incorporate this into our basic driving game I just need to make car to honk horn each time I press space bar. So now it sounds when I press space bar but then game freezes for the time it sounds. and after the sound is complete it throws an exception. Right at the point where alc check errors function

  4. Pingback: Porting my game engine from SFML to SDL | Au moelleux

Leave a Comment