用 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.
先呼叫 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 可能會造成聲音不連續或是爆音的可能性. 所以還是得認命點用單一執行緒解碼.
除了 AAC codec, 我還需要一個 mp4 檔案解析器來解析目前位置. 我目前採用的是 mp4v2.
事前的準備
下載完 fdk-aac 之後在 msys2 解開
之後再編譯
先 include 下列的 header 檔:
最後面的參數請維持1就好, fdk-aac 支援多層解碼, 但是我們目前只需要一層.
MPEG4 AAC 解碼前需要先作一些設定. 在 fdk-aac 文件中有提到, 在解碼前 Library 需要取得 raw AudioSpecificConfig (ASC) 或是 StreamMuxConfig (SMC) 資訊. 這部份可以從 MP4GetTrackESConfiguration 取得, 第二個設定是告訴 decoder 如果遇到超過 2 聲道的 AAC 要重新混音成兩聲道. 其他選項可以參照 source code 的 header 檔.
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 總數
trackNumber 在後面我有程式碼做搜尋適當的型態, 需要注意的是無論是 track 或是 frame 在 mp4v2 都是從 1 開頭. 在 mp4v2 中的 sample 單位其實是一個 AAC frame. 為了怕混淆所以要先釐清一下.
接下來就是取得 AAC Frame
在此需要注意的是 byteReaded 是告訴 mp4v2 你有多大的 buffer 可以讓它放 AAC Frame, 之後它會把實際的 frame 大小寫回到 byteReaded 裡面, 所以記得每次一定要重設成最大值 (5000), 不然 buffer 太小他是不會填 AAC Data 的.
然後就是解碼了
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.
資料的填入以及解碼
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.
後記
原始程式碼:
#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); }
留言
張貼留言