ExtendedAudioFileConvertTest/ExtendedAudioFileConvertOperation.m
/* |
Copyright (C) 2016 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
Demonstrates converting audio using ExtAudioFile. |
*/ |
#import "ExtendedAudioFileConvertOperation.h" |
@import Darwin; |
@import AVFoundation; |
#pragma mark- Convert |
// our own error code when we cannot continue from an interruption |
enum { |
kMyAudioConverterErr_CannotResumeFromInterruptionError = 'CANT' |
}; |
typedef NS_ENUM(NSInteger, AudioConverterState) { |
AudioConverterStateInitial, |
AudioConverterStateRunning, |
AudioConverterStatePaused, |
AudioConverterStateDone |
}; |
@interface ExtendedAudioFileConvertOperation () |
// MARK: Properties |
@property (nonatomic, strong) dispatch_queue_t queue; |
@property (nonatomic, strong) dispatch_semaphore_t semaphore; |
@property (nonatomic, assign) AudioConverterState state; |
@end |
@implementation ExtendedAudioFileConvertOperation |
// MARK: Initialization |
- (instancetype)initWithSourceURL:(NSURL *)sourceURL destinationURL:(NSURL *)destinationURL sampleRate:(Float64)sampleRate outputFormat:(AudioFormatID)outputFormat { |
if ((self = [super init])) { |
_sourceURL = sourceURL; |
_destinationURL = destinationURL; |
_sampleRate = sampleRate; |
_outputFormat = outputFormat; |
_state = AudioConverterStateInitial; |
_queue = dispatch_queue_create("com.example.apple-samplecode.ExtendedAudioFileConvertTest.ExtendedAudioFileConvertOperation.queue", DISPATCH_QUEUE_CONCURRENT); |
_semaphore = dispatch_semaphore_create(0); |
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAudioSessionInterruptionNotification:) name:AVAudioSessionInterruptionNotification object:[AVAudioSession sharedInstance]]; |
} |
return self; |
} |
- (void)dealloc { |
[[NSNotificationCenter defaultCenter] removeObserver:self name:AVAudioSessionInterruptionNotification object:[AVAudioSession sharedInstance]]; |
} |
- (void)main { |
[super main]; |
// This should never run on the main thread. |
assert(![NSThread isMainThread]); |
// Set the state to running. |
__weak __typeof__(self) weakSelf = self; |
dispatch_sync(self.queue, ^{ |
weakSelf.state = AudioConverterStateRunning; |
}); |
// Get the source files. |
ExtAudioFileRef sourceFile = 0; |
if (![self checkError:ExtAudioFileOpenURL((__bridge CFURLRef _Nonnull)(self.sourceURL), &sourceFile) withErrorString:[NSString stringWithFormat:@"ExtAudioFileOpenURL failed for sourceFile with URL: %@", self.sourceURL]]) { |
return; |
} |
// Get the source data format. |
AudioStreamBasicDescription sourceFormat = {}; |
UInt32 size = sizeof(sourceFormat); |
if (![self checkError:ExtAudioFileGetProperty(sourceFile, kExtAudioFileProperty_FileDataFormat, &size, &sourceFormat) withErrorString:@"ExtAudioFileGetProperty couldn't get the source data format"]) { |
return; |
} |
// Setup the output file format. |
AudioStreamBasicDescription destinationFormat = {}; |
destinationFormat.mSampleRate = (self.sampleRate == 0 ? sourceFormat.mSampleRate : self.sampleRate); |
if (self.outputFormat == kAudioFormatLinearPCM) { |
// If the output format is PCM, create a 16-bit file format description. |
destinationFormat.mFormatID = self.outputFormat; |
destinationFormat.mChannelsPerFrame = sourceFormat.mChannelsPerFrame; |
destinationFormat.mBitsPerChannel = 16; |
destinationFormat.mBytesPerPacket = destinationFormat.mBytesPerFrame = 2 * destinationFormat.mChannelsPerFrame; |
destinationFormat.mFramesPerPacket = 1; |
destinationFormat.mFormatFlags = kLinearPCMFormatFlagIsPacked | kLinearPCMFormatFlagIsSignedInteger; // little-endian |
} else { |
// This is a compressed format, need to set at least format, sample rate and channel fields for kAudioFormatProperty_FormatInfo. |
destinationFormat.mFormatID = self.outputFormat; |
// For iLBC, the number of channels must be 1. |
destinationFormat.mChannelsPerFrame = (self.outputFormat == kAudioFormatiLBC ? 1 : sourceFormat.mChannelsPerFrame); |
// Use AudioFormat API to fill out the rest of the description. |
size = sizeof(destinationFormat); |
if (![self checkError:AudioFormatGetProperty(kAudioFormatProperty_FormatInfo, 0, NULL, &size, &destinationFormat) withErrorString:@"AudioFormatGetProperty couldn't fill out the destination data format"]) { |
return; |
} |
} |
printf("Source file format:\n"); |
[ExtendedAudioFileConvertOperation printAudioStreamBasicDescription:sourceFormat]; |
printf("Destination file format:\n"); |
[ExtendedAudioFileConvertOperation printAudioStreamBasicDescription:destinationFormat]; |
// Create the destination audio file. |
ExtAudioFileRef destinationFile = 0; |
if (![self checkError:ExtAudioFileCreateWithURL((__bridge CFURLRef _Nonnull)(self.destinationURL), kAudioFileCAFType, &destinationFormat, NULL, kAudioFileFlags_EraseFile, &destinationFile) withErrorString:@"ExtAudioFileCreateWithURL failed!"]) { |
return; |
} |
/* |
set the client format - The format must be linear PCM (kAudioFormatLinearPCM) |
You must set this in order to encode or decode a non-PCM file data format |
You may set this on PCM files to specify the data format used in your calls to read/write |
*/ |
AudioStreamBasicDescription clientFormat; |
if (self.outputFormat == kAudioFormatLinearPCM) { |
clientFormat = destinationFormat; |
} else { |
clientFormat.mFormatID = kAudioFormatLinearPCM; |
UInt32 sampleSize = sizeof(SInt32); |
clientFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked; |
clientFormat.mBitsPerChannel = 8 * sampleSize; |
clientFormat.mChannelsPerFrame = sourceFormat.mChannelsPerFrame; |
clientFormat.mFramesPerPacket = 1; |
clientFormat.mBytesPerPacket = clientFormat.mBytesPerFrame = sourceFormat.mChannelsPerFrame * sampleSize; |
clientFormat.mSampleRate = sourceFormat.mSampleRate; |
} |
printf("Client file format:\n"); |
[ExtendedAudioFileConvertOperation printAudioStreamBasicDescription:clientFormat]; |
size = sizeof(clientFormat); |
if (![self checkError:ExtAudioFileSetProperty(sourceFile, kExtAudioFileProperty_ClientDataFormat, size, &clientFormat) withErrorString:@"Couldn't set the client format on the source file!"]) { |
return; |
} |
size = sizeof(clientFormat); |
if (![self checkError:ExtAudioFileSetProperty(destinationFile, kExtAudioFileProperty_ClientDataFormat, size, &clientFormat) withErrorString:@"Couldn't set the client format on the destination file!"]) { |
return; |
} |
// Get the audio converter. |
AudioConverterRef converter = 0; |
size = sizeof(converter); |
if (![self checkError:ExtAudioFileGetProperty(destinationFile, kExtAudioFileProperty_AudioConverter, &size, &converter) withErrorString:@"Failed to get the Audio Converter from the destination file."]) { |
return; |
} |
/* |
Can the Audio Converter resume after an interruption? |
this property may be queried at any time after construction of the Audio Converter (which in this case is owned by an ExtAudioFile object) after setting its output format |
there's no clear reason to prefer construction time, interruption time, or potential resumption time but we prefer |
construction time since it means less code to execute during or after interruption time. |
*/ |
BOOL canResumeFromInterruption = YES; |
UInt32 canResume = 0; |
size = sizeof(canResume); |
OSStatus error = AudioConverterGetProperty(converter, kAudioConverterPropertyCanResumeFromInterruption, &size, &canResume); |
if (error == noErr) { |
/* |
we recieved a valid return value from the GetProperty call |
if the property's value is 1, then the codec CAN resume work following an interruption |
if the property's value is 0, then interruptions destroy the codec's state and we're done |
*/ |
if (canResume == 0) { |
canResumeFromInterruption = NO; |
} |
printf("Audio Converter %s continue after interruption!\n", (!canResumeFromInterruption ? "CANNOT" : "CAN")); |
} else { |
/* |
if the property is unimplemented (kAudioConverterErr_PropertyNotSupported, or paramErr returned in the case of PCM), |
then the codec being used is not a hardware codec so we're not concerned about codec state |
we are always going to be able to resume conversion after an interruption |
*/ |
if (error == kAudioConverterErr_PropertyNotSupported) { |
printf("kAudioConverterPropertyCanResumeFromInterruption property not supported - see comments in source for more info.\n"); |
} else { |
printf("AudioConverterGetProperty kAudioConverterPropertyCanResumeFromInterruption result %d, paramErr is OK if PCM\n", (int)error); |
} |
error = noErr; |
} |
// Setup buffers |
UInt32 bufferByteSize = 32768; |
char sourceBuffer[bufferByteSize]; |
/* |
keep track of the source file offset so we know where to reset the source for |
reading if interrupted and input was not consumed by the audio converter |
*/ |
SInt64 sourceFrameOffset = 0; |
// Do the read and write - the conversion is done on and by the write call. |
printf("Converting...\n"); |
while (YES) { |
// Set up output buffer list. |
AudioBufferList fillBufferList = {}; |
fillBufferList.mNumberBuffers = 1; |
fillBufferList.mBuffers[0].mNumberChannels = clientFormat.mChannelsPerFrame; |
fillBufferList.mBuffers[0].mDataByteSize = bufferByteSize; |
fillBufferList.mBuffers[0].mData = sourceBuffer; |
/* |
The client format is always linear PCM - so here we determine how many frames of lpcm |
we can read/write given our buffer size |
*/ |
UInt32 numberOfFrames = 0; |
if (clientFormat.mBytesPerFrame > 0) { |
// Handles bogus analyzer divide by zero warning mBytesPerFrame can't be a 0 and is protected by an Assert. |
numberOfFrames = bufferByteSize / clientFormat.mBytesPerFrame; |
} |
if (![self checkError:ExtAudioFileRead(sourceFile, &numberOfFrames, &fillBufferList) withErrorString:@"ExtAudioFileRead failed!"]) { |
return; |
} |
if (!numberOfFrames) { |
// This is our termination condition. |
error = noErr; |
break; |
} |
sourceFrameOffset += numberOfFrames; |
BOOL wasInterrupted = [self checkIfPausedDueToInterruption]; |
if ((error != noErr || wasInterrupted) && (!canResumeFromInterruption)) { |
// this is our interruption termination condition |
// an interruption has occured but the Audio Converter cannot continue |
error = kMyAudioConverterErr_CannotResumeFromInterruptionError; |
break; |
} |
error = ExtAudioFileWrite(destinationFile, numberOfFrames, &fillBufferList); |
// If we were interrupted in the process of the write call, we must handle the errors appropriately. |
if (error != noErr) { |
if (error == kExtAudioFileError_CodecUnavailableInputConsumed) { |
printf("ExtAudioFileWrite kExtAudioFileError_CodecUnavailableInputConsumed error %d\n", (int)error); |
/* |
Returned when ExtAudioFileWrite was interrupted. You must stop calling |
ExtAudioFileWrite. If the underlying audio converter can resume after an |
interruption (see kAudioConverterPropertyCanResumeFromInterruption), you must |
wait for an EndInterruption notification from AudioSession, then activate the session |
before resuming. In this situation, the buffer you provided to ExtAudioFileWrite was successfully |
consumed and you may proceed to the next buffer |
*/ |
} else if (error == kExtAudioFileError_CodecUnavailableInputNotConsumed) { |
printf("ExtAudioFileWrite kExtAudioFileError_CodecUnavailableInputNotConsumed error %d\n", (int)error); |
/* |
Returned when ExtAudioFileWrite was interrupted. You must stop calling |
ExtAudioFileWrite. If the underlying audio converter can resume after an |
interruption (see kAudioConverterPropertyCanResumeFromInterruption), you must |
wait for an EndInterruption notification from AudioSession, then activate the session |
before resuming. In this situation, the buffer you provided to ExtAudioFileWrite was not |
successfully consumed and you must try to write it again |
*/ |
// seek back to last offset before last read so we can try again after the interruption |
sourceFrameOffset -= numberOfFrames; |
if (![self checkError:ExtAudioFileSeek(sourceFile, sourceFrameOffset) withErrorString:@"ExtAudioFileSeek failed!"]) { |
return; |
} |
} else { |
[self checkError:error withErrorString:@"ExtAudioFileWrite failed!"]; |
return; |
} |
} |
} |
// Cleanup |
if (destinationFile) { ExtAudioFileDispose(destinationFile); } |
if (sourceFile) { ExtAudioFileDispose(sourceFile); } |
if (converter) { AudioConverterDispose(converter); } |
// Set the state to done. |
dispatch_sync(self.queue, ^{ |
weakSelf.state = AudioConverterStateDone; |
}); |
if (error == noErr) { |
if ([self.delegate respondsToSelector:@selector(audioFileConvertOperation:didCompleteWithURL:)]) { |
[self.delegate audioFileConvertOperation:self didCompleteWithURL:self.destinationURL]; |
} |
} |
} |
- (BOOL)checkError:(OSStatus)error withErrorString:(NSString *)string { |
if (error == noErr) { |
return YES; |
} |
if ([self.delegate respondsToSelector:@selector(audioFileConvertOperation:didEncounterError:)]) { |
NSError *err = [NSError errorWithDomain:@"AudioFileConvertOperationErrorDomain" code:error userInfo:@{NSLocalizedDescriptionKey : string}]; |
[self.delegate audioFileConvertOperation:self didEncounterError:err]; |
} |
return NO; |
} |
- (BOOL)checkIfPausedDueToInterruption { |
__block BOOL wasInterrupted = NO; |
__weak __typeof__(self) weakSelf = self; |
dispatch_sync(self.queue, ^{ |
assert(weakSelf.state != AudioConverterStateDone); |
while (weakSelf.state == AudioConverterStatePaused) { |
dispatch_semaphore_wait(weakSelf.semaphore, DISPATCH_TIME_FOREVER); |
wasInterrupted = YES; |
} |
}); |
// We must be running or something bad has happened. |
assert(self.state == AudioConverterStateRunning); |
return wasInterrupted; |
} |
// MARK: Notification Handlers. |
- (void)handleAudioSessionInterruptionNotification:(NSNotification *)notification { |
AVAudioSessionInterruptionType interruptionType = [notification.userInfo[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue]; |
printf("Session interrupted > --- %s ---\n", interruptionType == AVAudioSessionInterruptionTypeBegan ? "Begin Interruption" : "End Interruption"); |
__weak __typeof__(self) weakSelf = self; |
if (interruptionType == AVAudioSessionInterruptionTypeBegan) { |
dispatch_sync(self.queue, ^{ |
if (weakSelf.state == AudioConverterStateRunning) { |
weakSelf.state = AudioConverterStatePaused; |
} |
}); |
} else { |
NSError *error = nil; |
[[AVAudioSession sharedInstance] setActive:YES error:&error]; |
if (error != nil) { |
NSLog(@"AVAudioSession setActive failed with error: %@", error.localizedDescription); |
} |
if (self.state == AudioConverterStatePaused) { |
dispatch_semaphore_signal(self.semaphore); |
} |
dispatch_sync(self.queue, ^{ |
weakSelf.state = AudioConverterStateRunning; |
}); |
} |
} |
+ (void)printAudioStreamBasicDescription:(AudioStreamBasicDescription)asbd { |
char formatID[5]; |
UInt32 mFormatID = CFSwapInt32HostToBig(asbd.mFormatID); |
bcopy (&mFormatID, formatID, 4); |
formatID[4] = '\0'; |
printf("Sample Rate: %10.0f\n", asbd.mSampleRate); |
printf("Format ID: %10s\n", formatID); |
printf("Format Flags: %10X\n", (unsigned int)asbd.mFormatFlags); |
printf("Bytes per Packet: %10d\n", (unsigned int)asbd.mBytesPerPacket); |
printf("Frames per Packet: %10d\n", (unsigned int)asbd.mFramesPerPacket); |
printf("Bytes per Frame: %10d\n", (unsigned int)asbd.mBytesPerFrame); |
printf("Channels per Frame: %10d\n", (unsigned int)asbd.mChannelsPerFrame); |
printf("Bits per Channel: %10d\n", (unsigned int)asbd.mBitsPerChannel); |
printf("\n"); |
} |
@end |
Copyright © 2016 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2016-09-29