Skip to content

2.1. OpenAL

Ioannis Tsakpinis edited this page Mar 4, 2018 · 6 revisions

OpenAL is a cross-platform audio API for which LWJGL includes bindings. The API bears great similarities to OpenGL, and should thus be relatively easy to learn if you are familiar with OpenGL. While the goal of this guide is to show how to use OpenAL in LWJGL it also includes a basic explanation of some of the core concepts of OpenAL.

It is also worth mentioning that unlike OpenGL, OpenAL is split into to separate parts: Core AL (All functions that start with al) which contains functions related to audio playback, listener positioning and effects. ALC, the Audio Library Context (All functions that start with alc) deals with audio devices and resource management. When writing your game you will mostly be using the core AL functions.

A note on imports

Throughout this guide various functions from a variety of packages will be used. Most IDEs are able to automatically add the correct imports to your code. In case you are not using an IDE you can find a list of all imports in the complete code listing at the bottom of this document. Furthermore, you can reffer to the javadoc for a list of classes and their members.

Initial setup

Prerequisites

OpenAL and stb (for loading .ogg files) are required. Both of these are included in all presets, so if you are already running LWJGL you should have them.

Creating a device and context

The first step in setting up OpenAL is choosing which sound device to use. Because LWJGL uses OpenAL SOFT, a software implementation of OpenAL, we will use the virtual device provided by it. Device creation is done like so:

String defaultDeviceName = alcGetString(0, ALC_DEFAULT_DEVICE_SPECIFIER);
long device = alcOpenDevice(defaultDeviceName);

Now that we have a device we need to create a context and tell OpenAL that we want to use this context. The function used to create a context takes a list of attributes. We can simply pass in an array containing only a 0 to indicate that we are not passing in any attributes:

int[] attributes = {0};
long context = alcCreateContext(device, attributes);
alcMakeContextCurrent(context);

Finally we need to create an ALCCapabilities object for ALC and a ALCapabilities object for core AL. The objects are used internally for making function calls. Aditionally, they provide information about which features are available.

ALCCapabilities alcCapabilities = ALC.createCapabilities(device);
ALCapabilities alCapabilities = AL.createCapabilities(alcCapabilities);

Checks for which features are available can be done like so (As allways, reffer to the javadoc for a full list of things you can check for):

if(alCapabilities.OpenAL10) {
    //OpenAL 1.0 is supported
}

When we are done using OpenAL we can destroy the context and close the device:

alcDestroyContext(context);
alcCloseDevice(device);

Playing a sound

OpenAL splits playing a sound into two parts: Sources and buffers. A buffer contains the data for a single sound. In your application you will have one buffer per sound file. A source contains information about how to play a sound, including but not limited to position, gain, and pitch. This separation allows you to play a single sound multiple times while loading it into memory only once.

At this point you will need a simple sound clip that you can load and play, to test if everything is working correctly. Online tools such as Chiptone and bfxr can be used to easily create simple sound effects for games, and have presets for some common effects. Note that these tools export .wav files, this guide only shows how to load .ogg vorbis-encoded files. You can use a command line tool such as ffmpeg to convert a .wav to a .ogg.

Loading a sound

In order to use a sound with OpenAL we need its raw data in memory. Luckily, STB which is included in LWJGL has a simple utility to load an ogg-vorbis file. Note that this code uses some LWJGL utilities to allocate memory on the stack which is used for storing extra information returned by STB. For a more in depth guide on memory management in LWJGL you might want check out section 3 and 4 of these examples.

String fileName = "sound.ogg";

//Allocate space to store return information from the function
stackPush();
IntBuffer channelsBuffer = stackMallocInt(1);
stackPush();
IntBuffer sampleRateBuffer = stackMallocInt(1);

ShortBuffer rawAudioBuffer = stb_vorbis_decode_filename(fileName, channelsBuffer, sampleRateBuffer);

//Retreive the extra information that was stored in the buffers by the function
int channels = channelsBuffer.get();
int sampleRate = sampleRateBuffer.get();
//Free the space we allocated earlier
stackPop();
stackPop();

Next, we need to send the data to an OpenAL buffer, so it can be used. This is where the similarities to OpenGL start to apear. Just like in GL, we to request a buffer and then store data in it. In addition to giving OpenAL the raw data we also have to tell it in which format the data is.

//Find the correct OpenAL format
int format = -1;
if(channels == 1) {
    format = AL_FORMAT_MONO16;
} else if(channels == 2) {
    format = AL_FORMAT_STEREO16;
}

//Request space for the buffer
int bufferPointer = alGenBuffers();

