Programming with OpenAL in Pure C (How to Start?)


Sound and most of the time also music is mandatory if you want to be at least a little bit serious about programming your own game. OpenAL is a good choice for doing this, but how can you even start?

OpenAL Soft is a software implementation of the OpenAL 3D audio API. It is written in C, cross-platform and licensed under the LGPL. It is available for download and can be used within any C project as a library.

Wave Mixing Console
Image from Bokskapet at Pixabay

Learn more about OpenAL respectively OpenAL Soft and see it in action through a small and simple example in the following paragraphs.

Programming with OpenAL in Pure C

OpenAL is short for Open Audio Library. It is an API (Application Programming Interface) with the goal of efficiently rendering multichannel three-dimensional positional audio, usually in games. It is cross-platform and based on the graphics library OpenGL (Open Graphics Library), although “based” in this case means “inspiration”.

It was originally developed in 2000 and became an open source project soon after. But since its version 1.1 it is proprietary again, under the thumb of Creative Technology. However, there is an open source implementation called OpenAL Soft which you can find hereOpens in a new tab. (and which we will use in our example). It is licensed under the LGPL.

One thing that OpenAL cannot do is loading a wave file. In the early days ALUT (or freealut) was the library used to do this. Nowadays it is more or less depricated and you should use other libraries or even write your own waveloader.

Example: How to Start Programming OpenAL in Pure C

We write a C Program that loads a WAVE File from a specified path and plays it through the primary sound device with the usage of OpenAL. We will use the aforementioned WAVE Loader in order to get the sound from the hard disk drive.

In order to keep this example short and simple I refrained from error handling throughout. This is, of course, not an approach that you should take in real life!

Used Libraries and Defines

We need the OpenAL libraries which are located in an AL subfolder, the wave loading library (tinywav in this case) and some Standard C libraries. The defines make life easier for us and the code more readable.

/* INCLUDES */
#include <AL/al.h>
#include <AL/alc.h>
#include "Lib/tinywav/tinywav.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

/* DEFINES */
#define CHANNELS 2
#define BLOCK_SIZE 512
#define SAMPLE_RATE 48000
#define BUFFER_SIZE 307200

Function Declarations and Global Variables

We first declare all functions that we will need. You could also define them here but I like to have the main function as first defined function in my programs. It also makes it easier to factor it out into an interface later if you want to.

Next come all global variables that we need throughout the whole program. They are valid in all functions but also static which makes them local to the current .c-file (in this case main.c). It also helps to keep things clean when moving to interfaces or other modules.

/* FUNCTIONS */
static int initialize_openal();
static void list_audio_devices(const ALCchar* devices);
static void load_audio_file(const char* fileName, int* totalNumSamples);
static void fill_buffer();
static void select_source();
static void play_audio();
static void clean_up();

/* VARIABLES */
static float _bigBuffer[BUFFER_SIZE];
static ALCdevice* _device;
static ALCcontext* _context;
static ALuint _source;
static ALuint _buffer;

Initializing OpenAL

First we create our OpenAL device which is then needed to create our context. The context is the playground if you will. We list all available devices but always take the default one. You could modify the code to let the user (or a config file setting) chose the device.

...

static int initialize_openal();
static void list_audio_devices(const ALCchar* devices);

...

int initialize_openal()
{
	ALboolean enumeration;

	_device = alcOpenDevice(NULL);
	if (!_device) {
		/* Handle Errors */
		return -1;
	}

	enumeration = alcIsExtensionPresent(NULL, "ALC_ENUMERATION_EXT");
	if (enumeration == AL_FALSE) {
		/* Enumeration not supported */
		return -2;
	}

	list_audio_devices(alcGetString(NULL, ALC_DEVICE_SPECIFIER));

	_context = alcCreateContext(_device, NULL);
	if (!alcMakeContextCurrent(_context)) {
		/* Context creation failed */
		return -3;
	}

	return 0;
}

static void list_audio_devices(const ALCchar* devices)
{
	const ALCchar* device = devices, * next = devices + 1;
	size_t len = 0;

	fprintf(stdout, "Devices list:\n");
	fprintf(stdout, "-------------\n");
	while (device && *device != '\0' && next && *next != '\0') {
		fprintf(stdout, "%s\n", device);
		len = strlen(device);
		device += (len + 1);
		next += (len + 2);
	}
	fprintf(stdout, "-------------\n");
}

