iOS下解碼AAC並播放

前言

今天我們介紹一下如何在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 可以用來創建、初始化音頻文件;讀寫音頻數據;對音頻文件進行優化;讀取和寫入音頻格式信息等等,功能十分強大。

我們看一下用到的幾個函數原型及其參數說明。

  1. AudioFileOpenURL用於打開一個媒體文件。原型如下: enum { kAudioFileReadPermission = 0x01, kAudioFileWritePermission = 0x02, kAudioFileReadWritePermission = 0x03 }; extern OSStatus AudioFileOpenURL ( CFURLRef inFileRef, // 打開文件的路徑 SInt8 inPermissions, // 打開文件的權限。 讀/寫/讀寫三種權限 AudioFileTypeID inFileTypeHint, // 文件類型提示信息,如果明確知道就填入,如果不知道填0. AudioFileID * outAudioFile // 文件述符 ID );
  2. 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 //輸出值地址。 );
  3. 從媒體文件中讀取一幀數據 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步,直到文件結束。