//Send the data to OpenAL
alBufferData(bufferPointer, format, rawAudioBuffer, sampleRate);

//Free the memory allocated by STB
free(rawAudioBuffer);

We now have a sound loaded into memory. Before we move on it is worth going over how to delete a buffer. You should always delete your buffers when you no longer need them. Note that trying to delete a buffer while it is still bound to a source will cause an error.

alDeleteBuffers(bufferPointer);

Playing the sound

In order to play a sound we need a source. This is sort of like a speaker: We put it somewhere, we set a volume, and give it something to play. The source is created just like the buffer:

int sourcePointer = alGenSources();

//Assign our buffer to the source
alSourcei(sourcePointer, AL_BUFFER, bufferPointer);

Now the source can be played using alPlaySource:

alSourcePlay(sourcePointer);

This function will not block until the sound has finished playing. If you are not playing the sound as part of a game loop you might want to tell your program to wait for a second so the sound has time to ring out before moving on. This can be done rudimentarily with Thread.sleep(1000).

When you are done using a source you can delete it like so:

alDeleteSources(sourcePointer);

Handling errors

OpenAL reports any errors it encounters to you using the alGetError method. When calling this method it returns a code from a list of error codes. All these error codes are defined as constants. You should call this method frequently within your sound-related code while debugging in order to discover any mistakes you might have made. Below is a list of all error codes and an example showing how to use the method. By reading the documentation you can find a list of errors which each function can cause.

int error = alGetError();
if(error != AL_NO_ERROR) {
    //Handle the error
}

Error codes:

  • AL_NO_ERROR
  • AL_INVALID_NAME
  • AL_INVALID_ENUM
  • AL_INVALID_VALUE
  • AL_INVALID_OPERATION
  • AL_OUT_OF_MEMORY

Conclusion

In this simple demo we only play a single static sound. OpenAL is however designed with games in mind, and this short guide barely scratches the surface of what is possible. For further reference, the javadoc contains documentation for all OpenAL functions, and by skimming through it you can get a good idea of what tools you have at your hands. Also, the OpenAL Programmers Guide is worth giving a look at if you are interested in a more in-depth explanation of how different components work.

Complete code

Note that this sample needs LWJGL with OpenAL and stb to compile. The file sound.ogg is not included.

import org.lwjgl.openal.*;
import org.lwjgl.system.*;

import java.nio.*;

import static org.lwjgl.openal.AL10.*;
import static org.lwjgl.openal.ALC10.*;
import static org.lwjgl.stb.STBVorbis.*;
import static org.lwjgl.system.MemoryStack.*;
import static org.lwjgl.system.libc.LibCStdlib.*;
...

//Initialization
String defaultDeviceName = alcGetString(0, ALC_DEFAULT_DEVICE_SPECIFIER);
long   device            = alcOpenDevice(defaultDeviceName);

int[] attributes = {0};
long  context    = alcCreateContext(device, attributes);
alcMakeContextCurrent(context);

ALCCapabilities alcCapabilities = ALC.createCapabilities(device);
ALCapabilities  alCapabilities  = AL.createCapabilities(alcCapabilities);

ShortBuffer rawAudioBuffer;

int channels;
int sampleRate;

try (MemoryStack stack = stackPush()) {
    //Allocate space to store return information from the function
    IntBuffer channelsBuffer   = stack.mallocInt(1);
    IntBuffer sampleRateBuffer = stack.mallocInt(1);

    rawAudioBuffer = stb_vorbis_decode_filename("sound.ogg", channelsBuffer, sampleRateBuffer);

    //Retreive the extra information that was stored in the buffers by the function
    channels = channelsBuffer.get(0);
    sampleRate = sampleRateBuffer.get(0);
}

//Find the correct OpenAL format
int format = -1;
if (channels == 1) {
    format = AL_FORMAT_MONO16;
} else if (channels == 2) {
    format = AL_FORMAT_STEREO16;
}

//Request space for the buffer
int bufferPointer = alGenBuffers();

//Send the data to OpenAL
alBufferData(bufferPointer, format, rawAudioBuffer, sampleRate);

//Free the memory allocated by STB
free(rawAudioBuffer);

//Request a source
int sourcePointer = alGenSources();

//Assign the sound we just loaded to the source
alSourcei(sourcePointer, AL_BUFFER, bufferPointer);

//Play the sound
alSourcePlay(sourcePointer);

try {
    //Wait for a second
    Thread.sleep(1000);
} catch (InterruptedException ignored) {
}

//Terminate OpenAL
alDeleteSources(sourcePointer);
alDeleteBuffers(bufferPointer);
alcDestroyContext(context);
alcCloseDevice(device);