Sunday, June 21, 2009

Simple audio recording and playback

So, the Gents have been toying with iPhone audio recording and playback, and it has been an uphill struggle, to say the least. SpeakHere is the defacto standard demo everyone references, but it is quite abhorrent in both code style and in ease of understanding. The underlying technology, Audio Queues, are already (overly?) complicated, I'd like my demos to help ease that complication, not make it worse, thankyouverymuch. And though I often hear that Apple's documentation is excellent, I'm less then impressed so far. They are worse than Microsoft at documenting edge cases, and sometimes I wonder if the tech writers think that developers care more about making pretty interfaces than understanding how to use the core API's that their software has to interact with.

But just today, I found out that Apple provides AVAudioRecorder and AVAudioPlayback as super easy to use wrappers around the AudioQueue stuff,... as long as your needs aren't too advanced. It won't help us for what we want to do on the audio playback side of things, but it is a hell of a lot easier to use and understand for audio recording. I slapped together a sample program based solely on the docs on about an hour, and it worked the first time (an unfortunately unique experience with iPhone audio programming so far)! Apparently, these classes sit in the sweet spot between wanting to play more than a few seconds of audio at a time and not being a manic control freak sound engineer who has to tweak every last parameter to perfection.

Here's my setup code for the recording (note the NSDictionary for settings, I was going to try to provide some since the docs weren't clear what would happen if I didn't, but it started getting complicated so I just tried nil, and it worked! gasp! Reasonable defaults? It was more complicated to set up the filepath than to record the audio!):




////////
// Make a url for the file in the apps documents directory
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *recordFilePath = [documentsDirectory stringByAppendingPathComponent: @"recordedFile.caf"];
NSURL *url = [NSURL fileURLWithPath:recordFilePath];
////////

NSDictionary *settings = nil;

NSError *error = 0;
recorder = [[AVAudioRecorder alloc] initWithURL:url settings:settings error:&error];

if (error)
{
NSLog(@"An error occured while initializing the audio recorder! -- %@", error);
exit(-1);
}


And here's my record button code (gasp! It doesn't require 50+ lines just to record some audio?):



- (IBAction)record
{
NSLog(@"Record pressed");

if ([recorder isRecording])
{
NSLog(@"Stopping recording...");
[recorder stop];
}
else
{
NSLog(@"Starting recording...");
[recorder record];
}
}


The player setup code was even easier:



player = [AVAudioPlayer alloc];


and my play method:


- (IBAction)play
{
NSLog(@"Play pressed");

////////
// Make a url for the file in the apps documents directory
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *recordFilePath = [documentsDirectory stringByAppendingPathComponent: @"recordedFile.caf"];
NSURL *url = [NSURL fileURLWithPath:recordFilePath];
////////


NSError *error = 0;
[player initWithContentsOfURL:url error:&error];

if (error)
{
NSLog(@"An error occured while initializing the audio recorder! -- %@", error);
exit(-1);
}

[player play];
}


Yeah, I duplicated the file path setup code, it's experimental code, sue me. More importantly, notice that I broke with the objective-c [[X alloc] init] paradigm and went with something that feels 1000x hackier, calling init every time we are about to play. It seems that AVAudioPlayer wants an existing file when it is initialized, but since I may not have one recorded yet, it would error out.

Now, I'm not an objective-c programmer, this is all new to me, but what I did feels like the moral equivalent of pre-allocating some memory and calling placement new repeatedly without ever actually destroying the object, i.e. very very evil. In C++, all sorts of resource leaks may occur, but who knows in this wacky objective-c world? Apple's documentation doesn't explain what happens if init is called multiple times on AVAudioPlayer as far as I can tell, and that seems like a crucial piece of information.

2 comments:

  1. The alloc/init and retain/release cycles are key to understanding idiomatic Objective-C memory management. Unfortunately, they also differ enough from most other environments that it's quite confusing at first. In summary: it's more manual boilerplate than C++ smart pointers, but pretty consistent across the board.

    First, take a quick look at the documentation for -[NSObject init].

    I want to highlight that init operates on an object (the receiver of the init message), but it also returns an object. The object returned by init may not be the same object that handled the init message. In other words, [SomeClass alloc] and [[SomeClass alloc] init] might be different objects. Why? It can used to share instances rather than always creating new objects (similar to Java's Integer.valueOf()). It's also used to implement class clusters.

    As a result, you are encouraged to always call alloc and init in the same, nested expression (i.e. [[Class alloc] init] ). Never keep a reference around to the object that alloc returned. It may be OK with some classes (probably most classes), but it's not a good habit to get into.

    I agree that re-initting the same object might well lead to resource leaks and other bad stuff. The same is true in any environment. You may consider making the AVAudioPlayer a local variable inside the play method. I don't know whether -[AVAudioPlayer play] blocks or not. If it does not, it would be hard to know when to release instances.

    ReplyDelete