OpenAL step by step: loading and playing a sound

in OpenAL

In an earlier blog post I introduced the workings of OpenAL. Knowing the theory still doesn’t mean you know how to actually get a sound engine working. Something I found myself struggling with a lot when I started working with OpenAL for our game project was figuring out all the steps required to get a sound to play.

In this blog post I will be talking about the very basics of playing a sound. In the following blog post, I will approach the problem from the other end, and we will look at some of the high level management code that can be used to manage a large amount of sounds.

The setup

As before I will be using the OpenAL bindings as provided by the OpenTK C# library. Since I am only calling AL functions directly, you should have no problem translating this to any other language/library.

Further, remember that (especially on Windows) you will have to make sure that there is an OpenAL binary installed or present before it works. For Windows that means installing a software implementation or including the openal32.dll. For OSX and Linux you will need to copy the OpenTK.dll.config file to your binary folder.

The entire code can be found in this Gist in case you want to follow along.

So let’s start easy with the basic setup:

public static void Main(string[] args)
{
    if (args.Length < 1)
        throw new ArgumentException("Program needs at least one parameters.", "args");

    // Make audio context.
    var context = new AudioContext();

    // TODO: Here be code

    // Dispose AudioContext.
    context.Dispose();
}

We will be taking the sound to play from the arguments, so we add a simple argument check. Then we load a new AudioContext. This is an OpenTK class that will setup OpenTK. This context is thread-specific, so if you are planning on playing sound effects from a separate thread, keep this in mind.

Loading the sound

Sound can be stored in many different formats. Wave is the most simple one, as it will store the sound data uncompressed. This means we can just load the binary data from the file directly and convert it to something usable.

I will not go into how exactly the Wave file header looks. Most of it is pretty straight-forward, and some of it is not needed. There are for example compressed Wave files with a more complex header, but we’ll just choose to not accept those files.

The entire code to load the Wave header is as follows:

private static void getData(Stream stream, out IList<short[]> buffers, out ALFormat alFormat, out int sampleRate)
{
    using (var reader = new BinaryReader(stream))
    {
        // RIFF header
        var signature = new string(reader.ReadChars(4));
        if (signature != "RIFF")
            throw new NotSupportedException("Specified stream is not a wave file.");

        reader.ReadInt32(); // riffChunkSize

        var format = new string(reader.ReadChars(4));
        if (format != "WAVE")
            throw new NotSupportedException("Specified stream is not a wave file.");

        // WAVE header
        var formatSignature = new string(reader.ReadChars(4));
        if (formatSignature != "fmt ")
            throw new NotSupportedException("Specified wave file is not supported.");

        int formatChunkSize = reader.ReadInt32();
        reader.ReadInt16(); // audioFormat
        int numChannels = reader.ReadInt16();
        sampleRate = reader.ReadInt32();
        reader.ReadInt32(); // byteRate
        reader.ReadInt16(); // blockAlign
        int bitsPerSample = reader.ReadInt16();

        if (formatChunkSize > 16)
            reader.ReadBytes(formatChunkSize - 16);

        var dataSignature = new string(reader.ReadChars(4));

        if (dataSignature != "data")
            throw new NotSupportedException("Only uncompressed wave files are supported.");

        reader.ReadInt32(); // dataChunkSize

        // TODO: load data here
    }
}

We make a getData method that takes a stream and outputs the information we need: the data (buffers), the type of file (ALFormat: e.g. whether the sound is mono or stereo), and the sample rate. We create a BinaryReader which allows us to read binary data directly from the file.

For more information on the wave header, check this specification or the Wikipedia article.

To get the ALFormat, we can use two variables we already have:

alFormat = getSoundFormat(numChannels, bitsPerSample);

getSoundFormat is a very simple helper method that converts the variables into an enum value:

private static ALFormat getSoundFormat(int channels, int bits)
{
    switch (channels)
    {
        case 1: return bits == 8 ? ALFormat.Mono8 : ALFormat.Mono16;
        case 2: return bits == 8 ? ALFormat.Stereo8 : ALFormat.Stereo16;
        default: throw new NotSupportedException("The specified sound format is not supported.");
    }
}

