Thursday, 1 August 2013


Playing Audio Samples Using OpenAL on iOS


Before we get into this tutorial I need to give you fair warning. Don't use this code in your iOS projects! It is rubbish and you shouldn't use OpenAL this way. The aim of this tutorial is to introduce you to the concepts of OpenAL. In the next tutorial, OpenAL on iOS - The 12 Step Program, you will find some code that you can actually use in your iOS apps.

Ok, with that out of the way lets learn a little bit about OpenAL.

What is OpenAL?


OpenAL is a cross-platform API for playing audio and was developed for use in gaming. OpenAL has many powerful features that we can take advantage of. OpenAL is a C API. If you haven't used or seen any code written in C it will look a little foreign to you. Don't let this discourage you! There is a little good news to put you at ease. Objective-C is based on C, so any code you write in C is completely acceptable in Objective-C. That means we don't have to do any additional work to make our C code work in an Objective-C environment. It will just look a little different.

Low Latency Playback


With OpenAL we can preload audio samples into buffers and play them at any time we choose. Since the samples can be preloaded, when you ask for a sound to be played it happens very quickly. 

Mixing Simultaneous Sounds


OpenAL allows you to play multiple samples at the same time. Mixing of samples happens automatically. This is very important in gaming. For example, you can have laser blast, explosions and aircraft sounds all being played at the same time.

Audio Effects


OpenAL allows you to define the location of a sound in space. This is know as positional or 3D audio. It's great for audio effects such as objects moving across the screen.

As objects move away from you the sound they product becomes fainter. OpenAL allows you to include distance effects to your objects sounds. As they move off screen you can reduce the gain of the sound they produce.

OpenAL allows you to alter the pitch of a sound. This is great for creating audio effects such as the Doppler Effect (as objects move towards you they have a higher pitch and the moment they move past you they have a lower pitch).

A Bit of Background…


Ok, so why did I start using OpenAL? I am not a game developer. It would be nice and who knows, maybe one day I will have the privilege but for now I am developing music apps. You can check some of them out on my site www.musicopoulos.com. As with games, music apps need low latency sounds that can be mixed together. When a user touches a piano key, the sound need to be played immediately. And if they touch a second key, the second sound should be played over the first sound. These are my goals for OpenAL. If your goals go beyond this then thats fine, you still have to start with the basics.

So lets get into some code! 

If you would like to follow along with this tutorial (which I recommend you do), you will need to:

  • Create a new Xcode project (call this project anything you like)
  • Create a new file, I call mine AudioSamplePlayer, that inherits from NSObject
  • Import the frameworks OpenAL.framework and AudioToolbox.framework
  • In your AudioSamplePlayer.h file (or the header for the file you created), import the following:
    • #import <OpenAl/al.h>
    • #import <OpenAl/alc.h>
    • #include <AudioToolbox/AudioToolbox.h>

Create a new method and call it playSound. Your header and implementation file should look something like this:

#import <Foundation/Foundation.h>

#import <OpenAl/al.h>
#import <OpenAl/alc.h>
#include <AudioToolbox/AudioToolbox.h>

@interface AudioSamplePlayer : NSObject

- (void) playSound;

@end

#import "AudioSamplePlayer.h"

@implementation AudioSamplePlayer

- (void) playSound
{
    
}

@end

Declare 2 static variables in your AudioSamplePlayer.m file. We will get to these in a second.

#import "AudioSamplePlayer.h"

@implementation AudioSamplePlayer

static ALCdevice *openALDevice;

static ALCcontext *openALContext;

- (void) playSound
{
    
}

@end

The first thing we need to do with OpenAL is open a device. A device is a physical thing that you use to process sound. For example a sound card would be a device. There can only be one device, and this is why we declared the static variable ALCdevice *openALDevice.

In your init method, use the function alcOpenDevice() to initialise the device. Note the use of 'NULL' here indicates that we want the default device.

#import "AudioSamplePlayer.h"

@implementation AudioSamplePlayer

static ALCdevice *openALDevice;

static ALCcontext *openALContext;

- (id)init
{
    self = [super init];
    if (self)
    {
        openALDevice = alcOpenDevice(NULL);
    }
    return self;
}

- (void) playSound
{
    
}

@end


