Tuesday 5 April 2011

iOS: A Fading AVAudioPlayer

In a recent iOS project I needed to play some background music and have it fade smoothly in and out when it starts and stops.

The "play some music" part is easy. iOS give us AVAudioPlayer in the AVFoundation framework. Super sweet and easy to use. However the "fade in and out" bit doesn't come for free.

The canonical solution suggested on the web is to hand-craft some nasty looping logic to get the job done. This is 2011 and we can do better than that. And, indeed, here it is, in all it's Objective C glory.

My solution makes interesting use of categories, associative references and blocks. It's worth strolling through the implementation if you're interested in any of these. If not, you can just grab my code from the Gitorious project and use it in blissful ignorance.

1. Add a category

First, we open up our own category on the AVAudioPlayer class and define the methods we would ideally like the class to provide:
@interface AVAudioPlayer (PGFade)
- (void) stopWithFadeDuration:(NSTimeInterval)duration;
- (void) playWithFadeDuration:(NSTimeInterval)duration;
@end

That's the joys of categories - you can extend existent classes in your own application easily to make it look like methods were part of the original class interface. The common convention is to save this in a file called "AVAudioPlayer+PGFade.h".

Now client code can create a bog-standard AVAudioPlayer object, and call my new methods as if they were part of the base interface:
AVAudioPlayer *player = [AVAudioPlayer alloc] initWith...]; // however you want it set up
[player playWithFadeDuration:2.0];

2. Associative References

Now, our implementation of this is going to require some instance variables (ivars) to work with. The problem with categories is that they only allow you to add methods - you can't extend the set of instance variables defined in the class' @interface.

Or can you?

Associative references to the rescue! This is a handy Objective C runtime facility that allows you to associate another object with an existing object, with a lookup system very much like a dictionary - referenced by a void* key value.

The associated object is lifetime-managed with the original object, so when you release the parent, all associated objects are also released.

Using this facility we can "graft on" some of our own private ivars to the original class. Dirty, but effective.

One of the variables I need is a boolean variable tracking whether a fade is currently in progress. I actually expose this as a property called fading in the category interface. The implementation looks like this:
@implementation AVAudioPlayer (PGFade)

static char fadingKey;

- (BOOL) fading
{
NSNumber *number = (NSNumber *)objc_getAssociatedObject(self, &fadingKey);
return number && number.boolValue;
}

- (void) setFading:(BOOL)fading
{
objc_setAssociatedObject(self, &fadingKey, [NSNumber numberWithBool:fading], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

That's relatively simple. We just use two objective C runtime functions,  objc_getAssociatedObject and objc_setAssociatedObject. For the association we use the address of a static variable - this will be an unambiguous value in the whole program. The object we store is an NSNumber object initialised with the value of our boolean.

It's a simple and effective trick.

3. Blocks

The final bit is for bonus points. You can see the implementation of my fade routine in the example project code itself. It's pretty simple - I just initialise the fade state in a few more "associative reference" variables and schedule a method call with performSelector:withObject:afterDelay. Each time this is called, I adjust the AVAudioPlayer's volume, and schedule a new call if one is needed.

However, to truly perform a stop with fade, we need to ramp the audio volume down, and when it gets to zero stop the player. This could be achieved with some more state variables and clumsy logic, but iOS now provides us with blocks which are perfect for this kind of activity.

For sanity, I typedef a block (closure) type. The syntax practically identical to typedefing a pointer-to-function in C, but with ^ instead of *.

typedef void (^AVAudioPlayerFadeCompleteBlock)();

I make one more associated variable of this type, the block to call on completion:
- (AVAudioPlayerFadeCompleteBlock) fadeCompletion
{
return (AVAudioPlayerFadeCompleteBlock)objc_getAssociatedObject(self, &fadeCompletionKey);
}

- (void) setFadeCompletion:(AVAudioPlayerFadeCompleteBlock)completion
{
objc_setAssociatedObject(self, &fadeCompletionKey, completion, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

Note that blocks can be traded like objective C objects. You can retain, copy, and release them. In this case our code needs to take a copy (using the OBJC_ASSOCIATION_COPY_NONATOMIC flag). This ensures that any set block exists on the heap (persistent) rather than the default on-the-stack location.

Now, the internal "audio fade" routine need only check whether there is a completion block registered when the fade is complete, and if so call it:
    if (fadeIsComplete)
{
self.fading = NO;
AVAudioPlayerFadeCompleteBlock completion = self.fadeCompletion;
if (completion) completion();
self.fadeCompletion = nil;
}

Invoking a block looks just like calling a function. Once called, we clear out the stored block in case it is holding other resources that should be released.

This provides an elegant way to perform any arbitrary action after the fade is complete.

That's all, folks!

The trio of Objective C facilities used here work together well to provide a simple and elegant solution to a problem that is often achieved with complex "strings-and-glue" logic. Check out the entire project on Gitorious here.

14 comments:

Josh Parmenter said...

Hi Pete, Just checked out your fading audio player code... I was getting ready to start building something similar when I found your post while looking around a bit. Quite elegant! Thanks for making it available, and I will make sure to let you know if I end up using some bits of it with a project I am now working on.
Cheers,
Josh Parmenter
http://www.realizedsound.net/josh

Pete Goodliffe said...

Glad its useful. I'd love to hear how you use it.

Anonymous said...

Thanks Pete. I couldn't have done this as neat and clean myself.

Anonymous said...

This is great - thanks. Is there any kind of licence on this code? E.g. MIT, Apache?

Pete Goodliffe said...

From the License section in the release notes (included in the source):

Feel free to incorporate this code in your own applications. I'd appreciate hearing from you if you do so. It's nice to know that I've been helpful. Attribution is welcomed, but not required.

Tim said...

Nice solution. question from a noob: why use objective c category versus a subclass of AVAudioPlayer?

Anonymous said...

Very helpful!!! Thank you so much!!!

Anonymous said...

Thank you so much for this excellent solution!

Pete Goodliffe said...

@Tim: a very good question!

In the simplest case, a subclass would indeed work. But if you required multiple orthogonal extensions of AVAudioPlayer you couldn't use them all at once (since Obj-C only allows single inheritance).

So subclassing is less flexible than opening a category. It imposes a more rigid structure to the code.

Also, extending a system component like this "feels" better done as a category, rather than a subclass, I think.

And, ultimately, this technique is plain cool :-)

Tim said...

Many thanks for this. Incorporated into a project and uploaded the relevant code onto GitHub. https://github.com/toolmanGitHub/BDAPAudioPlayerController

Unknown said...

Hi, Pete... cool code, thank you. Using it in a children's story called "The Miniature Polar Bear": http://www.indiegogo.com/miniature-polar-bear

Raimundo said...

Thanks Pete, very helpful solution!

There is one problem with this code - it generates memory leaks when using blocks. I've ran through the code and this part should be extended like this:

if (fadeIsComplete)
{
self.fading = NO;
AVAudioPlayerFadeCompleteBlock completion = self.fadeCompletion;
if (completion) completion();
self.fadeCompletion = nil;
CFBridgingRelease((__bridge CFTypeRef)(completion)); //added
}


Also there's a problem with runloops. If fade starts and user starts scrolling any uiscrollview when fading is not done yet, it stops fading and continues only when scrolling is finished. To fix this, you can use

[self performSelector:@selector(fadeFunction) withObject:nil afterDelay:fadeInterval inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]];

instead of
[self performSelector:@selector(fadeFunction) withObject:nil afterDelay:fadeInterval];

John Shackelford said...

Pete,

This cool stuff. Using in my app Possession. Very helpful.

Pete Goodliffe said...

Glad it's still useful!