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.
MovieWriter.m
/* |
File: MovieWriter.m |
Author: QuickTime DTS |
Change History (most recent first): |
<3> 09/19/06 modified to perform audio extraction/conversion for QTExtractAndConvertToMovie |
<2> 03/24/06 must pass NSError objects to exportCompleted |
<1> 11/10/05 initial release as part of ExtractMovieAudioToAIFF |
© Copyright 2005 - 2006 Apple Computer, Inc. All rights reserved. |
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. |
*/ |
/* |
QTExtractAndConvertToMovie contains two Objective-C objects that are used together to |
implement audio extraction and audio conversion from a QuickTime Movie's sound track to |
an movie file. |
The first is a simple class called MovieWriter (this is a modified version of the |
AIFFWriter class that was included with the ExtractMovieAudioToAIFF and |
QTExtractAndConvertToAIFF samples). MovieWriter uses QuickTime's Movie Audio Extraction, |
Movie Storage and Media Creation APIs. The second is another simple class called |
AudioConverter which encapsulates Core Audio's Audio Converter API. |
The sample uses an instance of the MovieWriter class to easily set up audio extraction |
from a QTKit QTMovie to a new destination Movie and its associated movie file (or movie |
storage as it's called in the API). MovieWriter uses an instance of the AudioConverter |
class to perform the conversion to a user selected destination format which is |
configured by the Standard Audio Dialog Component. The Audio Converter uses a Movie |
Audio Extraction Session in the Read Input Procedure to pull audio out of the source |
Movie. |
QTExtractAndConvertToMovie demonstrates how to call AddMediaSample2 and supports VBR as |
well as CBR audio encoding formats. |
NOTE: This sample uses the default extraction channel layout which is the aggregate |
channel layout of the Movie (for example, all Rights mixed together, all Left Surrounds |
mixed together, etc). |
References: |
http://developer.apple.com/documentation/QuickTime/Conceptual/QT7UpdateGuide/Chapter02/chapter_2_section_6.html |
http://developer.apple.com/documentation/MusicAudio/Conceptual/CoreAudioOverview/WhatsinCoreAudio/chapter_3_section_4.html |
*/ |
#import "MovieWriter.h" |
#pragma mark ---- MovieWriterProgressInfo ---- |
// MovieWriterProgressInfo is the object passed back to the progress callback |
// if implemented by the client. It contains information regarding the current |
// state of the export session which can be used to drive a UI. |
@interface MovieWriterProgressInfo (Private) |
- (BOOL)continueOperation; |
- (void)setPhase:(MovieWriterExportOperationPhase)value; |
- (void)setProgressValue:(NSNumber *)value; |
- (void)setExportStatus:(NSError *)status; |
@end |
@implementation MovieWriterProgressInfo |
- (MovieWriterExportOperationPhase)phase |
{ |
return phase; |
} |
- (NSNumber *)progressValue |
{ |
return [[progressValue retain] autorelease]; |
} |
- (NSError *)exportStatus { |
return [[exportStatus retain] autorelease]; |
} |
- (BOOL)continueOperation; |
{ |
return continueOperation; |
} |
- (void)setContinueOperation:(BOOL)value |
{ |
continueOperation = value; |
} |
- (void)setPhase:(MovieWriterExportOperationPhase)value |
{ |
phase = value; |
} |
- (void)setProgressValue:(NSNumber *)value |
{ |
if (progressValue != value) { |
[progressValue release]; |
progressValue = [value copy]; |
} |
} |
- (void)setExportStatus:(NSError *)status |
{ |
if (exportStatus != status) { |
[exportStatus release]; |
exportStatus = [status copy]; |
} |
} |
- (void)dealloc |
{ |
[progressValue release]; |
[exportStatus release]; |
[super dealloc]; |
} |
@end |
#pragma mark ---- MovieWriter private interface ---- |
@interface MovieWriter (Private) |
- (void)exportOnMainThreadCallBack:(id)inObject; |
- (void)exportOnWorkerThread:(id)inObject; |
- (OSStatus)convertAudioToFile:(SInt64 *)ioNumFrames; |
- (OSStatus)configureExtractionSessionWithMovie:(Movie)inMovie; |
- (OSStatus)createOutputFile:(AudioStreamBasicDescription *)inOutputASBD; |
- (void)setMovieConversionDuration; |
- (void)exportCompletedNotification:(NSError *)inError; |
- (MovieAudioExtractionRef)audioExtractionSession; |
- (AudioBufferList *)extractionBufferList; |
- (UInt32)extractionBytesPerFrame; |
- (BOOL)extractionComplete; |
- (void)setExtractionComplete:(BOOL)flag; |
@end |
@implementation MovieWriter |
#pragma mark ---- Audio Converter InputDataProc Implementation ---- |
// Read Input Procedure used to supply data to the Audio Converter |
static OSStatus ReadInputProc(AudioConverterRef inAudioConverter, |
UInt32 *ioNumberDataPackets, |
AudioBufferList *ioData, |
AudioStreamPacketDescription **outDataPacketDescription, |
void *inUserData) |
{ |
#pragma unused(inAudioConverter, outDataPacketDescription) |
if (NULL == inUserData) return paramErr; |
MovieWriter *theMovieWriter = (MovieWriter *)inUserData; |
AudioBufferList *extractionBufferList; |
UInt32 i, flags; |
UInt32 numFrames = kMaxExtractionFrameCount; |
UInt32 bytesPerFrame; |
MovieAudioExtractionRef audioExtractionSession; |
OSStatus err = noErr; |
extractionBufferList = [theMovieWriter extractionBufferList]; |
bytesPerFrame = [theMovieWriter extractionBytesPerFrame]; |
audioExtractionSession = [theMovieWriter audioExtractionSession]; |
if (![theMovieWriter extractionComplete]) { |
for (i = 0; i < extractionBufferList->mNumberBuffers; i++) { |
extractionBufferList->mBuffers[i].mDataByteSize = numFrames * bytesPerFrame; |
} |
// read the number of requested samples from the movie |
// we're using movie audio extraction therefore we know we're supplying pcm so packet descriptions are unnecessary |
err = MovieAudioExtractionFillBuffer(audioExtractionSession, &numFrames, extractionBufferList, &flags); |
if (flags & kQTMovieAudioExtractionComplete) { |
[theMovieWriter setExtractionComplete:YES]; |
} |
*ioNumberDataPackets = numFrames; |
for (i = 0; i < extractionBufferList->mNumberBuffers; i++) { |
ioData->mBuffers[i] = extractionBufferList->mBuffers[i]; |
} |
} else { |
*ioNumberDataPackets = 0; |
} |
return err; |
} |
#pragma mark ---- initialization/dealocation ---- |
- (id)init; |
{ |
if (self = [super init]) { |
mLock = [[NSLock alloc] init]; |
mProgressInfo = [[MovieWriterProgressInfo alloc] init]; |
} |
return self; |
} |
- (void)dealloc |
{ |
if (mFileName) { |
[mFileName release]; |
} |
if (mAudioExtractionSession){ |
MovieAudioExtractionEnd(mAudioExtractionSession); |
} |
if (mMyAudioConverter) { |
[mMyAudioConverter release]; |
} |
if (mQTMovie) { |
[mQTMovie release]; |
} |
if (mSoundDescription) { |
DisposeHandle((Handle)mSoundDescription); |
mSoundDescription = NULL; |
} |
if (mExtractionBuffList) { |
free(mExtractionBuffList); |
} |
if (mExtractionBuffer) { |
free(mExtractionBuffer); |
} |
if (mOutputBufferList) { |
free(mOutputBufferList); |
} |
if (mOutputBuffer) { |
free(mOutputBuffer); |
} |
if (mPacketDescriptions) { |
free(mPacketDescriptions); |
} |
[mLock release]; |
[mProgressInfo release]; |
[super dealloc]; |
} |
#pragma mark ---- public ---- |
// main method call that will produce an output movie file - it will try to |
// export the source movie on a separate thread but if it can't will schedule |
// callbacks on the main thread |
- (OSStatus)exportFromMovie:(QTMovie *)inMovie toFile:(NSString *)inFullPath |
{ |
BOOL extractionOnWorkerThread = NO; |
BOOL continueExport = YES; |
Handle cloneHandle = NULL; |
FSRef fileRef; |
NSString *directory; |
OSStatus err = noErr; |
// sanity |
if (nil == inMovie || nil == inFullPath) return paramErr; |
// if we're busy already doing an export return |
if (![mLock tryLock]) return kObjectInUseErr; |
mIsConverting = YES; |
// if the client implemented a progress proc. call it now |
if (TRUE == mDelegateShouldContinueOp) { |
[mProgressInfo setPhase:MovieWriterExportBegin]; |
[mProgressInfo setProgressValue:nil]; |
[mProgressInfo setExportStatus:nil]; |
continueExport = [[self delegate] shouldContinueOperationWithProgressInfo:mProgressInfo]; |
if (NO == continueExport) goto bail; |
} |
directory = [inFullPath stringByDeletingLastPathComponent]; |
mFileName = [[NSString alloc] initWithString:[inFullPath lastPathComponent]]; |
// retain the QTMovie object passed in, we need it for the duration of |
// the export regardless of what the client decides to do with it |
mQTMovie = [inMovie retain]; |
// create a new Audio Converter object for this conversion operation |
mMyAudioConverter = [AudioConverter newAudioConverterWithMovie:[mQTMovie quickTimeMovie] status:&err]; |
if (NULL == mMyAudioConverter) goto bail; |
// set the Audio Converter Read Input Data Proc |
[mMyAudioConverter setInputDataProc:ReadInputProc]; |
// if the file already exists, delete it |
err = FSPathMakeRef((const UInt8*)[inFullPath fileSystemRepresentation], &fileRef, false); |
if (err == noErr) { |
err = FSDeleteObject(&fileRef); |
if (err) goto bail; |
} |
err = FSPathMakeRef((const UInt8*)[directory fileSystemRepresentation], &mParentRef, NULL); |
if (err) goto bail; |
// set the movies duration in floating-point seconds |
[self setMovieConversionDuration]; |
// clone the movie and see if we can migrate it to a worker thread for extraction |
cloneHandle = NewHandle(0); |
if (NULL == cloneHandle) { err = memFullErr; goto bail; } |
err = PutMovieIntoHandle([mQTMovie quickTimeMovie], cloneHandle); |
if (err) goto bail; |
err = NewMovieFromHandle(&mCloneMovie, cloneHandle, newMovieActive, NULL); |
if (err != noErr || mCloneMovie == NULL) goto bail; |
// if we couldn't migrate this movie, export from the movie on the main thread |
if (DetachMovieFromCurrentThread(mCloneMovie) == noErr) { |
extractionOnWorkerThread = YES; |
} else { |
DisposeMovie(mCloneMovie); |
mCloneMovie = NULL; |
} |
if (extractionOnWorkerThread == YES) { |
// export on a worker thread if we can... |
[NSThread detachNewThreadSelector:@selector(exportOnWorkerThread:) toTarget:self withObject:nil]; |
} else { |
// ...if not, we're on the main thread so just call the main-thread worker method |
[self exportOnMainThreadCallBack:nil]; |
} |
bail: |
if (cloneHandle) DisposeHandle(cloneHandle); |
if (err) [self exportCompletedNotification:[NSError errorWithDomain:NSOSStatusErrorDomain code:err userInfo:nil]]; |
return err; |
} |
- (BOOL)isConverting |
{ |
return mIsConverting; |
} |
#pragma mark ---- private ---- |
// 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)inObject |
{ |
#pragma unused(inObject) |
BOOL continueExport = YES; |
OSStatus err; |
// prepare for extraction if this is the first entry |
if (NULL == mAudioExtractionSession) { |
err = [self configureExtractionSessionWithMovie:[mQTMovie quickTimeMovie]]; |
if (err) goto bail; |
} |
// create the file |
// set the number of total samples to export |
if (0 == mOutputMovieStorageDH) { |
AudioStreamBasicDescription *outputASBD = [mMyAudioConverter outputFormat]; |
err = [self createOutputFile:outputASBD]; |
if (err) goto bail; |
mTotalNumberOfOutputFrames = mMovieDuration ? (mMovieDuration * outputASBD->mSampleRate) : -1; |
mOneSegmentOfOutputFrames = (0.50 * outputASBD->mSampleRate); |
if (-1 == mTotalNumberOfOutputFrames) { mConversionComplete = YES; err = paramErr; } |
} |
// perform conversion |
if (!mConversionComplete) { |
// if the client implemented a progress proc. call it now |
if (TRUE == mDelegateShouldContinueOp) { |
NSNumber *progressValue = [NSNumber numberWithFloat:(float)((float)mOutputFramesCompleted / (float)mTotalNumberOfOutputFrames)]; |
[mProgressInfo setPhase:MovieWriterExportPercent]; |
[mProgressInfo setProgressValue:progressValue]; |
[mProgressInfo setExportStatus:nil]; |
continueExport = [[self delegate] shouldContinueOperationWithProgressInfo:mProgressInfo]; |
if (NO == continueExport) { err = userCanceledErr; goto bail; } |
} |
// convert and write a half-second's worth of data |
SInt64 numFramesThisSlice = mOneSegmentOfOutputFrames; |
// extract and convert the audio and write it to the file |
err = [self convertAudioToFile:&numFramesThisSlice]; |
if (err) goto bail; |
mOutputFramesCompleted += numFramesThisSlice; |
} |
bail: |
if (err || mConversionComplete) { |
// we're done so close the file if there was no error or delete the file if we errored out |
if (mOutputMovieStorageDH) { |
if (noErr == err) { |
Track soundTrack = GetMediaTrack(mSoundMedia); |
err = EndMediaEdits(mSoundMedia); |
if (noErr == err) |
err = InsertMediaIntoTrack(soundTrack, 0, 0, GetMediaDuration(mSoundMedia), fixed1); |
if (noErr == err) |
err = AddMovieToStorage(mOutputMovie, mOutputMovieStorageDH); |
if (err) DataHDeleteFile(mOutputMovieStorageDH); |
} else { |
DataHDeleteFile(mOutputMovieStorageDH); |
} |
CloseMovieStorage(mOutputMovieStorageDH); |
mOutputMovieStorageDH = 0; |
if (mOutputMovie) { DisposeMovie(mOutputMovie); mOutputMovie = 0; } |
} |
// call the completion routine to clean up |
[self exportCompletedNotification:[NSError errorWithDomain:NSOSStatusErrorDomain code:err userInfo:nil]]; |
} else { |
// reschedule to perform this routine again on the next run loop cycle |
[self performSelectorOnMainThread:@selector(exportOnMainThreadCallBack:) |
withObject:(id)nil |
waitUntilDone:NO]; |
} |
} |
// this method will be performed on a background thread |
- (void)exportOnWorkerThread:(id)inObject |
{ |
#pragma unused(inObject) |
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; |
BOOL continueExport = YES; |
OSStatus err; |
[NSThread setThreadPriority:[NSThread threadPriority]+.1]; |
// attach the movie to this thread |
err = EnterMoviesOnThread(0); |
if (err) goto bail;; |
err = AttachMovieToCurrentThread(mCloneMovie); |
if (err) goto bail; |
// prepare extraction session |
if (NULL == mAudioExtractionSession) { |
err = [self configureExtractionSessionWithMovie:mCloneMovie]; |
if (err) goto done; |
} |
// create the file |
if (0 == mOutputMovieStorageDH) { |
AudioStreamBasicDescription *outputASBD = [mMyAudioConverter outputFormat]; |
err = [self createOutputFile:outputASBD]; |
if (err) goto bail; |
mTotalNumberOfOutputFrames = mMovieDuration ? (mMovieDuration * outputASBD->mSampleRate) : -1; |
mOneSegmentOfOutputFrames = (5.0 * outputASBD->mSampleRate); // five seconds of audio at a time |
if (-1 == mTotalNumberOfOutputFrames) { mConversionComplete = YES; err = paramErr; } |
} |
// loop until stopped from an external event, or finished the entire operation |
while (YES == continueExport && NO == mConversionComplete) { |
if (!mConversionComplete) { |
// if the client implemented a progress proc. call it now we wait for the |
// progress fuction to return before continuing so we can check the return code |
if (TRUE == mDelegateShouldContinueOp) { |
NSNumber *progressValue = [NSNumber numberWithFloat:(float)((float)mOutputFramesCompleted / (float)mTotalNumberOfOutputFrames)]; |
[mProgressInfo setPhase:MovieWriterExportPercent]; |
[mProgressInfo setProgressValue:progressValue]; |
[mProgressInfo setExportStatus:nil]; |
[[self delegate] performSelectorOnMainThread:@selector(shouldContinueOperationWithProgressInfo:) |
withObject:(id)mProgressInfo |
waitUntilDone:YES]; |
continueExport = [mProgressInfo continueOperation]; |
if (NO == continueExport) { err = userCanceledErr; break; } |
} |
// read numSamplesThisSlice number of samples |
SInt64 numFramesThisSlice = mOneSegmentOfOutputFrames; |
// extract and convert the audio and write it to the file |
err = [self convertAudioToFile:&numFramesThisSlice]; |
if (err) break; |
mOutputFramesCompleted += numFramesThisSlice; |
} |
} |
done: |
// detach the exported movie from this thread |
DetachMovieFromCurrentThread(mCloneMovie); |
ExitMoviesOnThread(); |
if (mOutputMovieStorageDH) { |
if (noErr == err) { |
Track soundTrack = GetMediaTrack(mSoundMedia); |
err = EndMediaEdits(mSoundMedia); |
if (noErr == err) |
err = InsertMediaIntoTrack(soundTrack, 0, 0, GetMediaDuration(mSoundMedia), fixed1); |
if (noErr == err) |
err = AddMovieToStorage(mOutputMovie, mOutputMovieStorageDH); |
if (err) DataHDeleteFile(mOutputMovieStorageDH); |
} else { |
DataHDeleteFile(mOutputMovieStorageDH); |
} |
CloseMovieStorage(mOutputMovieStorageDH); |
mOutputMovieStorageDH = 0; |
if (mOutputMovie) { DisposeMovie(mOutputMovie); mOutputMovie = 0; } |
} |
bail: |
// call the completion routine to clean up on the main thread |
[self performSelectorOnMainThread:@selector(exportCompletedNotification:) |
withObject:(id)[NSError errorWithDomain:NSOSStatusErrorDomain code:err userInfo:nil] |
waitUntilDone:YES]; |
[pool release]; |
} |
// convert a slice of PCM audio and write it to a movie file - we proceed serially |
// from the last position, 'ploc' specifies the file offset that the converted buffer should be written to |
- (OSStatus)convertAudioToFile:(SInt64 *)ioNumFrames |
{ |
UInt32 i; |
UInt32 numFrames, numPackets; |
OSStatus err; |
AudioStreamBasicDescription *extractionASBD = [mMyAudioConverter inputFormat]; |
AudioStreamBasicDescription *outputASBD = [mMyAudioConverter outputFormat]; |
// the AudioConverter doesn't seem to like to emit one packet at a time for QDesign/QualComm, |
// so make sure there are at least 2 packets per buffer |
numFrames = *ioNumFrames; |
numPackets = 2 + (numFrames / outputASBD->mFramesPerPacket); |
/* First time though we'll need to set up the Extraction Buffer and BufferList |
This is the buffer Movie Audio Extraction is writing into, in other words the source |
buffers for the Audio Converter |
We're always pulling non-interleaved buffers of LPCM from Movie Audio Extraction |
When working with non-interleaved audio, you need to allocate n buffers of bufferSize, |
and assign each mData to one of them |
NOTE - The extraction pull size will always be 4096 is this sample (kMaxExtractionFrameCount) |
*/ |
if (NULL == mExtractionBuffList) { |
mExtractionBufferSize = (kMaxExtractionFrameCount * extractionASBD->mBytesPerFrame); |
mExtractionBuffer = (char *)malloc(mExtractionBufferSize * extractionASBD->mChannelsPerFrame); |
if (NULL == mExtractionBuffer) { |
err = memFullErr; |
goto bail; |
} |
mExtractionBuffList = (AudioBufferList *)calloc(1, offsetof(AudioBufferList, mBuffers[extractionASBD->mChannelsPerFrame])); |
if (NULL == mExtractionBuffList) { |
err = memFullErr; |
goto bail; |
} |
mExtractionBuffList->mNumberBuffers = extractionASBD->mChannelsPerFrame; |
for (i = 0; i < extractionASBD->mChannelsPerFrame; i++) { |
mExtractionBuffList->mBuffers[i].mNumberChannels = 1; |
mExtractionBuffList->mBuffers[i].mDataByteSize = mExtractionBufferSize; |
mExtractionBuffList->mBuffers[i].mData = &mExtractionBuffer[i * mExtractionBufferSize]; |
} |
} |
/* First time though we'll need to set up the Output BufferList |
We're always converting to interleaved data since we're writing to a file |
so one output buffer is all we need |
*/ |
if (NULL == mOutputBufferList) { |
// calculate the output buffer size |
if (0 == outputASBD->mBytesPerPacket) { |
// if bytes per packet value is 0 in the asbd, we will need to use the |
// largest possible packet size - this information is available from the audio converter |
UInt32 propertySize = sizeof(UInt32); |
[mMyAudioConverter getProperty:kAudioConverterPropertyMaximumOutputPacketSize: &propertySize: &mMaxPacketSize]; |
mOutputBufferSize = (numPackets * mMaxPacketSize); |
} else { |
mOutputBufferSize = (numPackets * outputASBD->mBytesPerPacket); |
} |
mOutputBuffer = (char *)malloc(mOutputBufferSize); |
if (NULL == mOutputBuffer) { |
err = memFullErr; |
goto bail; |
} |
mOutputBufferList = (AudioBufferList *)calloc(1, sizeof(AudioBufferList)); |
if (NULL == mOutputBufferList) { |
err = memFullErr; |
goto bail; |
} |
// externally framed formats requires packet descriptions |
if ([mMyAudioConverter formatRequiresPacketDescriptions]) { |
mPacketDescriptions = (AudioStreamPacketDescription*)calloc(numPackets, sizeof(AudioStreamPacketDescription)); |
if (NULL == mPacketDescriptions) { |
err = memFullErr; |
goto bail; |
} |
} |
// tell out custom audio converter class about the output buffer, that's where it's going to put the converted audio |
[mMyAudioConverter setOutputAudioBufferList:mOutputBufferList]; |
} |
// make sure we don't try to convert more than we allocated room for |
if (0 == outputASBD->mBytesPerPacket) { |
if (numPackets > (mOutputBufferSize / mMaxPacketSize)) { |
numPackets = mOutputBufferSize / mMaxPacketSize; |
} |
} else { |
if (numPackets > (mOutputBufferSize / outputASBD->mBytesPerPacket)) { |
numPackets = mOutputBufferSize / outputASBD->mBytesPerPacket; |
} |
} |
// set up the buffer list each time through |
mOutputBufferList->mNumberBuffers = 1; |
mOutputBufferList->mBuffers[0].mNumberChannels = outputASBD->mChannelsPerFrame; |
mOutputBufferList->mBuffers[0].mDataByteSize = mOutputBufferSize; |
mOutputBufferList->mBuffers[0].mData = mOutputBuffer; |
err = [mMyAudioConverter convert:&numPackets :mPacketDescriptions :self]; |
if (err) goto bail; |
numFrames = numPackets * outputASBD->mFramesPerPacket; |
// we have completed the conversion when AudioConverterFillComplexBuffer returns zero packets |
mConversionComplete = (numPackets == 0); |
// write it to the movie file |
if (numPackets > 0) { |
// if the format requires packet descriptions, we need to add them one at a time |
// packet descriptions describe the packet layout of a buffer of data where the size of |
// each packet may not be the same or where there is extraneous data between packets |
if (mPacketDescriptions) { |
for (i = 0; i < numPackets; i++) { |
err = AddMediaSample2(mSoundMedia, // the Media |
mOutputBufferList->mBuffers[0].mData + mPacketDescriptions[i].mStartOffset, // const UInt8 * dataIn |
mPacketDescriptions[i].mDataByteSize, // ByteCount size |
(mPacketDescriptions[i].mVariableFramesInPacket ? mPacketDescriptions[i].mVariableFramesInPacket : outputASBD->mFramesPerPacket), // TimeValue64 decodeDurationPerSample |
0, // TimeValue64 displayOffset |
(SampleDescriptionHandle)mSoundDescription, // SampleDescriptionHandle sampleDescriptionH |
1, // ItemCount numberOfSamples |
0, // MediaSampleFlags sampleFlags |
NULL); // TimeValue64 * sampleDecodeTimeOut |
if (err) goto bail; |
} |
} else { |
// numSamples should be the same as numFrames calculated above |
UInt32 numSamples = (mOutputBufferList->mBuffers[0].mDataByteSize / outputASBD->mBytesPerPacket) * outputASBD->mFramesPerPacket; |
// make believe the frames in each packet are individually accessible (decodeDurationPerSample == 1) |
// this is commonly known as "THE LIE" and while it is the truth for PCM it is a lie for all CBR compressed formats |
err = AddMediaSample2(mSoundMedia, // the Media |
mOutputBufferList->mBuffers[0].mData, // const UInt8 * dataIn |
mOutputBufferList->mBuffers[0].mDataByteSize, // ByteCount size |
1, // TimeValue64 decodeDurationPerSample |
0, // TimeValue64 displayOffset |
(SampleDescriptionHandle)mSoundDescription, // SampleDescriptionHandle sampleDescriptionH |
numSamples, // ItemCount numberOfSamples |
0, // MediaSampleFlags sampleFlags |
NULL); // TimeValue64 * sampleDecodeTimeOut |
if (err) goto bail; |
} |
mLocationInFile += numPackets; |
} |
bail: |
if (err) numFrames = 0; |
*ioNumFrames = numFrames; |
return err; |
} |
// this method prepare the specified movie for extraction by opening an extraction session, configuring |
// and setting the output ASBD and the output layout if one exists - it also sets the start time to 0 |
// and calculates the total number of samples to export |
- (OSStatus) configureExtractionSessionWithMovie:(Movie)inMovie |
{ |
OSStatus err; |
// open a movie audio extraction session |
err = MovieAudioExtractionBegin(inMovie, 0, &mAudioExtractionSession); |
if (err) goto bail; |
// set the extraction ASBD |
err = MovieAudioExtractionSetProperty(mAudioExtractionSession, |
kQTPropertyClass_MovieAudioExtraction_Audio, |
kQTMovieAudioExtractionAudioPropertyID_AudioStreamBasicDescription, |
sizeof(AudioStreamBasicDescription), |
[mMyAudioConverter inputFormat]); |
if (err) goto bail; |
// set the extraction channel layout |
err = MovieAudioExtractionSetProperty(mAudioExtractionSession, |
kQTPropertyClass_MovieAudioExtraction_Audio, |
kQTMovieAudioExtractionAudioPropertyID_AudioChannelLayout, |
[mMyAudioConverter channelLayoutSize], |
[mMyAudioConverter channelLayout]); |
if (err) goto bail; |
// set the chosen render quality - this is the same quality value set on the Audio Converter |
err = MovieAudioExtractionSetProperty(mAudioExtractionSession, |
kQTPropertyClass_MovieAudioExtraction_Audio, |
kQTMovieAudioExtractionAudioPropertyID_RenderQuality, |
sizeof(UInt32), [mMyAudioConverter renderQuality]); |
if (err) goto bail; |
// set the extraction start time - we always start at zero, but you don't have to |
// this is the default but we set it anyway for completeness |
TimeRecord startTime = { 0, 0, GetMovieTimeScale(inMovie), GetMovieTimeBase(inMovie) }; |
err = MovieAudioExtractionSetProperty(mAudioExtractionSession, |
kQTPropertyClass_MovieAudioExtraction_Movie, |
kQTMovieAudioExtractionMoviePropertyID_CurrentTime, |
sizeof(TimeRecord), &startTime); |
if (err) goto bail; |
bail: |
return err; |
} |
// create and configure the output file |
- (OSStatus)createOutputFile:(AudioStreamBasicDescription *)inOutputASBD |
{ |
Handle outputMovieDataRef; |
OSType outputMovieDataRefType; |
CFStringRef outputMovieFullPathString; |
OSStatus err; |
err = QTNewDataReferenceFromFSRefCFString(&mParentRef, (CFStringRef)mFileName, 0, &outputMovieDataRef, &outputMovieDataRefType); |
if (err) goto bail; |
// create a new movie file. |
// If you're using CreateMovieFile, consider switching to CreateMovieStorage, which is long-file-name aware and besides, FSSpecs are dead |
err = CreateMovieStorage(outputMovieDataRef, outputMovieDataRefType, 'TVOD', 0, createMovieFileDeleteCurFile, &mOutputMovieStorageDH, &mOutputMovie); |
if (err) goto bail; |
// remember a URL for the movie file so we can open it in QuickTime Player after export is complete |
err = QTGetDataReferenceFullPathCFString(outputMovieDataRef, outputMovieDataRefType, kQTPOSIXPathStyle, &outputMovieFullPathString); |
if (err) goto bail; |
mOutputMovieFileURL = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, outputMovieFullPathString, kCFURLPOSIXPathStyle, false); |
err = QTSoundDescriptionCreate([mMyAudioConverter outputFormat], |
[mMyAudioConverter channelLayout], [mMyAudioConverter channelLayoutSize], |
[mMyAudioConverter magicCookie], [mMyAudioConverter magicCookieSize], |
kQTSoundDescriptionKind_Movie_LowestPossibleVersion, &mSoundDescription); |
if (err) goto bail; |
// create the sound track |
Track track = NewMovieTrack(mOutputMovie, 0, 0, kFullVolume); |
if (err = GetMoviesError()) goto bail; |
mSoundMedia = NewTrackMedia(track, SoundMediaType, (TimeScale)inOutputASBD->mSampleRate, NULL, 0); |
if (err = GetMoviesError()) goto bail; |
err = BeginMediaEdits(mSoundMedia); |
bail: |
if (outputMovieDataRef) DisposeHandle(outputMovieDataRef); |
if (outputMovieFullPathString) CFRelease(outputMovieFullPathString); |
return err; |
} |
// calculate the duration of the longest audio track in the movie |
// if the audio tracks end at time N and the movie is much |
// longer we don't want to keep converting - the Movie Audio Extration |
// (the API we're using to provide source) will happily |
// return zeroes until it reaches the movie duration |
- (void)setMovieConversionDuration |
{ |
TimeValue maxDuration = 0; |
UInt8 i; |
SInt32 trackCount = GetMovieTrackCount([mQTMovie quickTimeMovie]); |
if (trackCount) { |
for (i = 1; i < trackCount + 1; i++) { |
Track aTrack = GetMovieIndTrackType([mQTMovie quickTimeMovie], |
i, |
SoundMediaType, |
movieTrackMediaType); |
if (aTrack) { |
TimeValue aDuration = GetTrackDuration(aTrack); |
if (aDuration > maxDuration) maxDuration = aDuration; |
} |
} |
mMovieDuration = (Float64)maxDuration / (Float64)GetMovieTimeScale([mQTMovie quickTimeMovie]); |
} |
} |
// this completion method gets called at the end of the extraction session or may be called |
// earlier if an error has occured - its main purpose is to clean up the world so a MovieWriter |
// instance can be used over and over again |
// |
// in this particular case if we completed successfully we launch QTPlayer with the .mov file |
// and if an error occurs we pass it back to the client though the progress info object |
- (void) exportCompletedNotification:(NSError *)inError |
{ |
if (noErr == [inError code]) { |
NSWorkspace *ws = [NSWorkspace sharedWorkspace]; |
[ws openFile:[(NSURL *)mOutputMovieFileURL path] withApplication:@"QuickTime Player"]; |
} |
if (mFileName) { |
[mFileName release]; |
mFileName = nil; |
} |
if (mOutputMovieFileURL) { |
CFRelease(mOutputMovieFileURL); |
mOutputMovieFileURL = NULL; |
} |
if (mAudioExtractionSession){ |
MovieAudioExtractionEnd(mAudioExtractionSession); |
mAudioExtractionSession = NULL; |
} |
if (mMyAudioConverter) { |
[mMyAudioConverter release]; |
mMyAudioConverter = nil; |
} |
if (mQTMovie) { |
[mQTMovie release]; |
mQTMovie = nil; |
} |
mMovieDuration = 0; |
if (mCloneMovie) { |
DisposeMovie(mCloneMovie); |
mCloneMovie = NULL; |
} |
mExtractionComplete = NO; |
mConversionComplete = NO; |
mIsConverting = NO; |
mLocationInFile = 0; |
mOutputFramesCompleted = 0; |
mTotalNumberOfOutputFrames = 0; |
mOneSegmentOfOutputFrames = 0; |
if (mOutputMovieStorageDH) { |
CloseComponent(mOutputMovieStorageDH); |
mOutputMovieStorageDH = 0; |
} |
mSoundMedia = 0; |
if (mSoundDescription) { |
DisposeHandle((Handle)mSoundDescription); |
mSoundDescription = NULL; |
} |
if (mExtractionBuffList) { |
free(mExtractionBuffList); |
mExtractionBuffList = NULL; |
} |
if (mExtractionBuffer) { |
free(mExtractionBuffer); |
mExtractionBuffer = NULL; |
mExtractionBufferSize = 0; |
} |
if (mOutputBufferList) { |
free(mOutputBufferList); |
mOutputBufferList = NULL; |
} |
if (mOutputBuffer) { |
free(mOutputBuffer); |
mOutputBuffer = NULL; |
mOutputBufferSize = 0; |
} |
if (mPacketDescriptions) { |
free(mPacketDescriptions); |
mPacketDescriptions = NULL; |
} |
[mLock unlock]; |
// if the client implemented a progress proc. call it now |
if (TRUE == mDelegateShouldContinueOp) { |
[mProgressInfo setPhase:MovieWriterExportEnd]; |
[mProgressInfo setProgressValue:nil]; |
[mProgressInfo setExportStatus:((noErr != [inError code]) && (userCanceledErr != [inError code]) ? inError : nil)]; |
[[self delegate] shouldContinueOperationWithProgressInfo:mProgressInfo]; |
} |
} |
- (MovieAudioExtractionRef)audioExtractionSession; |
{ |
return mAudioExtractionSession; |
} |
- (AudioBufferList *)extractionBufferList |
{ |
return mExtractionBuffList; |
} |
- (UInt32)extractionBytesPerFrame |
{ |
AudioStreamBasicDescription *extractionASBD = [mMyAudioConverter inputFormat]; |
return extractionASBD->mBytesPerFrame; |
} |
- (BOOL)extractionComplete |
{ |
return mExtractionComplete; |
} |
- (void)setExtractionComplete:(BOOL)flag |
{ |
mExtractionComplete = flag; |
} |
#pragma mark ---- delegate ---- |
// methods to support setting up a delegate of this class |
- (id)delegate |
{ |
return mDelegate; |
} |
- (void)setDelegate:(id)inDelegate |
{ |
mDelegate = inDelegate; |
mDelegateShouldContinueOp = [mDelegate respondsToSelector:@selector(shouldContinueOperationWithProgressInfo:)]; |
} |
@end |
Copyright © 2006 Apple Computer, Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2006-11-08