iPhone Programming Part 6: Multiple Sounds with OpenAL

So today we want to do the hard stuff I talked about in the last post.
We will do that in three steps.

  1. Writing an OpenAL sound file loader.
  2. Writing an OpenAL wrapper class wich is more comfortable than using the OpenAL functions directly.
  3. Writing the actual code to play some audio files.
1. Writing an OpenAL sound file loader

Actually there is a sample code within the apples developers support files. While Apples iPhone SDK is not under NDA anymore I am able to post the code here:

/*
 
    File: MyOpenALSupport.h
Abstract: OpenAL-related support functions
 Version: 1.3
 
Disclaimer: IMPORTANT:  This Apple software is supplied to you by Apple
Inc. ("Apple") in consideration of your agreement to the following
terms, and your use, installation, modification or redistribution of
this Apple software constitutes acceptance of these terms.  If you do
not agree with these terms, please do not use, install, modify or
redistribute this Apple software.
 
In consideration of your agreement to abide by the following terms, and
subject to these terms, Apple grants you a personal, non-exclusive
license, under Apple's copyrights in this original Apple software (the
"Apple Software"), to use, reproduce, modify and redistribute the Apple
Software, with or without modifications, in source and/or binary forms;
provided that if you redistribute the Apple Software in its entirety and
without modifications, you must retain this notice and the following
text and disclaimers in all such redistributions of the Apple Software.
Neither the name, trademarks, service marks or logos of Apple Inc. may
be used to endorse or promote products derived from the Apple Software
without specific prior written permission from Apple.  Except as
expressly stated in this notice, no other rights or licenses, express or
implied, are granted by Apple herein, including but not limited to any
patent rights that may be infringed by your derivative works or by other
works in which the Apple Software may be incorporated.
 
The Apple Software is provided by Apple on an "AS IS" basis.  APPLE
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
 
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
 
Copyright (C) 2008 Apple Inc. All Rights Reserved.
 
 */
 
#import <OpenAL/al.h>
#import <OpenAL/alc.h>
#import <AudioToolbox/AudioToolbox.h>
#import <AudioToolbox/ExtendedAudioFile.h>
 
typedef ALvoid	AL_APIENTRY	(*alBufferDataStaticProcPtr) (const ALint bid, ALenum format, ALvoid* data, ALsizei size, ALsizei freq);
ALvoid  alBufferDataStaticProc(const ALint bid, ALenum format, ALvoid* data, ALsizei size, ALsizei freq);
 
void* MyGetOpenALAudioData(CFURLRef inFileURL, ALsizei *outDataSize, ALenum *outDataFormat, ALsizei*	outSampleRate);
/*
 
    File: MyOpenALSupport.c
Abstract: OpenAL-related support functions
 Version: 1.3
 
Disclaimer: IMPORTANT:  This Apple software is supplied to you by Apple
Inc. ("Apple") in consideration of your agreement to the following
terms, and your use, installation, modification or redistribution of
this Apple software constitutes acceptance of these terms.  If you do
not agree with these terms, please do not use, install, modify or
redistribute this Apple software.
 
In consideration of your agreement to abide by the following terms, and
subject to these terms, Apple grants you a personal, non-exclusive
license, under Apple's copyrights in this original Apple software (the
"Apple Software"), to use, reproduce, modify and redistribute the Apple
Software, with or without modifications, in source and/or binary forms;
provided that if you redistribute the Apple Software in its entirety and
without modifications, you must retain this notice and the following
text and disclaimers in all such redistributions of the Apple Software.
Neither the name, trademarks, service marks or logos of Apple Inc. may
be used to endorse or promote products derived from the Apple Software
without specific prior written permission from Apple.  Except as
expressly stated in this notice, no other rights or licenses, express or
implied, are granted by Apple herein, including but not limited to any
patent rights that may be infringed by your derivative works or by other
works in which the Apple Software may be incorporated.
 
The Apple Software is provided by Apple on an "AS IS" basis.  APPLE
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
 
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
 
Copyright (C) 2008 Apple Inc. All Rights Reserved.
 
*/
 
