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.
ExtractAndPlay.c
/* |
File: ExtractAndPlay |
Description: Code to use the Audio Extraction APIs to extract audio out of QuickTime movies |
Author: Apple DTS |
Copyright: © Copyright 2005 Apple Computer, Inc. All rights reserved. |
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. |
Change History (most recent first): |
*/ |
#import <CoreAudio/CoreAudio.h> |
#include "ExtractAndPlay.h" |
#include "CoreAudioUtils.h" |
#include "MovieAudioExtractionUtils.h" |
#pragma mark ----- Typedefs ----- |
// NEW |
struct ExtractionInfoForCallback |
{ |
AudioStreamBasicDescription asbd; |
AudioChannelLayout *layout; |
UInt32 layoutSize; |
Boolean discrete; |
SInt64 samplesRemaining; |
Boolean playerUnitStarted; |
ScheduledAudioSlice *sliceList; |
Float64 sampleTimeStamp; |
MovieAudioExtractionRef extractionSessionRef; |
TimeRecord startTime; |
Float64 duration; |
}; |
typedef struct ExtractionInfoForCallback ExtractionInfoForCallback; |
static Movie mCurrentMovie = nil; |
static AUGraphPlayerRef mAUGraphUnit; |
static AudioUnit mPlayerUnit, mEffectUnit; |
static CFRunLoopSourceRef mRunLoopRef; |
static Boolean mStopPreview; |
static Boolean mUseEffectAU; |
static TimeRecord *mStartTimePtr; |
static TimeRecord mStartTime; |
#pragma mark ----- Constants ----- |
// number of slices we try to keep in the play queue |
const UInt32 kNumSlices = 30; |
// Maximum size, in frames, of the MovieAudioExtractionFillBuffer calls |
const UInt32 kMaxExtractionPacketCount = 4096; |
// flags used to update our UI (play button) |
const Boolean kExtractionStarted = false; |
const Boolean kExtractionStopped = true; |
#ifndef fieldOffset |
#define fieldOffset(type, field) ((size_t) &((type *) 0)->field) |
#endif |
#pragma mark ----- Prototypes ----- |
static const void *MyInfoRetainCallBack (const void *info); |
static void MyInfoReleaseCallBack (const void *info); |
static void MyRunLoopPerformCallBack (void *info); |
static void MyRunLoopScheduleCallBack (void *info, |
CFRunLoopRef rl, |
CFStringRef mode); |
static OSStatus ExtractSliceAndScheduleToPlay(MovieAudioExtractionRef extraction, |
AudioStreamBasicDescription asbd, |
AudioUnit playerUnit, |
ScheduledAudioSlice* slice, |
SInt64 *ioNumSamples, |
Boolean *extractionComplete); |
static UInt32 PreviewBufferScheduleSlices(ScheduledAudioSlice *sliceList, |
UInt32 numSlices, |
MovieAudioExtractionRef extraction, |
AudioStreamBasicDescription asbd, |
Float64 * ioSampleTimeStamp, |
SInt64* ioSamplesRemaining, |
Boolean * outExtractionComplete); |
static OSStatus PreviewBufferAllocate(ScheduledAudioSlice *sliceList, |
UInt32 numSlices, |
AudioStreamBasicDescription asbd, |
void * condLock); |
static void PreviewBufferDeallocate(ScheduledAudioSlice *sliceList, UInt32 numSlices); |
static void PreviewCompleted(); |
static void PreviewOnMainThreadCallBack(ExtractionInfoForCallback *info); |
static void StartPreview(); |
static OSStatus GetMovieExtractionParameters( Movie theMovie, |
AudioChannelLayout **layout, |
UInt32 *layoutSize, |
AudioStreamBasicDescription *asbd, |
TimeRecord *startTime, |
Float64 *duration, |
Boolean *allDiscrete); |
#pragma mark ----- Run Loop callbacks ----- |
// The callback used to add a retain for the source on |
// the info pointer for the life of the source, and may be |
// used for temporary references the source needs to take. |
// This callback returns the actual info pointer to store in |
// the source, almost always just the pointer passed as the |
// parameter. |
static const void *MyInfoRetainCallBack (const void *info) |
{ |
return (info); |
} |
// The callback used to remove a retain previously |
// added for the source on the info pointer. |
static void MyInfoReleaseCallBack (const void *info) |
{ |
free((void *)info); |
} |
// This callback is called when the source has been marked "signaled" with the |
// CFRunLoopSourceSignal() function, and should do whatever |
// handling is required for the source. |
static void MyRunLoopPerformCallBack (void *info) |
{ |
PreviewOnMainThreadCallBack((ExtractionInfoForCallback *)info); |
} |
// This callback is called whenever the source is added to a run loop mode. |
static void MyRunLoopScheduleCallBack ( void *info, |
CFRunLoopRef rl, |
CFStringRef mode) |
{ |
PreviewOnMainThreadCallBack(info); |
} |
#pragma mark ----- AUScheduledSoundPlayer ----- |
// Extract a slice of audio and schedule it for play in the AUScheduledSoundPlayer. |
// The ScheduledAudioSlice parameter contains the AudioBufferList to fill with |
// extracted audio data (which is always deinterleaved, since we're passing |
// it along to an AUGraph). 'playerUnit' specifies an AUScheduledSoundPlayer |
// instance to which the filled slice is queued. |
static OSStatus ExtractSliceAndScheduleToPlay(MovieAudioExtractionRef extraction, |
AudioStreamBasicDescription asbd, |
AudioUnit playerUnit, |
ScheduledAudioSlice* slice, |
SInt64 *ioNumSamples, |
Boolean *extractionComplete) |
{ |
OSStatus err = noErr; |
UInt32 bufIndex; |
UInt32 flags; |
UInt32 numFrames = *ioNumSamples; |
// Make sure the buffer list has all the buffer sizes reset. |
for (bufIndex = 0; bufIndex < slice->mBufferList->mNumberBuffers; bufIndex++) |
{ |
slice->mBufferList->mBuffers[bufIndex].mDataByteSize = *ioNumSamples * asbd.mBytesPerPacket; |
} |
// Extract into the slice's bufferlist |
err = MovieAudioExtractionFillBuffer(extraction, &numFrames, slice->mBufferList, &flags); |
*extractionComplete = (flags & kQTMovieAudioExtractionComplete); |
if (!err && (numFrames != 0)) |
{ |
// Fill in the slice frame count |
slice->mNumberFrames = numFrames; |
err = AudioUnitSetProperty(playerUnit, |
kAudioUnitProperty_ScheduleAudioSlice, |
kAudioUnitScope_Global, |
0, |
slice, |
sizeof(ScheduledAudioSlice)); |
require(err == noErr, bail); |
} |
bail: |
if (err) |
numFrames = 0; |
*ioNumSamples = numFrames; |
return err; |
} |
// Fill and schedule the audio slice buffers. |
// Return the number of slices free once we're done. |
static UInt32 PreviewBufferScheduleSlices(ScheduledAudioSlice *sliceList, |
UInt32 numSlices, |
MovieAudioExtractionRef extraction, |
AudioStreamBasicDescription asbd, |
Float64 * ioSampleTimeStamp, |
SInt64* ioSamplesRemaining, |
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, |
mPlayerUnit, |
&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); |
} |
// Allocate and initialize audio slice buffers for the AUScheduledSoundPlayer |
static OSStatus PreviewBufferAllocate(ScheduledAudioSlice *sliceList, |
UInt32 numSlices, |
AudioStreamBasicDescription asbd, |
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; // round to Altivec boundary |
mallocSize = listSize; |
bufSize = kMaxExtractionPacketCount * asbd.mBytesPerPacket; |
bufSize = ((bufSize + 15) / 16) * 16; // round to Altivec boundary |
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) (nil); |
sliceList[sliceNumber].mFlags = kScheduledAudioSliceFlag_Complete; |
sliceList[sliceNumber].mReserved = 0; |
} |
bail: |
return (err); |
} |
// Free the audio slice buffers |
static void PreviewBufferDeallocate(ScheduledAudioSlice *sliceList, UInt32 numSlices) |
{ |
UInt32 sliceNumber; |
for (sliceNumber = 0; sliceNumber < numSlices ; sliceNumber++) |
{ |
if (sliceList[sliceNumber].mBufferList != nil) |
{ |
free (sliceList[sliceNumber].mBufferList); |
} |
} |
} |
static void PreviewCompleted() |
{ |
// Stop and dispose the playback AudioUnit Graph |
CloseAUGraphPlayer(mAUGraphUnit); |
mAUGraphUnit = nil; |
// Set the Preview button back to its original state |
// Send custom event to our window to tell it status |
// of extraction so we can update our UI (button) elements |
SendExtractionStatusEventToWindow(kExtractionStopped); |
} |
// 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. |
static void PreviewOnMainThreadCallBack(ExtractionInfoForCallback *info) |
{ |
AudioStreamBasicDescription asbd = info->asbd; |
AudioChannelLayout *layout = info->layout; |
UInt32 layoutSize = info->layoutSize; |
TimeRecord startTime = info->startTime; |
SInt64 samplesRemaining = info->samplesRemaining; |
Boolean discrete = info->discrete; |
Boolean playerUnitStarted = info->playerUnitStarted; |
Float64 sampleTimeStamp = info->sampleTimeStamp; |
ScheduledAudioSlice *sliceList = info->sliceList; |
MovieAudioExtractionRef extraction = info->extractionSessionRef; |
OSStatus err = noErr; |
//----------------------------------------------------------- |
// Prepare for extraction if this is the first entry |
if (extraction == NULL) |
{ |
// Open a session, configure audio format |
err = PrepareMovieForExtraction(mCurrentMovie, |
&extraction, |
discrete, |
asbd, |
&layout, |
&layoutSize, |
startTime); |
if (layout) |
{ |
free(layout); |
} |
require(err == noErr, bail); |
// Save for future entries |
info->extractionSessionRef = extraction; |
} |
// Prepare audio "slice" list if this is the first time through |
if (sliceList == nil) |
{ |
sliceList = (ScheduledAudioSlice *) calloc(kNumSlices, sizeof(ScheduledAudioSlice)); |
if (sliceList == nil) |
{ |
err = memFullErr; |
goto bail; |
} |
err = PreviewBufferAllocate(sliceList, |
kNumSlices, |
asbd, |
(void *)nil); |
require(err == noErr, bail); |
info->sliceList = sliceList; |
} |
//----------------------------------------------------------- |
UInt32 numSlicesFree; |
Boolean extractionComplete = false; |
if (!mStopPreview) |
{ |
// Iterate through our slice list. |
// For each completed (or never used) slice, fill and schedule it. |
numSlicesFree = PreviewBufferScheduleSlices (sliceList, |
kNumSlices, |
extraction, |
asbd, |
&sampleTimeStamp, |
&samplesRemaining, |
&extractionComplete); |
} |
if (!playerUnitStarted) |
{ |
// Start the AUGraph player |
err = StartAUGraphPlayer(mAUGraphUnit); |
require(err == noErr, bail); |
info->playerUnitStarted = true; |
} |
// Save updated state for the next time through this method |
info->samplesRemaining = samplesRemaining; |
info->sampleTimeStamp = 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 == kNumSlices); |
bail: |
// check for: |
// - extraction errors |
// - extraction completed normally |
// - extraction was stopped by user |
if (err || extractionComplete || mStopPreview) |
{ |
if (extraction) |
{ |
MovieAudioExtractionEnd(extraction); |
} |
StopAUGraphPlayer(mAUGraphUnit); |
if (sliceList != nil) |
{ |
PreviewBufferDeallocate(sliceList,kNumSlices); |
free (sliceList); |
} |
if (mRunLoopRef != NULL) |
{ |
CFRunLoopSourceInvalidate (mRunLoopRef); |
CFRelease(mRunLoopRef); |
mRunLoopRef = NULL; |
} |
// Call the completion routine to reset the UI |
PreviewCompleted(); |
} |
else |
{ |
// Reschedule to perform this routine again on the next run loop cycle |
// Use CFRunLoopSource to schedule playing of slices on the main thread |
CFRunLoopSourceContext myContext; |
myContext.version = 0; |
myContext.info = info; |
myContext.retain = &MyInfoRetainCallBack; |
myContext.release = &MyInfoReleaseCallBack; |
myContext.copyDescription = nil; |
myContext.equal = nil; |
myContext.hash = nil; |
myContext.schedule = &MyRunLoopScheduleCallBack; |
myContext.cancel = nil; |
myContext.perform = &MyRunLoopPerformCallBack; |
// do CFRunLoopSource initialization if not already done |
if (mRunLoopRef == NULL) |
{ |
// Create a CFRunLoopSource object. |
mRunLoopRef = CFRunLoopSourceCreate (kCFAllocatorDefault, |
0, |
&myContext); |
if (mRunLoopRef) |
{ |
// add the CFRunLoopSource object to a run loop |
CFRunLoopAddSource (CFRunLoopGetCurrent(), |
mRunLoopRef, |
kCFRunLoopCommonModes); |
} |
} |
else |
{ |
// Signal the CFRunLoopSource, marking it as ready to fire |
CFRunLoopSourceSignal (mRunLoopRef); |
} |
} |
} |
#pragma mark ----- Audio Extraction ----- |
// Method invoked by the Preview Start button |
void DoStartMoviePreview(Movie inMovie, TimeRecord *inMovieStartTime, Boolean inUseEffect) |
{ |
// if we are given a different movie let's dispose the current one |
if (mCurrentMovie && (mCurrentMovie != inMovie)) |
{ |
DisposeMovie(mCurrentMovie); |
mCurrentMovie = NULL; |
} |
mCurrentMovie = inMovie; |
// add a Core Audio Effect Audio Unit to our playback? |
mUseEffectAU = inUseEffect; |
// If a movie start time is specified we'll use it, otherwise |
// use a default start time of 0 |
mStartTimePtr = nil; |
if (inMovieStartTime != nil) |
{ |
memcpy(&mStartTime, inMovieStartTime, sizeof(TimeRecord)); |
mStartTimePtr = &mStartTime; |
} |
// Extract and play audio (asynchronously) |
StartPreview(); |
// Send custom event to our window to tell it status |
// of extraction so we can update our UI (button) elements. |
SendExtractionStatusEventToWindow(kExtractionStarted); |
} |
// Start a preview playback |
static void StartPreview() |
{ |
OSStatus err = noErr; |
// when this is set true, preview stops on the next cycle |
mStopPreview = false; |
// initialize our run loop source reference value |
// this is used for our rendering async callbacks |
mRunLoopRef = NULL; |
AudioStreamBasicDescription asbd; |
AudioChannelLayout *layout = nil; |
UInt32 layoutSize = 0; |
Boolean discrete = false; |
Float64 duration = 0; |
TimeRecord startTime; |
// Get default values for extraction layout, layoutsize, |
// extraction startTime, duration and whether we need to |
// extract in the "All Channels Discrete" mode |
err = GetMovieExtractionParameters(mCurrentMovie, |
&layout, |
&layoutSize, |
&asbd, |
&startTime, |
&duration, |
&discrete); |
require(err == noErr, bail); |
// override default movie start time if another time value was specified |
if (mStartTimePtr != nil) |
{ |
// use the specified start time |
memcpy(&startTime, mStartTimePtr, sizeof(struct TimeRecord)); |
} |
// Build an AU Graph with a scheduled sound player unit and |
// an output unit for playback -- and insert an effect unit |
// in the chain if specified by the user |
if (mUseEffectAU) |
{ |
err = BuildAUGraphPlayerWithEffect(layout, layoutSize, &asbd, &mAUGraphUnit, &mPlayerUnit, &mEffectUnit); |
} |
else |
{ |
err = BuildAUGraphPlayer(layout, layoutSize, &asbd, &mAUGraphUnit, &mPlayerUnit); |
} |
require(err == noErr, bail); |
// refCon information that will be sent to the preview thread/timer |
ExtractionInfoForCallback *info = nil; |
info = malloc(sizeof (struct ExtractionInfoForCallback)); |
require(info != nil, bail); |
info->asbd = asbd; |
info->layout = layout; |
info->layoutSize = layoutSize; |
info->startTime = startTime; |
info->duration = duration; |
info->extractionSessionRef = NULL; |
info->discrete = false; |
info->sliceList = nil; |
info->playerUnitStarted = false; |
info->samplesRemaining = (duration > 0) ? (duration * asbd.mSampleRate) : -1; |
info->sampleTimeStamp = 0; |
// for now do all on main thread... |
PreviewOnMainThreadCallBack(info); |
bail: |
if (err) |
{ |
if (layout) |
{ |
free(layout); |
} |
} |
} |
// Method invoked by the Preview Stop button |
void DoStopMoviePreview() |
{ |
// Set the stop flag. The rest of the cleanup will occur in PreviewCompleted. |
mStopPreview = true; |
} |
// Fill in all the parameters necessary to configure an audio extraction |
// from the panel UI and cached values. |
static OSStatus GetMovieExtractionParameters( |
Movie theMovie, |
AudioChannelLayout **layout, |
UInt32 *layoutSize, |
AudioStreamBasicDescription *asbd, |
TimeRecord *startTime, |
Float64 *duration, |
Boolean *allDiscrete) |
{ |
OSStatus err = noErr; |
AudioChannelLayout *summaryLayout; |
// 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; // 0 indicates that the entire movie is to be extracted |
*allDiscrete = false; // true means do export of all audio channels without mixing |
// Start Time |
// by default we'll start at time 0 |
TimeRecord tr; |
tr.value.hi = 0; |
tr.value.lo = 0; |
tr.scale = GetMovieTimeScale(theMovie); |
tr.base = GetMovieTimeBase(theMovie); |
memcpy(startTime, &tr, sizeof(struct TimeRecord)); |
// Duration |
*duration = GetMovieExtractionDuration(theMovie) - GetMovieTime (theMovie, startTime); |
// ASBD |
err = GetDefaultExtractionLayout(theMovie, nil, &summaryLayout, asbd); |
// Return a copy of the cached extraction layout, which should always be current |
*layoutSize = fieldOffset(AudioChannelLayout, mChannelDescriptions[summaryLayout->mNumberChannelDescriptions]); |
*layout = (AudioChannelLayout*) calloc(1, *layoutSize); |
if (*layout == nil) |
{ |
err = memFullErr; |
goto bail; |
} |
memcpy(*layout, summaryLayout, *layoutSize); |
free (summaryLayout); |
bail: |
if (err) |
{ |
if (*layout) |
{ |
free(*layout); |
} |
} |
return (err); |
} |
Copyright © 2006 Apple Computer, Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2006-01-03