How my app, Tomorrow, records and play audios using AVAudioRecorder and AVAudioPlayer (Part I)
The name of my next app is going to be Tomorrow. Record inspiring messages today, get it tomorrow. The next day, it's gone. Very excited. I've created a quick and dirty website: http://tomorrow.gives My brother, David, will be redesigning the splash page to make it much better.
Today shall be a day where we discuss AVAudioRecorder. There are quite a few unique situations with AVFoundation. While AVAudioRecorder and AVAudioPlayer are very easy to use, it took a really long time to make sure that I was doing everything correctly and everything was performing the way I wanted it to. On stack overflow, you'll find code of it being in the viewcontroller and in it's simplest form. I'll show you a refactored version of AVAudioRecorder through a singleton design pattern.
First, let's set up some private properties:
@interface AudioController () @property (nonatomic, strong) AVAudioRecorder *recorder; @property (nonatomic, strong) AVAudioPlayer *player; @end
We want to make sure these are in the .m file and not the .h file because other classes do not need to know what's happening with these particular properties.
The next thing that needs to be done, initialize the AVAudioRecorder. This is the press and hold of the green button in ther previous gif.
The way that I accomplished this by using a singleton handler:
+ (AudioController *)sharedInstance { static AudioController *sharedInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [[AudioController alloc] init]; }); return sharedInstance; }
The way that AVAudioRecorder is initialized: - initWithURL:settings:error:
The URL is the file system location is recorded to. The settings is the settings for the recording session The error returns, by-reference, a description of the error, if an error occurs. It is best to make sure to preset NSError *error = nil and pass in &error into the parameter to make sure that you are able to detect an error if one exists.
So first what I did was I added an NSURL to the file:
@property (nonatomic, strong) NSURL *url;
The reason for this is because we're going to be using the same url for the start of the recording and stopping of the recording. One way to initialize an NSURL is -fileURLWithPathComponents. This is a class method that returns a newly created NSURL object as a file URL with specified path components. This is what we need because the path components are separated by forward-slashes (/) in the returned URL.
So here's a lot of private methods that I used create an easy name for me to distinguish recordings, get a directory for the file to be a part of, and set up the recorder settings:
- (NSString *)nowString { NSDate *now = [NSDate date]; NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; [formatter setDateFormat:@"MMMdyyyy+HHMMss"]; NSString *nowString = [formatter stringFromDate:now]; NSString *destinationString = [NSString stringWithFormat:@"%@.aac", nowString]; return destinationString; } - (NSArray *)documentsPath { NSArray *documentsPath = [NSArray arrayWithObjects:[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject], [self nowString], nil]; return documentsPath; } -(NSDictionary *)getRecorderSettings { NSMutableDictionary *recordSettings = [[NSMutableDictionary alloc] init]; [recordSettings setValue:[NSNumber numberWithInt:kAudioFormatMPEG4AAC] forKey:AVFormatIDKey]; [recordSettings setValue:[NSNumber numberWithFloat:44100.0] forKey:AVSampleRateKey]; [recordSettings setValue:[NSNumber numberWithInt:2] forKey:AVNumberOfChannelsKey]; [recordSettings setValue:[NSNumber numberWithInt:AVAudioQualityHigh] forKey:AVEncoderAudioQualityKey]; [recordSettings setValue:[NSNumber numberWithBool:NO] forKey:AVLinearPCMIsBigEndianKey]; [recordSettings setValue:[NSNumber numberWithBool:NO] forKey:AVLinearPCMIsFloatKey]; return recordSettings; }
I wanted my audio files to be named by "Month/Day/Year-Hour/Min/Sec.aac." While I could've used a UUID, this gives me a much simpler, easier way to distinguish if there are any timing issues or delays in other parts of my code as it is a timestamp of when a recording has occurred.
The documents path was confusing for me initially, and I'm not quite certain I've fully grasped it yet. There are numerous spots where one can save on the iPhone. It can be in a temporary directory or on the home screen, etc.
When you look in the documentation regarding NSSearchPathForDirectoryInDomain, it says:
"Creates a list of directory search paths. Creates a list of path strings for the specified directories in the specified domains. The list is in the order in which you should search the directories. If expandTilde is YES, tildes are expanded as described in stringByExpandingTildeInPath."
I wanted to put it in the home dirctory so I used : NSDocumentDirectory, NSUserDomainMask
And the other object we're putting into the array is the date filename string we created earlier.
Finally, settings. We want to make sure we are using key-value coding. So create a dictionary that can contain a bunch of values. The two things that were very crucial in making sure that it worked correctly:
[recordSettings setValue:[NSNumber numberWithBool:NO] forKey:AVLinearPCMIsBigEndianKey]; [recordSettings setValue:[NSNumber numberWithBool:NO] forKey:AVLinearPCMIsFloatKey];
Then I create a public method that records the audio to a directory:
- (AVAudioRecorder *)recordAudioToDirectory { NSError *error = nil; self.url = [NSURL fileURLWithPathComponents:[self documentsPath]]; self.recorder = [[AVAudioRecorder alloc] initWithURL:self.url settings:[self getRecorderSettings] error:&error]; [self.recorder prepareToRecord]; self.recorder.delegate = self; self.recorder.meteringEnabled = YES; [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:nil]; [[AVAudioSession sharedInstance] setActive:YES error:&error]; [self.recorder record]; return self.recorder; }
Hooray! All of that just to record the audio. Now the stopping has a bunch more and I'll do a part two because it uses another singleton handler for saving into Core Data! :)