Now that we have our device we need to set up the context. The context is the combination of the person hearing the sound and the air in which the sound travels. In other words, in what context is the sound being played?

#import "AudioSamplePlayer.h"

@implementation AudioSamplePlayer

static ALCdevice *openALDevice;

static ALCcontext *openALContext;

- (id)init
{
    self = [super init];
    if (self)
    {
        openALDevice = alcOpenDevice(NULL);
        
        openALContext = alcCreateContext(openALDevice, NULL);
        alcMakeContextCurrent(openALContext);
    }
    return self;
}

- (void) playSound
{
    
}

@end

In these 2 lines of code we are declaring a variable named openALContext of type ALCcontext and using the function alcCreateContext() to initialise the context. This function takes 2 variables:

  1. A valid ALCdevice, which we created previously.
  2. A list of attributes. Note, we are not providing any attributes so we are passing in NULL.

We are then using the function alcMakeContextCurrent() to make our context the current context.

We are now ready to create a source. A 'source' is something that produces sound, like a speaker. When we play a sound, we need to specify a source to play it through. We are going to concentrate on the playSound method you created earlier.

- (void) playSound
{
    NSUInteger sourceID;
    alGenSources(1, &sourceID);
}

So what's going on here? First, we declare a sourceID of type NSUInteger. We then generate a source using the function alGenSources(), passing in a pointer to our declared sourceID. The alGenSources() function takes 2 variables:

  1. The number of sources you would like to create.
  2. A pointer to where the generated source reference should be placed.

We are generating a single source and placing the generated source reference into our sourceID variable. You can generate up to 32 sources and pass in a pointer to an array, and this function will populate the array with sound sources. In the next tutorial, we will look at a better way to create and storing a collection of sourceID's.

Now the next part is a little involved. What we are going to do is locate our audio sample, open the file, determine the size of the audio data, load that data into a buffer, ready to play our sample.

So, lets get a reference to our audio sample.

- (void) playSound
{
    NSUInteger sourceID;
    alGenSources(1, &sourceID);

    NSString *audioFilePath = [[NSBundle mainBundle] pathForResource:@"ting" ofType:@"caf"];
    NSURL *audioFileURL = [NSURL fileURLWithPath:audioFilePath];
}

If you have dealt with saving and loading files in Objective-C this will look familiar to you. In the first line of code we are getting the path to a file called 'ting.caf' and returning that path as an NSString object. We are then using that string to generate an NSURL.

Now that we have a reference to the audio sample, we can open the file. However, we have to do this in an OpenAL audio friendly way.

- (void) playSound
{
    NSUInteger sourceID;
    alGenSources(1, &sourceID);

    NSString *audioFilePath = [[NSBundle mainBundle] pathForResource:@"ting" ofType:@"caf"];
    NSURL *audioFileURL = [NSURL fileURLWithPath:audioFilePath];

    AudioFileID afid;
    OSStatus openAudioFileResult = AudioFileOpenURL((__bridge CFURLRef)audioFileURL, kAudioFileReadPermission, 0, &afid);
    
    if (0 != openAudioFileResult)
    {
        NSLog(@"An error occurred when attempting to open the audio file %@: %ld", audioFilePath, openAudioFileResult);
        return;
    }
}

Audio File Services uses an AudioFileID to reference an audio file. The function AudioFileOpenURL() will open the audio sample and place the data into our AudioFileID variable, afid. The AudioFileOpenURL() function takes 4 parameters:

  1. A URL path to the file. Note, in the code above we generated an NSURL. We need to cast this to be of type CFURLRef. In this example, we are performing a __bridge cast as we are using Automatic Reference Counting (ARC).
  2. The type of permissions used to open the file.
  3. A hit for the file type. Note, in our case we are passing 0, which indicates that we are not providing a file hint.
  4. A pointer to an AudioFileID

AudioFileOpenURL() returns OSStatus. If the if() statement, we are checking if there was an error while opening the file. If an error has occurred, we will print a log statement and return (the remaining code will not be executed).

With the audio sample open, we need to determine the size of the audio data. Now, an audio file contains all sorts of information, but we are only interested in the audio data.

