bookmark_borderOpenAL Sucks, Write your own Audio Mixer

For one of my recent projects, as part of a video recording feature I needed to save mixed audio from OpenAL.

The only problem was, you simply cannot do that using any OpenAL implementation i’ve seen. The only thing which comes remotely close it is the WAV-out device in OpenAL Soft, which for all intensive purposes is useless.

So after lots of research, I ended up writing my own audio mixer. But “how do you write an audio mixer?” you may ask. It is in fact quite simple, assuming you keep it simple.

All an audio mixer does is take input samples, multiplies them according to the volume then adds (mixes) them together into an output buffer.

The output buffer typically uses a higher bit-depth than the buffers which are mixed, otherwise there’s a fair chance for clipping. As a final step the output buffer is converted to the correct sampling rate for output, or in my case recording.

My Implementation

For my mixer, I decided upon the following abstraction which handles managing audio channels and buffers

  • SoundMixer – where all channels are mixed
  • SoundChannel – a channel of audio, which links to one or more buffers of audio samples
  • SoundBuffer – where the actual audio samples to be mixed are stored

The actual mixing takes place in a scratch buffer, which is repeatedly filled then converted and dumped to the target output. The mixing function is as simple as this!

// For each channel....
short* ptr = (short*)in;
float lVol = (channel->mLeftVolume*mMasterVolume);  // calculate volume
float rVol = (channel->mRightVolume*mMasterVolume);
for (int i=0; i

In the end the whole thing works like this:

short out[44100*2*2]; // output samples (stereo, 16bit pcm, 2 seconds)

SoundMixer *mixer = new SoundMixer(32, 44100); // #channels, sampling rate

SoundChannel *channel = mixer->allocChannel();
SoundBuffer *buffer = mixer->allocBuffer();
SoundBuffer *buffer2 = mixer->allocBuffer();

// Make some buffer data

unsigned short random[11025]; // 1s random data
for (int i=0; i<11025; i++) {
   random[i] = randi(0,65536);
}

unsigned short random2[11025]; // 1s random data
for (int i=0; i<11025; i++) {
   random2[i] = sin((float)i) * (65536/4);
}

// Store data in buffers
// (samples, data, source sampling rate, dest sampling rate, format)
buffer->buffer(11025, (unsigned char*)random, 11025, 44100, SOFT_MONO16); 
buffer2->buffer(11025, (unsigned char*)random2, 11025, 44100, SOFT_MONO16);

// Setup channel
channel->mBuffer = NULL;
channel->mLeftVolume = 1.0;  // volume is 0->1.0
channel->mRightVolume = 1.0;
channel->mLeftVolume = 100;
channel->play();             // state is now "playing"

channel->mBufferQueue.pushBuffer(buffer);  // this will play first
channel->mBufferQueue.pushBuffer(buffer2); // followed by this

// Actually mix the audio
mixer->mix(out, 44100/2);       // 0.5 seconds
mixer->mMasterVolume = 100;
mixer->mix(out+44100, 44100/2); // +0.5 seconds
mixer->mix(out+44100, 44100/2); // +0.5 seconds

// Dump
FILE *fp = fopen("dump.raw", "w");
fwrite(out, 1, sizeof(out), fp);
fclose(fp);

// Cleanup
mixer->freeChannel(channel);
mixer->freeBuffer(buffer);
delete mixer;

On top of this fancy audio mixer, I basically made my own wrapper for OpenAL. It basically dispatches OpenAL instructions to both the systems OpenAL and my audio mixer.

ALvoid     talBufferData( ALuint   buffer,
                          ALenum   format,
                          ALvoid*  data,
                          ALsizei  size,
                          ALsizei  freq )
{
   sMainBackend->talBufferData(buffer, format, data, size, freq);
   sSecondBackend->talBufferData((ALuint)mBufferMap.retreive(buffer), format, data, size, freq);
}

So there you have it, a simple audio mixer which works great when you want to do cool things such as record audio from a game.

In case you dont fancy writing your own audio mixer however, there are plenty of good examples of complete audio mixers scattered about. E.g. OpenAL Soft, Allegro (addons).