iOS下 AAC 音頻編碼

編碼結構圖

前言

iOS下Apple為我們提供了非常方便的音頻編解碼工具AudioToolbox。該工具中包含了常見的編解碼庫,如AAC、iLBC、OPUS等。今天我們就介紹一下如何使用 AudioToolbox 進行AAC音頻的編碼工作。

AAC編碼的基本流程

在 iOS 中進行AAC編碼的流程比較簡單,按以下幾步即可完成。

  • 設置AAC編器的輸入、輸出格式。
  • 創建AAC編碼器。
  • 轉碼。
  • 得到AAC編碼數據後,增加ADTS頭。該頭用於區分每個AAC數據幀。

下面我們詳細介紹每一步。

設置轉碼格式

在創建編碼器之前,我們首先要設置好編碼器的輸入數據格式和輸出數據格式。比如輸入數據是單聲道還是雙聲道,數據是什麼格式的,取樣率是多少等。同樣的,輸出參數是AAC,還是OPUS? 每個傳輸包的大小等。只有這樣,AudioToolbox才清楚他要創建一個什麼樣的編解碼器。

當然,這與創建編碼器的函數也有關。該函數的前兩個輸入參數就是音頻輸入格式和輸出格式。函數原型如下:

AudioConverterNewSpecific(      inSourceFormat: AudioStreamBasicDescription, //輸入參數      inDestinationFormat: AudioStreamBasicDescription, //輸出參數      inNumberClassDescriptions: UInt32, //音頻描述符數量      inClassDescriptions: AudioClassDescription, //音頻描述符數組      outAudioConverter: AudioConverterRef //編碼器      ) -> OSStatus

所以,基於以上兩個原因,在創建編碼器之前一定要先將輸入、輸出格式設置好。

下面我們來看一下設置輸入、輸出格式的程式碼。

AudioStreamBasicDescription inAudioStreamBasicDescription =   *CMAudioFormatDescriptionGetStreamBasicDescription((CMAudioFormatDescriptionRef)   CMSampleBufferGetFormatDescription(sampleBuffer));

上面這段程式碼就是輸入格式的設置。這裡用到了一個小技巧,設置編碼器的輸入格式是通過傳入的第一個音頻數據包來獲得的。因為,在iOS中每個音影片的輸入數據中都包含了必要的參數。而iOS也為我們提供了提取這些數據的方法,非常方便。

下面的程式碼是對編碼器輸出格式的設置。 注釋已經寫的非常詳細了。

// 先將輸出描述符清0  AudioStreamBasicDescription outAudioStreamBasicDescription = {0};    // 設置取樣率,有 32K, 44.1K,48K  outAudioStreamBasicDescription.mSampleRate = 44100;    // 音頻格式可以設置為 :  // kAudioFormatMPEG4AAC_HE  // kAudioFormatMPEG4AAC_HE_V2  // kAudioFormatMPEG4AAC  outAudioStreamBasicDescription.mFormatID = kAudioFormatMPEG4AAC;    // 指明格式的細節. 設置為 0 說明沒有子格式。  // 如果 mFormatID 設置為 kAudioFormatMPEG4AAC_HE 該值應該為0  outAudioStreamBasicDescription.mFormatFlags = kMPEG4Object_AAC_LC;    // 每個音頻包的位元組數.  // 該欄位設置為 0, 表明包里的位元組數是變化的。  // 對於使用可變包大小的格式,請使用AudioStreamPacketDescription結構指定每個數據包的大小。  outAudioStreamBasicDescription.mBytesPerPacket = 0;    // 每個音頻包幀的數量. 對於未壓縮的數據設置為 1.  // 動態碼率格式,這個值是一個較大的固定數字,比如說AAC的1024。  // 如果是動態幀數(比如Ogg格式)設置為0。  outAudioStreamBasicDescription.mFramesPerPacket = 1024;    // 每個幀的位元組數。對於壓縮數據,設置為 0.  outAudioStreamBasicDescription.mBytesPerFrame = 0;    // 音頻聲道數  outAudioStreamBasicDescription.mChannelsPerFrame = 1;    // 壓縮數據,該值設置為0.  outAudioStreamBasicDescription.mBitsPerChannel = 0;    // 用於位元組對齊,必須是0.  outAudioStreamBasicDescription.mReserved = 0; 

下一步,我們來創建編碼器。

創建編解碼器

創建編碼器除了上面說的要設置輸入輸出數據格式外,還要告訴 AudioToolbox 是創建編碼器還是創建解碼器;是創建 AAC 的,還是創建OPUS的;是硬編碼還是軟編碼。

iOS為我們提供了 AudioClassDescription 來描述這些資訊。它包括下面三個欄位:

struct AudioClassDescription {      OSType  mType;      OSType  mSubType;      OSType  mManufacturer;  };
  • mType: 指明提編碼器還是解碼器。kAudioDecoderComponentType/kAudioEncoderComponentType。
  • mSubType: 指明是 AAC, iLBC 還是 OPUS等。
  • mManufacturer: 指明是軟編還是硬編碼。

了解了上面的資訊後,我們再來看下面的程式碼就很好理解了。

  • 首先通過 AudioFormatGetPropertyInfo 獲取音頻屬性資訊。在這裡就是獲得所有與 格式ID一致的描術資訊的個數。格式ID在這裡就是 kMPEG4Object_AAC_LC
  • 然後,使用 AudioFormatGetProperty 獲取音頻格式屬性值,在這裡就是得到所有的音頻描述符。
  • 找到與用戶指定一致的描述符。
  • 最後調用 AudioConverterNewSpecific 創建轉碼器。
...    AudioClassDescription audioClassDescription;  memset(&audioClassDescription, 0, sizeof(audioClassDescription));  UInt32 size;    //根據編碼格式,獲取描述符個數。  NSAssert(AudioFormatGetPropertyInfo(kAudioFormatProperty_Encoders,              sizeof(outAudioStreamBasicDescription.mFormatID),              &outAudioStreamBasicDescription.mFormatID,              &size) == noErr, nil);    uint32_t count = size / sizeof(AudioClassDescription);    //取出所有的描述符  AudioClassDescription descriptions[count];  NSAssert(AudioFormatGetProperty(kAudioFormatProperty_Encoders,              sizeof(outAudioStreamBasicDescription.mFormatID),              &outAudioStreamBasicDescription.mFormatID,              &size,              descriptions) == noErr, nil);    //找出與輸出格式一致的軟編描述符  for (uint32_t i = 0; i < count; i++) {        if ((outAudioStreamBasicDescription.mFormatID == descriptions[i].mSubType) &&          (kAppleSoftwareAudioCodecManufacturer == descriptions[i].mManufacturer)) {            memcpy(&audioClassDescription, &descriptions[i], sizeof(audioClassDescription));        }  }    //創建軟編碼器  NSAssert(audioClassDescription.mSubType == outAudioStreamBasicDescription.mFormatID &&              audioClassDescription.mManufacturer == kAppleSoftwareAudioCodecManufacturer, nil);    AudioConverterRef audioConverter;  memset(&audioConverter, 0, sizeof(audioConverter));  NSAssert(AudioConverterNewSpecific(&inAudioStreamBasicDescription,              &outAudioStreamBasicDescription,              1,              &audioClassDescription,              &audioConverter) == 0, nil);  ...

創建好編碼器後,還要修改一下編碼器的碼率。如果要正確的編碼,編碼碼率參數是必須設置的。程式碼如下:

...    UInt32 outputBitrate = 64000;  UInt32 propSize = sizeof(outputBitrate);    if(result == noErr) {      result = AudioConverterSetProperty(audioConverter,                   kAudioConverterEncodeBitRate,                   propSize,                   &outputBitrate);  }  ...

需要注意,AAC並不是隨便的碼率都可以支援。比如,如果PCM取樣率是44100KHz,那麼碼率可以設置64000bps,如果是16K,可以設置為32000bps。

設置好碼率後,可以通過 AudioConverterGetProperty 方法查詢一下是否已經設置成功。程式碼如下:

UInt32 value = 0;  size = sizeof(value);  AudioConverterGetProperty(audioConverter,              kAudioConverterPropertyMaximumOutputPacketSize,              &size,              &value);

下面我們來看下如何進行轉碼。

轉碼

iOS 使用 AudioConverterFillComplexBuffer 方法進行轉碼。它的參數如下:

AudioConverterFillComplexBuffer(              inAudioConverter: AudioConverterRef,              inInputDataProc: AudioConverterComplexInputDataProc,              inInputDataProcUserData: UnsafeMutablePointer,              ioOutputDataPacketSize: UnsafeMutablePointer<UInt32>,              outOutputData: UnsafeMutablePointer<AudioBufferList>,              outPacketDescription: AudioStreamPacketDescription              ) -> OSStatus
  • inAudioConverter : 轉碼器
  • inInputDataProc : 回調函數。用於將PCM數據餵給編碼器。
  • inInputDataProcUserData : 用戶自定義數據指針。
  • ioOutputDataPacketSize : 輸出數據包大小。
  • outOutputData : 輸出數據 AudioBufferList 指針。
  • outPacketDescription : 輸出包描述符。

下面是轉碼的具體程式碼:

  • 首先,創建一個 AudioBufferList,並將輸入數據存到 AudioBufferList里。
  • 其次,設置輸出。
  • 然後,調用 AudioConverterFillComplexBuffer 方法,該方法又會調用 inInputDataProc 回調函數,將輸入數據拷貝到編碼器中。
  • 最後,轉碼。將轉碼後的數據輸出到指定的輸出變數中。
//設置輸入  AudioBufferList inAaudioBufferList;  CMBlockBufferRef blockBuffer;  CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(sampleBuffer, NULL, &inAaudioBufferList, sizeof(inAaudioBufferList), NULL, NULL, 0, &blockBuffer);  NSAssert(inAaudioBufferList.mNumberBuffers == 1, nil);    //設置輸出  uint32_t bufferSize = inAaudioBufferList.mBuffers[0].mDataByteSize;  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,                      &inAaudioBufferList,                      &ioOutputDataPacketSize,                      &outAudioBufferList, NULL) == 0,  nil);    //將輸出數據變成 NSData 數據  NSData *data = [NSData                  dataWithBytes:outAudioBufferList.mBuffers[0].mData                  length:outAudioBufferList.mBuffers[0].mDataByteSize];    free(buffer);  CFRelease(blockBuffer);

下面我們看一下 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編碼部分就已經分析完了。但很多時候我們需要將 AAC 數據保存成文件。如果我們直接將一幀一幀的AAC數據直接寫入文件,再從AAC文件中讀取數據交由解碼器解碼,是無法成功的。原因很簡單,解碼器搞不清楚文件里每個 AAC 幀到底有多大。

解決的辦法是在每一幀前加一個頭。這是一個比較通用的做法。在AAC中加的頭格式我們稱為 ADTS頭。

增加ADTS頭

ADTS共7或9個位元組。一般情況下使用 7 位元組。它的結構如下:

Structure AAAAAAAA AAAABCCD EEFFFFGH HHIJKLMM MMMMMMMM MMMOOOOO OOOOOOPP (QQQQQQQQ QQQQQQQQ)

Letter Length (bits) Description

  • A 12 syncword 0xFFF, all bits must be 1
  • B 1 MPEG Version: 0 for MPEG-4, 1 for MPEG-2
  • C 2 Layer: always 0
  • D 1 protection absent, Warning, set to 1 if there is no CRC and 0 if there is CRC
  • E 2 profile, the MPEG-4 Audio Object Type minus 1
  • F 4 MPEG-4 Sampling Frequency Index (15 is forbidden)
  • G 1 private bit, guaranteed never to be used by MPEG, set to 0 when encoding, ignore when decoding
  • H 3 MPEG-4 Channel Configuration (in the case of 0, the channel configuration is sent via an inband PCE)
  • I 1 originality, set to 0 when encoding, ignore when decoding
  • J 1 home, set to 0 when encoding, ignore when decoding
  • K 1 copyrighted id bit, the next bit of a centrally registered copyright identifier, set to 0 when encoding, ignore when decoding
  • L 1 copyright id start, signals that this frame's copyright id bit is the first bit of the copyright id, set to 0 when encoding, ignore when decoding
  • M 13 frame length, this value must include 7 or 9 bytes of header length: FrameLength = (ProtectionAbsent == 1 ? 7 : 9) + size(AACFrame)
  • O 11 Buffer fullness
  • P 2 Number of AAC frames (RDBs) in ADTS frame minus 1, for maximum compatibility always use 1 AAC frame per ADTS frame
  • Q 16 CRC if protection absent is 0

下面是具體程式碼。通過上面的描述就非常容易理解了。

- (NSData*) adtsDataForPacketLength:(NSUInteger)packetLength {      int adtsLength = 7;      char *packet = malloc(sizeof(char) * adtsLength);      // Variables Recycled by addADTStoPacket      int profile = 2;  //AAC LC      //39=MediaCodecInfo.CodecProfileLevel.AACObjectELD;      int freqIdx = 4;  //44.1KHz      int chanCfg = 1;  //MPEG-4 Audio Channel Configuration. 1 Channel front-center      NSUInteger fullLength = adtsLength + packetLength;      // fill in ADTS data      packet[0] = (char)0xFF; // 11111111     = syncword      packet[1] = (char)0xF9; // 1111 1 00 1  = syncword MPEG-2 Layer CRC      packet[2] = (char)(((profile-1)<<6) + (freqIdx<<2) +(chanCfg>>2));      packet[3] = (char)(((chanCfg&3)<<6) + (fullLength>>11));      packet[4] = (char)((fullLength&0x7FF) >> 3);      packet[5] = (char)(((fullLength&7)<<5) + 0x1F);      packet[6] = (char)0xFC;      NSData *data = [NSData dataWithBytesNoCopy:packet length:adtsLength freeWhenDone:YES];      return data;  }

小結

本文主要講解了 iOS 下 如何進行 AAC 編碼。它的流程豐常簡單。包括:

  • 設置輸入、輸出格式。
  • 創建AAC編碼器。
  • 轉碼。
  • 增加ADTS頭。

這裡的難點是參數的設置。而且很多參數之間是聯動的,所以設置時要特別小心。

另外,通過本文你可以了解到,其實在iOS下,其它音頻編碼的流程與AAC編碼的流程都是一樣的,我們只需要調整不同的參數即可。