- (void) playSound
{
    NSUInteger sourceID;
    alGenSources(1, &sourceID);

    NSString *audioFilePath = [[NSBundle mainBundle] pathForResource:@"ting" ofType:@"caf"];
    NSURL *audioFileURL = [NSURL fileURLWithPath:audioFilePath];

    AudioFileID afid;
    OSStatus openAudioFileResult = AudioFileOpenURL((__bridge CFURLRef)audioFileURL, kAudioFileReadPermission, 0, &afid);
    
    if (0 != openAudioFileResult)
    {
        NSLog(@"An error occurred when attempting to open the audio file %@: %ld", audioFilePath, openAudioFileResult);
        return;
    }

    UInt64 audioDataByteCount = 0;
    UInt32 propertySize = sizeof(audioDataByteCount);
    OSStatus getSizeResult = AudioFileGetProperty(afid, kAudioFilePropertyAudioDataByteCount, &propertySize, &audioDataByteCount);
    
    if (0 != getSizeResult)
    {
        NSLog(@"An error occurred when attempting to determine the size of audio file %@: %ld", audioFilePath, getSizeResult);
    }
}

So what on earth is happening here! As we said above, an audio file consist of many things, but we want the audio data. The function AudioFileGetProperty() will query the audio file and extract the property we are searching for, the audio data. Let's look at the parameters this function takes so we can see how it does this. The parameters are:

  1. The audio file we want to extract the information from.
  2. The property type we want to extract.
  3. The size of that property type.
  4. A pointer to the place we want the requested property to be delivered to.

The audio data byte count we are after is calculated as a UInt64. We declare a variable of this type called audioDataByteCount and set it initially to zero. Next, we create a variable of type UInt32 and set it to the size of the audioDataByteCount. We are then using this information to call the AudioFileGetProperty() function and return an OSStatus. Finally we check getSizeResult for any errors.

Now that we have the audio data property, we need to read that information.

- (void) playSound
{
    NSUInteger sourceID;
    alGenSources(1, &sourceID);

    NSString *audioFilePath = [[NSBundle mainBundle] pathForResource:@"ting" ofType:@"caf"];
    NSURL *audioFileURL = [NSURL fileURLWithPath:audioFilePath];

    AudioFileID afid;
    OSStatus openAudioFileResult = AudioFileOpenURL((__bridge CFURLRef)audioFileURL, kAudioFileReadPermission, 0, &afid);
    
    if (0 != openAudioFileResult)
    {
        NSLog(@"An error occurred when attempting to open the audio file %@: %ld", audioFilePath, openAudioFileResult);
        return;
    }

    UInt64 audioDataByteCount = 0;
    UInt32 propertySize = sizeof(audioDataByteCount);
    OSStatus getSizeResult = AudioFileGetProperty(afid, kAudioFilePropertyAudioDataByteCount, &propertySize, &audioDataByteCount);
    
    if (0 != getSizeResult)
    {
        NSLog(@"An error occurred when attempting to determine the size of audio file %@: %ld", audioFilePath, getSizeResult);
    }
    
    UInt32 bytesRead = (UInt32)audioDataByteCount;
    
    void *audioData = malloc(bytesRead);
    
    OSStatus readBytesResult = AudioFileReadBytes(afid, false, 0, &bytesRead, audioData);
    
    if (0 != readBytesResult)
    {
        NSLog(@"An error occurred when attempting to read data from audio file %@: %ld", audioFilePath, readBytesResult);
    }
    
    AudioFileClose(afid);
}

We using the function AudioFileReadBytes() to read in our audio data. This function takes the following parameters:

  1. The audio file we want to read the data from.
  2. A bool that determines if we want to cache the data
  3. The position we want to start reading the data from
  4. A pointer to the number of bytes we want to read
  5. A pointer to a block of memory we can store the data

Now, we already have our AudioFileID, afid, we don't want to cache the data and we need to start from the beginning of the file. That is the first 3 parameters taken care of. We previously calculated the byte size of the audio data however, this gave us a variable of type UInt64. Our AudioFileReadBytes() function requires our bytes size to be of type UInt32. To achieve this, we are creating a new variable called bytesRead of type UInt32 and initialise it by casting our audioDataByteCount variable to be of type UInt32.

To store the data read in, we are allocating, malloc(), enough memory to store data the size of bytesRead and referencing that location in memory with our audioData variable.