#include "MyOpenALSupport.h"
 
ALvoid  alBufferDataStaticProc(const ALint bid, ALenum format, ALvoid* data, ALsizei size, ALsizei freq)
{
	static	alBufferDataStaticProcPtr	proc = NULL;
 
    if (proc == NULL) {
        proc = (alBufferDataStaticProcPtr) alcGetProcAddress(NULL, (const ALCchar*) "alBufferDataStatic");
    }
 
    if (proc)
        proc(bid, format, data, size, freq);
 
    return;
}
 
void* MyGetOpenALAudioData(CFURLRef inFileURL, ALsizei *outDataSize, ALenum *outDataFormat, ALsizei*	outSampleRate)
{
	OSStatus err = noErr;
	SInt64 theFileLengthInFrames = 0;
	AudioStreamBasicDescription theFileFormat;
	UInt32 thePropertySize = sizeof(theFileFormat);
	ExtAudioFileRef extRef = NULL;
	void* theData = NULL;
	AudioStreamBasicDescription theOutputFormat;
 
	// Open a file with ExtAudioFileOpen()
	err = ExtAudioFileOpenURL(inFileURL, &extRef);
	if(err) { printf("MyGetOpenALAudioData: ExtAudioFileOpenURL FAILED, Error = %ld\n", err); goto Exit; }
 
	// Get the audio data format
	err = ExtAudioFileGetProperty(extRef, kExtAudioFileProperty_FileDataFormat, &thePropertySize, &theFileFormat);
	if(err) { printf("MyGetOpenALAudioData: ExtAudioFileGetProperty(kExtAudioFileProperty_FileDataFormat) FAILED, Error = %ld\n", err); goto Exit; }
	if (theFileFormat.mChannelsPerFrame > 2)  { printf("MyGetOpenALAudioData - Unsupported Format, channel count is greater than stereo\n"); goto Exit;}
 
	// Set the client format to 16 bit signed integer (native-endian) data
	// Maintain the channel count and sample rate of the original source format
	theOutputFormat.mSampleRate = theFileFormat.mSampleRate;
	theOutputFormat.mChannelsPerFrame = theFileFormat.mChannelsPerFrame;
 
	theOutputFormat.mFormatID = kAudioFormatLinearPCM;
	theOutputFormat.mBytesPerPacket = 2 * theOutputFormat.mChannelsPerFrame;
	theOutputFormat.mFramesPerPacket = 1;
	theOutputFormat.mBytesPerFrame = 2 * theOutputFormat.mChannelsPerFrame;
	theOutputFormat.mBitsPerChannel = 16;
	theOutputFormat.mFormatFlags = kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked | kAudioFormatFlagIsSignedInteger;
 
	// Set the desired client (output) data format
	err = ExtAudioFileSetProperty(extRef, kExtAudioFileProperty_ClientDataFormat, sizeof(theOutputFormat), &theOutputFormat);
	if(err) { printf("MyGetOpenALAudioData: ExtAudioFileSetProperty(kExtAudioFileProperty_ClientDataFormat) FAILED, Error = %ld\n", err); goto Exit; }
 
	// Get the total frame count
	thePropertySize = sizeof(theFileLengthInFrames);
	err = ExtAudioFileGetProperty(extRef, kExtAudioFileProperty_FileLengthFrames, &thePropertySize, &theFileLengthInFrames);
	if(err) { printf("MyGetOpenALAudioData: ExtAudioFileGetProperty(kExtAudioFileProperty_FileLengthFrames) FAILED, Error = %ld\n", err); goto Exit; }
 
	// Read all the data into memory
	UInt32		dataSize = theFileLengthInFrames * theOutputFormat.mBytesPerFrame;;
	theData = malloc(dataSize);
	if (theData)
	{
		AudioBufferList		theDataBuffer;
		theDataBuffer.mNumberBuffers = 1;
		theDataBuffer.mBuffers[0].mDataByteSize = dataSize;
		theDataBuffer.mBuffers[0].mNumberChannels = theOutputFormat.mChannelsPerFrame;
		theDataBuffer.mBuffers[0].mData = theData;
 
		// Read the data into an AudioBufferList
		err = ExtAudioFileRead(extRef, (UInt32*)&theFileLengthInFrames, &theDataBuffer);
		if(err == noErr)
		{
			// success
			*outDataSize = (ALsizei)dataSize;
			*outDataFormat = (theOutputFormat.mChannelsPerFrame > 1) ? AL_FORMAT_STEREO16 : AL_FORMAT_MONO16;
			*outSampleRate = (ALsizei)theOutputFormat.mSampleRate;
		}
		else
		{
			// failure
			free (theData);
			theData = NULL; // make sure to return NULL
			printf("MyGetOpenALAudioData: ExtAudioFileRead FAILED, Error = %ld\n", err); goto Exit;
		}
	}
 
Exit:
	// Dispose the ExtAudioFileRef, it is no longer needed
	if (extRef) ExtAudioFileDispose(extRef);
	return theData;
}
Writing an OpenAL wrapper class wich is more comfortable than using the OpenAL functions directly.

