Attention! For Japanese customers, my Japanese blog might help.

Jul 15, 2016

Reading MP3 files using ExtAudioFile functions

This article is the English version of my Japanese article.

ExtAudioFile functions provide the easer way to convert non-LPCM into LPCM  than AudioFile functions do.

The following code converts MP3 or M4A files into LPCM data (big-endian).

#include <CoreServices/CoreServices.h>
#include <AudioToolbox/AudioToolbox.h>

#define _ERR_RETURN(err)  {if(noErr != err){printf("%d - err:%d\n", __LINE__, err); return err;}}

typedef AudioStreamBasicDescription ASBD;
typedef AudioBufferList ABL;

OSStatus DecodeExtFileAtPath(const char *inputFilePath, const char *outputFilePath);
void SetStandardDescription(AudioStreamBasicDescription *descPtr);
static inline ABL MakeABL(UInt32 ch, UInt32 bytes, void *buf);

int main(int argc, char* argv[]) {
    if (argc != 3) {
        printf("usage: ./Mp3Decoder inFile outFile\n");
        return 0;
    }
    
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT0), ^{
            OSStatus err = DecodeExtFileAtPath(argv[1], argv[2]);
            printf("done. err:%d\n", err);
    });
    
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    
    return 0;
}

/*
Reads an audio file specified by inputFilePath, and write to outputFilePath as CDDA quality binary data (big-endian).
Although this function is designed for non-LPCM files such as  MP3 and AAC, .wav and .aiff files are also acceptable.
When failed, an OSStatus at failed function will be returned.
 */
OSStatus DecodeExtFileAtPath(const char *inputFilePath, const char *outputFilePath) {
    CFURLRef url = CFURLCreateWithBytes(NULL,
                                        (const UInt8 *)inputFilePath,
                                        strlen(inputFilePath),
                                        kCFStringEncodingUTF8,
                                        NULL);
    
    ExtAudioFileRef file;
    OSStatus err = ExtAudioFileOpenURL(url,
                                       &file);
    _ERR_RETURN(err);
    
    CFRelease(url);
    
    ASBD clientDesc;
    SetStandardDescription(&clientDesc);
    UInt32 size = sizeof(clientDesc);
    
    err = ExtAudioFileSetProperty(file,
                                  kExtAudioFileProperty_ClientDataFormat,
                                  size,
                                  &clientDesc);
    _ERR_RETURN(err);
    
    SInt64 fileFrameLength;
    size = sizeof(fileFrameLength);
    err = ExtAudioFileGetProperty(file,
                                  kExtAudioFileProperty_FileLengthFrames,
                                  &size,
                                  &fileFrameLength);
    _ERR_RETURN(err);
    
    const UInt32 numFramesToReadInACycle = 1024*1024;
    const UInt32 bufferSize = clientDesc.mBytesPerFrame * numFramesToReadInACycle;
    void *buffer = malloc(bufferSize);
    FILE *fp = fopen(outputFilePath, "w");
    SInt64 frameOffset = 0;
    while (frameOffset != fileFrameLength) {
        UInt32 numFramesToRead = numFramesToReadInACycle;
        AudioBufferList list = MakeABL(clientDesc.mChannelsPerFrame,
                                       bufferSize,
                                       buffer);
        err = ExtAudioFileRead(file,
                               &numFramesToRead,
                               &list);
        _ERR_RETURN(err);  
         // 0 if end-of-file
        if (numFramesToRead == 0) {
            break;
        }
        fwrite(list.mBuffers[0].mData,
               list.mBuffers[0].mDataByteSize,
               1,
               fp);
        
        frameOffset += numFramesToRead;
    }
    fclose(fp);
    free(buffer);
    
    err = ExtAudioFileDispose(file);
    return err;
}

/*
 Sets CDDA-quality ASBD to descPtr.
 For little-endian processor, it should be noted this function returns big-endian format.
 */
void SetStandardDescription(AudioStreamBasicDescription *descPtr) {
    descPtr->mSampleRate = 44100.0;
    descPtr->mFormatID = kAudioFormatLinearPCM;
    descPtr->mFormatFlags = kAudioFormatFlagIsBigEndian |
                            kAudioFormatFlagIsSignedInteger |
                            kAudioFormatFlagIsPacked;
    descPtr->mBytesPerPacket = 4;
    descPtr->mBytesPerFrame = 4;
    descPtr->mFramesPerPacket = 1;
    descPtr->mChannelsPerFrame = 2;
    descPtr->mBitsPerChannel = 16;
}

static inline ABL MakeABL(UInt32 ch, UInt32 bytes, void *buf) {
    ABL list;
    list.mNumberBuffers = 1;
    list.mBuffers[0].mNumberChannels = ch;
    list.mBuffers[0].mDataByteSize = bytes;
    list.mBuffers[0].mData = buf;
    return list;
}

Compared to AudioFile functions, ExtAudioFile funcs are more comfortable because AudioConverter is unnecessary.
(annoying frame conversion calculations or data input callback are NOT needed!)

Andmore, total frames are available by using kExtAudioFileProperty_FileLengthFrames property.
The property helps us to estimate total byte size of LPCM data roughly before conversion.

However, note that there are cases that kExtAudioFileProperty_FileLengthFrames property does not return correct values.
So, you should check the number of read frames returned by ExtAudioFileRead() in a loop because the function returns 0 when reaches EOF.

Oh, by the way, I have not tried VBR files yet. But these will also be OK.