Retired Document
Important: This sample code may not represent best practices for current development. The project may use deprecated symbols and illustrate technologies and techniques that are no longer recommended.
AudioExtractionWindowController.mm
/* |
File: AudioExtractionWindowController.mm |
Abstract: Implements the Audio Extraction Window. |
1. Demonstrates the opening and configuration of an audio |
extraction session (setting of layout, start time, duration, |
etc.) and how to preview and perform extraction on a worker |
thread, or alternatively, on a main thread. |
2. Demonstrates how an Audio Context Insert can be used |
during extraction. |
3. Also shows how CoreAudio can be used for playback of |
the extracted audio. |
Version: 1.0 |
Contains modifies versions of code that is part of the |
QTAudioExtractionPanel sample code project, available |
online and introduced originally at WWDC 2005 Session 201: |
"Harnessing the Audio Capabilities of QuickTime 7" |
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple |
Computer, Inc. ("Apple") in consideration of your agreement to the |
following terms, and your use, installation, modification or |
redistribution of this Apple software constitutes acceptance of these |
terms. If you do not agree with these terms, please do not use, |
install, modify or redistribute this Apple software. |
In consideration of your agreement to abide by the following terms, and |
subject to these terms, Apple grants you a personal, non-exclusive |
license, under Apple's copyrights in this original Apple software (the |
"Apple Software"), to use, reproduce, modify and redistribute the Apple |
Software, with or without modifications, in source and/or binary forms; |
provided that if you redistribute the Apple Software in its entirety and |
without modifications, you must retain this notice and the following |
text and disclaimers in all such redistributions of the Apple Software. |
Neither the name, trademarks, service marks or logos of Apple Computer, |
Inc. may be used to endorse or promote products derived from the Apple |
Software without specific prior written permission from Apple. Except |
as expressly stated in this notice, no other rights or licenses, express |
or implied, are granted by Apple herein, including but not limited to |
any patent rights that may be infringed by your derivative works or by |
other works in which the Apple Software may be incorporated. |
The Apple Software is provided by Apple on an "AS IS" basis. APPLE |
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION |
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS |
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND |
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. |
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL |
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, |
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED |
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), |
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE |
POSSIBILITY OF SUCH DAMAGE. |
Copyright © 2006-2008 Apple Inc. All Rights Reserved. |
*/ |
#import "AudioExtractionWindowController.h" |
// Conditions for NSConditionLock |
#define AUDIO_SLICE_RENDERED 1 |
#define WAIT_FOR_AUDIO_SLICE_RENDER 2 |
// Maximum size, in frames, of the MovieAudioExtractionFillBuffer calls |
#define kMaxExtractionPacketCount (4096) |
NSString *QTAudioContextInsertMovieTracksChangedNotification = @"QTAudioContextInsertMovieTracksChanged"; |
// AUScheduledSoundPlayer callback called when a slice of scheduled audio has been played |
// Used during extraction preview |
static void previewAudioSliceCompletionProc(void *userData, |
struct ScheduledAudioSlice *bufferList); |
@implementation AudioExtractionController |
#pragma mark |
#pragma mark ---- Init, Dealloc, Post-Nib Loading --- |
- (id) init |
{ |
[super initWithWindowNibName:@"AudioExtractionWindow"]; |
if (self) |
{ |
mMovieDocument = [[NSDocumentController sharedDocumentController] currentDocument]; |
mClonedMovie = nil; |
[self setExtractionTime:nil isStartTime:YES isInit:YES]; |
[self setExtractionTime:nil isStartTime:NO isInit:YES]; |
mCommonChannelLabelOptions = [[NSMutableArray alloc] init]; |
mSummaryLayout = NULL; |
mExtractionLayout = NULL; |
mGraphUnit = nil; |
mExportFileID = nil; |
} |
return self; |
} |
- (void) dealloc |
{ |
[[NSNotificationCenter defaultCenter] removeObserver:self]; |
[mCommonChannelLabelOptions release]; |
if (mSummaryLayout) free(mSummaryLayout); |
if (mExtractionLayout) free(mExtractionLayout); |
[mCurrentExtractStartTime release]; |
[mCurrentExtractEndTime release]; |
[mClonedMovie release]; |
[super dealloc]; |
} |
- (void)awakeFromNib |
{ |
// Summary ASBD of the movie that |
// this window is associated with |
[self getSummaryASBD]; |
// UI set-up |
[self populateExtractChannelsSelectorPopUpButton]; |
[self refreshExtractionTableView]; |
[self setExtractionTime:nil isStartTime:YES isInit:YES]; |
[self setExtractionTime:nil isStartTime:NO isInit:YES]; |
} |
- (void)windowDidLoad |
{ |
[super windowDidLoad]; |
[[self window] setTitle:[NSString stringWithFormat:@"Audio Extraction Panel : %@", (NSString*)[[self movie] attributeForKey:QTMovieDisplayNameAttribute]]]; |
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowWillClose:) name:NSWindowWillCloseNotification object:nil]; |
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(movieTracksChanged:) name:QTAudioContextInsertMovieTracksChangedNotification object:nil]; |
} |
#pragma mark |
#pragma mark ---- IB actions ---- |
// Action method invoked when user selects an extraction |
// layout in the extraction layout pop-up button menu |
- (IBAction) doSelectExtractionChannelLayout:(id)sender |
{ |
int newTag; |
int index; |
OSStatus err = noErr; |
if ([mMovieDocument movie] == nil) |
{ |
return; |
} |
// The tag is used to identify the AudioChannelLayout selected from |
// amongst the preset options available in the extraction layout |
// pop-up button's menu |
newTag = [[uiExtractionLayoutSelectorPopUpButton selectedItem] tag]; |
// Tag of -1 indicates a custom layout (which doesn't need to do anything). |
if (newTag == -1) |
{ |
return; |
} |
// If we have a cached extraction layout, toss it and make a new one. |
if (mExtractionLayout != NULL) |
{ |
free(mExtractionLayout); |
mExtractionLayout = NULL; |
} |
switch (newTag) |
{ |
// Tag of 0 indicates the default (summary) layout |
case 0: |
err = getDefaultExtractionLayout([[mMovieDocument movie] quickTimeMovie], nil, &mExtractionLayout, nil); |
break; |
// Special value indicates All Channels Discrete extraction (no mixing) |
case kAudioChannelLayoutTag_DiscreteInOrder: |
err = getDiscreteExtractionLayout([[mMovieDocument movie] quickTimeMovie], nil, &mExtractionLayout); |
break; |
// Expand the layout tag associated with the menu item into a channel layout with descriptions |
default: |
err = getLayoutForTag((AudioChannelLayoutTag)newTag, nil, &mExtractionLayout); |
break; |
} |
if (err) |
{ |
return; |
} |
// If there is a Custom item in the popup, remove it now, |
// since we have just selected a layout preset. |
index = [uiExtractionLayoutSelectorPopUpButton indexOfItemWithTitle:@"Custom"]; |
if (index != -1) |
{ |
// We found a Custom item |
// Remove the item and the separator preceding it. |
[[uiExtractionLayoutSelectorPopUpButton menu] removeItemAtIndex:index]; |
[[uiExtractionLayoutSelectorPopUpButton menu] removeItemAtIndex:index-1]; |
} |
// Since the extraction layout has changed, reload |
// the extraction channel layout table. |
[uiExtractionChannelLayoutTableView reloadData]; |
} |
// Action method invoked by the Preview Start button |
- (IBAction) doStartPreview:(id)sender |
{ |
// Stop playback of current movie, so we can hear the preview |
[[mMovieDocument movie] stop]; |
// If we are currently exporting, signal the export to stop. |
// We are allowed only one extraction session per movie. |
// If we are exporting, stop and wait for it to complete. |
if (mExportFileID != nil) |
{ |
mStopExport = true; |
do |
{ |
[[NSRunLoop currentRunLoop] runUntilDate:nil]; |
} while (mExportFileID != nil); |
} |
// Extract and play audio, on another thread if possible. |
// Else, fall back to playing on main thread. |
[self startPreview]; |
// change the button state |
[sender setTitle:@"Stop Preview"]; |
[sender setAction:@selector(doStopPreview:)]; |
} |
// Action method invoked by the Preview Stop button |
- (IBAction) doStopPreview:(id)sender |
{ |
// Set the stop flag. The rest of the cleanup will occur in previewCompletedNotification. |
mStopPreview = true; |
} |
// Notification for the Export Start button |
- (IBAction) doStartExport:(id)sender |
{ |
NSSavePanel *savePanel; |
savePanel = [NSSavePanel savePanel]; |
[savePanel setRequiredFileType:@"aiff"]; |
// If we are previewing, stop and wait for it to complete. |
// We are allowed only one extraction session per movie, |
// so can't preview and extract at the same time. |
if (mGraphUnit != nil) |
{ |
mStopPreview = true; |
do |
{ |
[[NSRunLoop currentRunLoop] runUntilDate:nil]; |
} while (mGraphUnit != nil); |
} |
// Open a save panel to get a target file specification. |
// startExport will be invoked when 'OK' is pressed. |
[savePanel beginSheetForDirectory:nil |
file:nil |
modalForWindow:[self window] |
modalDelegate:self |
didEndSelector:@selector(startExport: returnCode: contextInfo:) |
contextInfo:nil]; |
} |
// Action method for the Export Stop button |
- (IBAction) doStopExport:(id)sender |
{ |
// Set the stop flag. The rest of the cleanup will occur in exportCompletedNotification. |
mStopExport = true; |
} |
// Action method for text entry in the Start Time and End Time fields |
- (IBAction) doChangeExtractionTime:(id)sender |
{ |
if ([mMovieDocument movie] == nil) |
{ |
return; |
} |
QTTime startLimit; |
QTTime endLimit; |
QTTime theTime; |
// Set the valid ranges according to the field we are setting. |
if (sender == uiExtractionStartTimeTextField) |
{ |
// Start Time can range from 0 to the just before the current End Time |
startLimit = QTMakeTime(0L, (long) mSummaryASBD.mSampleRate); |
endLimit = [mCurrentExtractEndTime QTTimeValue]; |
endLimit.timeValue -= 1; |
} |
else // sender == uiExtractionEndTimeTextField |
{ |
// End Time can range from just after the current Start Time to the end of the movie |
startLimit = [mCurrentExtractStartTime QTTimeValue]; |
startLimit.timeValue += 1; |
endLimit = [[[mMovieDocument movie] attributeForKey:QTMovieDurationAttribute] QTTimeValue]; |
} |
// Parse the user's string into a QTTime |
theTime = [self QTTimeFromString:[sender stringValue] timeScale:startLimit.timeScale]; |
// If the time is not in the valid range, reset the text item from the value. |
if ((QTTimeCompare(theTime, startLimit) == NSOrderedAscending) |
/* || (QTTimeCompare(theTime, endLimit) == NSOrderedDescending) */) |
{ |
if (sender == uiExtractionStartTimeTextField) |
{ |
// Set the Start Time back to where it was. |
[self setExtractionTime:mCurrentExtractStartTime isStartTime:YES isInit:NO]; |
} |
else if (sender == uiExtractionEndTimeTextField) |
{ // Re-init the End Time to the duration of the movie. |
[self setExtractionTime:mCurrentExtractEndTime isStartTime:NO isInit:YES]; |
} |
} |
else |
{ |
// Set the current field to the parsed value. |
[self setExtractionTime:[NSValue valueWithQTTime:theTime] |
isStartTime:(sender == uiExtractionStartTimeTextField) isInit:NO]; |
} |
} |
#pragma mark |
#pragma mark ---- Getter ---- |
- (QTMovie *)movie |
{ |
return [mMovieDocument movie]; |
} |
#pragma mark |
#pragma mark ---- Setters ---- |
// Set the extraction start and end time textfields, and update the cached start and end times |
// If isInit, a default start time of 0 and end time of duration is set |
// Else, the time is set to theTimeValue that is passed to this method |
- (void) setExtractionTime:(NSValue *)theTimeValue isStartTime:(BOOL)isStart isInit:(BOOL)isInit |
{ |
[uiExtractionEndTimeTextField setStringValue:@""]; |
if ([mMovieDocument movie] == nil) |
{ |
return; |
} |
// Set to default start and end times |
if (isInit) |
{ |
// start time text field |
if (isStart) |
{ |
QTTime startTime = QTMakeTime(0L, (long) mSummaryASBD.mSampleRate); |
[mCurrentExtractStartTime release]; |
mCurrentExtractStartTime = [[NSValue valueWithQTTime:startTime] retain]; |
[uiExtractionStartTimeTextField setStringValue:[self StringFromQTTime:startTime]]; |
} |
else |
{ // end time text field |
if ([[mMovieDocument movie] attributeForKey:QTMovieHasDurationAttribute]) |
{ |
QTTime endTime = [[[mMovieDocument movie] attributeForKey:QTMovieDurationAttribute] QTTimeValue]; |
[mCurrentExtractEndTime release]; |
mCurrentExtractEndTime = [[NSValue valueWithQTTime:endTime] retain]; |
[uiExtractionEndTimeTextField setStringValue:[self StringFromQTTime:endTime]]; |
} |
mUseExtractionEndTime = false; |
} |
} |
// Set to the time that is passed to this method |
else |
{ |
// start time text field |
if (isStart) |
{ |
if (theTimeValue == nil) |
{ |
return; |
} |
[theTimeValue retain]; |
[mCurrentExtractStartTime release]; |
mCurrentExtractStartTime = theTimeValue; |
[uiExtractionStartTimeTextField setStringValue:[self StringFromQTTime:[mCurrentExtractStartTime QTTimeValue]]]; |
} |
else |
{ // end time text field |
if (theTimeValue == nil) |
{ |
return; |
} |
[theTimeValue retain]; |
[mCurrentExtractEndTime release]; |
mCurrentExtractEndTime = theTimeValue; |
[uiExtractionEndTimeTextField setStringValue:[self StringFromQTTime:[mCurrentExtractEndTime QTTimeValue]]]; |
mUseExtractionEndTime = true; |
} |
} |
} |
// Make a string representation of the time |
// represented by a QTTime |
- (NSString*) StringFromQTTime:(QTTime) time |
{ |
NSString *timeString = nil; |
// construct the time string |
if ((time.flags & kQTTimeIsIndefinite) == 0) |
{ |
NSString* sign = @""; |
if (time.timeValue < 0) |
{ |
sign = @"-"; |
time.timeValue = -time.timeValue; |
} |
// calculate the time in days, hours, minutes, seconds |
long long divisor = 24LL * 60LL * 60LL * (long long)time.timeScale; |
long long days = time.timeValue / divisor; |
time.timeValue -= days * divisor; |
divisor /= 24; |
long long hours = time.timeValue / divisor; |
time.timeValue -= hours * divisor; |
divisor /= 60; |
long long minutes = time.timeValue / divisor; |
time.timeValue -= minutes * divisor; |
divisor /= 60; |
double seconds = (double) time.timeValue / (double) divisor; |
// note that we don't care how many digits the day & timescale is |
timeString = [NSString stringWithFormat:@"%@%lld:%02lld:%02lld:%02.2f", |
sign, days, hours, minutes, seconds]; |
} |
return timeString; |
} |
// Construct a QTTime representation of timeString |
- (QTTime) QTTimeFromString:(NSString*)timeString timeScale:(long)scale |
{ |
NSScanner *scanner; |
NSString *string; |
NSTimeInterval times[3] = {0}; |
double secs, tempsecs; |
QTTime rettime; |
// init |
scanner = [NSScanner scannerWithString:timeString]; |
[scanner setCharactersToBeSkipped:[NSCharacterSet characterSetWithCharactersInString:@":"]]; |
// parse the time (days:hr:min:sec.ttt) |
if ([scanner scanUpToString:@":" intoString:(&string)] && ![scanner isAtEnd]) |
{ |
times[0] = [string intValue]; |
if ([scanner scanUpToString:@":" intoString:(&string)] && ![scanner isAtEnd]) |
{ |
times[1] = [string intValue]; |
if ([scanner scanUpToString:@":" intoString:(&string)] && ![scanner isAtEnd]) |
{ |
times[2] = [string intValue]; |
times[1] *= 60; // hrs to minutes |
times[0] *= 60 * 24; // days to minutes |
times[0] += times[1] + times[2]; |
} |
else |
{ |
times[0] *= 60; // hrs to minutes |
times[0] += times[1]; |
} |
} |
} |
secs = (double) times[0] * 60.; // minutes to seconds |
// If we consumed everything, the last string was our sec.ttt segment, |
// so reset the scanner to parse just that. |
if ([scanner isAtEnd]) |
{ |
scanner = [NSScanner scannerWithString:string]; |
} |
if ([scanner scanDouble:&tempsecs]) |
{ |
secs += tempsecs; |
} |
rettime.timeValue = (long long)(secs * (double) scale); |
rettime.timeScale = scale; |
rettime.flags = 0; |
return rettime; |
} |
#pragma mark |
#pragma mark ---- UI refresh/population ---- |
// Populate the extraction layout pop-up button with a menu that |
// contains some commonly used AudioChannelLayout options as presets |
-(void) populateExtractChannelsSelectorPopUpButton |
{ |
[uiExtractionLayoutSelectorPopUpButton setAutoenablesItems:NO]; |
[uiExtractionLayoutSelectorPopUpButton removeAllItems]; |
if ([mMovieDocument movie] == nil) |
{ |
return; |
} |
[uiExtractionLayoutSelectorPopUpButton addItemWithTitle:@"Mono"]; |
[[uiExtractionLayoutSelectorPopUpButton lastItem] setTag:(int)kAudioChannelLayoutTag_Mono]; |
[uiExtractionLayoutSelectorPopUpButton addItemWithTitle:@"Stereo (L R)"]; |
[[uiExtractionLayoutSelectorPopUpButton lastItem] setTag:(int)kAudioChannelLayoutTag_Stereo]; |
[uiExtractionLayoutSelectorPopUpButton addItemWithTitle:@"Quadraphonic (L R Ls Rs)"]; |
[[uiExtractionLayoutSelectorPopUpButton lastItem] setTag:(int)kAudioChannelLayoutTag_Quadraphonic]; |
[uiExtractionLayoutSelectorPopUpButton addItemWithTitle:@"5.0 (L R C Ls Rs)"]; |
[[uiExtractionLayoutSelectorPopUpButton lastItem] setTag:(int)kAudioChannelLayoutTag_MPEG_5_0_A]; |
[uiExtractionLayoutSelectorPopUpButton addItemWithTitle:@"5.1 (L R C LFE Ls Rs)"]; |
[[uiExtractionLayoutSelectorPopUpButton lastItem] setTag:(int)kAudioChannelLayoutTag_MPEG_5_1_A]; |
[uiExtractionLayoutSelectorPopUpButton addItemWithTitle:@"7.1 (L R C LFE Ls Rs Lc Rc)"]; |
[[uiExtractionLayoutSelectorPopUpButton lastItem] setTag:(int)kAudioChannelLayoutTag_MPEG_7_1_A]; |
[uiExtractionLayoutSelectorPopUpButton addItemWithTitle:@"All Discrete"]; |
[[uiExtractionLayoutSelectorPopUpButton lastItem] setTag:(int)kAudioChannelLayoutTag_DiscreteInOrder]; |
// Use tag == 0 to flag the default (summary) layout. |
// We will use tag == -1 to flag a custom layout. |
[uiExtractionLayoutSelectorPopUpButton addItemWithTitle:@"Default"]; |
[[uiExtractionLayoutSelectorPopUpButton lastItem] setTag:(int)0]; |
[uiExtractionLayoutSelectorPopUpButton selectItem:[uiExtractionLayoutSelectorPopUpButton lastItem]]; |
} |
// Refresh the extraction layout table if the underlying channel layouts may have changed |
-(void) refreshExtractionTableView |
{ |
// Throw away the cached extraction layout and reload to refresh it. |
if (mExtractionLayout != nil) |
{ |
int tag; |
// Tag of 0 indicates the default (summary) layout |
// Tag of kAudioChannelLayoutTag_DiscreteInOrder indicates All Channels Discrete extraction |
tag = [[uiExtractionLayoutSelectorPopUpButton selectedItem] tag]; |
if ((tag == 0) || (tag == (int)kAudioChannelLayoutTag_DiscreteInOrder)) |
{ |
free(mExtractionLayout); |
mExtractionLayout = nil; |
} |
} |
if (mExtractionLayout == nil) |
{ |
// Refresh the panel if it is being displayed |
[uiExtractionChannelLayoutTableView reloadData]; |
} |
} |
// Get the summary ASBD of the current movie. |
-(void)getSummaryASBD |
{ |
if ([self movie] == nil) |
{ |
return; |
} |
(void) getMovieSummaryASBD([[self movie] quickTimeMovie], &mSummaryASBD); |
} |
// Create the channel labels array that is used to populate the |
// channel assignment options menu in the extraction layout table |
- (void)getCommonChannelLabelOptions |
{ |
// This tag gets us most of the labels that we are interested in |
AudioChannelLayoutTag tag = kAudioChannelLayoutTag_MPEG_7_1_A; |
UInt32 layoutSize = 0; |
AudioChannelLayout *layout = NULL; |
UInt32 channel = 0; |
// This array doesn't exist yet, so let's make it it will contain some commonly used channel labels |
if ([mCommonChannelLabelOptions count] == 0) |
{ |
if (noErr != AudioFormatGetPropertyInfo (kAudioFormatProperty_ChannelLayoutForTag, sizeof(AudioChannelLayoutTag), |
&tag, &layoutSize)) |
{ |
goto bail; |
} |
layout = (AudioChannelLayout*)malloc(layoutSize); |
if (noErr != AudioFormatGetProperty(kAudioFormatProperty_ChannelLayoutForTag, sizeof(AudioChannelLayoutTag), |
&tag, &layoutSize, layout)) |
{ |
goto bail; |
} |
if (noErr != expandChannelLayout(&layout, &layoutSize)) |
{ |
goto bail; |
} |
// Commonly used labels |
for (channel = 0 ; channel < layout->mNumberChannelDescriptions; channel++) |
{ |
AudioChannelLabel thisLabel = (AudioChannelLabel)(layout->mChannelDescriptions[channel]).mChannelLabel; |
[mCommonChannelLabelOptions addObject:[NSNumber numberWithInt:thisLabel]]; |
} |
// Center Surround |
[mCommonChannelLabelOptions addObject:[NSNumber numberWithInt:kAudioChannelLabel_CenterSurround]]; |
if (layout) |
{ |
free(layout); |
} |
} |
bail:; |
} |
// Fills in a menu item with the channel name correspoding to the AudioChannelLabel |
// specified. Also sets the menu item's tag to the AudioChannelLabel. |
- (void) fillInMenuItem:(NSMenuItem*)theMenuItem forChannelLabel:(AudioChannelLabel)theLabel |
{ |
AudioChannelDescription acd = {0}; |
NSString* channelLabelStr; |
UInt32 channelLabelStringSize = sizeof(NSString*); |
acd.mChannelLabel = theLabel; |
// Get the name for this channel |
if (noErr == AudioFormatGetProperty(kAudioFormatProperty_ChannelName, |
sizeof(AudioChannelDescription), |
&acd, |
&channelLabelStringSize, |
&channelLabelStr)) |
{ |
[theMenuItem setTitle:channelLabelStr]; // title = The channel name string |
[theMenuItem setTag:(int)acd.mChannelLabel]; // tag = AudioChannelLabel |
} |
[channelLabelStr release]; |
} |
#pragma mark |
#pragma mark ---- Extraction and Write To File ---------- |
// Fill in all the parameters necessary to configure an audio extraction |
// from the panel UI and cached values. |
-(OSStatus) getExtractionParameters:(AudioChannelLayout**)layout |
outLayoutSize:(UInt32*)layoutSize |
outASBD:(AudioStreamBasicDescription*)asbd |
startTime:(TimeRecord*)startTime |
duration:(Float64*)duration |
allDiscrete:(Boolean*)allDiscrete |
insertRegInfo:(InsertRegistryInfo*)regInfoRef |
{ |
OSStatus err = noErr; |
AudioChannelLayoutTag selectedLayoutTag; |
QTTime durationInQTTime; |
TimeRecord durationTimeRecord; |
// Set all the parameters to safe values |
// in the event that this method has to bail on an error |
*layout = nil; |
*layoutSize = 0; |
*duration = 0; // setting to 0 indicates that the entire movie is to be extracted |
*allDiscrete = NO; |
// Get the extraction layout that has been selected in the extraction layout pop-up, |
// just to check for All Channels Discrete. |
selectedLayoutTag = (AudioChannelLayoutTag)[[uiExtractionLayoutSelectorPopUpButton selectedItem] tag]; |
// Start Time |
(void) QTGetTimeRecord([mCurrentExtractStartTime QTTimeValue], startTime); |
// Duration |
// Just extract the entire file if the UI end time wasn't changed. |
if (mUseExtractionEndTime) { |
durationInQTTime = QTTimeDecrement([mCurrentExtractEndTime QTTimeValue], [mCurrentExtractStartTime QTTimeValue]); |
(void) QTGetTimeRecord(durationInQTTime, &durationTimeRecord); |
// Convert to floating-point seconds |
*duration = *((TimeValue64*) &durationTimeRecord.value) / (Float64) durationTimeRecord.scale; |
} |
// ASBD |
*asbd = mSummaryASBD; |
asbd->mChannelsPerFrame = mExtractionLayout->mNumberChannelDescriptions; |
// All channels discrete |
if (selectedLayoutTag == kAudioChannelLayoutTag_DiscreteInOrder) |
{ |
*allDiscrete = YES; |
} |
// Create an Audio Context insert processor for use during extraction |
// The registration information returned is set as a property on the extraction sesssion during MovieAudioExtraction |
// session configuration |
[[mMovieDocument acInsertManager] createProcessorForExtractionAndGetRegistrationInfo:(InsertRegistryInfo*)regInfoRef]; |
// Return a copy of the cached extraction layout, which should always be current |
*layoutSize = fieldOffset(AudioChannelLayout, mChannelDescriptions[mExtractionLayout->mNumberChannelDescriptions]); |
*layout = (AudioChannelLayout*) calloc(1, *layoutSize); |
if (*layout == nil) |
{ |
goto bail; |
} |
memcpy(*layout, mExtractionLayout, *layoutSize); |
bail: |
if (err) |
{ |
if (*layout) |
{ |
free(*layout); |
} |
if (regInfoRef) |
{ |
if (regInfoRef->theInsertRegistryInfo) |
{ |
free(regInfoRef->theInsertRegistryInfo); |
} |
} |
} |
return (err); |
} |
// This method checks if the export can be done on a worker thread. If so, it spawns a thread, |
// else it does the export on the main thread |
- (void) startExport:(NSSavePanel *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo |
{ |
AudioStreamBasicDescription asbd; |
AudioChannelLayout *layout = nil; |
UInt32 layoutSize = 0; |
TimeRecord startTime; |
Boolean discrete = NO; |
InsertRegistryInfo regInfo = { 0 }; |
Float64 duration = 0.; // extraction duration, in seconds |
Boolean extractionOnWorkerThread = NO; |
FSRef parentRef, fileRef; |
UInt32 fileType = kAudioFileAIFFType; |
NSString *directory = [[[NSString alloc] initWithString:[sheet directory]] retain]; |
NSString *fileName = [[[NSString alloc] initWithString:[[sheet filename] lastPathComponent ]] retain]; |
InfoForCallback *info = [[InfoForCallback alloc] init]; |
OSStatus err = noErr; |
if (returnCode == NSOKButton) |
{ |
// Close the Save As sheet |
[sheet close]; |
// Change the button state |
[uiExtractionExportButton setTitle:@"Stop Export"]; |
[uiExtractionExportButton setAction:@selector(doStopExport:)]; |
//------------------------------------------------------- |
// Clone the movie, test if clone can be migrated to a |
// worker thread for extraction, and set a flag accordingly |
mStopExport = false; // when this is set true, export stops on the next cycle |
mClonedMovie = [[mMovieDocument movie] copyWithZone:nil]; |
if (!mClonedMovie) |
{ |
err = memFullErr; |
goto bail; |
} |
if ([mClonedMovie detachFromCurrentThread]) |
{ |
extractionOnWorkerThread = YES; |
} |
else |
{ |
// If we could not migrate this movie, dispose the clone and |
// export from the original movie on the main thread. |
[mClonedMovie release]; |
mClonedMovie = nil; |
} |
// Read the UI, get the required extraction layout, layoutsize, |
// extraction startTime, duration and whether we need to |
// extract in the "All Channels Discrete" mode |
err = [self getExtractionParameters:(AudioChannelLayout**)&layout |
outLayoutSize:(UInt32*)&layoutSize |
outASBD:&asbd |
startTime:(TimeRecord*)&startTime |
duration:(Float64*)&duration |
allDiscrete:(Boolean*)&discrete |
insertRegInfo:(InsertRegistryInfo*)®Info]; |
if (err) |
{ |
goto bail; |
} |
// Set the output ASBD to 16-bit interleaved PCM big-endian integers |
asbd.mFormatID = kAudioFormatLinearPCM; |
asbd.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | |
kAudioFormatFlagIsBigEndian | |
kAudioFormatFlagIsPacked; |
asbd.mFramesPerPacket = 1; |
asbd.mBitsPerChannel = 16; |
asbd.mBytesPerFrame = 2 * asbd.mChannelsPerFrame; |
asbd.mBytesPerPacket = 2 * asbd.mChannelsPerFrame; |
//--------------------------------------------------------------- |
// Open the AIFF file, configure it for out extraction output layout |
// If the file we want to save to exists, delete it |
err = FSPathMakeRef((const UInt8*)[[sheet filename] fileSystemRepresentation], &fileRef, NULL); |
if (err == noErr) |
{ |
FSDeleteObject(&fileRef); |
} |
FSPathMakeRef((const UInt8*)[directory fileSystemRepresentation], &parentRef, NULL); |
err = AudioFileCreate(&parentRef, (CFStringRef)fileName, fileType, &asbd, 0, &fileRef, &mExportFileID); |
if (err) |
{ |
goto bail; |
} |
// If we do an All Channels Discrete extraction, create an unlabeled multi-channel file. |
// Otherwise, set the channel labels that we've specified for the extraction. |
if (!discrete) |
{ |
err = AudioFileSetProperty(mExportFileID, |
kAudioFilePropertyChannelLayout, |
layoutSize, |
(void*) layout); |
if (err) |
{ |
goto bail; |
} |
} |
//--------------------------------------------------------------------- |
// We are now ready to extract the audio and write to the AIFF file. |
// Package the extraction information in an object. |
// If we cannot migrate this job to the worker thread, do the extraction |
// on the main thread. Else spawn an extraction thread to do the extraction. |
QTTime startTimeInQTTime = QTMakeTimeWithTimeRecord(startTime); |
[info setASBD:asbd]; |
[info setDiscrete:discrete]; |
[info setLayout:layout]; |
[info setLayoutSize:layoutSize]; |
[info setStartTime:startTimeInQTTime]; |
[info setSamplesRemaining:duration ? (SInt64)(duration * asbd.mSampleRate) : -1]; |
[info setLocationInFile:0]; |
[info setInsertRegInfo:regInfo]; |
// If we can export on a worker thread, go do it |
if (extractionOnWorkerThread == YES) |
{ |
[NSThread detachNewThreadSelector:@selector(exportExtractionThread:) toTarget:self withObject:info]; |
} |
else |
{ |
// Otherwise, since we're on the main thread we can just call the main-thread worker method |
[self exportOnMainThreadCallBack:(id)info]; |
} |
} |
bail: |
[info release]; |
if (fileName) |
{ |
[fileName release]; |
} |
if (directory) |
{ |
[directory release]; |
} |
// If there was an error, we never spawned the worker thread to close the cloned movie |
if (err) |
{ |
if (layout) |
{ |
free(layout); |
} |
if (regInfo.theInsertRegistryInfo) |
{ |
free(regInfo.theInsertRegistryInfo); |
} |
if (extractionOnWorkerThread == YES) |
{ |
[mClonedMovie release]; |
mClonedMovie = nil; |
} |
if (mExportFileID) |
{ |
(void) AudioFileClose(mExportFileID); |
mExportFileID = nil; |
FSDeleteObject(&fileRef); |
} |
} |
} |
// Called when export is completed |
// Changes the button state |
- (void) exportCompletedNotification:(id)object |
{ |
// Set the Preview button back to its original state |
[uiExtractionExportButton setTitle:@"Export"]; |
[uiExtractionExportButton setAction:@selector(doStartExport:)]; |
} |
-(void) exportExtractionThread:(id)theObject |
{ |
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; |
OSStatus err = noErr; |
[NSThread setThreadPriority:[NSThread threadPriority]+.1]; |
// Unpack the information passed to this thread |
InfoForCallback *info = (InfoForCallback*) theObject; |
AudioStreamBasicDescription asbd = [info asbd]; |
AudioChannelLayout *layout = [info layout]; |
UInt32 layoutSize = [info layoutSize]; |
QTTime startTimeInQTTime = [info startTime]; |
SInt64 samplesRemaining; |
Boolean discrete = [info discrete]; |
SInt64 ploc = [info locationInFile]; |
TimeRecord startTime; |
MovieAudioExtractionRef extraction = nil; |
InsertRegistryInfo regInfo = [info insertRegInfo]; |
QTGetTimeRecord(startTimeInQTTime, &startTime); |
Boolean enterMoviesSucceeded=NO, attachedToThread = NO; |
// ----------------------------------------------------- |
// Attach the movie to this thread. |
[QTMovie enterQTKitOnThread]; |
enterMoviesSucceeded = YES; |
if (![mClonedMovie attachToCurrentThread]) |
{ |
goto bail; |
} |
attachedToThread = YES; |
// --------------------------------------------------------- |
// Prepare for extraction |
// Open a session, configure the session |
err = prepareMovieForExtraction([mClonedMovie quickTimeMovie], |
regInfo, |
&extraction, |
discrete, |
asbd, |
&layout, |
&layoutSize, |
startTime, |
&samplesRemaining); |
if ([info samplesRemaining] != -1) { |
samplesRemaining = [info samplesRemaining]; |
} |
if (layout) |
{ |
free(layout); |
} |
if (regInfo.theInsertRegistryInfo) |
{ |
free(regInfo.theInsertRegistryInfo); |
} |
if (err) |
{ |
goto bail; |
} |
//------------------------------------------------------------ |
// Start exporting slices and loop until everything is written out |
Boolean extractionComplete; |
extractionComplete = NO; |
// Loop until stopped from an external event, or we've finished the entire extraction |
while (!mStopExport && !extractionComplete) |
{ |
// If there are any samples left to export... |
if (samplesRemaining == 0) |
{ |
extractionComplete = YES; |
} |
if (!extractionComplete) |
{ |
// We will read numSamplesThisSlice number of samples |
SInt64 numSamplesThisSlice = samplesRemaining; |
if ((numSamplesThisSlice > kMaxExtractionPacketCount) || (numSamplesThisSlice == -1)) |
{ |
numSamplesThisSlice = kMaxExtractionPacketCount; |
} |
err = extractAudioToFile(extraction, mExportFileID, asbd, &numSamplesThisSlice, &ploc, &extractionComplete); |
if (err) |
{ |
break; |
} |
if (samplesRemaining != -1) |
{ |
samplesRemaining -= numSamplesThisSlice; |
} |
} |
} |
bail: |
(void) AudioFileClose(mExportFileID); |
mExportFileID = nil; |
if (extraction) |
{ |
(void) MovieAudioExtractionEnd(extraction); |
} |
[mClonedMovie release]; |
mClonedMovie = nil; |
if (enterMoviesSucceeded) |
{ |
[QTMovie exitQTKitOnThread]; |
} |
// Schedule the completion routine to execute on the main thread |
[self performSelectorOnMainThread:@selector(exportCompletedNotification:) |
withObject:(id)nil |
waitUntilDone:NO]; |
[pool release]; |
} |
// This callback is scheduled on the main thread. |
// In order to keep from locking up the UI, it does one slice of export, |
// writes it to file and then reschedule itself. |
-(void) exportOnMainThreadCallBack:(id)object |
{ |
OSStatus err = noErr; |
// Unpack the information passed to this method |
InfoForCallback *info = (InfoForCallback *) object; |
AudioStreamBasicDescription asbd = [info asbd]; |
AudioChannelLayout *layout = [info layout]; |
UInt32 layoutSize = [info layoutSize]; |
QTTime startTimeInQTTime = [info startTime]; |
SInt64 samplesRemaining; |
Boolean discrete = [info discrete]; |
SInt64 ploc = [info locationInFile]; |
TimeRecord startTime; |
MovieAudioExtractionRef extraction = [info extractionSessionRef]; |
InsertRegistryInfo regInfo = [info insertRegInfo]; |
QTGetTimeRecord(startTimeInQTTime, &startTime); |
// --------------------------------------------------------- |
// Prepare for extraction if this is the first entry |
if (extraction == nil) |
{ |
// Open a session, configure audio format |
err = prepareMovieForExtraction([[mMovieDocument movie] quickTimeMovie], |
regInfo, |
&extraction, |
discrete, |
asbd, |
&layout, |
&layoutSize, |
startTime, |
&samplesRemaining); |
if ([info samplesRemaining] != -1) { |
samplesRemaining = [info samplesRemaining]; |
} |
if (layout) |
{ |
free(layout); |
} |
if (regInfo.theInsertRegistryInfo) |
{ |
free(regInfo.theInsertRegistryInfo); |
} |
if (err) |
{ |
goto bail; |
} |
// Save for future entries |
[info setExtractionSession:extraction]; |
} |
// Export a slice now and then reschedule for another entry soon. |
Boolean extractionComplete; |
extractionComplete = NO; |
if (!mStopExport) |
{ |
// If there are any samples left to export... |
if (samplesRemaining == 0) |
{ |
extractionComplete = YES; |
} |
if (!extractionComplete) |
{ |
// We will read numSamplesThisSlice number of samples |
SInt64 numSamplesThisSlice = samplesRemaining; |
if ((numSamplesThisSlice > kMaxExtractionPacketCount) || (numSamplesThisSlice == -1)) |
{ |
numSamplesThisSlice = kMaxExtractionPacketCount; |
} |
err = extractAudioToFile(extraction, mExportFileID, asbd, &numSamplesThisSlice, &ploc, &extractionComplete); |
if (err) |
{ |
goto bail; |
} |
if (samplesRemaining != -1) |
{ |
samplesRemaining -= numSamplesThisSlice; |
if (samplesRemaining == 0) |
{ |
extractionComplete = YES; |
} |
} |
} |
} |
// Save updated state for the next time through this method |
[info setLocationInFile:ploc]; |
[info setSamplesRemaining:samplesRemaining]; |
bail: |
if (err || extractionComplete || mStopExport) |
{ |
err = AudioFileClose(mExportFileID); |
mExportFileID = nil; |
if (extraction) |
{ |
(void) MovieAudioExtractionEnd(extraction); |
} |
// Call the completion routine to reset the UI |
[self exportCompletedNotification:(id)nil]; |
} |
else |
{ |
// Reschedule to perform this routine again on the next run loop cycle |
[self performSelectorOnMainThread:@selector(exportOnMainThreadCallBack:) |
withObject:(id)info |
waitUntilDone:NO]; |
} |
} |
#pragma mark |
#pragma mark ---- Extraction Preview (Playback through CoreAudio) ------------- |
// Start a preview playback |
- (void) startPreview |
{ |
AudioStreamBasicDescription asbd; |
AudioChannelLayout *layout = nil; |
UInt32 layoutSize = 0; |
TimeRecord startTime; |
Boolean discrete = NO; |
Float64 duration = 0.; |
Boolean extractionOnWorkerThread = NO; |
OSStatus err = noErr; |
InsertRegistryInfo regInfo = { 0 }; |
InfoForCallback *info = [[InfoForCallback alloc] init]; |
// Clone the movie, test if clone can be migrated to a |
// worker thread for extraction and playback, and set a flag accordingly |
mStopPreview = false; // when this is set true, preview stops on the next cycle |
mClonedMovie = [[mMovieDocument movie] copyWithZone:nil]; |
if (!mClonedMovie) |
{ |
err = memFullErr; |
goto bail; |
} |
if ([mClonedMovie detachFromCurrentThread]) |
{ |
extractionOnWorkerThread = YES; |
} |
else |
{ |
// If we could not migrate this movie, dispose the clone and |
// extract from the original movie on the main thread. |
[mClonedMovie release]; |
mClonedMovie = nil; |
} |
// Read the UI, get the required extraction layout, layoutsize, |
// extraction startTime, duration and whether we need to |
// extract in the "All Channels Discrete" mode |
err = [self getExtractionParameters:(AudioChannelLayout**)&layout |
outLayoutSize:(UInt32*)&layoutSize |
outASBD:&asbd |
startTime:(TimeRecord*)&startTime |
duration:(Float64*)&duration |
allDiscrete:(Boolean*)&discrete |
insertRegInfo:(InsertRegistryInfo*)®Info]; |
if (err) |
{ |
goto bail; |
} |
// Build an AU Graph with a scheduled sound player unit and |
// an output unit for playback |
err = BuildAUGraphPlayer(layout, layoutSize, &asbd, &mGraphUnit, &mPlayerAudioUnit); |
if (err) |
{ |
goto bail; |
} |
// Package the refCon information that will be sent to the preview thread/timer |
QTTime startTimeInQTTime = QTMakeTimeWithTimeRecord(startTime); |
[info setASBD:asbd]; |
[info setDiscrete:discrete]; |
[info setLayout:layout]; |
[info setLayoutSize:layoutSize]; |
[info setStartTime:startTimeInQTTime]; |
[info setSamplesRemaining:duration ? (SInt64)(duration * asbd.mSampleRate) : -1]; |
[info setInsertRegInfo:regInfo]; |
// If we can preview on a worker thread, go do it |
if (extractionOnWorkerThread == YES) |
{ |
[NSThread detachNewThreadSelector:@selector(previewExtractionThread:) toTarget:self withObject:info]; |
} |
else |
{ |
// Otherwise, since we're on the main thread we can just call the main-thread worker method |
[self previewOnMainThreadCallBack:(id)info]; |
} |
bail: |
[info release]; |
// If there was an error, we never spawned the worker thread to close the cloned movie |
if (err) |
{ |
if (layout) |
{ |
free(layout); |
} |
if (regInfo.theInsertRegistryInfo) |
{ |
free(regInfo.theInsertRegistryInfo); |
} |
if (extractionOnWorkerThread == YES) |
{ |
[mClonedMovie release]; |
mClonedMovie = nil; |
} |
} |
} |
- (void) previewCompletedNotification:(id)object |
{ |
// Stop and dispose the playback AudioUnit Graph |
(void) CloseAUGraphPlayer(mGraphUnit); |
mGraphUnit = nil; |
// Set the Preview button back to its original state |
[uiExtractionPreviewButton setTitle:@"Preview"]; |
[uiExtractionPreviewButton setAction:@selector(doStartPreview:)]; |
} |
const UInt32 numSlices = 3; // number of slices we try to keep in the play queue |
// Slice Rendered Completion Callback |
void previewAudioSliceCompletionProc(void *userData, struct ScheduledAudioSlice *bufferList) |
{ |
id condLock = (NSConditionLock*)userData; |
// Signal the sleeping preview thread that a slice has been rendered. |
if (condLock != nil) |
{ |
[condLock lock]; |
[condLock unlockWithCondition:AUDIO_SLICE_RENDERED]; |
} |
} |
// This is the method that executes on a spawned thread to do the preview. |
- (void) previewExtractionThread:(id)theObject |
{ |
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; |
OSStatus err = noErr; |
//[NSThread setThreadPriority:[NSThread threadPriority]+.1]; |
id condLock = [[NSConditionLock alloc] initWithCondition:AUDIO_SLICE_RENDERED]; |
// Unpack the information passed to this thread |
InfoForCallback *info = (InfoForCallback*) theObject; |
AudioStreamBasicDescription asbd = [info asbd]; |
AudioChannelLayout *layout = [info layout]; |
UInt32 layoutSize = [info layoutSize]; |
QTTime startTimeInQTTime = [info startTime]; |
SInt64 samplesRemaining; |
Boolean discrete = [info discrete]; |
TimeRecord startTime; |
MovieAudioExtractionRef extraction = nil; |
Boolean extractionComplete = NO; |
UInt32 numSlicesFree = numSlices; |
ScheduledAudioSlice sliceList[numSlices]; |
Float64 sampleTimeStamp = 0.; |
Boolean playerUnitStarted = false; |
InsertRegistryInfo regInfo = [info insertRegInfo]; |
QTGetTimeRecord(startTimeInQTTime, &startTime); |
Boolean enterMoviesSucceeded=NO, attachedToThread = NO; |
// ----------------------------------------------------- |
// Attach the movie to this thread. |
[QTMovie enterQTKitOnThread]; |
enterMoviesSucceeded = YES; |
if (![mClonedMovie attachToCurrentThread]) |
{ |
goto bail; |
} |
attachedToThread = YES; |
// --------------------------------------------------------- |
// Prepare for extraction |
// Open a session, configure audio format |
err = prepareMovieForExtraction([mClonedMovie quickTimeMovie], |
regInfo, |
&extraction, |
discrete, |
asbd, |
&layout, |
&layoutSize, |
startTime, |
&samplesRemaining); |
if ([info samplesRemaining] != -1) { |
samplesRemaining = [info samplesRemaining]; |
} |
if (layout) |
{ |
free(layout); |
} |
if (regInfo.theInsertRegistryInfo) |
{ |
free(regInfo.theInsertRegistryInfo); |
} |
if (err) |
{ |
goto bail; |
} |
//----------------------------------------------------------- |
// Start scheduling slices and loop until everything is played out |
err = [self previewBufferAllocate:sliceList |
numSlices:numSlices |
asbd:asbd |
lock:(void *)condLock]; |
if (err) |
{ |
goto bail; |
} |
// Loop until stopped from an external event, or we've played out the entire extraction |
while (!mStopPreview && !(extractionComplete && (numSlicesFree == numSlices))) |
{ |
// The first time, this does not block. |
// Thereafter, it blocks until a completion callback has set it. |
// Once acquired, unlock it immediately, but clear the state. |
[condLock lockWhenCondition:AUDIO_SLICE_RENDERED]; |
[condLock unlockWithCondition:WAIT_FOR_AUDIO_SLICE_RENDER]; |
// Iterate through our slice list. |
// For each completed (or never used) slice, fill and schedule it. |
numSlicesFree = [self previewBufferScheduleSlices:sliceList |
numSlices:(UInt32)numSlices |
extractionSession:(MovieAudioExtractionRef)extraction |
asbd:(AudioStreamBasicDescription)asbd |
timeStamp:(Float64*)&sampleTimeStamp |
remaining:(SInt64*)&samplesRemaining |
complete:(Boolean*)&extractionComplete]; |
// If all the slices are free now, we didn't succeed in queueing any more |
if (numSlicesFree == numSlices) |
{ |
break; |
} |
// Start the AUGraph, it is wasn't already running. |
// Then the next time around the loop, we'll wait until the slice completion callback |
// has set the condition lock state. |
if (playerUnitStarted == false) |
{ |
// Start the AUGraph player |
err = StartAUGraphPlayer(mGraphUnit); |
if (err) |
{ |
goto bail; |
} |
playerUnitStarted = true; |
} |
} |
bail: |
if (extraction) |
{ |
(void) MovieAudioExtractionEnd(extraction); |
} |
(void) StopAUGraphPlayer(mGraphUnit); |
[self previewBufferDeallocate:sliceList numSlices:numSlices]; |
[mClonedMovie release]; |
mClonedMovie = nil; |
if (enterMoviesSucceeded) |
{ |
[QTMovie exitQTKitOnThread]; |
} |
// Schedule the completion routine to execute on the main thread |
[self performSelectorOnMainThread:@selector(previewCompletedNotification:) |
withObject:(id)nil |
waitUntilDone:NO]; |
[condLock release]; |
[pool release]; |
} |
// This callback is scheduled on the main thread. |
// In order to keep from locking up the UI, schedules what it can, and then reschedules itself. |
-(void) previewOnMainThreadCallBack:(id)object |
{ |
OSStatus err = noErr; |
// Unpack the information passed to this thread |
InfoForCallback *info = (InfoForCallback *) object; |
AudioStreamBasicDescription asbd = [info asbd]; |
AudioChannelLayout *layout = [info layout]; |
UInt32 layoutSize = [info layoutSize]; |
QTTime startTimeInQTTime = [info startTime]; |
SInt64 samplesRemaining; |
Boolean discrete = [info discrete]; |
TimeRecord startTime; |
Boolean playerUnitStarted = [info playerUnitStarted]; |
Float64 sampleTimeStamp = [info sampleTimeStamp]; |
ScheduledAudioSlice *sliceList = [info sliceList]; |
UInt32 numSlicesFree = 0; |
MovieAudioExtractionRef extraction = [info extractionSessionRef]; |
InsertRegistryInfo regInfo = [info insertRegInfo]; |
QTGetTimeRecord(startTimeInQTTime, &startTime); |
// --------------------------------------------------------- |
// Prepare for extraction if this is the first entry |
if (extraction == nil) |
{ |
// Open a session, configure audio format |
err = prepareMovieForExtraction([[mMovieDocument movie] quickTimeMovie], |
regInfo, |
&extraction, |
discrete, |
asbd, |
&layout, |
&layoutSize, |
startTime, |
&samplesRemaining); |
if ([info samplesRemaining] != -1) { |
samplesRemaining = [info samplesRemaining]; |
} |
if (layout) |
{ |
free(layout); |
} |
if (regInfo.theInsertRegistryInfo) |
{ |
free(regInfo.theInsertRegistryInfo); |
} |
if (err) |
{ |
goto bail; |
} |
// Save for future entries |
[info setExtractionSession:extraction]; |
} |
if (sliceList == nil) |
{ |
sliceList = (ScheduledAudioSlice *) calloc(numSlices, sizeof(ScheduledAudioSlice)); |
if (sliceList == nil) |
{ |
err = memFullErr; |
goto bail; |
} |
err = [self previewBufferAllocate:sliceList |
numSlices:numSlices |
asbd:asbd |
lock:(void *)nil]; |
if (err) |
{ |
goto bail; |
} |
[info setSliceList:sliceList]; |
} |
//----------------------------------------------------------- |
Boolean extractionComplete; |
extractionComplete = NO; |
if (!mStopPreview) |
{ |
// Iterate through our slice list. |
// For each completed (or never used) slice, fill and schedule it. |
numSlicesFree = [self previewBufferScheduleSlices:sliceList |
numSlices:(UInt32)numSlices |
extractionSession:(MovieAudioExtractionRef)extraction |
asbd:(AudioStreamBasicDescription)asbd |
timeStamp:(Float64*)&sampleTimeStamp |
remaining:(SInt64*)&samplesRemaining |
complete:(Boolean*)&extractionComplete]; |
} |
if (!playerUnitStarted) |
{ |
// Start the AUGraph player |
err = StartAUGraphPlayer(mGraphUnit); |
if (err) |
{ |
goto bail; |
} |
[info setPlayerUnitStarted:YES]; |
} |
// Save updated state for the next time through this method |
[info setSamplesRemaining:samplesRemaining]; |
[info setSampleTimeStamp:sampleTimeStamp]; |
// Set the complete flag if we could not queue anything, for what ever reason |
// (ie, we finished playing everything or we never queued anything). |
extractionComplete = (numSlicesFree == numSlices); |
bail: |
if (err || extractionComplete || mStopPreview) |
{ |
if (extraction) |
{ |
(void) MovieAudioExtractionEnd(extraction); |
} |
(void) StopAUGraphPlayer(mGraphUnit); |
if (sliceList != nil) |
{ |
[self previewBufferDeallocate:sliceList numSlices:numSlices]; |
free (sliceList); |
} |
// Call the completion routine to reset the UI |
[self previewCompletedNotification:(id)nil]; |
} |
else |
{ |
// Reschedule to perform this routine again on the next run loop cycle |
[self performSelectorOnMainThread:@selector(previewOnMainThreadCallBack:) |
withObject:(id)info |
waitUntilDone:NO]; |
} |
} |
// Allocate and initialize audio slice buffers for the AUScheduledSoundPlayer |
-(OSStatus) previewBufferAllocate:(ScheduledAudioSlice *)sliceList |
numSlices:(UInt32)numSlices |
asbd:(AudioStreamBasicDescription)asbd |
lock:(void *)condLock |
{ |
UInt32 sliceNumber; |
OSStatus err = noErr; |
bzero(sliceList, numSlices * sizeof(ScheduledAudioSlice)); |
for (sliceNumber = 0; sliceNumber < numSlices ; sliceNumber++) |
{ |
AudioBufferList *bufList = nil; |
UInt32 bufNumber = 0; |
UInt32 bufSize, listSize; |
UInt32 mallocSize; |
// Accumulate the size of all the memory objects we need for this slice. |
// Then allocate it and parcel it out. |
// Make sure all sizes are rounded up to Altivec boundaries; |
listSize = fieldOffset(AudioBufferList, mBuffers[asbd.mChannelsPerFrame]); |
listSize = ((listSize + 15) / 16) * 16; |
mallocSize = listSize; |
bufSize = kMaxExtractionPacketCount * asbd.mBytesPerPacket; |
bufSize = ((bufSize + 15) / 16) * 16; |
mallocSize += (bufSize * asbd.mChannelsPerFrame); |
bufList = (AudioBufferList*) calloc(1, mallocSize); |
if (bufList == nil) |
{ |
err = memFullErr; |
goto bail; |
} |
bufList->mNumberBuffers = asbd.mChannelsPerFrame; |
for (bufNumber = 0; bufNumber < bufList->mNumberBuffers; bufNumber++) |
{ |
bufList->mBuffers[bufNumber].mNumberChannels = 1; |
bufList->mBuffers[bufNumber].mData = (char *) bufList + listSize + (bufNumber * bufSize); |
bufList->mBuffers[bufNumber].mDataByteSize = bufSize; |
} |
sliceList[sliceNumber].mBufferList = bufList; |
sliceList[sliceNumber].mNumberFrames = kMaxExtractionPacketCount; |
sliceList[sliceNumber].mTimeStamp.mFlags = kAudioTimeStampSampleTimeValid; |
sliceList[sliceNumber].mCompletionProcUserData = condLock; |
sliceList[sliceNumber].mCompletionProc = (ScheduledAudioSliceCompletionProc) |
((condLock == nil) ? nil : previewAudioSliceCompletionProc); |
sliceList[sliceNumber].mFlags = kScheduledAudioSliceFlag_Complete; |
sliceList[sliceNumber].mReserved = 0; |
} |
bail: |
return (err); |
} |
// Free the audio slice buffers |
-(void) previewBufferDeallocate:(ScheduledAudioSlice *)sliceList |
numSlices:(UInt32)numSlices |
{ |
UInt32 sliceNumber; |
for (sliceNumber = 0; sliceNumber < numSlices ; sliceNumber++) |
{ |
if (sliceList[sliceNumber].mBufferList != nil) |
{ |
free (sliceList[sliceNumber].mBufferList); |
} |
} |
} |
// Fill and schedule the audio slice buffers. |
// Return the number of slices free once we're done. |
-(UInt32) previewBufferScheduleSlices:(ScheduledAudioSlice *)sliceList |
numSlices:(UInt32)numSlices |
extractionSession:(MovieAudioExtractionRef)extraction |
asbd:(AudioStreamBasicDescription)asbd |
timeStamp:(Float64*)ioSampleTimeStamp |
remaining:(SInt64*)ioSamplesRemaining |
complete:(Boolean*)outExtractionComplete |
{ |
UInt32 sliceNumber; |
UInt32 numSlicesFree = 0; |
OSStatus err; |
// Iterate through our slice list. |
// For each completed (or never used) slice, fill and schedule it. |
for (sliceNumber = 0; sliceNumber < numSlices ; sliceNumber++) |
{ |
if (sliceList[sliceNumber].mFlags & kScheduledAudioSliceFlag_Complete) |
{ |
// Count this slice as free until we fill it up |
numSlicesFree++; |
// If there are any samples left to extract... |
if (*ioSamplesRemaining == 0) |
{ |
*outExtractionComplete = true; |
} |
if (!*outExtractionComplete) |
{ |
// We will read numSamplesThisSlice number of samples |
SInt64 numSamplesThisSlice = *ioSamplesRemaining; |
if ((numSamplesThisSlice > kMaxExtractionPacketCount) || (numSamplesThisSlice == -1)) |
{ |
numSamplesThisSlice = kMaxExtractionPacketCount; |
} |
sliceList[sliceNumber].mTimeStamp.mSampleTime = *ioSampleTimeStamp; |
err = extractSliceAndScheduleToPlay(extraction, |
asbd, |
mPlayerAudioUnit, |
&sliceList[sliceNumber], |
&numSamplesThisSlice, |
outExtractionComplete); |
if (!err && (numSamplesThisSlice > 0)) |
{ |
numSlicesFree--; |
} else |
{ |
sliceList[sliceNumber].mFlags = kScheduledAudioSliceFlag_Complete; |
} |
// Whether there was an error or not, numSamplesThisSlice is valid |
*ioSampleTimeStamp += numSamplesThisSlice; |
if (*ioSamplesRemaining != -1) |
{ |
*ioSamplesRemaining -= numSamplesThisSlice; |
} |
} |
} |
} |
return (numSlicesFree); |
} |
#pragma mark |
#pragma mark ---- NSTableView DataSource Methods ---------- |
-(int) numberOfRowsInTableView:(NSTableView*)tableView |
{ |
OSStatus err; |
UInt32 numberOfRows = 0; |
if ([mMovieDocument movie]) |
{ |
// Extraction Layout table |
if ([tableView isEqual:uiExtractionChannelLayoutTableView]) |
{ |
// If we have a cached extraction layout, use it. |
// Otherwise, initialize and cache it here. |
if (mExtractionLayout == NULL) |
{ |
// Tag of -1 indicates a custom layout (which we won't have with a nil mExtractionLayout) |
// Tag of 0 indicates the default (summary) layout |
if ([[uiExtractionLayoutSelectorPopUpButton selectedItem] tag] == 0) |
{ |
err = getDefaultExtractionLayout([[mMovieDocument movie] quickTimeMovie], nil, &mExtractionLayout, nil); |
} |
// If we are doing an AllChannelsDiscrete extraction, construct an appropriate channel layout |
else if ((AudioChannelLayoutTag)[[uiExtractionLayoutSelectorPopUpButton selectedItem] tag] == |
kAudioChannelLayoutTag_DiscreteInOrder) |
{ |
err = getDiscreteExtractionLayout([[mMovieDocument movie] quickTimeMovie], nil, &mExtractionLayout); |
} |
// Otherwise, get the layout for the tag identified in the menu item |
else |
{ |
err = getLayoutForTag((AudioChannelLayoutTag)[[uiExtractionLayoutSelectorPopUpButton selectedItem] tag], |
nil, &mExtractionLayout); |
} |
if (err == noErr) |
{ |
numberOfRows = mExtractionLayout->mNumberChannelDescriptions; |
} |
} |
else |
{ |
numberOfRows = mExtractionLayout->mNumberChannelDescriptions; |
} |
} |
} |
return numberOfRows; |
} |
- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(int)row |
{ |
id objectValue = nil; |
NSString *column_id = [tableColumn identifier]; |
if ([tableView isEqual:uiExtractionChannelLayoutTableView]) |
{ |
if ([column_id isEqualToString:@"channel"]) |
{ |
objectValue = [NSNumber numberWithInt:row]; |
} |
else if ([column_id isEqualToString:@"assignment"]) |
{ |
// If we are in All Channels Discrete mode, you cannot select anything |
if ((AudioChannelLayoutTag)[[uiExtractionLayoutSelectorPopUpButton selectedItem] tag] == kAudioChannelLayoutTag_DiscreteInOrder) |
{ |
objectValue = (id)[NSNumber numberWithInt:0]; |
} |
else |
{ |
UInt32 index; |
// The channel selectors are always in the same order, with the custom value, if any, at the end |
for (index = 0; index < [mCommonChannelLabelOptions count]; index++) |
{ |
if (mExtractionLayout->mChannelDescriptions[row].mChannelLabel == |
(AudioChannelLabel)[[mCommonChannelLabelOptions objectAtIndex:index] intValue]) |
{ |
objectValue = [NSNumber numberWithInt:index]; |
break; |
} |
} |
// If we didn't find the label, then the last item (which index now reflects) is the right one. |
if (objectValue == nil) |
{ |
objectValue = [NSNumber numberWithInt:index]; |
} |
} |
} |
} |
return objectValue; |
} |
-(void) tableView:(NSTableView*)tableView setObjectValue:(id)value forTableColumn:(NSTableColumn*)tableColumn row:(int)row |
{ |
if ([tableView isEqual:uiExtractionChannelLayoutTableView]) |
{ |
int index; |
AudioChannelLabel channelLabel; |
// The extraction layout should never be nil here |
if (mExtractionLayout == nil) |
{ |
return; |
} |
// Get the channel label for this popup selector index. |
// If it is not in the table of names, it is the custom item. |
index = [(NSNumber*)value intValue]; |
if (index < (int)[mCommonChannelLabelOptions count]) |
{ |
channelLabel = (AudioChannelLabel)[[mCommonChannelLabelOptions objectAtIndex:index] intValue]; |
} |
else |
{ |
// This should never happen, since there is no callback when the selected item doesn't change. |
// Once a custom tag is set to one from the table, the custom menu item is removed. |
channelLabel = mExtractionLayout->mChannelDescriptions[row].mChannelLabel; |
} |
// If the channel label changed, then we make a new layout and associate it with a channel tag, if possible |
if (channelLabel != mExtractionLayout->mChannelDescriptions[row].mChannelLabel) |
{ |
AudioChannelLayout *newLayout = NULL; |
AudioChannelLayoutTag newTag; |
size_t size; |
Boolean foundMatch = false; |
// Make an updated layout and replace the cached copy |
size = fieldOffset(AudioChannelLayout, mChannelDescriptions[mExtractionLayout->mNumberChannelDescriptions]); |
newLayout = (AudioChannelLayout *) calloc(1, size); |
if (newLayout == NULL) |
{ |
return; |
} |
memcpy(newLayout, mExtractionLayout, size); |
newLayout->mChannelDescriptions[row].mChannelLabel = channelLabel; |
free(mExtractionLayout); |
mExtractionLayout = newLayout; |
// See if this new layout matches any of the top-level popup selector tags |
(void) getTagForLayout(mExtractionLayout, size, &newTag); |
if (newTag != kAudioChannelLayoutTag_UseChannelDescriptions) |
{ |
for (index = 0; index < [uiExtractionLayoutSelectorPopUpButton numberOfItems]; index++) |
{ |
// If the channel tag matches one in the top-level list, then set the top-level item to it. |
if (newTag == (AudioChannelLayoutTag)[[uiExtractionLayoutSelectorPopUpButton itemAtIndex:index] tag]) |
{ |
foundMatch = true; |
[uiExtractionLayoutSelectorPopUpButton selectItemAtIndex:index]; |
} |
} |
} |
// If we found a match, make sure "Custom" is no longer in the popup, otherwise make sure it is. |
index = [uiExtractionLayoutSelectorPopUpButton indexOfItemWithTitle:@"Custom"]; |
if (foundMatch) |
{ |
if (index != -1) |
{ |
// Remove the item and the separator preceding it. |
[[uiExtractionLayoutSelectorPopUpButton menu] removeItemAtIndex:index]; |
[[uiExtractionLayoutSelectorPopUpButton menu] removeItemAtIndex:index-1]; |
} |
} |
else |
{ |
if (index == -1) |
{ |
// Add a separating line and "Custom" menu item |
[[uiExtractionLayoutSelectorPopUpButton menu] addItem:[NSMenuItem separatorItem]]; |
[uiExtractionLayoutSelectorPopUpButton addItemWithTitle:@"Custom"]; |
[[uiExtractionLayoutSelectorPopUpButton lastItem] setTag:-1]; |
index = [uiExtractionLayoutSelectorPopUpButton indexOfItemWithTitle:@"Custom"]; |
} |
[uiExtractionLayoutSelectorPopUpButton selectItemAtIndex:index]; |
} |
} |
} |
} |
#pragma mark |
#pragma mark ---- NSTableColumn Delegate Method ---- |
// Delegate method for NSTableColumn subclass PopUpTableColumn |
- (id)dataCellForRow:(int)row forTable:(NSTableView*)tableView |
{ |
NSPopUpButtonCell *dataCell = [[NSPopUpButtonCell alloc] initTextCell:@"" pullsDown:NO]; |
NSMenu *menu = [[NSMenu alloc] init]; |
NSMenuItem *menuItem; |
int index; |
[dataCell setControlSize:NSMiniControlSize]; |
[dataCell setFont:[NSFont menuFontOfSize:10]]; |
// All Discrete is effectively immutable: we only populate the menu with the specific discrete channel label |
if ((AudioChannelLayoutTag)[[uiExtractionLayoutSelectorPopUpButton selectedItem] tag] == |
kAudioChannelLayoutTag_DiscreteInOrder) |
{ |
menuItem = [[NSMenuItem alloc] init]; |
[self fillInMenuItem:menuItem forChannelLabel:(mExtractionLayout->mChannelDescriptions[row].mChannelLabel)]; |
[menu addItem:menuItem]; |
[menuItem release]; |
[dataCell setEditable:NO]; // make sure setObjectValue never gets called |
} |
else |
{ |
// Populate the popup with a standard set of labels, |
// but make sure the current label name is in the list (at the end, if necessary). |
Boolean labelFound = false; |
// Add the commonly used labels to our menu |
[self getCommonChannelLabelOptions]; |
for (index = 0; index < (int)[mCommonChannelLabelOptions count]; index++) |
{ |
menuItem = [[NSMenuItem alloc] init]; |
[self fillInMenuItem:menuItem forChannelLabel:(AudioChannelLabel)[[mCommonChannelLabelOptions objectAtIndex:index] intValue]]; |
if (mExtractionLayout && |
(mExtractionLayout->mChannelDescriptions[row].mChannelLabel == (AudioChannelLabel)[[mCommonChannelLabelOptions objectAtIndex:index] intValue])) |
{ |
labelFound = true; |
} |
[menu addItem:menuItem]; |
[menuItem release]; |
} |
if (mExtractionLayout && !labelFound) |
{ |
// Make sure the current channel label is in the popup |
menuItem = [[NSMenuItem alloc] init]; |
[self fillInMenuItem:menuItem forChannelLabel:mExtractionLayout->mChannelDescriptions[row].mChannelLabel]; |
[menu addItem:menuItem]; |
[menuItem release]; |
} |
} |
[dataCell setMenu:menu]; |
[menu release]; |
return dataCell; |
} |
#pragma mark |
#pragma mark ---- Window Notifications ---- |
- (void) windowWillClose:(NSNotification*)notification |
{ |
NSWindowController *controller = [[notification object] windowController]; |
if ([controller isKindOfClass:[MovieWindowController class]]) |
{ |
// A movie window is being closed |
if ([controller window] == [[mMovieDocument movieWindowController] window]) |
{ |
// If that window belongs to the movie we're inspecting, |
// then close our own window |
[[self window] close]; |
} |
} |
// The extraction panel is being closed |
if ([controller window] == [self window]) |
{ |
// the extraction window is closing, |
// stop any ongoing preview |
[self doStopPreview:self]; |
} |
} |
// listen to the track changed callback, and refresh the extraction layout and table |
- (void)movieTracksChanged:(NSNotification *)notification |
{ |
QTMovie *changedMovie = (QTMovie*)[notification object]; |
if (changedMovie == [mMovieDocument movie]) |
{ |
[self getSummaryASBD]; |
[self refreshExtractionTableView]; |
} |
} |
@end |
Copyright © 2008 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2008-01-21