Lots and Lots of sounds in openAL

There has been a flurry of openAL related stuff going on in my life in the last few days, and that reminded me that I have a few half-finished posts that I keep meaning to finish..

Also, I have found at least one other blog that has copied my code (and even my blog posts!) with out even rewording the text!, so that is kinda cool i guess :-) (although I would have liked at least a courtesy link :-)

OK, today: lots and lots of sounds.

In my older posts I mentioned that there is an upper limit to the number of simultaneous sounds you can play in openAL. (in other words the most concurrent sources). At some point (around 32) you can no longer get any more sources via:

alGenSources(1, &sourceID); 

the downside is that this just quietly fails, and does not seem to raise an error. The moral of this story: don’t ask for more than 32 sources! (note: this is on iPhone OS 2.2, might be different on other OS versions and platforms)

So what does this mean? it means that no matter what, you cannot have more than 32 sounds playing in the same instant. This really should never be a problem. If your goal is to simulate and entire orchestra by playing each instrument individually, then you are probably not going to want to be doing it on the iPhone. (although you can actually get past this limit by building the buffers yourself, but really, more than 32 sounds? come on!)

That said, you may have more than 32 sounds in your app, and if you follow my simple example from past posts, i suggested that you assign each sound buffer to a single source and just call that source when you need to play the sound.

This works well for lots of scenarios. However, not so much if you have more than 32 sounds in your app. So how do we deal with that? I mentioned it briefly in an older post: you need to have a way to hand out the sources you are not using to the buffers that need to be played.

This is actually pretty simple and I will show you how!

First, you may want to preload your sources. This is not strictly necessary, but I like to keep the processing to a minimum at the point I want to play the sounds, so I like to have everything ready to go up front.

// note: MAX_SOURCES is how many source you want 
// to preload.  should keep it below 32
-(void)preloadSources 
{
        // lazy init of my data structure
	if (sources == nil) sources = [[NSMutableArray alloc] init];
	
	// we want to allocate all the sources we will need up front
	NSUInteger sourceCount = MAX_SOURCES;
	NSInteger sourceIndex;
	NSUInteger sourceID;
	// build a bunch of sources and load them into our array.
	for (sourceIndex = 0; sourceIndex < sourceCount; sourceIndex++) {
		alGenSources(1, &sourceID);
		[sources addObject:[NSNumber numberWithUnsignedInt:sourceID]];
	}	
}
   

pretty simple really, just have openAL build a bunch of sources and store those IDs somewhere. You can actually have openAL build all the sources at once by calling alGenSources with a number besides 1, and store them in a C-style array, but I like to keep my stuff obj-c because I am lazy and dont like to malloc/free if I dont have to.

Now, the old play sound method looked like so:

// the main method: grab the sound ID from the library
// and start the source playing
- (void)playSound:(NSString*)soundKey
{
	NSNumber * numVal = [soundDictionary objectForKey:soundKey];
	if (numVal == nil) return;
	NSUInteger sourceID = [numVal unsignedIntValue];
	alSourcePlay(sourceID);
}

But that was back in the olden days when we assigned a single source to a single buffer. Now we dont have any source pre-assigned, we need to attach them at play-time.

so we want something like this:

- (NSUInteger)playSound:(NSString*)soundKey gain:(ALfloat)gain pitch:(ALfloat)pitch loops:(BOOL)loops
{ 
	ALenum err = alGetError(); // clear error code 
	
	// first, find the buffer we want to play
	NSNumber * numVal = [soundLibrary objectForKey:soundKey];
	if (numVal == nil) return 0;
	NSUInteger bufferID = [numVal unsignedIntValue];	
	
	// now find an available source
	NSUInteger sourceID = [self _nextAvailableSource];	
	
	// make sure it is clean by resetting the source buffer to 0
	alSourcei(sourceID, AL_BUFFER, 0);
	// attach the buffer to the source
	alSourcei(sourceID, AL_BUFFER, bufferID); 
	
	// set the pitch and gain of the source
	alSourcef(sourceID, AL_PITCH, pitch);
	alSourcef(sourceID, AL_GAIN, gain);
	
	// set the looping value
	if (loops) {
		alSourcei(sourceID, AL_LOOPING, AL_TRUE);		
	} else {
		alSourcei(sourceID, AL_LOOPING, AL_FALSE);				
	}
	// check to see if there are any errors
	err = alGetError(); 
	if (err != 0) {
		[self _error:err note:@"Error Playing Sound!"];		
		return 0;
	}
        // now play!
	alSourcePlay(sourceID);	
	return sourceID; // return the sourceID so I can stop loops easily
} 

