iOS下解碼AAC並播放
- 2020 年 4 月 1 日
- 筆記
前言
今天我們介紹一下如何在iOS進行AAC解碼,並使用AudioUnit播放解碼後的PCM數據。
基本流程
iOS系統對音頻處理做了三層封裝。包括應用層、服務層和硬件層。如下圖所示:

我們本次使用的都是服務層的接口。也就是上圖中被紅色框起來的部分。該層更接近於底層,所以靈活性更大,性能也更好。尤其對於直播相關的項目最好使用該層接口。
在iOS下進行音頻解碼及播放的大體流程如下:
- 打開 AAC 文件。
- 獲取音頻格式信息。如通道數,採樣率等。
- 從 AAC 文件中取出一幀 AAC 數據。
- 使用 AudioToolbox 解碼 AAC 數據包。
- 將解碼後的 PCM 數據送給 AudioUnit 播放聲音。
- 重複 3-5 步,直到整個 AAC 文件被讀完。
下面我們對以上每一步做詳細介紹。
Audio File
上面流程中第1、2、3步使用Audio File
服務。Audio File
可以用來創建、初始化音頻文件;讀寫音頻數據;對音頻文件進行優化;讀取和寫入音頻格式信息等等,功能十分強大。
我們看一下用到的幾個函數原型及其參數說明。
- AudioFileOpenURL用於打開一個媒體文件。原型如下: enum { kAudioFileReadPermission = 0x01, kAudioFileWritePermission = 0x02, kAudioFileReadWritePermission = 0x03 }; extern OSStatus AudioFileOpenURL ( CFURLRef inFileRef, // 打開文件的路徑 SInt8 inPermissions, // 打開文件的權限。 讀/寫/讀寫三種權限 AudioFileTypeID inFileTypeHint, // 文件類型提示信息,如果明確知道就填入,如果不知道填0. AudioFileID * outAudioFile // 文件述符 ID );
- AudioFileGetProperty 獲取音視頻格式信息。原型如下: enum { kAudioFilePropertyFileFormat = 'ffmt', kAudioFilePropertyDataFormat = 'dfmt', kAudioFilePropertyIsOptimized = 'optm', kAudioFilePropertyMagicCookieData = 'mgic', kAudioFilePropertyAudioDataByteCount = 'bcnt', kAudioFilePropertyAudioDataPacketCount = 'pcnt', kAudioFilePropertyMaximumPacketSize = 'psze', kAudioFilePropertyDataOffset = 'doff', kAudioFilePropertyChannelLayout = 'cmap', kAudioFilePropertyDeferSizeUpdates = 'dszu', kAudioFilePropertyMarkerList = 'mkls', kAudioFilePropertyRegionList = 'rgls', kAudioFilePropertyChunkIDs = 'chid', kAudioFilePropertyInfoDictionary = 'info', kAudioFilePropertyPacketTableInfo = 'pnfo', kAudioFilePropertyFormatList = 'flst', kAudioFilePropertyPacketSizeUpperBound = 'pkub', kAudioFilePropertyReserveDuration = 'rsrv', kAudioFilePropertyEstimatedDuration = 'edur', kAudioFilePropertyBitRate = 'brat', kAudioFilePropertyID3Tag = 'id3t', kAudioFilePropertySourceBitDepth = 'sbtd', kAudioFilePropertyAlbumArtwork = 'aart', kAudioFilePropertyAudioTrackCount = 'atct', kAudioFilePropertyUseAudioTrack = 'uatk' }; extern OSStatus AudioFileGetProperty( AudioFileID inAudioFile, //文件描述符,通過 AudioFileOpenURL 獲取。 AudioFilePropertyID inPropertyID, //屬性ID,如上所示 UInt32 * ioDataSize, // 輸出值空間大小 void * outPropertyData //輸出值地址。 );
- 從媒體文件中讀取一幀數據 extern OSStatus AudioFileReadPacketData ( AudioFileID inAudioFile, // 文件描述符 Boolean inUseCache, // 是否使用cache, 一般不用 UInt32 * ioNumBytes, // 輸入輸出參數 AudioStreamPacketDescription * outPacketDescriptions, //輸出參數 SInt64 inStartingPacket, // 要讀取的第一個數據包的數據包索引。 UInt32 * ioNumPackets, // 輸入輸出參數 void * outBuffer //輸出內存地址 );
- ioNumBytes: 該參數是輸入輸出參數。也就是說在調用該函數時,需要傳入它。在函數執行完成後,該函數會返回輸出值。在輸入時,表示outBuffer參數的大小(以位元組為單位)。在輸出時,表示實際讀取的位元組數。 如果在ioNumPackets參數中請求的數據包數目的位元組大小小於在outBuffer參數中傳遞的緩衝區大小,則輸入和輸出值將會有所不同。在這種情況下,該參數的輸出值小於其輸入值。
- outPacketDescriptions: 輸出參數,讀取數據包的描述數組。您在此參數中傳遞的數組必須足夠大,以適應ioNumPackets參數中請求的數據包數量的描述。該參數僅適用於可變比特率數據。 如果正在讀取的文件包含諸如線性PCM的恆定比特率(CBR)數據,則該參數不會被填充。 如果文件的數據格式為CBR,則傳遞NULL。
- ioNumPackets: 輸入輸出參數。在輸入時,要讀取的數據包數。在輸出時,實際讀取的數據包數。
- outBuffer: 您分配以保存讀取數據包的內存。通過將請求的數據包(ioNumPackets參數)乘以文件中音頻數據的典型數據包大小來確定適當的大小。對於未壓縮的音頻格式,數據包等於一個幀。
以上就是本文用到的三個Audio File
相關函數的介紹。下面我們介紹一下 AAC 解碼的相關內容。
AAC 解碼
AAC 解碼與 AAC 編碼的邏輯非常類似。
- 首先,設置音頻的輸入與輸出格式。在這裡音頻的輸入格式可以通過上一節中的 AudioFileGetProperty 方法從文件中提取來。
- 其次,創建 AAC 解碼器。
- 解碼。
設置輸出格式
輸入格式由通過Audio File
獲取。下面是輸出格式的代碼。如下:
AudioStreamBasicDescription outputFormat; memset(&outputFormat, 0, sizeof(outputFormat)); outputFormat.mSampleRate = 44100; outputFormat.mFormatID = kAudioFormatLinearPCM; outputFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked; outputFormat.mChannelsPerFrame = 1; outputFormat.mFramesPerPacket = 1; outputFormat.mBitsPerChannel = 16; outputFormat.mBytesPerFrame = inputFormat.mBitsPerChannel / 8 * inputFormat.mChannelsPerFrame; outputFormat.mBytesPerPacket = inputFormat.mBytesPerFrame * inputFormat.mFramesPerPacket;
創建解碼器除了上面說的要設置輸入、輸出數據格式外,還要告訴 AudioToolbox 是創建編碼器還是創建解碼器;如果是解碼器,還要指定子類型為 lpcm;是硬解碼還是軟解碼。
iOS為我們提供了 AudioClassDescription 來描述這些信息。它包括下面三個字段:
struct AudioClassDescription { OSType mType; OSType mSubType; OSType mManufacturer; };
- mType: 指明提編碼器還是解碼器。kAudioDecoderComponentType/kAudioEncoderComponentType。
- mSubType: 指明是 lpcm。
- mManufacturer: 指明是軟編還是硬編碼。
創建解碼器
有了上面的輸入、輸出格式及 AudioClassDescription 信息後,我們就可以創建解碼器了。代碼如下:
AudioConverterRef audioConverter; memset(&audioConverter, 0, sizeof(audioConverter)); NSAssert(AudioConverterNewSpecific(&inputFramat, & outputFormat, 1, &audioClassDescription, &audioConverter) == 0, nil);
通過上面的代碼,編碼器就創建好了。下面我們來進行解碼。
解碼
與編碼一樣,iOS 使用 AudioConverterFillComplexBuffer 方法進行解碼。它的參數如下:
AudioConverterFillComplexBuffer( inAudioConverter: AudioConverterRef, inInputDataProc: AudioConverterComplexInputDataProc, inInputDataProcUserData: UnsafeMutablePointer, ioOutputDataPacketSize: UnsafeMutablePointer<UInt32>, outOutputData: UnsafeMutablePointer<AudioBufferList>, outPacketDescription: AudioStreamPacketDescription ) -> OSStatus
- inAudioConverter : 轉碼器
- inInputDataProc : 回調函數。用於將AAC數據餵給解碼器。
- inInputDataProcUserData : 用戶自定義數據指針。
- ioOutputDataPacketSize : 輸出數據包大小。
- outOutputData : 輸出數據 AudioBufferList 指針。
- outPacketDescription : 輸出包描述符。
解碼的具體步驟如下:
- 首先,從媒體文件中取出一個音視幀。
- 其次,設置輸出地址。
- 然後,調用 AudioConverterFillComplexBuffer 方法,該方法又會調用 inInputDataProc 回調函數,將輸入數據拷貝到編碼器中。
- 最後,解碼。將解碼後的數據輸出到指定的輸出變量中。
具體代碼如下所示:
... //從媒體文件中讀取一幀數據 OSStatus status = AudioFileReadPacketData( audioFileID, NO, &ioNumBytes, //想要讀的io位元組數量 audioPacketFormats, //每個包的描述信息數組 idxStartReadPacket, //第一個包的開始位置索引 ioNumberDataPackets,//想要讀的包的數量 convertBuffer); //輸出地址 ... //設置輸入 AudioBufferList inAaudioBufferList; inAaudioBufferList.mBuffers[0].mDataByteSize = ioNumBytes; inAaudioBufferList.mBuffers[0].mData = convertBuffer; //設置輸出 uint8_t *buffer = (uint8_t *)malloc(bufferSize); memset(buffer, 0, bufferSize); AudioBufferList outAudioBufferList; outAudioBufferList.mNumberBuffers = 1; outAudioBufferList.mBuffers[0].mNumberChannels = inAaudioBufferList.mBuffers[0].mNumberChannels; outAudioBufferList.mBuffers[0].mDataByteSize = bufferSize; outAudioBufferList.mBuffers[0].mData = buffer; UInt32 ioOutputDataPacketSize = 1; //轉碼 NSAssert( AudioConverterFillComplexBuffer(audioConverter, inInputDataProc, //在該函數中要將上面的 convertBuffer 數據拷貝到解碼器的ioData里。 &inAaudioBufferList, &ioOutputDataPacketSize, &outAudioBufferList, NULL) == 0, nil);
下面我們看一下 inInputDataProc 這個回調函數的具體實現。其中 inUserData
就是在 AudioConverterFillComplexBuffer 方法中傳入的第三個參數,也就是輸入數據。
inInputDataProc 回調函數的作用就是將輸入數據拷貝到 ioData 中。ioData 就是解碼器解碼時用到的真正輸入緩衝區。
OSStatus inInputDataProc(AudioConverterRef inAudioConverter, UInt32 *ioNumberDataPackets, AudioBufferList *ioData, AudioStreamPacketDescription **outDataPacketDescription, void *inUserData) { AudioBufferList audioBufferList = *(AudioBufferList *)inUserData; ioData->mBuffers[0].mData = audioBufferList.mBuffers[0].mData; ioData->mBuffers[0].mDataByteSize = audioBufferList.mBuffers[0].mDataByteSize; return noErr; }
至此,AAC解碼部分就已經分析完了。下我們再看一下如何將解碼後的 PCM 數據播放出來。
播放 PCM
我們使用 iOS 中的 AudioUnit 工具來播放 PCM。AudioUnit的使用步驟如下:
- 設置音頻組件描述。其作用是通過該描述信息,可以在iOS中找到相關的音頻組件。
- 根據描述查找音視組件。
- 創建 AudioUnit 實例。
- 設置 AudioUnit 屬性。
- 播放 PCM。
下面我們來詳細介紹下每步:
設置音頻描述
// 描述音頻元件 AudioComponentDescription desc; desc.componentType = kAudioUnitType_Output; desc.componentSubType = kAudioUnitSubType_RemoteIO; desc.componentFlags = 0; desc.componentFlagsMask = 0; desc.componentManufacturer = kAudioUnitManufacturer_Apple;
該描述信息表明,我們使用AudioUnit的輸出組件。
查找音頻組件
// 查找一個組件 AudioComponent inputComponent = AudioComponentFindNext(NULL, &desc);
創建 AudioUnit
OSStatus status; AudioComponentInstance audioUnit; // 獲得 Audio Unit status = AudioComponentInstanceNew(inputComponent, &audioUnit); checkStatus(status);
設置屬性
#define kOutputBus 0 #define kInputBus 1 ... UInt32 flag = 1; // 為播放打開 IO status = AudioUnitSetProperty(audioUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, kOutputBus, &flag, sizeof(flag)); checkStatus(status); // 設置播放格式 status = AudioUnitSetProperty(audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, kOutputBus, & outputFormat, //參見編碼器格式 sizeof(audioFormat)); checkStatus(status); // 設置聲音輸出回調函數。當speaker需要數據時就會調用回調函數去獲取數據。它是 "拉" 數據的概念。 callbackStruct.inputProc = playbackCallback; callbackStruct.inputProcRefCon = self; status = AudioUnitSetProperty(audioUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Global, kOutputBus, &callbackStruct, sizeof(callbackStruct)); checkStatus(status);
播放PCM
AudioOutputUnitStart(audioUnit);
小結
本文介紹了如何將一個AAC文件播放出來的步驟。它包括:
- 打開 AAC 媒體文件。
- 獲取 AAC 媒體格式。
- 從 AAC 文件中讀取一個 AAC 音頻幀。
- 通過 AudioToolbox 解決 AAC 到 PCM。
- 通過 AudioUnit 播放 PCM。
- 循環執行 3-5步,直到文件結束。