What you really want is a singelton pattern here, or how apple named this pattern : shared class.

So lets first take a look how you would write a singelton class in objective c. Situations could arise where you want a singleton instance (created and controlled by the class factory method) but also have the ability to create other instances as needed through allocation and initialization. In these cases, you would not override allocWithZone: and the other methods following it as shown in the following code listing :

static MySingeltonClass *sharedSingeltonInstance = nil;
 
+ (MySingeltonClass*)sharedManager
{
    @synchronized(self) {
        if (sharedSingeltonInstance == nil) {
            [[self alloc] init]; // assignment not done here
        }
    }
    return sharedSingeltonInstance;
}
 
+ (id)allocWithZone:(NSZone *)zone
{
    @synchronized(self) {
        if (sharedSingeltonInstance == nil) {
            sharedSingeltonInstance = [super allocWithZone:zone];
            return sharedSingeltonInstance;  // assignment and return on first allocation
        }
    }
    return nil; //on subsequent allocation attempts return nil
}
 
- (id)copyWithZone:(NSZone *)zone
{
    return self;
}
 
- (id)retain
{
    return self;
}
 
- (unsigned)retainCount
{
    return UINT_MAX;  //denotes an object that cannot be released
}
 
- (void)release
{
    //do nothing
}
 
- (id)autorelease
{
    return self;
}

Now thats pretty straight forward – so lets create our OpenAL wrapper class. We need some functions to initialize the device, loading and control sound files. Your interface should look like this :

//
//  MyOpenAL.h
//
 
#import <Foundation/Foundation.h>
#import <OpenAL/al.h>
#import <OpenAL/alc.h>
#import <AudioToolbox/AudioToolbox.h>
 
@interface MyOpenAL : NSObject {
 
	ALCcontext* mContext; // stores the context (the 'air')
	ALCdevice* mDevice; // stores the device
	NSMutableArray * bufferStorageArray; // stores the buffer ids from openAL
	NSMutableDictionary * soundDictionary; // stores our soundkeys
}
 
// if you want to access directly the buffers or our sound dictionary
@property (nonatomic, retain) NSMutableArray * bufferStorageArray;
@property (nonatomic, retain) NSMutableDictionary * soundDictionary;
 
- (id) init; // init once
- (bool) initOpenAL; // no need to make it public, but I post it here to show you which methods we need. initOpenAL will be called within init process once.
- (void) playSoundWithKey:(NSString*)soundKey; // play a sound by name
- (void) stopSoundWithKey:(NSString*)soundKey; // stop a sound by name
- (bool) isPlayingSoundWithKey:(NSString *) soundKey; // check if sound is playing by name
- (void) rewindSoundWithKey:(NSString *) soundKey; // rewind a sound by name so its playing again
- (bool) loadSoundWithKey:(NSString *)_soundKey File:(NSString *)_file Ext:(NSString *) _ext Loop:(bool)loops; // load a sound and give it a name
 
+ (MyOpenAL*)sharedMyOpenAL; // access to our instance
 
@end