Now we have to load the actual sound data. First let’s just read the remainder of the file into a single byte array.

var data = reader.ReadBytes((int)reader.BaseStream.Length);

Because sound buffers have a limited capacity, we might have to separate this data into small chunks. That is why we return a list of buffers, instead of a single byte array.

buffers = new List<short[]>();

Now we start filling small buffer arrays until we have chunked all our data.

int count;
int i = 0;
const int bufferSize = 16384;

while ((count = (Math.Min(data.Length, (i + 1) * bufferSize * 2) - i * bufferSize * 2) / 2) > 0)
{
    var buffer = new short[bufferSize];
    convertBuffer(data, buffer, count, i * bufferSize * 2);
    buffers.Add(buffer);
    i++;
}

In the check of the while loop we calculate the length of the next buffer array to make. This is either the maximum buffer size, or possibly less in case of the last chunk. We create a new array of the maximum buffer size (NB: this could be a smaller array for the last chunk, but I have found that OpenAL will pad your sound with random data at the end). Then we call the convertBuffer method that will take a chunk of binary data and convert it into a usable format: an array of shorts.

private static void convertBuffer(byte[] inBuffer, short[] outBuffer, int length, int inOffset = 0)
{
    for (int i = 0; i < length; i++)
        outBuffer[i] = BitConverter.ToInt16(inBuffer, inOffset + 2 * i);
}

After this, we have all the data stored in a usable format and we can get started on feeding it to OpenAL.

Interacting with OpenAL

Let’s go back to our Main method and see how it looks now:

public static void Main(string[] args)
{
    if (args.Length < 1)
        throw new ArgumentException("Program needs at least one parameters.", "args");

    // Make audio context.
    var context = new AudioContext();

    // Declare variables.
    IList<short[]> buffers;
    ALFormat alFormat;
    int sampleRate;

    // Get the data from the wave file.
    getData(File.OpenRead(args[0]), out buffers, out alFormat, out sampleRate);

    // TODO: Tell OpenAL to play the buffers here

    // Dispose AudioContext.
    context.Dispose();
}

Until now we haven’t done anything with OpenAL yet, except for initialising it. Remember that we audio in OpenAL is played from sources, and that the sources play the sounds from buffers. We have already gone over how to play data already, but let’s quickly go over it again. Since we already chunked our data, we know exactly how many buffers we need:

var bufferHandles = AL.GenBuffers(buffers.Count);

We use the GenBuffers method to request buffers from OpenAL. We can use the BufferData method to fill the buffers with data.

for (int i = 0; i < buffers.Count; i++)
    AL.BufferData(bufferHandles[i], alFormat, buffers[i], buffers[i].Length * sizeof(short), sampleRate);

Remark that we use sizeof(short) since we need to provide the exact size of the data in bytes to the BufferData method.

Now that we have uploaded the data to OpenAL, we can get started on the source. Getting a source from OpenAL works very similar as getting buffers:

var sourceHandle = AL.GenSource();

Every operation we can do on sources in OpenAL has a method name that starts with Source, so queue buffers we can use SourceQueueBuffers, and to start playing we can use SourcePlay.

AL.SourceQueueBuffers(sourceHandle, bufferHandles.Length, bufferHandles);
AL.SourcePlay(sourceHandle);

The only thing left is to wait until the sound is finished playing and then dispose of all the resources to prevent memory leaks.

while (AL.GetSourceState(sourceHandle) != ALSourceState.Stopped)
    System.Threading.Thread.Sleep(100);

AL.DeleteSource(sourceHandle);
AL.DeleteBuffers(bufferHandles);
context.Dispose();

Conclusion

And that’s it. This week was a pretty light take on OpenAL, but using this we can get started on managing the sound from a higher level. There was a bit of repetition going on in this blog, but I hope it is still useful as a more step-by-step guide, since I remember missing one when I worked on my own audio library.

I hope to see you again next time for a more high level take on sound effect handling, and until then feel free to leave questions and feedback as comment, or contact me on my Twitter: @tomrijnbeek

Place comment

Your email address will not be published. Required fields are marked *