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.
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.
ReplyDeleteFirst, 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.
Awesome work Marc!!! Thanks.
ReplyDelete