So now the easy stuff is done we get to the more complex implementation:

//
//  MyOpenAL.m
//
 
#import "MyOpenAL.h"
#import "MyOpenALSupport.h"
 
@implementation MyOpenAL
 
@synthesize bufferStorageArray;
@synthesize soundDictionary;
 
static MyOpenAL *sharedMyOpenAL = nil;
 
- (void) shutdownMyOpenAL {
	@synchronized(self) {
        if (sharedMyOpenAL != nil) {
			[self dealloc]; // assignment not done here
        }
    }
}
 
+ (MyOpenAL*)sharedMyOpenAL {
 
    @synchronized(self) {
        if (sharedMyOpenAL == nil) {
            [[self alloc] init]; // assignment not done here
        }
    }
    return sharedMyOpenAL;
}
 
+ (id)allocWithZone:(NSZone *)zone
{
    @synchronized(self) {
        if (sharedMyOpenAL == nil) {
            sharedMyOpenAL = [super allocWithZone:zone];
            return sharedMyOpenAL;  // assignment and return on first allocation
        }
    }
    return nil; //on subsequent allocation attempts return nil
}
 
- (id)copyWithZone:(NSZone *)zone
{
    return self;
}
 
- (id)retain
{
    return self;
}
 
- (unsigned)retainCount
{
 
	return UINT_MAX;  //denotes an object that cannot be released
}
 
- (void)release
{
    //do nothing
}
 
- (id)autorelease
{
    return self;
}
 
// seek in audio file for the property 'size'
// return the size in bytes
-(UInt32)audioFileSize:(AudioFileID)fileDescriptor
{
	MARK;
	UInt64 outDataSize = 0;
	UInt32 thePropSize = sizeof(UInt64);
	OSStatus result = AudioFileGetProperty(fileDescriptor, kAudioFilePropertyAudioDataByteCount, &thePropSize, &outDataSize);
	if(result != 0) NSLog(@"cannot find file size");
	return (UInt32)outDataSize;
}
 
// start up openAL
-(bool) initOpenAL
{
	MARK;
	// Initialization
	mDevice = alcOpenDevice(NULL); // select the "preferred device"
	if (mDevice) {
		// use the device to make a context
		mContext=alcCreateContext(mDevice,NULL);
		// set my context to the currently active one
		alcMakeContextCurrent(mContext);
		return true;
	}
	return false;
}
 
-(id) init {
		MARK;
	if (self = [super init] ) {
		if ([self initOpenAL]) {
			self.bufferStorageArray = [[[NSMutableArray alloc]init]autorelease];
			self.soundDictionary = [[[NSMutableDictionary alloc]init]autorelease];
 
		}
		return self;
	}
	[self release];
	return nil;
}
 
-(void) dealloc {
 
		MARK;
	// delete the sources
	for (NSNumber * sourceNumber in [soundDictionary allValues]) {
		NSUInteger sourceID = [sourceNumber unsignedIntegerValue];
		alDeleteSources(1, &sourceID);
	}
 
	self.soundDictionary=nil;
 
	// delete the buffers
	for (NSNumber * bufferNumber in bufferStorageArray) {
		NSUInteger bufferID = [bufferNumber unsignedIntegerValue];
		alDeleteBuffers(1, &bufferID);
	}
	self.bufferStorageArray=nil;
 
	// destroy the context
	alcDestroyContext(mContext);
	// close the device
	alcCloseDevice(mDevice);
 
	[super dealloc];
}
 
// the main method: grab the sound ID from the library
// and start the source playing
- (void)playSoundWithKey:(NSString*)soundKey
{
		MARK;
	NSNumber * numVal = [soundDictionary objectForKey:soundKey];
	if (numVal == nil) return;
	NSUInteger sourceID = [numVal unsignedIntValue];
	alSourcePlay(sourceID);
}
 
- (void)stopSoundWithKey:(NSString*)soundKey
{
	MARK;
	NSNumber * numVal = [soundDictionary objectForKey:soundKey];
	if (numVal == nil) return;
	NSUInteger sourceID = [numVal unsignedIntValue];
	alSourceStop(sourceID);
}
 
