Mistakes Were Made: Audio and ARC

There’s a mistake I have, to my own great embarrassment, made twice, so I think it’s worth writing up for posterity. This one’s brief: you can laugh at me and move on.

Under ARC, AVAudioPlayers don’t retain themselves while playing.

A brief recap: ARC takes memory management off the minds of developers by automating the insertion of retains and releases according to a simple set of rules. This is much to the delight of new developers, and equally to the chagrin of grizzled oldsters. (I admit to having felt more than a bit of the latter at the outset.)

AVAudioPlayer is the simplest API to get an audio file from your Resources directory to your user’s ears.

AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithContentsOfURL:audioPath error:nil];
[player play];

And you’re set! Under manual memory management, anyway.

What happens with ARC?  The newly created player is retained. As soon as the method ends, however, there is no longer anything referring to it. Even though it’s still doing stuff — the audio file you’re playing will presumably last longer than a millisecond — it’s released, and happily cuts off the audio early.

Having reasoned through it, the solution is of course trivial. Make the AVAudioPlayer an instance variable of whatever class is creating it, and it won’t be released until the controlling object is. Audio bliss.

Of course, I feel that the mistake is perhaps not entirely mine. AVAudioPlayer really should retain itself while it’s playing audio, and release itself when it finishes. To that end, I’ve filed rdar://12635205; feel free to dupe it if you agree. And for general interest, here’s the sample code I included to demonstrate the problem:

#define IVAR_PLAYER 0

#import "SMViewController.h"
#import <AVFoundation/AVFoundation.h>

@interface SMViewController ()
{
#if IVAR_PLAYER
	AVAudioPlayer *player;
#endif
}

@end

@implementation SMViewController

- (IBAction)playSound:(id)sender {
#if !IVAR_PLAYER
	AVAudioPlayer *player;
#endif
	NSURL *audioPath = [NSURL fileURLWithPath:[[NSBundle mainBundle]
											   pathForResource:@"audio" ofType:@"mp3"]];
	player = [[AVAudioPlayer alloc] initWithContentsOfURL:audioPath
													error:nil];
	[player play];
	
	// delay a bit to keep the run loop alive long enough to let the audio start playing
	// don't do this in production code, obviously! It is a bad way to do basically anything.
	for (int i = 0; i < 1000; i++)
		NSLog(@"%d", i);

@end
Joel Kraut

About Joel Kraut

Developing on Apple platforms for, holy shit, over ten years now. Find me on linkedin and twitter. My personal website is foon.us.
This entry was posted in Explanation and tagged , , , , , , , . Bookmark the permalink.