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

2016年7月6日水曜日

AudioToolboxを使ってMP3やAACなどのnon-LPCMをLPCMデータに変換する

MP3の読み込みがどうしても上手くいかないので
検証用コードを書いてみました。
なんとか意図した通り動くようになりました。

とりあえずノンリニアな場合はASPD(AudioStreamPacketDescription)とmagic cookieを
意識する必要がありますね。
昔(OS X 10.3辺り)はもっと雑なコードでも読めていたはずなんですが…。

ファイルの読み込み処理そのものはAudioConverterの入力コールバックで行っていて、
その際得られたASPDがあれば一緒に返してあげる感じですね。

検証用コードは以下の通りで、次はこれをworker-threadで行った時にどうなるかを
調べたいと思います。
#include <CoreServices/CoreServices.h>
#include <AudioToolbox/AudioToolbox.h>

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

OSStatus DecodeFileAtPath(const char *inputFilePath, const char *outputFilePath);
OSStatus MyACComplexInputProc(AudioConverterRef inAudioConverter,
                              UInt32 *ioNumberDataPackets,
                              AudioBufferList *ioData,
                              AudioStreamPacketDescription **outDataPacketDescription,
                              void* inUserData);
void SetStandardDescription(AudioStreamBasicDescription *descPtr);

typedef struct MyConverterData {
    AudioFileID fileID;
    UInt64 totalPacketCount;
    UInt64 packetOffset;
} MyConverterData;

int main(int argc, char* argv[]) {
    if (argc != 3) {
        printf("usage: Mp3Decoder inFile outFile\n");
        return 0;
    }
    
    OSStatus err = DecodeFileAtPath((const char *)(argv[1]), (const char *)argv[2]);
    printf("done. err:%d\n", err);
    
    return 0;
}

/*
    inputFilePathで指定したオーディオファイルを読み込み
    CDフォーマット(ただしbig-endian)のバイナリデータとして
    outputFilePathにファイルとして書き出す。
    主にMP3, AACなどnon-LPCMデータのLPCMデータへの変換動作確認用。(一応.wav, .aiffも読める)
    処理に失敗した場合は失敗箇所でのOSStatusを返す。
 */
OSStatus DecodeFileAtPath(const char *inputFilePath, const char *outputFilePath) {
    OSStatus err = noErr;
    AudioFileID fileID;
    MyConverterData myConverterData = {0, 0, 0};
    
    // ファイルを開いてファイルのASBD, パケット数を取得する
    AudioStreamBasicDescription fileDesc;
    {
        CFURLRef url;
        url = CFURLCreateWithBytes(NULL,
                                   (const UInt8 *)inputFilePath,
                                   strlen(inputFilePath),
                                   kCFStringEncodingUTF8,
                                   NULL);
        
        err = AudioFileOpenURL(url,
                               fsRdPerm,
                               0,
                               &fileID);
        _ERR_RETURN(err);
        
        myConverterData.fileID = fileID;
        UInt32 size = sizeof(AudioStreamBasicDescription);
        memset(&fileDesc, 0, size);
        
        err = AudioFileGetProperty(fileID,
                                   kAudioFilePropertyDataFormat,
                                   &size,
                                   &fileDesc);
        _ERR_RETURN(err);
        size = sizeof(myConverterData.totalPacketCount);
        err = AudioFileGetProperty(fileID,
                                   kAudioFilePropertyAudioDataPacketCount,
                                   &size,
                                   &myConverterData.totalPacketCount);
        _ERR_RETURN(err);
    }
    
    // 出力フォーマット(ASBD)を設定する
    AudioStreamBasicDescription outDesc;
    SetStandardDescription(&outDesc);
    
    // ファイルのASBDと出力のASBDを使ってコンバータを作成する
    AudioConverterRef converter;
    {
        err = AudioConverterNew(&fileDesc, &outDesc, &converter);
        _ERR_RETURN(err);
    }

    // magic cookieがあればコンバータにセットする
    // (これをしないとAudioConverterFillComplexBuffer()!datが返る
    {
        UInt32 size = 0;
        err = AudioFileGetPropertyInfo(fileID,
                                       kAudioFilePropertyMagicCookieData,
                                       &size,
                                       NULL);
        if(noErr == err){
            void *magic = calloc(1, size);
            err = AudioFileGetProperty(fileID,
                                       kAudioFilePropertyMagicCookieData,
                                       &size,
                                       magic);
            if(noErr == err){
                printf("magic cookie info found.\n");
                err = AudioConverterSetProperty(converter,
                                                kAudioConverterDecompressionMagicCookie,
                                                size,
                                                magic);
                _ERR_RETURN(err);
            }
            err = noErr;
            if(NULL != magic){
                free(magic);
            }
            _ERR_RETURN(err);
        }else{
            printf("magic cookie info not found.\n");
            
        }
    }
    
    const UInt32 maxNumPacketsInACycle = 100; // 1ループあたりに読み込むパケット数
    FILE *fp = fopen(outputFilePath, "w");
    if (fp == NULL) {
        printf( "fopen failed.");
        return writErr;
    }
    
    err = noErr;
    
    // ファイル読み込み、デコード、書き込みが終わるまで繰り返す
    while(err == noErr){
        // 最後のパケットまで処理済みならループを抜ける
        if (myConverterData.packetOffset == myConverterData.totalPacketCount) break;
        
        UInt32 numPackets = maxNumPacketsInACycle; // 読み込みたい最大パケット数
        UInt32 numFrames = fileDesc.mFramesPerPacket * numPackets; // 読み込みたい最大フレーム数
        
        AudioBufferList list;
        // listの準備は必要(なければinputProc呼ばれず即-50)
        list.mNumberBuffers = 1;
        list.mBuffers[0].mNumberChannels = 2;
        list.mBuffers[0].mDataByteSize = numFrames * outDesc.mBytesPerFrame; // デコード後の最大バイトサイズ
        list.mBuffers[0].mData = malloc(list.mBuffers[0].mDataByteSize);
        err = AudioConverterFillComplexBuffer(converter,
                                              MyACComplexInputProc,
                                              &myConverterData,
                                              &numPackets,
                                              &list,
                                              NULL);
        
        if (err == noErr) {
            // デコードデータを書き込む
            fwrite(list.mBuffers[0].mData, list.mBuffers[0].mDataByteSize, 1, fp);
        }
        free(list.mBuffers[0].mData);
    }
    
    // 後始末
    fclose(fp);
    err = AudioConverterDispose(converter);
    _ERR_RETURN(err);
    
    err = AudioFileClose(fileID);
    return err;
}