Ok, it is important to note in this new method that we are no longer getting a sourceID out of our soundLibrary, but a buffer ID, so you will need to store the buffers instead of the sources. (I will leave that as an exercise for the reader, but if this doesnt make sense, I can post my buffer-loading code as well. However it is really just exactly like the stuff in the earlier posts)

OK, so that method looks pretty easy! wait! what is this line:

	// now find an available source
	NSUInteger sourceID = [self _nextAvailableSource];

Ahh yes, this is where the magic happens. It is pretty simple tho: we just step through our list of sources, find one that is not being used, and return it.

-(NSUInteger)_nextAvailableSource
{
	NSInteger sourceState; // a holder for the state of the current source
	
	// first check: find a source that is not being used at the moment.
	for (NSNumber * sourceNumber in sources) {
		alGetSourcei([sourceNumber unsignedIntValue], AL_SOURCE_STATE, &sourceState);
		// great! we found one! return it and shunt
		if (sourceState != AL_PLAYING) return [sourceNumber unsignedIntValue];
	}
	
	// in the case that all our sources are being used, we will find the first non-looping source
	// and return that.
	// first kick out an error
       NSLog(@"available source overrun, increase MAX_SOURCES");
	
	NSInteger looping;
	for (NSNumber * sourceNumber in sources) {
		alGetSourcei([sourceNumber unsignedIntValue], AL_LOOPING, &looping);
		if (!looping) {
			// we found one that is not looping, cut it short and return it
			NSUInteger sourceID = [sourceNumber unsignedIntValue];
			alSourceStop(sourceID);
			return sourceID;			
		}
	}
	
	// what if they are all loops? arbitrarily grab the first one and cut it short
	// kick out another error
       NSLog(@"available source overrun, all used sources looping");
	
	NSUInteger sourceID = [[sources objectAtIndex:0] unsignedIntegerValue];
	alSourceStop(sourceID);
	return sourceID;
}

So, what is happening here? Really, 99% of the cases will be caught in the first loop. You just run through the array, find one that is not playing and return it. In my experience, even in a pretty sound-intense app (like a beat-box where you are mashing on keys and playing samples) you rarely get more than a couple of sounds that are actually playing at once (unless your samples are very long) so this will return very quickly.

But what if everything is already playing? You have a few options: Easy: increase you max_sources until this problem goes away, harder: (but still pretty easy) generate new sources on the fly when you run out.

Now if you manage to use up all 32 sources and still find yourself calling this method for another source, then the last 2 chunks of code in the above method will take care of it. The way you handle a source overrun is really application specific. but in the above case I am presuming that new sounds are more important than old sounds, and loops are more important than effects: so we have a 2 step process to find a sound to kill.

first we look to find the first non-looping sound and we kill it (cutting it off) and return that source. In the second case we have MAX loops running, so we just grab the first source, cut it short and return it.

Now, your app might have different requirements if you run out of sources. so you could handle that in lots of different ways. This was probably the easiest and it works for all the apps I have done (I tend to 'tune' the MAX number until I never get an overrun, even during heavy sound usage, so these are really just worst case 'graceful failing')

OK, that is about it for today!
Happy sound playing :-)

Oh! One last thing: the code above will work fine, but it is not my production code :-) I tend to remove all the extra error checking and stuff like that to keep the code clean and easy to read for the examples, so be sure to put error checks back into the code if you plan to use it !

This entry was posted in code, iPhone, openAL. Bookmark the permalink.

One Response to Lots and Lots of sounds in openAL

  1. miked says:

    Just wanted to say thanks for this tutorial and your earlier one on OpenAL, its just what I needed.

    I’m doing a series of tutorials on my blog http://www.71squared.co.uk on game programming, just sharing what I’m learning really. I’ve been using the SoundEngine but I wanted to roll my own so I could learn how its done.

    Your tutorial was perfect for that and is going to form the core of the sound engine I’m doing.

    I’ll be doing a video tutorial on it and I’ll be sure to mention you and your site as the source of info and inspiration.

    Thanks again

    Mike

Leave a Reply