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 Type | OpenALC Type | OpenGL Type | C++ Typedef | Description |
---|---|---|---|---|
ALboolean | ALCboolean | GLboolean | std::int8_t | 8-bit boolean value |
ALbyte | ALCbyte | GLbyte | std::int8_t | signed 8-bit 2’s-complement integer |
ALubyte | ALCubyte | GLubyte | std::uint8_t | unsigned 8-bit integer |
ALchar | ALCchar | GLchar | char | character |
ALshort | ALCshort | GLshort | std::int16_t | signed 16-bit 2’s-complement integer |
ALushort | ALCushort | GLushort | std::uint16_t | unsigned 16-bit integer |
ALint | ALCint | GLint | std::int32_t | signed 32-bit 2’s-complement integer |
ALuint | ALCuint | GLuint | std::uint32_t | unsigned 32-bit integer |
ALsizei | ALCsizei | GLsizei | std::int32_t | non-negative 32-bit binary integer size |
ALenum | ALCenum | GLenum | std::uint32_t | enumerated 32-bit value |
ALfloat | ALCfloat | GLfloat | float | 32-bit IEEE 754 floating-point |
ALdouble | ALCdouble | GLdouble | double | 64-bit IEEE 754 floating-point |
ALvoid | ALCvoid | GLvoid | void | void 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__)
Code language: C++ (cpp)
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;
}
Code language: C++ (cpp)
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);
}
Code language: C++ (cpp)
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);
}
Code language: C++ (cpp)
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 */
}
Code language: C++ (cpp)
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 */
}
Code language: C++ (cpp)
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;
}
Code language: C++ (cpp)
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;
}
Code language: C++ (cpp)
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 */
}
Code language: C++ (cpp)
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? */
}
Code language: C++ (cpp)
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 */
}
Code language: C++ (cpp)
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 Name | Description |
---|---|
ALC_FREQUENCY | Frequency for mixing in the output buffer, measured in Hz |
ALC_REFRESH | Refresh intervals, measured in Hz |
ALC_SYNC | 0 or 1 indicating whether it should be a sychronous or asynchronous context |
ALC_MONO_SOURCES | A 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_SOURCES | Same 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 */
}
Code language: C++ (cpp)
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 */
}
Code language: C++ (cpp)
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.
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;
}
Code language: C++ (cpp)
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;
}
Code language: C++ (cpp)
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);
Code language: C++ (cpp)
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;
}
Code language: C++ (cpp)
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));
Code language: C++ (cpp)
Which neatly folds into calculating the duration of the sound data with:
std::size_t duration = numberOfSamples / sampleRate;
Code language: C++ (cpp)
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
Code language: C++ (cpp)
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);
Code language: C++ (cpp)
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);
}
Code language: C++ (cpp)
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);
Code language: C++ (cpp)
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.
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?
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.
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
Hi Mckl, your best bet is to move this into the forums here https://indiegamedev.net/forums. When you do, post your entire code and errors so I can take a look through it.
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
Come back into thee forum. This will require a fair bit of work to look at your other game code around it.
Pingback: Porting my game engine from SFML to SDL | Au moelleux