As with previous functions, AudioFileReadBytes() returns an OSStatus and we are checking for any errors.

Now that we have read in the data, we close the file using the AudioFileClose() function, passing in our AudioFileID, afid.

Ok, we are almost there! We have opened our audio sample and read its audio data. Now we can place that data into a buffer.

- (void) playSound
{
    NSUInteger sourceID;
    alGenSources(1, &sourceID);

    NSString *audioFilePath = [[NSBundle mainBundle] pathForResource:@"ting" ofType:@"caf"];
    NSURL *audioFileURL = [NSURL fileURLWithPath:audioFilePath];

    AudioFileID afid;
    OSStatus openAudioFileResult = AudioFileOpenURL((__bridge CFURLRef)audioFileURL, kAudioFileReadPermission, 0, &afid);
    
    if (0 != openAudioFileResult)
    {
        NSLog(@"An error occurred when attempting to open the audio file %@: %ld", audioFilePath, openAudioFileResult);
        return;
    }

    UInt64 audioDataByteCount = 0;
    UInt32 propertySize = sizeof(audioDataByteCount);
    OSStatus getSizeResult = AudioFileGetProperty(afid, kAudioFilePropertyAudioDataByteCount, &propertySize, &audioDataByteCount);
    
    if (0 != getSizeResult)
    {
        NSLog(@"An error occurred when attempting to determine the size of audio file %@: %ld", audioFilePath, getSizeResult);
    }
    
    UInt32 bytesRead = (UInt32)audioDataByteCount;
    
    void *audioData = malloc(bytesRead);
    
    OSStatus readBytesResult = AudioFileReadBytes(afid, false, 0, &bytesRead, audioData);
    
    if (0 != readBytesResult)
    {
        NSLog(@"An error occurred when attempting to read data from audio file %@: %ld", audioFilePath, readBytesResult);
    }
    
    AudioFileClose(afid);

    ALuint outputBuffer;
    alGenBuffers(1, &outputBuffer);
    
    alBufferData(outputBuffer, AL_FORMAT_STEREO16, audioData, bytesRead, 44100);
    
    if (audioData)
    {
        free(audioData);
        audioData = NULL;
    }
}

The first thing we need to do is create our buffer. We declare a variable of type ALuint called outputBuffer. We then using the function alGenBuffers() to generate our buffer. This function takes 2 parameters:

  1. The number of buffer you would like to create.
  2. A pointer to where the generated buffer reference should be placed.

We are generating a single buffer and placing the generated buffer reference into our outputBuffer variable. You can generate up to 256 buffers and pass in a pointer to an array, and this function will populate the array with buffers. In the next tutorial, we will look at a better way to create and storing a collection of buffers.

Now that we have our buffer, we can copy the audio data we extracted into the buffer. We do this using the alBufferData() function, which takes the following parameters:

  1. A variable of type Aluint where the buffer ID will be stored.
  2. The audio format
  3. The audio data we want to copy to the buffer
  4. The size of the data
  5. The frequency of the audio sample

In our case, we are setting the frequency to 44100 and choosing the AL_FORMAT_STEREO16 format. I will talk a little about audio format at the end of this tutorial.

Now that we have copied the data to our buffer, we can release the memory we allocated earlier.

We now have everything we need to play our audio sample, we just need to put it all together.

