Skip to content
Newer
Older
100644 598 lines (476 sloc) 17.4 KB
e619ece @phildow Initial commit
authored Nov 9, 2011
1 //
2 // SproutedLeopardAudioRecorder.m
3 // Sprouted AVI
4 //
5 // Created by Philip Dow on 5/1/08.
6 // Copyright Philip Dow / Sprouted. All rights reserved.
7 // All inquiries should be directed to developer@journler.com
8 //
9
10 /*
11 Redistribution and use in source and binary forms, with or without modification, are permitted
12 provided that the following conditions are met:
13
14 * Redistributions of source code must retain the above copyright notice, this list of conditions
15 and the following disclaimer.
16
17 * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
18 and the following disclaimer in the documentation and/or other materials provided with the
19 distribution.
20
21 * Neither the name of the author nor the names of its contributors may be used to endorse or
22 promote products derived from this software without specific prior written permission.
23
24 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
25 WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
26 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
27 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
28 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
29 INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
30 TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
31 ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32 */
33
34
35 #import <SproutedAVI/SproutedLeopardAudioRecorder.h>
36 #import <SproutedAVI/SproutedAVIAlerts.h>
37 #import <SproutedAVI/PDMovieSlider.h>
38
39 #define kMeterTimerInterval 1.0/15
40 #define kPlaybacklockTimerInterval 1.0/30
41 #define kInitialGain 0.45
42
43 #define kFormatQuickTimeMovie 0
44 #define kFormatMP3 1
45
46 #define kScriptWasCancelledError -128
47
48 @implementation SproutedLeopardAudioRecorder
49
50 - (id) initWithController:(SproutedAVIController*)controller
51 {
52 if ( self = [super initWithController:controller] )
53 {
54 [NSBundle loadNibNamed:@"AudioRecorder_105" owner:self];
55
56 _unsavedRecording = NO;
57
58 // set up some default recording info
59 [self setRecordingTitle:[NSString string]];
60 [self setRecordingDate:[NSCalendarDate calendarDate]];
61
62 // the save location
63 NSString *tempDir = [self cachesFolder];
64 if ( tempDir == nil )
65 {
66 tempDir = NSTemporaryDirectory();
67 if ( tempDir == nil ) tempDir = [NSString stringWithString:@"/tmp"];
68 }
69 else
70 {
71 tempDir = [tempDir stringByAppendingPathComponent:@"com.sprouted.avi.audiorecorder"];
72 if ( ![[NSFileManager defaultManager] fileExistsAtPath:tempDir] )
73 {
74 if ( ![[NSFileManager defaultManager] createDirectoryAtPath:tempDir attributes:nil] )
75 {
76 tempDir = NSTemporaryDirectory();
77 if ( tempDir == nil ) tempDir = [NSString stringWithString:@"/tmp"];
78 }
79 }
80 }
81
82 // the actual file
83 NSString *dateTime = [[NSDate date] descriptionWithCalendarFormat:@"%H%M%S" timeZone:nil locale:nil];
84 [self setMovPath:[tempDir stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.mov", dateTime]]];
85 [self setMp3Path:[tempDir stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.mp3", dateTime]]];
86
87 // remove any existing files
88 if ( [[NSFileManager defaultManager] fileExistsAtPath:[self movPath]] )
89 [[NSFileManager defaultManager] removeFileAtPath:[self movPath] handler:self];
90
91 if ( [[NSFileManager defaultManager] fileExistsAtPath:[self mp3Path]] )
92 [[NSFileManager defaultManager] removeFileAtPath:[self mp3Path] handler:self];
93 }
94
95 return self;
96 }
97
98 #pragma mark -
99
100 - (BOOL) recorderShouldClose:(NSNotification*)aNotification error:(NSError**)anError
101 {
102 BOOL shouldClose = YES;
103
104 if ( _recording )
105 {
106 shouldClose = NO;
107 *anError = [self stillRecordingError];
108 }
109 else if ( _unsavedRecording == YES && [self warnsWhenUnsavedChanges] )
110 {
111 shouldClose = NO;
112 *anError = [self unsavedChangesError];
113 }
114
115 return shouldClose;
116 }
117
118 - (BOOL) recorderWillLoad:(NSNotification*)aNotification
119 {
120 // runtime security check prevents subclassing
121 if ( ![self isMemberOfClass:[SproutedLeopardAudioRecorder class]] )
122 return NO;
123
124 BOOL success = [self setupRecording];
125 if ( !success)
126 {
127 [self takedownRecording];
128
129 NSString *errorMessage = NSLocalizedStringFromTableInBundle(
130 @"no audio capture msg",
131 @"Localizable",
132 [NSBundle bundleWithIdentifier:@"com.sprouted.avi"],
133 @"");
134 NSString *errorInfo = NSLocalizedStringFromTableInBundle(
135 @"no audio capture info",
136 @"Localizable",
137 [NSBundle bundleWithIdentifier:@"com.sprouted.avi"],
138 @"");
139
140 NSString *myError = [NSString stringWithFormat:@"%@\n\n%@", errorMessage, errorInfo];
141 [self setError:myError];
142 }
143 else
144 {
145 [insertButton setEnabled:NO];
146 [recProgress setUsesThreadedAnimation:YES];
147 [volumeSlider setFloatValue:kInitialGain];
148 }
149
150 return success;
151 }
152
153 - (BOOL) recorderDidLoad:(NSNotification*)aNotification
154 {
155 [mCaptureSession startRunning];
156
157 mAudioLevelTimer = [[NSTimer scheduledTimerWithTimeInterval:kMeterTimerInterval
158 target:self
159 selector:@selector(updateAudioLevels:)
160 userInfo:nil
161 repeats:YES] retain];
162
163 return YES;
164 }
165
166 - (BOOL) recorderWillClose:(NSNotification*)aNotification
167 {
168 // must be performed before deallocation
169 // deallocation won't occur otherwise
170 [self takedownRecording];
171
172 [recorderController unbind:@"contentObject"];
173 [recorderController setContent:nil];
174
175 [player pause:self];
176 [player setMovie:nil];
177
178 if ( updatePlaybackLocTimer )
179 {
180 [updatePlaybackLocTimer invalidate];
181 [updatePlaybackLocTimer release], updatePlaybackLocTimer = nil;
182 }
183
184 if ( [[NSFileManager defaultManager] fileExistsAtPath:[self movPath]] )
185 [[NSFileManager defaultManager] removeFileAtPath:[self movPath] handler:self];
186
187 if ( [[NSFileManager defaultManager] fileExistsAtPath:[self mp3Path]] )
188 [[NSFileManager defaultManager] removeFileAtPath:[self mp3Path] handler:self];
189
190 return YES;
191 }
192
193 #pragma mark -
194
195 - (BOOL) setupRecording
196 {
197 // Create the capture session
198 mCaptureSession = [[QTCaptureSession alloc] init];
199
200 // Connect inputs and outputs to the session
201 BOOL success = NO;
202 NSError *localError;
203
204 QTCaptureDevice *audioDevice = [QTCaptureDevice defaultInputDeviceWithMediaType:QTMediaTypeSound];
205 success = [audioDevice open:&localError];
206
207 if (!success)
208 {
209 audioDevice = nil;
210 [self setError:[self audioCaptureError]];
211 goto bail;
212 }
213 else if (audioDevice)
214 {
215 mCaptureAudioDeviceInput = [[QTCaptureDeviceInput alloc] initWithDevice:audioDevice];
216 success = [mCaptureSession addInput:mCaptureAudioDeviceInput error:&localError];
217 if (!success)
218 {
219 // Handle error
220 [self setError:[self audioCaptureError]];
221 goto bail;
222 }
223 }
224
225 // Create the movie file output and add it to the session
226 mCaptureMovieFileOutput = [[QTCaptureMovieFileOutput alloc] init];
227 success = [mCaptureSession addOutput:mCaptureMovieFileOutput error:&localError];
228 if (!success)
229 {
230 // Handle error
231 [self setError:[self audioCaptureError]];
232 goto bail;
233 }
234
235 [mCaptureMovieFileOutput setDelegate:self];
236
237 // Set the compression for the audio/video that is recorded to the hard disk.
238 NSEnumerator *connectionEnumerator = [[mCaptureMovieFileOutput connections] objectEnumerator];
239 QTCaptureConnection *connection;
240
241 NSString *audioCompression = @"QTCompressionOptionsHighQualityAACAudio";
242
243 // iterate over each output connection for the capture session and specify the desired compression
244 while ( (connection = [connectionEnumerator nextObject]) )
245 {
246 NSString *mediaType = [connection mediaType];
247 QTCompressionOptions *compressionOptions = nil;
248
249 // specify the video and audio compression options
250 // (note: a list of other valid compression types can be found in the QTCompressionOptions.h interface file)
251 if ([mediaType isEqualToString:QTMediaTypeSound])
252 {
253 // use AAC Audio: @"QTCompressionOptionsHighQualityAACAudio"
254 compressionOptions = [QTCompressionOptions compressionOptionsWithIdentifier:audioCompression];
255 }
256
257 // set the compression options for the movie file output
258 [mCaptureMovieFileOutput setCompressionOptions:compressionOptions forConnection:connection];
259 }
260
261 bail:
262
263 return success;
264 }
265
266 - (BOOL) takedownRecording
267 {
268 if (mAudioLevelTimer != nil)
269 {
270 [mAudioLevelTimer invalidate];
271 [mAudioLevelTimer release], mAudioLevelTimer = nil;
272 }
273
274 if ( [mCaptureSession isRunning] )
275 [mCaptureSession stopRunning];
276
277 if ([[mCaptureAudioDeviceInput device] isOpen])
278 [[mCaptureAudioDeviceInput device] close];
279
280 return YES;
281 }
282
283 #pragma mark -
284 #pragma mark Making the Recording
285
286 - (IBAction)recordPause:(id)sender
287 {
288 if ( _recording )
289 {
290 [self stopRecording:sender];
291 [recordButton accessibilitySetOverrideValue:NSLocalizedStringFromTableInBundle(
292 @"play description",
293 @"Localizable",
294 [NSBundle bundleWithIdentifier:@"com.sprouted.avi"],
295 nil)
296 forAttribute:NSAccessibilityDescriptionAttribute];
297 }
298 else
299 {
300 [self startRecording:sender];
301 [recordButton accessibilitySetOverrideValue:NSLocalizedStringFromTableInBundle(
302 @"stop description",
303 @"Localizable",
304 [NSBundle bundleWithIdentifier:@"com.sprouted.avi"],
305 nil)
306 forAttribute:NSAccessibilityDescriptionAttribute];
307 }
308 }
309
310 - (IBAction) startRecording:(id)sender
311 {
312 _recording = YES;
313 _recordingStart = GetCurrentEventTime();
314 [mCaptureMovieFileOutput recordToOutputFileURL:[NSURL fileURLWithPath:[self movPath]]];
315 }
316
317 - (IBAction) stopRecording:(id)sender
318 {
319 if ( _recording )
320 {
321 _recording = NO;
322 _unsavedRecording = YES;
323 [mCaptureMovieFileOutput recordToOutputFileURL:nil];
324 }
325 else
326 {
327 NSBeep();
328 }
329 }
330
331 - (IBAction)setChannelGain:(id)sender
332 {
333 // no idea if this is even possible
334
335 // update the volume image display
336 [volumeImage setImage:[self volumeImage:[sender floatValue] minimumVolume:[sender minValue]]];
337 }
338
339 #pragma mark -
340
341 - (void)captureOutput:(QTCaptureFileOutput *)captureOutput
342 didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL
343 forConnections:(NSArray *)connections
344 dueToError:(NSError *)error
345 {
346 [self takedownRecording];
347 [self prepareForPlaying];
348 }
349
350 - (void) prepareForPlaying
351 {
352 // switch the preview view out and replace it with the quicktime view
353
354 if ( [QTMovie canInitWithFile:[self movPath]] )
355 {
356 // prepare the movie
357 QTMovie *movie = [[QTMovie alloc] initWithFile:[self movPath] error:nil];
358
359 // play the movie
360 [player setMovie:movie];
361
362 // set the playback volume and playback volume slider
363 [volumeSlider setFloatValue:0.9];
364 [volumeSlider setAction:@selector(changePlaybackVolume:)];
365 [self changePlaybackVolume:volumeSlider];
366
367 // set playback duration values
368 double movieLength = [movie duration].timeValue;
369 [playbackLocSlider setMaxValue:movieLength];
370 [playbackLocSlider setFloatValue:0.0];
371
372 // register a notification to grab the end of the movie
373 [[NSNotificationCenter defaultCenter] addObserver:self
374 selector:@selector(movieEnded:)
375 name:QTMovieDidEndNotification
376 object:movie];
377
378 // prepare a timer to handle an update of the playback
379 updatePlaybackLocTimer = [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:.1]
380 interval:kPlaybacklockTimerInterval
381 target:self
382 selector:@selector(playlockCallback:)
383 userInfo:nil
384 repeats:YES];
385
386 [[NSRunLoop currentRunLoop] addTimer:updatePlaybackLocTimer forMode:NSDefaultRunLoopMode]; // or NSModalPanelRunLoopMode
387
388 // clean up
389 [movie release];
390 }
391 else
392 {
393 [[NSAlert unreadableAudioFile] runModal];
394
395 [recordButton setEnabled:NO];
396 [fastforward setEnabled:NO];
397 [rewind setEnabled:NO];
398 }
399
400 // set the record button to play/pause mode
401 NSBundle *myBundle = [NSBundle bundleWithIdentifier:@"com.sprouted.avi"];
402
403 [recordButton setImage:[[[NSImage alloc] initWithContentsOfFile:[myBundle pathForImageResource:@"playrecording.png"]] autorelease]];
404 [recordButton setAlternateImage:[[[NSImage alloc] initWithContentsOfFile:[myBundle pathForImageResource:@"pauserecording.png"]] autorelease]];
405 [recordButton setAction:@selector(playPause:)];
406
407 // make the playback slider visible and hide the metering view
408 NSPoint playlockFrame = [mAudioLevelMeter frame].origin;
409
410 [playbackLocSlider retain];
411 [playbackLocSlider removeFromSuperviewWithoutNeedingDisplay];
412 [playbackLocSlider setFrameOrigin:playlockFrame];
413
414 // get the playback slider ready to fade in
415 [playbackLocSlider setHidden:YES];
416 [[self view] addSubview:playbackLocSlider];
417
418 NSDictionary *theDict = [[[NSDictionary alloc] initWithObjectsAndKeys:
419 fastforward, NSViewAnimationTargetKey,
420 NSViewAnimationFadeInEffect, NSViewAnimationEffectKey, nil] autorelease];
421
422 NSDictionary *otherDict = [[[NSDictionary alloc] initWithObjectsAndKeys:
423 rewind, NSViewAnimationTargetKey,
424 NSViewAnimationFadeInEffect, NSViewAnimationEffectKey, nil] autorelease];
425
426 NSDictionary *playbackDict = [[[NSDictionary alloc] initWithObjectsAndKeys:
427 playbackLocSlider, NSViewAnimationTargetKey,
428 NSViewAnimationFadeInEffect, NSViewAnimationEffectKey, nil] autorelease];
429
430 NSDictionary *meteringDict = [[[NSDictionary alloc] initWithObjectsAndKeys:
431 mAudioLevelMeter, NSViewAnimationTargetKey,
432 NSViewAnimationFadeOutEffect, NSViewAnimationEffectKey, nil] autorelease];
433
434 NSViewAnimation *theAnim = [[NSViewAnimation alloc] initWithViewAnimations:
435 [NSArray arrayWithObjects:theDict, otherDict, playbackDict, meteringDict, nil]];
436
437 [theAnim startAnimation];
438
439 // remove the metering view once the animation is complete
440 [mAudioLevelMeter removeFromSuperview];
441 [insertButton setEnabled:YES];
442
443 // clean up
444 [theAnim release];
445 [playbackLocSlider release];
446 }
447
448 #pragma mark -
449 #pragma mark Playing the Recording
450
451 - (IBAction) changePlaybackVolume:(id)sender
452 {
453 [[player movie] setVolume:[sender floatValue]];
454 [volumeImage setImage:[self volumeImage:[sender floatValue] minimumVolume:[sender minValue]]];
455 }
456
457 - (IBAction)playPause:(id)sender
458 {
459 if ( !_playingMovie )
460 {
461 [recordButton accessibilitySetOverrideValue:NSLocalizedStringFromTableInBundle(
462 @"stop description",
463 @"Localizable",
464 [NSBundle bundleWithIdentifier:@"com.sprouted.avi"],
465 nil)
466 forAttribute:NSAccessibilityDescriptionAttribute];
467
468 [player play:sender];
469 }
470 else
471 {
472 [recordButton accessibilitySetOverrideValue:NSLocalizedStringFromTableInBundle(
473 @"play description",
474 @"Localizable",
475 [NSBundle bundleWithIdentifier:@"com.sprouted.avi"],
476 nil)
477 forAttribute:NSAccessibilityDescriptionAttribute];
478
479 [player pause:sender];
480 }
481
482 _playingMovie = !_playingMovie;
483 }
484
485 - (IBAction) fastForward:(id)sender
486 {
487 [player stepForward:self];
488 [recordButton setState:NSOffState];
489 [self playlockCallback:nil];
490
491 _playingMovie = NO;
492 }
493
494 - (IBAction) rewind:(id)sender
495 {
496 [player stepBackward:self];
497 [recordButton setState:NSOffState];
498 [self playlockCallback:nil];
499
500 _playingMovie = NO;
501 }
502
503 #pragma mark -
504
505 - (void) playlockCallback:(NSTimer*)aTimer
506 {
507 // called to update the playback position on the playlock slider
508
509 NSString *timeString;
510 QTTime current;
511
512 current = [[player movie] currentTime];
513 [playbackLocSlider setDoubleValue:current.timeValue];
514
515 timeString = QTStringFromTime(current);
516 if ( timeString != nil ) [timeField setStringValue:[timeString substringWithRange:NSMakeRange(2, 8)]];
517
518 }
519
520 - (void) movieEnded:(NSNotification*)aNotification
521 {
522 // a callpack when the movie ends - reset the play button and playing status
523
524 _playingMovie = NO;
525 [recordButton setState:NSOffState];
526 }
527
528 #pragma mark -
529
530 - (void)updateAudioLevels:(NSTimer *)aTimer
531 {
532 // update the timer if recording
533 if ( _recording )
534 [self updateTimeAndSizeDisplay:aTimer];
535
536 // Get the mean audio level from the movie file output's audio connections
537 float totalDecibels = 0.0;
538
539 QTCaptureConnection *connection = nil;
540 NSUInteger i = 0;
541 NSUInteger numberOfPowerLevels = 0; // Keep track of the total number of power levels in order to take the mean
542
543 for (i = 0; i < [[mCaptureMovieFileOutput connections] count]; i++)
544 {
545 connection = [[mCaptureMovieFileOutput connections] objectAtIndex:i];
546
547 // QTCaptureConnectionAudioAveragePowerLevelsAttribute
548 // QTCaptureConnectionAudioPeakHoldLevelsAttribute
549
550 if ([[connection mediaType] isEqualToString:QTMediaTypeSound])
551 {
552 NSArray *powerLevels = [connection attributeForKey:QTCaptureConnectionAudioAveragePowerLevelsAttribute];
553 NSUInteger j, powerLevelCount = [powerLevels count];
554
555 for (j = 0; j < powerLevelCount; j++)
556 {
557 NSNumber *decibels = [powerLevels objectAtIndex:j];
558 totalDecibels += [decibels floatValue];
559 numberOfPowerLevels++;
560 }
561 }
562 }
563
564 if (numberOfPowerLevels > 0 )
565 [mAudioLevelMeter setFloatValue:(pow(10., 0.05 * (totalDecibels / (float)numberOfPowerLevels)) * 51.0)];
566 else
567 [mAudioLevelMeter setFloatValue:0];
568 }
569
570 - (void) updateTimeAndSizeDisplay:(NSTimer*)aTimer
571 {
572 // update the seconds
573 int totalSeconds = (int)(GetCurrentEventTime() - _recordingStart);
574
575 int hours = (int)(floor(totalSeconds / 3600));
576 int hoursLeftover = (int)(floor(totalSeconds % 3600));
577
578 int minutes = (int)(floor(hoursLeftover / 60 ));
579 int seconds = (int)floor(hoursLeftover) % 60;
580
581 [timeField setStringValue:[NSString stringWithFormat:@"%i%i:%i%i:%i%i",
582 hours/10,
583 hours%10,
584 minutes/10,
585 minutes%10,
586 seconds/10,
587 seconds%10]];
588
589 // update the size
590 UInt64 totalSize = [mCaptureMovieFileOutput recordedFileSize] / 1024; // = kBytes
591 UInt64 mbs = totalSize / 1000;
592 UInt64 kbs = (totalSize % 1000) / 100;
593
594 [sizeField setStringValue:[NSString stringWithFormat:@"%qu.%quMB", mbs, kbs]];
595 }
596
597 @end
Something went wrong with that request. Please try again.