用 Fraunhofer AAC + MP4v2 解碼 m4a 格式 AAC 音樂檔

因為專案需求, 所以在網上找了一些 AAC codec, 比較有名的大概就是 FAAD2 跟 FFMPEG 的 libav 的 aac plugin. 後來在網上有人把 Android 內部使用的 AAC codec library (Fraunhofer AAC)從 Opencore 抽出來獨立維護. 大部分的應用都是把他編成 FFMPEG 的 plugin 以取代原來的 AAC code. 有關方面的資訊可以用 google 搜尋 fdk-aac.


除了 AAC codec, 我還需要一個 mp4 檔案解析器來解析目前位置. 我目前採用的是 mp4v2.

事前的準備

  1. 先下載 fdk-aac 以及 mp4v2
  2. 編譯 fdk-aac 以及 mp4v2
下載以及編譯 mp4v2 在此已經寫過一篇, 所以不再詳述.

下載以及編譯 fdk-aac


下載完 fdk-aac 之後在 msys2 解開

tar zxvf fdk-aac-0.1.3.tar.gz

之後再編譯

cd fdk-aac-0.1.3
./configure --prefix=`echo ~`/fdk-aac
make && make install

解碼AAC檔案


先 include 下列的 header 檔:

#include "mp4v2/mp4v2.h"
#include "fdk-aac/aacdecoder_lib.h"

開啟 MP4 檔案以及 AAC Decoder


MP4FileHandle *reader = (MP4FileHandle *) MP4Read(mp4Path);
HANDLE_AACDECODER aacDecoder = aacDecoder_Open(TT_MP4_RAW, 1);

最後面的參數請維持1就好, fdk-aac 支援多層解碼, 但是我們目前只需要一層.

解碼前的設定


unsigned char conf[128];
UINT confBytes = 128;

MP4GetTrackESConfiguration(reader, trackNumber, (uint8_t **) &conf, (uint32_t *) &confBytes);
aacDecoder_ConfigRaw(aacDecoder, (unsigned char **)&conf, (uint32_t *) &confBytes);
aacDecoder_SetParam(aacDecoder, AAC_PCM_OUTPUT_CHANNELS, 2);

MPEG4 AAC 解碼前需要先作一些設定. 在 fdk-aac 文件中有提到, 在解碼前 Library 需要取得 raw AudioSpecificConfig (ASC)  或是  StreamMuxConfig (SMC) 資訊. 這部份可以從 MP4GetTrackESConfiguration 取得, 第二個設定是告訴 decoder 如果遇到超過 2 聲道的 AAC 要重新混音成兩聲道. 其他選項可以參照 source code 的 header 檔.

Buffer 的設定


Input buffer 我是訂死在 5000 bytes, 這數字應該很安全. 如果想要更精確的數字可以利用 MP4GetTrackMaxSampleSize 去取得.

Output buffer 就很固定了, AAC-LC 最大固定是 1024 個  samples,  HE-AAC/HE-AACv2 則是2048 個 samples. 以本文的 case 一個 sample 換算成 byte 就是 16bit(2 bytes) * 2 channels = 4 bytes.

資料的填入以及解碼


先取得 frame 總數

unsigned int frameCount = MP4GetTrackNumberOfSamples(reader, trackNumber);

trackNumber 在後面我有程式碼做搜尋適當的型態, 需要注意的是無論是 track 或是 frame 在 mp4v2 都是從 1 開頭. 在 mp4v2 中的 sample 單位其實是一個 AAC frame. 為了怕混淆所以要先釐清一下.

接下來就是取得 AAC Frame

MP4ReadSample(reader, trackNumber, i, (uint8_t **) &inBuff, &byteReaded, NULL, NULL, NULL, NULL);

在此需要注意的是 byteReaded 是告訴 mp4v2 你有多大的 buffer 可以讓它放 AAC Frame, 之後它會把實際的 frame 大小寫回到 byteReaded 裡面, 所以記得每次一定要重設成最大值 (5000), 不然 buffer 太小他是不會填 AAC Data 的.

然後就是解碼了

AAC_DECODER_ERROR errorCode;
char *inBufferArray[1];
unsigned int inBuffReaded[1];
unsigned int byteFilled = 0;
CStreamInfo *theInfo; inBufferArray[0] = (char *) inBuffer; inBuffReaded[0] = byteReaded; byteFilled = byteReaded; theInfo = aacDecoder_GetStreamInfo(aacDecoder); errorCode = aacDecoder_Fill(aacDecoder, inBufferArray, inBuffReaded, &byteFilled); errorCode = aacDecoder_DecodeFrame(aacDecoder, (INT_PCM *) outBuffer, theInfo->frameSize, 0);

先呼叫 aacDecoder_Fill 把資料填進 internal buffer 裡面, 然後再做解碼. 在此需要注意的是因為 fdk-aac 支援多層, 所以他的讀取是以 array of pointer 去讀取, 那要送進 internal buffer 的時候也是要包裝成這種格式. ffmpeg-fdk-aac wrap 的 source code 我有看過, 它的包法很危險, 因為它剛好是放在 structure 裡, 如果不是就 GG 了... 這是題外話.

接下來就是呼叫 aacDecoder_DecodeFrame 了, 基本上如果前面的部份都設定好不會有太大的問題, 不過還是要檢查 error code 比較保險.

aacDecoder_GetStreamInfo 永遠取得上一個 frame 的資料. 所以我有先解一個 frame 去取得該有的資訊, 這是假設每個 AAC Frame 的資訊是不改變的狀況下. 如果要做完全確認的話最好的作法是解碼後去呼叫 aacDecoder_GetStreamInfo 取得 CStreamInfo.

