How can I get AAC encoding with ExtAudioFile on iOS to work?

11.3k views Asked by At

I need to convert a WAVE file into an AAC encoded M4A file on iOS. I'm aware that AAC encoding is not supported on older devices or in the simulator. I'm testing that before I run the code. But I still can't get it to work.

I looked into Apple's very own iPhoneExtAudioFileConvertTest example and I thought I followed it exactly, but still no luck!

Currently, I get a -50 (= error in user parameter list) while trying to set the client format on the destination file. On the source file, it works.

Below is my code. Any help is very much appreciated, thanks!

UInt32 size;

// Open a source audio file.
ExtAudioFileRef sourceAudioFile;
ExtAudioFileOpenURL( (CFURLRef)sourceURL, &sourceAudioFile );

// Get the source data format
AudioStreamBasicDescription sourceFormat;
size = sizeof( sourceFormat );
result = ExtAudioFileGetProperty( sourceAudioFile, kExtAudioFileProperty_FileDataFormat, &size, &sourceFormat );

// Define the output format (AAC).
AudioStreamBasicDescription outputFormat;
outputFormat.mFormatID = kAudioFormatMPEG4AAC;
outputFormat.mSampleRate = 44100;
outputFormat.mChannelsPerFrame = 2;

// Use AudioFormat API to fill out the rest of the description.
size = sizeof( outputFormat );
AudioFormatGetProperty( kAudioFormatProperty_FormatInfo, 0, NULL, &size, &outputFormat);

// Make a destination audio file with this output format.
ExtAudioFileRef destAudioFile;
ExtAudioFileCreateWithURL( (CFURLRef)destURL, kAudioFileM4AType, &outputFormat, NULL, kAudioFileFlags_EraseFile, &destAudioFile );

 // Create canonical PCM client format.
AudioStreamBasicDescription clientFormat;
clientFormat.mSampleRate = sourceFormat.mSampleRate;
clientFormat.mFormatID = kAudioFormatLinearPCM;
clientFormat.mFormatFlags = kAudioFormatFlagIsPacked | kAudioFormatFlagIsSignedInteger;
clientFormat.mChannelsPerFrame = 2;
clientFormat.mBitsPerChannel = 16;
clientFormat.mBytesPerFrame = 4;
clientFormat.mBytesPerPacket = 4;
clientFormat.mFramesPerPacket = 1;

// Set the client format in source and destination file.
size = sizeof( clientFormat );
ExtAudioFileSetProperty( sourceAudioFile, kExtAudioFileProperty_ClientDataFormat, size, &clientFormat );
size = sizeof( clientFormat );
ExtAudioFileSetProperty( destAudioFile, kExtAudioFileProperty_ClientDataFormat, size, &clientFormat );

// Make a buffer
int bufferSizeInFrames = 8000;
int bufferSize = ( bufferSizeInFrames * sourceFormat.mBytesPerFrame );
UInt8 * buffer = (UInt8 *)malloc( bufferSize );
AudioBufferList bufferList;
bufferList.mNumberBuffers = 1;
bufferList.mBuffers[0].mNumberChannels = clientFormat.mChannelsPerFrame;
bufferList.mBuffers[0].mData = buffer;
bufferList.mBuffers[0].mDataByteSize = ( bufferSize );

while( TRUE )
{
    // Try to fill the buffer to capacity.
    UInt32 framesRead = bufferSizeInFrames;
    ExtAudioFileRead( sourceAudioFile, &framesRead, &bufferList );

    // 0 frames read means EOF.
    if( framesRead == 0 )
        break;

    // Write.
    ExtAudioFileWrite( destAudioFile, framesRead, &bufferList );
}

free( buffer );

// Close the files.
ExtAudioFileDispose( sourceAudioFile );
ExtAudioFileDispose( destAudioFile );
3

There are 3 answers

1
Sebastian On

Answered my own question: I had to pass this problem to my colleague and he got it to work! I never had the chance to analyze my original problem but I thought, I'd post it here for the sake of completeness. The following method is called from within an NSThread. Parameters are set via the 'threadDictionary' and he created a custom delegate to transmit progress feedback (sorry, SO doesn't understand the formatting properly, the following is supposed to be one block of method implementation):