- (void) playSound
{
    NSUInteger sourceID;
    alGenSources(1, &sourceID);

    NSString *audioFilePath = [[NSBundle mainBundle] pathForResource:@"ting" ofType:@"caf"];
    NSURL *audioFileURL = [NSURL fileURLWithPath:audioFilePath];

    AudioFileID afid;
    OSStatus openAudioFileResult = AudioFileOpenURL((__bridge CFURLRef)audioFileURL, kAudioFileReadPermission, 0, &afid);
    
    if (0 != openAudioFileResult)
    {
        NSLog(@"An error occurred when attempting to open the audio file %@: %ld", audioFilePath, openAudioFileResult);
        return;
    }

    UInt64 audioDataByteCount = 0;
    UInt32 propertySize = sizeof(audioDataByteCount);
    OSStatus getSizeResult = AudioFileGetProperty(afid, kAudioFilePropertyAudioDataByteCount, &propertySize, &audioDataByteCount);
    
    if (0 != getSizeResult)
    {
        NSLog(@"An error occurred when attempting to determine the size of audio file %@: %ld", audioFilePath, getSizeResult);
    }
    
    UInt32 bytesRead = (UInt32)audioDataByteCount;
    
    void *audioData = malloc(bytesRead);
    
    OSStatus readBytesResult = AudioFileReadBytes(afid, false, 0, &bytesRead, audioData);
    
    if (0 != readBytesResult)
    {
        NSLog(@"An error occurred when attempting to read data from audio file %@: %ld", audioFilePath, readBytesResult);
    }
    
    AudioFileClose(afid);

    ALuint outputBuffer;
    alGenBuffers(1, &outputBuffer);
    
    alBufferData(outputBuffer, AL_FORMAT_STEREO16, audioData, bytesRead, 44100);
    
    if (audioData)
    {
        free(audioData);
        audioData = NULL;
    }

    alSourcef(sourceID, AL_PITCH, 1.0f);
    alSourcef(sourceID, AL_GAIN, 1.0f);
    
    alSourcei(sourceID, AL_BUFFER, outputBuffer);
    
    alSourcePlay(sourceID);
}

First, let's give the source we created earlier some parameters so that it knows how to play the audio sample. We are using the function alSourcef() to set both the pitch and gain to 1 (we could choose any float value between 0.0 and 1.0).

We then attach the buffer to the source using the function alSourcei() and finally, use the function alSourcePlay(), passing in our sourceID to play our audio sample.

That's it! Everything you 'need' to play an audio sample using OpenAL. But as I said at the beginning of this article, this code is terrible. Yes, it works, but it has some serious problems. We will fix those in the next tutorial. What I wanted to do here is just introduce you to the basic concepts. After you understand this tutorial, you can take the next step and get the most out of OpenAL.

So other than dumping everything into a single method, what else is wrong with this code? Have a look at the first 2 points about OpenAL at the top of this tutorial. We are after low latency playback and mixing of simultaneous sounds. This code is not giving us any of that. Each time we play the audio sample, we are opening the file, copying the data to the buffer and attaching it to a single source before playing the sound. What we should be doing is loading the audio sample into a buffer once, and then access that buffer each time we want to play the sound. Also, we are only using one source. If the source is playing a sound when we try to play a second sound, the first sound will be cut short. They will not mix together. What we have done in this tutorial is lay the foundation. In the next tutorial we will solve these problems and give you some code you can actually work with.

Now, I said I would talk about audio format at the end of this tutorial.

You can get OpenAL to play all sorts of audio formats. There are mechanisms for converting audio formats on the fly. There are however overheads with doing this type of thing. You best bet is to use a format that OpenAL likes. This makes things much smoother. The right format to use is linear-PCM little-endian 16 bit caf files. If your audio files are in a different format, thats ok. You can use Terminal on OS X to convert them to the right format. Open Terminal and navigate to the directory that houses your audio files. Use the command:

afconvert -f caff -d LEI16@44100 audioFile.wav audioFile.caf

In this example, afconvert will take an audio file of type wav and convert it to the desired format. Your original file will remain in tact.

That concludes this tutorial. You can download a sample app on GitHub, https://github.com/OhNo789/BasicOpenAL, but please please please don't use this for any production products. It's for your own good. Stay tuned for the second instalment where I will show you how get a little more out of OpenAL.

5 comments:

  1. Hi there!

    First of all thanks for this great tutorial!

    I get a warning:
    {
    NSUInteger sourceID;
    alGenSources(1, "here ---->"&sourceID);

    It says: "Incompatible pointer typers passing 'NSUInteger *' (aka 'unsigned long *') to parameter of tyer 'ALuint *' (aka 'unsigned int *')

    Any ideas about that?

    best /Rasmus


    ReplyDelete
  2. Oi dude thanks for the tut so far. Code compiles and runs, but all I get out of my speakers is like 0.5 seconds of white noise.

    Any advice?

    ReplyDelete
  3. Best explanation ever! Thank you pal!

    ReplyDelete
  4. Wow such a wonderful explanation......

    ReplyDelete
  5. Alexander and Ramus, convert those files to CAF audio files, or edit the setup of the audio engine to take into account the type of file format you are using.

    ReplyDelete