Blog,code,meta,openAL

Imitation is the sincerest form of flattery19 May

Funny story for the day:

Here is how this works:

Step 1: write an original article on your blog about a useful thing (like OpenAL) (we will call this the 'original post' and the 'original blogger')
Step 2: someone else likes your article and cuts and pastes it into their blog, they change a few words here and there, but mostly it is left identical (errors and everything) (let's call him 'copy blogger', and we will call this the 'copy post')
Step 3: wait a while
Step 4: somebody notices that 'copy post' is very similar to 'original post' and tells 'Copy Blogger' that someone has copied his post! Mon Dieu!
Step 5: Copy Blogger emails Original Blogger and tells him that if he is going to be copying his (plagiarised) posts then Original Blogger should have the common courtesy to provide a linkback to the 'Copy post' from the 'Original post'!
Step 6: Original Blogger laughs and laughs.

If this is all too abstract, then I will simplify it: I wrote a post about OpenAL on the iPhone in 2008, right after the NDA was lifted. This single article is in the top 5 for traffic for my site. Lots of people have seen it. Presumably there are lots of OpenAL codebases out there that have snippets of code from that article, which is great!

That article has a bunch of code snippets and instructions on how to build your own simple OpenAL sound player. (but does not provide a working version, you have to do that yourself :-)

Someone (whom shall remain nameless) took that article and copied it into his website, and claimed authorship of it. This was in early 2009. Now to his credit, he did change a few words here and there, and he took the code snippets and put them into a single file and filled in the gaps. This is exactly what I had hoped people would do! (with the exception of claiming authorship of my words, that is kinda dickish)

I actually noticed that this had happened and made a fleeting mention of it at the top of my post about 'lots of sounds' in OpenAL. But to be honest, I didnt really care all that much about it. Hey, it's the internet, people steal anything that isn't bolted down, and they sometimes steal that stuff too.

However, this morning, I get an email from our random internet plagiarist telling me: (and I quote) "just want to notice that someone told me it seems your article has alot in common with my own article postet on [RADACTED]. If your article is related to that, you should post a linkback or something like that. "

He wasn't a dick about it, he was pretty cool. He is probably a stand-up kinda guy.

To be honest, it has been a looooong time since I noticed that he had copied me, and I had basically forgotten about it. I dutifully went to his site and had a look (because I was curious, and had forgotten about the whole thing, and frankly OpenAL isn't that complicated, and there are only so many ways to do it, so really most articles on OpenAL on the iphone could be considered 'similar' on many ways, and if his site was good, then I would link to it anyway) and when I saw my own words staring back at me I remembered the whole thing from last year and I laughed and laughed.

Then I sent him a kinda shitty reply, sorry about that internet plagiarist dude, I probably should have waited till I had breakfast before replying to your email.

At the end of the day, I post stuff here so that people hopefully get something out of it. If you want to copy all the code here and all the txt here and post it on your site, well, that is perfectly possible, and not illegal. But taking ownership of my words is a bit of a douchebag move. At least have the common courtesy to re-word it.

Cheers!
-Ben

code,iPhone,openAL

Streaming in OpenAL04 May

I have posted a few posts on OpenAL that have been very popular:

One thing that people keep asking me is how to playback gigantic files in OpenAL. Things like songs or audio books. (This post has been slowly growing over the past few months and I am finally going to put the final bits in and post it!)

The answer to this problem (in case you didn't read the title of the post) is streaming.

First off, lets define what I really mean when I say streaming. In all of the above code samples, the sound buffers were fully resident in memory. This works great for smaller sounds like button click sounds and swords clanging sounds for games and whatnot. However, for big files like full quality songs and really anything over about a dozen seconds long, putting it all into memory (especially on the iPhone) becomes a bit of a problem.

To solve this we read the data off of the disk in chunks, filling small buffers one at a time and then playing them back one after the other. All the while, in the background we are removing the old buffers and replacing them with new buffers so that we only ever have a few seconds of sound in memory at one time.

Here is a crappy diagram that I spent way too much time on, and it is still crappy:

OK, at the top we have the 'standard' way to do thing (the way described in all those posts linked above). Basically:

  • Step1: Load the sound data from a file into an OpenAL Buffer
  • Step2: Connect the OpenAL Buffer to an OpenAL source with something like: alSourcei(sourceID, AL_BUFFER, bufferID);
  • step 3: Start source playing with with alSourcePlay(sourceID);

Pretty simple.

Streaming is a bit more complicated, but still pretty simple (in concept). The first three steps are really very similar to the 'standard' way above.

  • Step A: Load in a few chunks of the big file into all the waiting OpenAL buffers.
  • Step B: Queue up all the newly filled buffers into a single OpenAL source via alSourceQueueBuffers(sourceID, 1, &bufferID);
  • Step C: Start the source playing with alSourcePlay(sourceID); This will begin to consume the queued up buffers.
  • Step D: In a background thread, check to see if there are any used buffers, and if so then load the next chunk of the sound file into the used buffer
  • Step E: Queue up this newly filled buffer to the source with alSourceQueueBuffers(sourceID, 1, &bufferID); As long as there are still bufferes queued up, the source will continue to play them

So there it is, five easy steps. Lets look at them one at a time:

Step A: Load in a few chunks of the big file into all the waiting OpenAL buffers.

Ok first thing we need to do is to prepare a place for all this state data to live. This will be kinda like preloading the streaming file, execept that we dont load any sound into memory just yet, we are just going to get ready to do that.

 
// this queues up the specified file for streaming
-(NSMutableDictionary*)initializeStreamFromFile:(NSString*)fileName format:(ALenum)format freq:(ALsizei)freq
{
	// first, open the file
	AudioFileID fileID = [self _openAudioFile:fileName];
 
	// find out how big the actual audio data is
	UInt32 fileSize = [self _audioFileSize:fileID];
 

First thing, we open the file. This doesnt load it into memory, it just gives us a handle to some data that we need. Specifically the file size. We will need this to calculate the number of buffers we need to play the whole file.

 
 
	UInt32 bufferSize = OPENAL_STREAMING_BUFFER_SIZE;
	UInt32 bufferIndex = 0;
 
	// ok, now we build a data record for this streaming file
	// before, with straight sounds this is just a soundID
	// but with the streaming sound, we need more info
	NSMutableDictionary * record = [NSMutableDictionary dictionary];
	[record setObject:fileName forKey:@"fileName"];
	[record setObject:[NSNumber numberWithUnsignedInteger:fileSize] forKey:@"fileSize"];
	[record setObject:[NSNumber numberWithUnsignedInteger:bufferSize] forKey:@"bufferSize"];
	[record setObject:[NSNumber numberWithUnsignedInteger:bufferIndex] forKey:@"bufferIndex"];
	[record setObject:[NSNumber numberWithInteger:format] forKey:@"format"];
	[record setObject:[NSNumber numberWithInteger:freq] forKey:@"freq"];
	[record setObject:[NSNumber numberWithBool:NO] forKey:@"isPlaying"];
 

Next we are going to make a streaming sound record. This dictionary will hold all of the state for this stream.

The OPENAL_STREAMING_BUFFER_SIZE is how many bytes of data we want in each buffer chunk. I have had good luck with 48000, this is about a second at high quality so I have plenty of time to grab the next chunk.

The bufferIndex is the current buffer that is full. For now that is zero of course.

Format and Freq we will use when we eventually setup our source. and isPlaying will let us know if we need to be refilling the buffers or not.

 
 
	// this will hold our buffer IDs
	NSMutableArray * bufferList = [NSMutableArray array];
	int i;
	for (i = 0; i < 3; i++) {
		NSUInteger bufferID;
		// grab a buffer ID from openAL
		alGenBuffers(1, &bufferID);
 
		[bufferList addObject:[NSNumber numberWithUnsignedInteger:bufferID]];
	}	
 
	[record setObject:bufferList forKey:@"bufferList"];
 

This is where the actual buffer references will live. In theory you can get away with just two buffers. One to play and one to fill while the other is playing. However If you get any stitch in your background thread, or do any heavy lifting on the processor that delays that refill, then you will get some nasty skipping. Instead I tend to use three buffers. This takes up more memory, but affords me a bit more robust playback.

 
	// close the file
	AudioFileClose(fileID);
 
	return record;
}
 

Finally close our audio file and return the record. Take that record returned from the above method and store it into a big NSMutableDictionary called soundLibrary with some key, like the name of the sound. (or in whatever data structure you want, that is how I do it, and that is how the sample code works)

We are still in Step A (well, pre step A even). We haven't loaded any buffers, but we are ready when that time comes.

Once you are ready to play your sound, we move to:

Steps A, B and C in one fell swoop

OK, lets define a play streaming sound method that does A, B and C for us. (and kick off Step D)

We would call this method when we want to actually begin playing the streaming sound, the key is whatever key you used to store the record into the sound library.

This method returns the sourceID so that the calling object can use it to stop the sound.

 
- (NSUInteger)playStream:(NSString*)soundKey gain:(ALfloat)gain pitch:(ALfloat)pitch loops:(BOOL)loops
{
	// if we are not active, then dont do anything
	if (!active) return 0;
 
	ALenum err = alGetError(); // clear error code
 

Just some housekeeping. I have a big boolean called 'active' that shuts off all sounds. Makes it nice and easy. Second we clear out the OpenAL errors so that anything in there will be a result of what we do in this method.

 
	// generally the 'play sound method' whoudl be called for all sounds
	// however if someone did call this one in error, it is nice to be able to handle it
	if ([[soundLibrary objectForKey:soundKey] isKindOfClass:[NSNumber class]]) {
		return [self playSound:soundKey gain:1.0 pitch:1.0 loops:loops];
	}
 

If you followed along on the other tutorials you would know that previously I had simply stored the NSNumber value for the buffer in the soundLibrary. This is a way to handle that if the streaming method gets called with the wrong key (ie a non-streaming sound) It just punts to the standard playSound method. Similarly in my playsound method I check to see if the record is an NSDictionary, and if so then it calls this method. That way you can just use the playSounds method with either streaming sounds or regular sounds and it all works dandy.

 
 
	// get our keyed sound record
	NSMutableDictionary * record = [soundLibrary objectForKey:soundKey];
 
	// first off, check to see if this sound is already playing
	if ([[record objectForKey:@"isPlaying"] boolValue]) return 0;
 

Ok, we grab the record and start to go through the state. If the sound is already playing then we get out early.

 
 
	// first, find the buffer we want to play
	NSArray * bufferList = [record objectForKey:@"bufferList"];
 
	// now find an available source
	NSUInteger sourceID = [self nextAvailableSource];
	alSourcei(sourceID, AL_BUFFER, 0);
 
	// reset the buffer index to 0
	[record setObject:[NSNumber numberWithUnsignedInteger:0] forKey:@"bufferIndex"];
 

Now we are going to move into Step A proper. Grab the buffer list, and get an available source. the Method nextAvailableSource simply goes through a big list of premade sources and finds one that is not being used currrently. I think I went over that in the 'lots of sounds' tutorial linked above.

Then we reset the bufferindex to 0. This is basically setting the playhead to the beginning of the sound. next, we fill the buffers

 
	// queue up the first 3 buffers on the source
	for (NSNumber * bufferNumber in bufferList) {
		NSUInteger bufferID = [bufferNumber unsignedIntegerValue];
		[self loadNextStreamingBufferForSound:soundKey intoBuffer:bufferID];
		alSourceQueueBuffers(sourceID, 1, &bufferID);
		err = alGetError();
		if (err != 0) [self _error:err note:@"Error alSourceQueueBuffers!"];
	}
 

Ok, this is pretty simple looking but there is the one magic method: loadNextStreamingBufferForSound: intoBuffer: I will get to this in a minute, but basically it grabs a chunk of the audio file based on the bufferIndex and loads it into the buffer. then it increments the bufferIndex so that the next time I call this method I will get the next chunk.
We load a chunk into every buffer in the buffer list (which in our case will be three buffers)
And here is the important part: (this would be the Step B part of the diagram)

 
alSourceQueueBuffers(sourceID, 1, &bufferID);
 

This is the magic OpenAL function call that makes this source a streaming source instead of a single buffer source. Basically it will continue to play as long as there are buffers queued up.

 
	// set the pitch and gain of the source
	alSourcef(sourceID, AL_PITCH, pitch);
	err = alGetError();
	if (err != 0) [self _error:err note:@"Error AL_PITCH!"];
	alSourcef(sourceID, AL_GAIN, gain);
	err = alGetError();
	if (err != 0) [self _error:err note:@"Error AL_GAIN!"];
	// streams should not be looping
	// we will handle that in the buffer refill code
	alSourcei(sourceID, AL_LOOPING, AL_FALSE);
	err = alGetError();
	if (err != 0) [self _error:err note:@"Error AL_LOOPING!"];
 

With our buffers loaded and queued on the source, we just need to set up the source with all the properties that were passed in. This is exactly like you would do it for a single buffer sound.

 
	// everything is queued, start the buffer playing
	alSourcePlay(sourceID);
	// check to see if there are any errors
	err = alGetError();
	if (err != 0) {
		[self _error:err note:@"Error Playing Stream!"];
		return 0;
	}
 

Ok, finally we move to Step C: and we start the source playing. From this point on, we are on the clock to keep the buffers filled up.

 
	// set up some state
	[record setObject:[NSNumber numberWithBool:YES] forKey:@"isPlaying"];
	[record setObject:[NSNumber numberWithBool:loops] forKey:@"loops"];
	[record setObject:[NSNumber numberWithUnsignedInteger:sourceID] forKey:@"sourceID"];
 
	// kick off the refill methods
	[NSThread detachNewThreadSelector:@selector(rotateBufferThread:) toTarget:self withObject:soundKey];
	return sourceID;
}
 

This last bit sets up the state we need to keep the buffers full, and kicks off a new thread to run in the background to keep our buffers full.

OK, before we move onto Step D lets have a look at our buffer loader method

loadNextStreamingBufferForSound: intoBuffer:

This is roughly equivalent to the method that you would use to load an entire file into a buffer for standard sound playback, only we are only going to be grabbing a small bit of the file. Luckily for us this is a pretty common thing you would want to do, so mostly all we have to worry about is keeping the state set properly.

 
// this takes the stream record, figures out where we are in the file
// and loads the next chunk into the specified buffer
-(BOOL)loadNextStreamingBufferForSound:(NSString*)key intoBuffer:(NSUInteger)bufferID
{
	// check some escape conditions
	if ([soundLibrary objectForKey:key] == nil) return NO;
	if (![[soundLibrary objectForKey:key] isKindOfClass:[NSDictionary class]]) return NO;
 

First off just some simple checks to make sure I am not trying to load a non-existent sound file, or a non-streaming file.

 
	// get the record
	NSMutableDictionary * record = [soundLibrary objectForKey:key];
 
	// open the file
	AudioFileID fileID = [self _openAudioFile:[record objectForKey:@"fileName"]];
 
	// now we need to calculate where we are in the file
	UInt32 fileSize = [[record objectForKey:@"fileSize"] unsignedIntegerValue];
	UInt32 bufferSize = [[record objectForKey:@"bufferSize"] unsignedIntegerValue];
	UInt32 bufferIndex = [[record objectForKey:@"bufferIndex"] unsignedIntegerValue];;
 

Grab the record that has all of our state information, and set up all of our variables.

 
 
	// how many chunks does the file have total?
	NSInteger totalChunks = fileSize/bufferSize;
 
	// are we past the end? if so get out
	if (bufferIndex > totalChunks) return NO;
 
	// this is where we need to start reading from the file
	NSUInteger startOffset = bufferIndex * bufferSize;
 
	// are we in the last chunk? it might not be the same size as all the others
	if (bufferIndex == totalChunks) {
		NSInteger leftOverBytes = fileSize - (bufferSize * totalChunks);
		bufferSize = leftOverBytes;
	}
 

Here we are just using our state info to figure out where in the file to look and how big of a chunk to take. If we are at the last chunk, then it may not be a full sized chunk, so we have to take that into account and change our data size.

 
	// this is where the audio data will live for the moment
	unsigned char * outData = malloc(bufferSize);
 
	// this where we actually get the bytes from the file and put them
	// into the data buffer
	UInt32 bytesToRead = bufferSize;
	OSStatus result = noErr;
	result = AudioFileReadBytes(fileID, false, startOffset, &bytesToRead, outData);
	if (result != 0) NSLog(@"cannot load stream: %@",[record objectForKey:@"fileName"]);
 
	// if we are past the end, and no bytes were read, then no need to Q a buffer
        // this should not happen if the math above is correct, but to be sae we will add it
	if (bytesToRead == 0) {
            free(outData);
            return NO; // no more file!
        }
 

Ok, here we do the actual meat of the method. We alloc some memory, and then use the AudioFileReadBytes() function to grab our desired slice of data from the big file. This loads our chunk of sound data into the outData memory. At this point we will proceed exactly like we would with a single-buffer sound.

 
 
	ALsizei freq = [[record objectForKey:@"freq"] intValue];
	ALenum format = [[record objectForKey:@"format"] intValue];
 
	// jam the audio data into the supplied buffer
	alBufferData(bufferID,format,outData,bytesToRead,freq);
 

Load out sound into our OpenAL buffer. easy.

 
	// clean up the buffer
	if (outData)
	{
		free(outData);
		outData = NULL;
	}
 
	AudioFileClose(fileID);
 

Do some cleanup.

 
	// increment the index so that next time we get the next chunk
	bufferIndex ++;
	// are we looping? if so then flip back to 0
	if ((bufferIndex > totalChunks) && ([[record objectForKey:@"loops"] boolValue])) {
		bufferIndex = 0;
	}
	[record setObject:[NSNumber numberWithUnsignedInteger:bufferIndex] forKey:@"bufferIndex"];
	return YES;
}
 

Finally we increment the bufferIndex so that the net time we call this method we get the next chunk of data in the sequence. If we are looping we reset the index to 0 at the end.

So, that is steps A, B, C, and the beginning of D.

Lets look more closely at our background thread now.

Step D (and E): The background thread to refill our buffers

OK, you may recall, like ten pages ago, that we kicked off a thread to refill the buffers in the background. Lets look at that:

 
-(void)rotateBufferThread:(NSString*)soundKey
{
	NSAutoreleasePool * apool = [[NSAutoreleasePool alloc] init];
	BOOL stillPlaying = YES;
	while (stillPlaying) {
		stillPlaying = [self rotateBufferForStreamingSound:soundKey];
		if (interrupted) 	{
			// slow down our thread during interruptions
			[NSThread sleepForTimeInterval:kBufferRefreshDelay * 3];
		} else {
			// normal thread delay
			[NSThread sleepForTimeInterval:kBufferRefreshDelay];
		}
	}
	[apool release];
}
 

This is a pretty simple method. Remember we are in a new thread, so we need to set up our own pool. Once we are no longer playing, then the thread ends. We make one call basically to rotateBufferForStreamingSound:. Finally if we are interrupted then we dont need to be refilling but our thread will still run (until we are terminated if that happens). To be good citizens we will decrease the amount of time we are checking the thread.

Otherwise we just come back in kBufferRefreshDelay seconds. Setting this number can be a bit tricky. If you set it too close to the actual time it takes to play your individual buffers (as you would think) then if it lags at all then you will fall behind and never be able to catch up. You want it to be more than once during every chunk, incase you need to load more than one chunk because of a slow thread. However, run it too often and you are wasting cycles. I have mine set to 0.25 seconds based on the 48000 byte buffer. I dont even remember why i came to these numbers and they might be terrible. but they do work. Feel free to tune them to your heart's desire.

Next up, the actual buffer rotator:

 
// this checks to see if there is a buffer that has been used up.
// if it finds one then it loads the next bit of the sound into that buffer
// and puts it into the back of the queue
-(BOOL)rotateBufferForStreamingSound:(NSString*)soundKey
{
	// make sure we arent trying to stream a normal sound
	if (![[soundLibrary objectForKey:soundKey] isKindOfClass:[NSDictionary class]]) return NO;
	if (interrupted) return YES; // we are still 'playing' but we arent loading new buffers
 
	// get the keyed record
	NSMutableDictionary * record = [soundLibrary objectForKey:soundKey];
	NSUInteger sourceID = [[record objectForKey:@"sourceID"] unsignedIntegerValue];	
 

First some defensive programming, if we are getting called with the wrong key then get out, if we are interrupted then we are not loading any new buffers, so get out (but return YES because we want to keep the thread alive)
Then we grab our ubiquitous record and start to fill in some variables.

 
	// check to see if we are stopped
	NSInteger sourceState;
	alGetSourcei(sourceID, AL_SOURCE_STATE, &sourceState);
	if (sourceState != AL_PLAYING) {
		[record setObject:[NSNumber numberWithBool:NO] forKey:@"isPlaying"];
		return NO; // we are stopped, do not load any more buffers
	}
 

First up: check to see if this source that we are meant to be loading buffers into is stopped. If it is, then we dont need to load any new buffers, and we want to return NO so that the thread finishes as well.

 
 
	// get the processed buffer count
	NSInteger buffersProcessed = 0;
	alGetSourcei(sourceID, AL_BUFFERS_PROCESSED, &buffersProcessed);
 
	// check to see if we have a buffer to deQ
	if (buffersProcessed > 0) {
		// great! deQ a buffer and re-fill it
		NSUInteger bufferID;
		// remove the buffer form the source
		alSourceUnqueueBuffers(sourceID, 1, &bufferID);
		// fill the buffer up and reQ!
		// if we cant fill it up then we are finished
		// in which case we dont need to re-Q
		// return NO if we dont have mroe buffers to Q
		if (![self loadNextStreamingBufferForSound:soundKey intoBuffer:bufferID]) return NO;
		// Q the loaded buffer
		alSourceQueueBuffers(sourceID, 1, &bufferID);
	}
 

Next up, the big event: we see how many buffers the source has processed since our last check. For every buffer that has been processed we should fill in a new one and queue it up.
Before we can queue up a new buffer, we need to dequeue the old one. We do this with a call to alSourceUnqueueBuffers(). This fills in our bufferID variable, we can use this buffer now for whatever we want. In this case we want to fill it up with the next chunk of sound data and then put it at the end of the queue.

We call our loadNextStreamingBufferForSound:intoBuffer: method and if it returns NO, then we are all done and we can get out.
If it returns YES then we can queue up the newly filled buffer.

 
	return YES;
}
 

Finally, if we have made it this far then it was a successful buffer load and we return a YES to keep our loop going.

That is about it. The way this is all built, once you cann alSourceStop() on your streaming sound ID, everything sorta cleans itself up. The buffers stop rotating, and the thread stops. You still have buffers in memory tho, so if you want to clean those up too, be sure to add that code.

To Sum Up

OK, so there are the five-ish steps to streaming sound glory with OpenAL.

Obviously I have left out all the other code you need to get this running, have a look at the articles linked at the top, they have most of the rest of it.

Also I should mention that all of this code comes from an earlier, simpler version of what I generally use as my 'sound engine' nowadays. (basically I have been working on finishing this tutorial for awhile now and my working code has evolved since then :-)
This is not to say the sample code here is necessarily inferior, in fact my current code does almost exactly the same things, only there is a bit more optimised state handling and some other things that make it faster/easier, but not as easy to explain in an already very long post. So feel free to take this code and make it better in your own way :-) (for instance, instead of using dictionaries for state, I now have some proper sound classes that hold all that state for me etc..)

Also, i should say that if this code crashes your machine, or bricks your phone or makes your cat lose all it's hair, it is not my fault, you have been warned.

Cheers!
-B

Blog,code,iPhone,openAL

alBufferDataStatic: why you should avoid it20 Sep

OK, So, I just got an email from Ken, a dude who surfed across this OpenAL stuff on my blog, had some troubles, and asked me for help. He is having trouble unloading buffers and then re-using them again without getting some esoteric OpenAL errors.

So I went through the snippets he sent me very quickly and threw out some advice. Ken came back with a few more questions and I went over the code again and noticed this:

 
          // use the static buffer data API
          alBufferDataStaticProc(buffer, format, data, size, freq);
 

Ah-HA! I said, that is probably your problem right there! alBufferDataStatic should be avoided unless you need it. When do you know if you will need it? Here is a rule of thumb: If you dont know the answer to that question then you dont need it.

(I don't want to seem to be picking on Ken, this problem is so common that I decided I needed to do a post about it (similar to my rant about never using the sample code from Apple in a shipping app))

OK, so why do I think you should avoid it? I am sure you have heard that if you want the best performance then you should be using alBufferDataStatic() right?! No.

Well, what is alBufferDataStatic() anyway? alBufferDataStatic puts the onus of memory management for all your buffer data onto your code.

Let me repeat that: When using alBufferDataStatic() YOU are responsible for all the memory management.

That is it. That is your big performance boost.

Wait! Really? that is all it does?

Yes.

The iPhone's shared memory space and the way it handles memory and the way your app gets jettisoned if you use too much is a very unique situation. A situation that requires that you often have to really closely handle your memory. So! That means that if you are trying to squeeze every single last byte of memory out of the iPhone so that your fully immersive 3d first-person-shooter can keep a consistent 30FPS then you will probably want to spend a few weeks tweaking your sound code so that you have very close control of your memory at all times.

For the other %99.9999 percent of apps that might want to use OpenAL (even if you are using lots of sounds) you are just adding extra headaches to your code by using alBufferDataStatic(). Here is another rule of thumb: If you are not employing a developer whose sole job is to handle the sound code in your app, then you don't need alBufferDataStatic().

Have a quick read of the technote that mentions alBufferDataStatic().

What does this really mean?

It means this: when you use alBufferData() you load your sound data into a local buffer (one that you have to malloc) then when you call alBufferData(), OpenAL copies all those bytes into it's own buffer that it deals with. Then you free your local storage and OpenAL now owns that buffer. The downside: for a brief moment, you have 2 copies of your sound in memory. The upside: very very easy memory management.

If you are getting jettisoned right here, during this buffer copy, then you may need alBufferDataStatic(). But more likely you have done a crap job of handling your memory elsewhere. (also it is good to note that if you are loading big sounds into memory you are doing it wrong anyway, those big sounds should be streamed).

Ok, you are absolutely, positively sure that you need to manage your own sound memory? Maybe you should be using alBufferDataStatic(), but first: here is yet another rule of thumb: if you are reading this blog post to see if you need to use it; you don't. go to your code right now and take it out. Just take it out.

Obviously I am being a bit hyperbolic in an attempt to make a point. I dont know about your code, but here is a small list of the most common bugs I find in my code:

  • Forgot to release an object properly and it leaks and eventually my app is jettisoned
  • Accidentally released an autoreleased object, causing a crash
  • Accidentally released an object owned by some other object, causing it to crash much much later
  • malloced some space and forgot to free it, causing a leak and eventual jettison
  • freed some malloced space and then tried to use it, causing my app to go batshit crazy

The list goes on. But can you see a common thread? Memory. Why would I want to add yet more memory management responsibilities when I dont need it? Why do developers always insist on making their own lives so much more difficult than they have to?

Here are a few platitudes:

  • Less code == Less bugs. More code == More bugs. Therefore Less Code is always better.
  • Do not add any code you dont need right now. (this is very common, you think to yourself: Hmm I am already here in this object, and I could see that I might need a method that does X in the future so I will just write it now so I dont have to write it then. Wrong. In that 'future' when you do need it, it will need to do Y, not X and you will have to re-write it. Or, more likely you will never need a method that does X, and you just wasted a bunch of time and added more code
  • Do not prematurely optimize. Get the code working first. Then go back and identify the places you need to smooth out. If you try to optimize as you go then you will end up spending all your time making that one method totally awesome (only to realize later that method only gets called once every few seconds, so really it could be the least efficient bit of code ever and nobody would ever notice.)

So, by using alBufferDataStatic() you are breaking all three of my lovely platitudes.

Dont do it.

You are only causing yourself grief and making your code buggy and shitty. Use alBufferData(). It works. It is easy. The highly experienced engineers who work on OpenAL have made sure that there are no memory issues with alBufferData(). If you decide to port your code, alBufferData() will still work! alBufferData() makes your breath smell minty! alBufferData() will totally pull chicks.

I have put OpenAL code into about 6 apps now. A few games and a few utilities. And one 'sound board' style app that played 32 sounds simultaneously (which was the max at the time, probably still is) and was able to handle gigantic sounds files (via streaming) without ever using alBufferDataStatic(). Was this some herculean task of super-awesome mad coding skillzzzz?

No. Just the opposite actually. I keep all my code as absolutely bare-minimum simple as I can.

Keep your code simple, don't use stuff you don't need.

Cheers!
-B

code,iPhone,multitouch,openAL,openSoundControl

WWDC 200918 May

I will be there.

If you are also going to be there, and you want to meet up and get a beer, or talk about anything on my blog or anything else really, then drop me a line at support@benbritten.com, or follow my rarely updated twitter: http://twitter.com/benbritten. I plan to use it during WWDC to help people find me and to help me find people. Otherwise I rarely use it, so dont go looking for profound wisdom from my twitter feed.

code,iPhone,openAL

Lots and Lots of sounds in openAL02 May

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 !

About

meMy full name is Ben Britten Smith.

I go by Ben Britten because Ben Smith is a bit too common and using my full name is a mouthful.

I live in Melbourne, Australia and service clients all over the globe.

Contact

Have some questions?

Feel free to contact me directly at support@benbritten.com with any questions you might have about any of the applications I support.

Thanks!

PHVsPjxsaT48c3Ryb25nPndvb19hYm91dDwvc3Ryb25nPiAtIGFib3V0LXdpZGdldDwvbGk+PGxpPjxzdHJvbmc+d29vX2FkX2JlbG93X2ltYWdlPC9zdHJvbmc+IC0gaHR0cDovL2JlbmJyaXR0ZW4uY29tL3dwLWNvbnRlbnQvdGhlbWVzL3ZpYnJhbnRjbXMvaW1hZ2VzL2FkNDY4LmpwZzwvbGk+PGxpPjxzdHJvbmc+d29vX2FkX2JlbG93X3VybDwvc3Ryb25nPiAtIGh0dHA6Ly93d3cud29vdGhlbWVzLmNvbTwvbGk+PGxpPjxzdHJvbmc+d29vX2FsdF9zdHlsZXNoZWV0PC9zdHJvbmc+IC0gYmVuYnJpdHRlbi5jc3M8L2xpPjxsaT48c3Ryb25nPndvb19ibG9ja19pbWFnZTwvc3Ryb25nPiAtIGh0dHA6Ly9iZW5icml0dGVuLmNvbS93cC1jb250ZW50L3RoZW1lcy92aWJyYW50Y21zL2ltYWdlcy9hZDMzNi5qcGc8L2xpPjxsaT48c3Ryb25nPndvb19ibG9ja191cmw8L3N0cm9uZz4gLSBodHRwOi8vd3d3Lndvb3RoZW1lcy5jb208L2xpPjxsaT48c3Ryb25nPndvb19ibG9nPC9zdHJvbmc+IC0gdHJ1ZTwvbGk+PGxpPjxzdHJvbmc+d29vX2Jsb2djYXQ8L3N0cm9uZz4gLSAvY2F0ZWdvcnkvYmxvZy88L2xpPjxsaT48c3Ryb25nPndvb19jYXRfbWVudTwvc3Ryb25nPiAtIGZhbHNlPC9saT48bGk+PHN0cm9uZz53b29fY29udGFjdDwvc3Ryb25nPiAtIGNvbnRhY3Q8L2xpPjxsaT48c3Ryb25nPndvb19jdXN0b21fY3NzPC9zdHJvbmc+IC0gPC9saT48bGk+PHN0cm9uZz53b29fY3VzdG9tX2Zhdmljb248L3N0cm9uZz4gLSBodHRwOi8vYmVuYnJpdHRlbi5jb20vZmF2aWNvbi5pY288L2xpPjxsaT48c3Ryb25nPndvb19mZWF0cGFnZXM8L3N0cm9uZz4gLSA1NDk8L2xpPjxsaT48c3Ryb25nPndvb19mZWVkYnVybmVyX3VybDwvc3Ryb25nPiAtIDwvbGk+PGxpPjxzdHJvbmc+d29vX2dvb2dsZV9hbmFseXRpY3M8L3N0cm9uZz4gLSA8L2xpPjxsaT48c3Ryb25nPndvb19ncmF2YXRhcjwvc3Ryb25nPiAtIHRydWU8L2xpPjxsaT48c3Ryb25nPndvb19sYXlvdXQ8L3N0cm9uZz4gLSBkZWZhdWx0LnBocDwvbGk+PGxpPjxzdHJvbmc+d29vX2xvZ288L3N0cm9uZz4gLSA8L2xpPjxsaT48c3Ryb25nPndvb19tYW51YWw8L3N0cm9uZz4gLSBodHRwOi8vd3d3Lndvb3RoZW1lcy5jb20vc3VwcG9ydC90aGVtZS1kb2N1bWVudGF0aW9uL3ZpYnJhbnRjbXMvPC9saT48bGk+PHN0cm9uZz53b29fbmF2X2V4Y2x1ZGU8L3N0cm9uZz4gLSAyLDgyLDU0OSw1NTMsNTY3LDUzMiw1MzQsNTM3LDgzMjwvbGk+PGxpPjxzdHJvbmc+d29vX3Nob3J0bmFtZTwvc3Ryb25nPiAtIHdvbzwvbGk+PGxpPjxzdHJvbmc+d29vX3Nob3dfYWQ8L3N0cm9uZz4gLSBmYWxzZTwvbGk+PGxpPjxzdHJvbmc+d29vX3Nob3dfbXB1PC9zdHJvbmc+IC0gZmFsc2U8L2xpPjxsaT48c3Ryb25nPndvb19zdGVwczwvc3Ryb25nPiAtIDEuLCAyLiwgMy48L2xpPjxsaT48c3Ryb25nPndvb190YWJiZXI8L3N0cm9uZz4gLSBmYWxzZTwvbGk+PGxpPjxzdHJvbmc+d29vX3RoZW1lbmFtZTwvc3Ryb25nPiAtIFZpYnJhbnRDTVM8L2xpPjwvdWw+