- (void)encodeToAAC
{
    RXAudioEncoderStatusType encoderStatus;
    OSStatus result = noErr;
    BOOL success = NO;
    BOOL cancelled = NO;
    UInt32 size;

    ExtAudioFileRef sourceAudioFile,destAudioFile;
    AudioStreamBasicDescription sourceFormat,outputFormat, clientFormat;

    SInt64 totalFrames;
    unsigned long long encodedBytes, totalBytes;

    int bufferSizeInFrames, bufferSize;
    UInt8 * buffer;
    AudioBufferList bufferList;

    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

    NSFileManager * fileManager = [[[NSFileManager alloc] init] autorelease];

    NSMutableDictionary * threadDict = [[NSThread currentThread] threadDictionary];

    NSObject<RXAudioEncodingDelegate> * delegate = (NSObject<RXAudioEncodingDelegate> *)[threadDict objectForKey:@"Delegate"];

    NSString *sourcePath = (NSString *)[threadDict objectForKey:@"SourcePath"];
    NSString *destPath = (NSString *)[threadDict objectForKey:@"DestinationPath"];

    NSURL * sourceURL = [NSURL fileURLWithPath:sourcePath];
    NSURL * destURL = [NSURL fileURLWithPath:destPath];

    // Open a source audio file.
    result = ExtAudioFileOpenURL( (CFURLRef)sourceURL, &sourceAudioFile );
    if( result != noErr )
    {
        DLog( @"Error in ExtAudioFileOpenURL: %ld", result );
        goto bailout;
    }

    // Get the source data format
    size = sizeof( sourceFormat );
    result = ExtAudioFileGetProperty( sourceAudioFile, kExtAudioFileProperty_FileDataFormat, &size, &sourceFormat );
    if( result != noErr )
    {
        DLog( @"Error in ExtAudioFileGetProperty: %ld", result );
        goto bailout;
    }

    // Define the output format (AAC).
    memset(&outputFormat, 0, sizeof(outputFormat));
    outputFormat.mFormatID = kAudioFormatMPEG4AAC;
    outputFormat.mSampleRate = 44100;
    outputFormat.mFormatFlags = kMPEG4Object_AAC_Main;
    outputFormat.mChannelsPerFrame = 2;
    outputFormat.mBitsPerChannel = 0;
    outputFormat.mBytesPerFrame = 0;
    outputFormat.mBytesPerPacket = 0;
    outputFormat.mFramesPerPacket = 1024;


    // Use AudioFormat API to fill out the rest of the description.
    //size = sizeof( outputFormat );
    //AudioFormatGetProperty( kAudioFormatProperty_FormatInfo, 0, NULL, &size, &outputFormat);

    // Make a destination audio file with this output format.
    result = ExtAudioFileCreateWithURL( (CFURLRef)destURL, kAudioFileM4AType, &outputFormat, NULL, kAudioFileFlags_EraseFile, &destAudioFile );
    if( result != noErr )
    {
        DLog( @"Error creating destination file: %ld", result );
        goto bailout;
    }

    // Create the canonical PCM client format.
    memset(&clientFormat, 0, sizeof(clientFormat));
    clientFormat.mSampleRate = sourceFormat.mSampleRate;
    clientFormat.mFormatID = kAudioFormatLinearPCM;
    clientFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked; //kAudioFormatFlagIsPacked | kAudioFormatFlagIsSignedInteger;
    clientFormat.mChannelsPerFrame = 2;
    clientFormat.mBitsPerChannel = 16;
    clientFormat.mBytesPerFrame = 4;
    clientFormat.mBytesPerPacket = 4;
    clientFormat.mFramesPerPacket = 1;

    // Set the client format in source and destination file.
    size = sizeof( clientFormat );
    result = ExtAudioFileSetProperty( sourceAudioFile, kExtAudioFileProperty_ClientDataFormat, size, &clientFormat );
    if( result != noErr )
    {
        DLog( @"Error while setting client format in source file: %ld", result );
        goto bailout;
    }
    size = sizeof( clientFormat );
    result = ExtAudioFileSetProperty( destAudioFile, kExtAudioFileProperty_ClientDataFormat, size, &clientFormat );
    if( result != noErr )
    {
        DLog( @"Error while setting client format in destination file: %ld", result );
        goto bailout;
    }

    // Make a buffer
    bufferSizeInFrames = 8000;
    bufferSize = ( bufferSizeInFrames * sourceFormat.mBytesPerFrame );
    buffer = (UInt8 *)malloc( bufferSize );

    bufferList.mNumberBuffers = 1;
    bufferList.mBuffers[0].mNumberChannels = clientFormat.mChannelsPerFrame;
    bufferList.mBuffers[0].mData = buffer;
    bufferList.mBuffers[0].mDataByteSize = ( bufferSize );

    // Obtain total number of audio frames to encode
    size = sizeof( totalFrames );
    result = ExtAudioFileGetProperty( sourceAudioFile, kExtAudioFileProperty_FileLengthFrames, &size, &totalFrames );
    if( result != noErr )
    {
        DLog( @"Error in ExtAudioFileGetProperty, could not get kExtAudioFileProperty_FileLengthFrames from sourceFile: %ld", result );
        goto bailout;
    }

    encodedBytes = 0;
    totalBytes = totalFrames * sourceFormat.mBytesPerFrame;
    [threadDict setValue:[NSValue value:&totalBytes withObjCType:@encode(unsigned long long)] forKey:@"TotalBytes"];

    if (delegate != nil)
        [self performSelectorOnMainThread:@selector(didStartEncoding) withObject:nil waitUntilDone:NO];

    while( TRUE )
    {
        // Try to fill the buffer to capacity.
        UInt32 framesRead = bufferSizeInFrames;
        result = ExtAudioFileRead( sourceAudioFile, &framesRead, &bufferList );
        if( result != noErr )
        {
            DLog( @"Error in ExtAudioFileRead: %ld", result );
            success = NO;
            break;
        }

        // 0 frames read means EOF.
        if( framesRead == 0 ) {
            success = YES;
            break;
        }

        // Write.
        result = ExtAudioFileWrite( destAudioFile, framesRead, &bufferList );
        if( result != noErr )
        {
            DLog( @"Error in ExtAudioFileWrite: %ld", result );
            success = NO;
            break;
        }

        encodedBytes += framesRead * sourceFormat.mBytesPerFrame;

        if (delegate != nil)
            [self performSelectorOnMainThread:@selector(didEncodeBytes:) withObject:[NSValue value:&encodedBytes withObjCType:@encode(unsigned long long)] waitUntilDone:NO];

        if ([[NSThread currentThread] isCancelled]) {
            cancelled = YES;
            DLog( @"Encoding was cancelled." );
            success = NO;
            break;
        }
    }

    free( buffer );

    // Close the files.
    ExtAudioFileDispose( sourceAudioFile );
    ExtAudioFileDispose( destAudioFile );

bailout:
    encoderStatus.result = result;
    [threadDict setValue:[NSValue value:&encoderStatus withObjCType:@encode(RXAudioEncoderStatusType)] forKey:@"EncodingError"];

    // Report to the delegate if one exists
    if (delegate != nil)
        if (success)
            [self performSelectorOnMainThread:@selector(didEncodeFile) withObject:nil waitUntilDone:YES];
        else if (cancelled)
            [self performSelectorOnMainThread:@selector(encodingCancelled) withObject:nil waitUntilDone:YES];
        else
            [self performSelectorOnMainThread:@selector(failedToEncodeFile) withObject:nil waitUntilDone:YES];

    // Clear the partially encoded file if encoding failed or is cancelled midway
    if ((cancelled || !success) && [fileManager fileExistsAtPath:destPath])
        [fileManager removeItemAtURL:destURL error:NULL];

    [threadDict setValue:[NSNumber numberWithBool:NO] forKey:@"isEncoding"];

    [pool release];
}
1
newenglander On

I tried out the code in Sebastian's answer and while it worked for uncompressed files (aif, wav, caf), it didn't for a lossy compressed file (mp3). I also had an error code of -50, but in ExtAudioFileRead rather than ExtAudioFileSetProperty. From this question I learned that this error signifies a problem with the function parameters. Turns out the buffer for reading the audio file had a size of 0 bytes, a result of this line:

int bufferSize = ( bufferSizeInFrames * sourceFormat.mBytesPerFrame );

Switching it to use the the bytes per frame from clientFormat instead (sourceFormat's value was 0) worked for me:

int bufferSize = ( bufferSizeInFrames * clientFormat.mBytesPerFrame );

This line was also in the question code, but I don't think that was the problem (but I had too much text for a comment).

0
Seth Kingsley On

Are you sure the sample rates match? Can you print the values for clientFormat and outputFormat at the point you’re getting the error? Otherwise I think you might need an AudioConverter.