後記


我之前有考慮過用 Multi-thread, GCD 或是其他平行運算的技術去加速 AAC 解碼速度, 但是後來發現 fdk-aac 似乎有用到連續性的數位濾波技術或是其他的方法去實作, 所以 fdk-aac 的解碼必須是連續並循序的. 因為我不懂 AAC 跟其他的 decoder 如何運作, 但是因為這個因素, 使用平行運算呼叫 fdk-aac 可能會造成聲音不連續或是爆音的可能性. 所以還是得認命點用單一執行緒解碼.

原始程式碼:


#include "mp4v2/mp4v2.h"
#include "fdk-aac/aacdecoder_lib.h"

AAC_DECODER_ERROR decodeAACFrame(HANDLE_AACDECODER aacDecoder, CStreamInfo &info, void *inBuffer, void *outBuffer, unsigned int inBufferSize)
{
    unsigned int byteFilled = 0;
    CStreamInfo *theInfo;
    char *inBufferArray[1];
    unsigned int inBuffReaded[1];
    unsigned int byteReaded = inBufferSize;
    AAC_DECODER_ERROR errorCode;

    theInfo = aacDecoder_GetStreamInfo(aacDecoder);

    inBufferArray[0] = (char *) inBuffer;
    inBuffReaded[0] = byteReaded;
    byteFilled = byteReaded;

    errorCode = aacDecoder_Fill(aacDecoder, inBufferArray, inBuffReaded, &byteFilled);

    if (errorCode != AAC_DEC_OK)
    {
        return errorCode;
    }

    errorCode = aacDecoder_DecodeFrame(aacDecoder, (INT_PCM *) outBuffer, theInfo->frameSize, 0);

    if (errorCode != AAC_DEC_OK)
    {
        return errorCode;
    }

    theInfo = aacDecoder_GetStreamInfo(aacDecoder);
    info = *theInfo;

    return errorCode;
}

unsigned int getFirstAudioTrack(MP4FileHandle *reader)
{
    unsigned int trackCount = MP4GetNumberOfTracks(reader, NULL, 0);

    if (trackCount == 0)
    {
        return 0;
    }

    for unsigned inti = 1; i <= trackCount; i++)
    {
        unsigned inttype = MP4GetTrackAudioMpeg4Type(reader, i);

        if ((type == MP4_MPEG4_AAC_LC_AUDIO_TYPE) ||
            (type == MP4_MPEG4_AAC_SSR_AUDIO_TYPE) ||
            (type == MP4_MPEG4_AAC_HE_AUDIO_TYPE))
        {
            return i;
        }
    }

    return 0;
}

void decodeMP4RawData(char *mp4Path, void **buffer, unsigned int *sizeInByte)
{
    MP4FileHandle *reader = (MP4FileHandle *) MP4Read(mp4Path);
    unsigned int trackNumber = _getFirstAudioTrack(reader);
    unsigned int frameCount = MP4GetTrackNumberOfSamples(reader, trackNumber);
    unsigned int byteReaded = 5000;
    HANDLE_AACDECODER aacDecoder = aacDecoder_Open(TT_MP4_RAW, 1);
    CStreamInfo info;
    AAC_DECODER_ERROR errorCode;

    if ((trackNumber == 0) || (frameCount == 0) || (aacDecoder == NULL))
    {
        *buffer = NULL;
        *sizeInByte = 0;
        return;
    }

    unsigned char conf[128];
    UINT confBytes = 128;

    MP4GetTrackESConfiguration(reader, trackNumber, (uint8_t **) &conf, (uint32_t *) &confBytes);
    errorCode = aacDecoder_ConfigRaw(aacDecoder, (unsigned char **)&conf, (uint32_t *) &confBytes);
    errorCode = aacDecoder_SetParam(aacDecoder, AAC_PCM_OUTPUT_CHANNELS, 2);

    char *inBuff = (char *) malloc(byteReaded);
    char *outBuff = (char *) malloc(4096 * sizeof(PicUInt16));

    MP4ReadSample(reader, trackNumber, 1, (uint8_t **) &inBuff, &byteReaded, NULL, NULL, NULL, NULL);
    errorCode = decodeAACFrame(aacDecoder, info, inBuff, outBuff, byteReaded);

    if (errorCode != AAC_DEC_OK)
    {
        *buffer = NULL;
        *sizeInByte = 0;
        return;
    }


    *sizeInByte = info.numChannels * info.frameSize * frameCount * sizeof(short);
    *buffer = (char *) malloc(*sizeInByte);
    memcpy(*buffer, outBuff, info.numChannels * info.frameSize * sizeof(short));
    free(outBuff);
    outBuff = (char *) (((char *) *buffer) + info.numChannels * info.frameSize * sizeof(short));

    for (unsigned int i = 2; i <= frameCount; i++)
    {
        byteReaded = 5000;
        MP4ReadSample(reader, trackNumber, i, (uint8_t **) &inBuff, &byteReaded, NULL, NULL, NULL, NULL);

        decodeAACFrame(aacDecoder, info, inBuff, outBuff, byteReaded);

        if (errorCode != AAC_DEC_OK)
        {
            free(inBuff);
            aacDecoder_Close(aacDecoder);
            MP4Close(reader);
            free(*buffer);
            *buffer = NULL;
            *sizeInByte = 0;
            return;
        }

        outBuff += info.numChannels * info.frameSize * sizeof(short);
    }

    free(inBuff);
    aacDecoder_Close(aacDecoder);
    MP4Close(reader);
}

留言

熱門文章