-(void) rewindSoundWithKey:(NSString *) soundKey {
 
	MARK;
	NSNumber * numVal = [soundDictionary objectForKey:soundKey];
	if (numVal == nil) return;
	NSUInteger sourceID = [numVal unsignedIntValue];
	alSourceRewind (sourceID);
}
 
-(bool) isPlayingSoundWithKey:(NSString *) soundKey {
 
	MARK;
	NSNumber * numVal = [soundDictionary objectForKey:soundKey];
	if (numVal == nil) return false;
	NSUInteger sourceID = [numVal unsignedIntValue];
 
	ALenum state;
 
    alGetSourcei(sourceID, AL_SOURCE_STATE, &state);
 
    return (state == AL_PLAYING);
}
 
-(bool) loadSoundWithKey:(NSString *)_soundKey File:(NSString *)_file Ext:(NSString *) _ext Loop:(bool)loops{
 
	START_TIMER;
 
	ALvoid * outData;
	ALenum  error = AL_NO_ERROR;
	ALenum  format;
	ALsizei size;
	ALsizei freq;
 
	NSBundle * bundle = [NSBundle mainBundle];
 
	// get some audio data from a wave file
	CFURLRef fileURL = (CFURLRef)[[NSURL fileURLWithPath:[bundle pathForResource:_file ofType:_ext]] retain];
 
	if (!fileURL)
	{
		END_TIMER(@"file not found.");
		return false;
	}
 
	outData = MyGetOpenALAudioData(fileURL, &size, &format, &freq);
 
	CFRelease(fileURL);
 
	if((error = alGetError()) != AL_NO_ERROR) {
		printf("error loading sound: %x\n", error);
		exit(1);
	}
 
	ALog(@"getting a free buffer from openAL.");
	NSUInteger bufferID;
	// grab a buffer ID from openAL
	alGenBuffers(1, &bufferID);
 
	ALog(@"loading audio data into openAL buffer.");
	// load the awaiting data blob into the openAL buffer.
	alBufferData(bufferID,format,outData,size,freq); 
 
	// save the buffer so we can release it later
	[bufferStorageArray addObject:[NSNumber numberWithUnsignedInteger:bufferID]];
 
	ALog(@"getting a free source from openAL.");
	NSUInteger sourceID;
	// grab a source ID from openAL
	alGenSources(1, &sourceID); 
 
	ALog(@"attatching the buffer to the source and setting up preferences");
	// attach the buffer to the source
	alSourcei(sourceID, AL_BUFFER, bufferID);
	// set some basic source prefs
	alSourcef(sourceID, AL_PITCH, 1.0f);
	alSourcef(sourceID, AL_GAIN, 1.0f);
	if (loops) alSourcei(sourceID, AL_LOOPING, AL_TRUE);
 
	// store this for future use
	[soundDictionary setObject:[NSNumber numberWithUnsignedInt:sourceID] forKey:_soundKey];	
 
	ALog(@"free %i bytes of temporary allocated memory.", size);
	// clean up the buffer
	if (outData)
	{
		free(outData);
		outData = NULL;
	}
 
	END_TIMER(@"Audiofile successfully loaded.");
	return true;
 
}
@end

You may noticed that I use some macros like “ALog” and “Mark” – you can find the definitions here.

Writing the actual code to play some audio files.

Notice that you can not load any file format. I successfully used aif and caf files – so maybe you want use them, too.

[[MyOpenAL sharedMyOpenAL] loadSoundWithKey:@"music" File:"backgroundMusic" Ext:"aif" Loop:true];
[[MyOpenAL sharedMyOpenAL] loadSoundWithKey:@"beep" File:"Sound1" Ext:"aif" Loop:false];
 
[[MyOpenAL sharedMyOpenAL] playSoundWithKey:@"music"];
[[MyOpenAL sharedMyOpenAL] playSoundWithKey:@"beep"];

That’s it. I hope this post was useful although I know its not going deep into the achitechture of openAL.

I like to announce my first Game in Appstore : Bomb Commander


About this entry