Loading the Wave File into the Buffer

For our example I will use the tinywav-Loader for loading our WAV fileOpens in a new tab.. It is a small C library that can read and write wav files and is easy to use.

I open the wav file and make sure that the format is as expected. Then I read the samples in chunks and add them to the big buffer. After all is read I close the wav file and return.

...
static void load_audio_file(const char* fileName, int* totalNumSamples);
...

static void load_audio_file(const char* fileName, int* totalNumSamples)
{
	TinyWav tw;
	int samplesProcessed = 0;
	int currentSample = 0;

	tinywav_open_read(&tw, fileName, TW_INLINE);

	if (tw.numChannels != CHANNELS || tw.h.SampleRate != SAMPLE_RATE) {
		fprintf(stdout, "WAV File Format Failure\n");
		return;
	}

	*totalNumSamples = tw.numFramesInHeader;
	while (samplesProcessed < *totalNumSamples) {
		float tw_buffer[CHANNELS * BLOCK_SIZE];
		int samplesRead = tinywav_read_f(&tw, tw_buffer, BLOCK_SIZE);

		for (currentSample = 0; currentSample < (samplesRead * CHANNELS); currentSample++) {
			_bigBuffer[samplesProcessed + currentSample] = tw_buffer[currentSample];
		}

		samplesProcessed += samplesRead * CHANNELS;
	}

	tinywav_close_read(&tw);
}
...

Selecting the Data and Playing the Audio

Now that we have the wave data ready in our buffer array we put it into the OpenAL buffer. Then we select the buffer as source for the OpenAL environment and make some decisions about the playback parameters.

I have the file “./Assets/piano.wav” in the corresponding folder on my hard drive. Make sure you have it, too or rename the path to an audio file that exists in your environment!

...
static void fill_buffer();
static void select_source();
...

static void fill_buffer()
{
	int totalNumSamples = 0;

	alGenBuffers((ALuint)1, &_buffer);

	load_audio_file("./Assets/piano.wav", &totalNumSamples);

	alBufferData(_buffer, AL_FORMAT_STEREO16, _bigBuffer, totalNumSamples, SAMPLE_RATE);
}

static void select_source()
{
	alGenSources((ALuint)1, &_source);
	alSourcef(_source, AL_PITCH, 1);
	alSourcef(_source, AL_GAIN, 1);
	alSource3f(_source, AL_POSITION, 0, 0, 0);
	alSource3f(_source, AL_VELOCITY, 0, 0, 0);
	alSourcei(_source, AL_LOOPING, AL_FALSE);

	alSourcei(_source, AL_BUFFER, _buffer);
}

Everything is prepared and we are ready to play the sounds. We call the alSourcePlay(…) function of OpenAL and loop as long as there is something left to play in the buffer.

...
static void play_audio();
...

static void play_audio()
{
	ALint source_state;

	alSourcePlay(_source);

	do {
		alGetSourcei(_source, AL_SOURCE_STATE, &source_state);
	} while (source_state == AL_PLAYING);
}

Cleaning Up

The sound has played so we can now exit our program. But before we do so we should clean up everything. That means we free all sources and buffers and destroy our context and device. This way we don’t produce memory leaks and give the control over the device back to the operating system.

...
static void clean_up();
...

static void clean_up()
{
	alDeleteSources(1, &_source);
	alDeleteBuffers(1, &_buffer);
	_device = alcGetContextsDevice(_context);
	alcMakeContextCurrent(NULL);
	alcDestroyContext(_context);
	alcCloseDevice(_device);
}

Putting It All Together

The groundwork is laid out and the hard work is done. All we have left to do is call our functions in the correct order. If everything went right we can hear the file playing and then see the program quit.

/* Includes, Defines, Declarations */
... 

int main()
{
	int error = 0;
	error = initialize_openal();
	if (error) { return error; }	
	fill_buffer();
	select_source();
	play_audio();	
	clean_up();

	return 0;
}

...
/* Definitions */

This was a first example of how to use OpenAL with Pure C. Now you can consider to use it for your own games.

Marco Lieblang

Professional Programmer since 2003, passionate Programmer since the mid 90's. Developing in many languages from C/C++ to Java, C#, Python and some more. And I also may know a bit about Assembly Languages and Retro Systems.

Recent Posts