/*
 コンバータ用コールバック関数
 必要なパケットだけファイルから読み込みioDataにセットして渡す。
 パケットのASBDが得られる場合はoutDataPacketDescriptionを通じて渡す。
 inUserDataMyConverterData変数へのポインタでなければならない
 */

OSStatus MyACComplexInputProc(AudioConverterRef inAudioConverter,
                              UInt32 *ioNumberDataPackets,
                              AudioBufferList *ioData,
                              AudioStreamPacketDescription **outDataPacketDescription,
                              void* inUserData) {
    // fileIDを取得する
    MyConverterData *myConverterDataPtr = (MyConverterData *)inUserData;
    AudioFileID fileID = myConverterDataPtr->fileID;
    
    // ファイルから読み込むデータサイズを決定する。
    // 最大パケットサイズを取得して、それに要求されているパケット数をかけて算出する。
    // FIXME: コールバックを呼ぶたびに取得しなくても良いはず
    UInt32 maxPacketSize;
    UInt32 dataSize = sizeof(maxPacketSize);
    OSStatus err = AudioFileGetProperty(fileID,
                                        kAudioFilePropertyMaximumPacketSize,
                                        &dataSize,
                                        &maxPacketSize);
    _ERR_RETURN(err);
    
    UInt32 numBytes = maxPacketSize * *ioNumberDataPackets; // 有り得る最大のデータサイズ
    
    // バッファ確保
    static void *buffer = NULL;
    if (buffer != NULL)
        free(buffer);
    buffer = malloc(numBytes);
    
    // ASPD用メモリ確保
    static AudioStreamPacketDescription *descs = NULL;
    if (descs != NULL)
        free(descs);
    descs = malloc(sizeof(AudioStreamPacketDescription) * *ioNumberDataPackets);
    
    // ファイル読み込み
    err = AudioFileReadPacketData(fileID,
                                  false,
                                  &numBytes,
                                  descs,
                                  myConverterDataPtr->packetOffset,
                                  ioNumberDataPackets,
                                  buffer);
    _ERR_RETURN(err);
    
    // 読み込んだデータをioDataにセットする
    ioData->mBuffers[0].mDataByteSize = numBytes;
    ioData->mBuffers[0].mData = buffer;
    
    // 得られたASPDをセットする。.wav.aiffNULLが入っているのでNULLならセットしない
    if (outDataPacketDescription != NULL) {
        *outDataPacketDescription = descs;
    }
    
    // 読み込み済みパケット数を更新する
    myConverterDataPtr->packetOffset += *ioNumberDataPackets;
    
    // デバッグ用出力
    if (myConverterDataPtr->packetOffset % 100 == 0) {
        printf("%llu/%llu\n", myConverterDataPtr->packetOffset, myConverterDataPtr->totalPacketCount);
    }
    
    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;
}

ちなみにプログラムに興味ない人はafconvertコマンドを使うと良いです。