Attention! Translated article might be found on my English blog.

2016年7月8日金曜日

ExtAudioFileを使ってMP3ファイルをPCMデータとして取得する

-- 2016/07/10追記 --

私が下の方で書いたコードだと、ファイルによってはいつまで経っても
whileループから抜け出さないケースに遭遇しました。

どうやらkExtAudioFileProperty_FileLengthFramesが返す総フレーム数と
実際の総フレーム数が異なるためwhileループの条件がfalseにならず
延々と読み込み処理を続けてしまうようでした。

ExtAudioFileRead()はファイルがEOFに達するとフレーム数には0が入るようです。
参考: Extended Audio File Services Reference

この動作を利用してExtAudioFileRead()の後に
        // 0ならend-of-fileらしい
        if (numFramesToRead == 0) {
            break;
        }
という処理を入れ、ループを抜ける仕組みを入れた方が良いでしょう。

-- 2016/07/10追記終了 --

前回の続き、というかExtAudioFileを使えば簡単にnon-LPCMを変換できることに気づきました…。
というわけで変更後のソースは以下の通り。
#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_DEFAULT, 0), ^{
            OSStatus err = DecodeExtFileAtPath(argv[1], argv[2]);
            printf("done. err:%d\n", err);
    });
    
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    
    return 0;
}

/*
 inputFilePathで指定したオーディオファイルを読み込み
 CDフォーマット(ただしbig-endian)のバイナリデータとして
 outputFilePathにファイルとして書き出す。
 主にMP3, AACなどnon-LPCMデータのLPCMデータへの変換動作確認用。(一応.wav, .aiffも読める)
 処理に失敗した場合は失敗箇所でのOSStatusを返す。
 */
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); 
        
        fwrite(list.mBuffers[0].mData,
               list.mBuffers[0].mDataByteSize,
               1,
               fp);
        
        frameOffset += numFramesToRead;
    }
    fclose(fp);
    free(buffer);
    
    err = ExtAudioFileDispose(file);
    return err;
}

/*
 descPtrCD音質のASBDをセットする。
 ただし、big-endianなのでlittle-endianなプロセッサで扱う場合は注意
 */
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;
}
AudioConverterが不要なのは良いですね。(実際はExtAudioFileが内包しているようです)
kExtAudioFileProperty_FileLengthFramesで総フレーム数が取得できるのもありがたい…。
まだVBRなファイルは試してませんが、問題なく行けそうな気がします。