By deans ~ May 22nd, 2009. Filed under: Resources.
As I mentioned in an earlier post, I’ve been dipping my toes into the deep waters of iPhone sound. One of the challenges that I ran into recently involved figuring out the best way to play a sound again, while the previous instance of the same sound was still playing. Although our current application features several (OK, sixteen) distinct sounds, the app often attempts to play a sound that is already playing. With
AudioServicesPlaySystemSound this just works. The subsequent attempts to play a sound simply play right along. Unfortunately, neither SoundEngine, nor AVAudioPlayer, work this way. If you try to start a SoundEngine effect, or an AVAudioPlayer, again, while it is already playing, nothing happens. It doesn’t queue; it just doesn’t play.
Before I go any further, I should share some of the details of the sounds for this app. They are all relatively short (roughly 2 seconds, or less). Their intensity peaks early, tapers quickly to a relatively low level, then trails off very gently for most of their duration. Their time domain plot looks something like a question mark rotated -90 degrees, then mirrored across the x-axis.
Since, as previously discussed, I couldn’t use AudioServices, I had to figure out a way to make either SoundEngine/OpenAL, or AVAudioPlayer (which was later rejected for a different reason), work. Some of my less embarrassing attempts to solve this included:
The first tactic that I tried was to simply stop the currently playing effect/player (for the sake of simplicity, I’ll just refer to both as players for the rest of this post) and start it again. Unfortunately, this introduced a distinct, and very unpleasant, click when the stop/start happened. If your app is a shooter, with lots of sharp explosions and weapon reports, this might not be a problem, but it was a showstopper for us.
The next thing that I tried was gradually reducing the volume of the current player before stopping it. That didn’t help, much.
With AVAudioPlayer, I also tried just setting the player back to the beginning, instead of stopping it.
[myPlayer setCurrentTime:0.0] – the results were actually worse than a stop/restart.
At that point, I decided to try a combination approach. If the subsequent attempt to play came early in the original player’s span, I just skipped the later attempt. If the attempt came well into the original, I gradually reduced the volume of the original, stopped it and then started the player again. This was better, because there were fewer stop/starts, but it still wasn’t perfect. Which leads us to the solution that I ended up implementing…
In the end, I went with a backup player technique. I have two players set up to play each sound (I used players here to be consistent. They’re really effects, since I’m using SoundEngine/OpenAL). If the first one is busy, I use the second one. If both are busy, I skip playing the sound. I was all set to go even deeper, to three, or even four, players for each sound. Fortunately, two gave good results, and the code was straightforward, so I stuck with a single level of backup.
I probably should have tried this first, but I was worried about both the memory and the execution overhead of having multiple players for each sound. My concern was not warranted. With the current implementation, the overhead is negligible, and the results are great.