iOS 實時音頻採集與播放

前言

在iOS中有很多方法可以進行音影片採集。如 AVCaptureDevice, AudioQueue以及Audio Unit。其中 Audio Unit是最底層的介面,它的優點是功能強大,延遲低; 而缺點是學習成本高,難度大。對於一般的iOS應用程式,AVCaptureDevice和AudioQueue完全夠用了。但對於音影片直播,最好還是使用 Audio Unit 進行處理,這樣可以達到最佳的效果,著名的 WebRTC 就使用的 Audio Unit 做的音頻採集與播放。今天我們就重點介紹一下Audio Unit的基本知識和使用。

下圖是 Audio Unit在 iOS架構中所處的位置:

基本概念

在介紹 Audio Unit 如何使用之前,先要介紹一下Audio Unit的基本概念,這樣更有利於我們理解對它的使用。

  • Audio Unit的種類 Audio Units共可分為四大類,並可細分為七種,可參考下表:
  • Audo Unit 的內部結構 參考下圖,Audio Unit 內部結構分為兩大部分,Scope 與Element。其中 scope 又分三種,分別是 input scope, output scope, global scope。而 element 則是 input scope 或 output scope 內的一部分。
  • Audio Unit 的輸入與輸出 下圖是一個 I/O type 的 Audio Unit,其輸入為麥克風,其輸出為喇叭。這是一個最簡單的Audio Unit使用範例。

ioUnit.png The input element is element 1 (mnemonic device: the letter 「I」 of the word 「Input」 has an appearance similar to the number 1) The output element is element 0 (mnemonic device: the letter 「O」 of the word 「Output」 has an appearance similar to the number 0)

使用流程概要

  1. 描述音頻元件(kAudioUnitType_Output/kAudioUnitSubType_RemoteIO /kAudioUnitManufacturerApple
  2. 使用 AudioComponentFindNext(NULL, &descriptionOfAudioComponent) 獲得 AudioComponent。AudioComponent有點像生產 Audio Unit 的工廠。
  3. 使用 AudioComponentInstanceNew(ourComponent, &audioUnit) 獲得 Audio Unit 實例。
  4. 使用 AudioUnitSetProperty函數為錄製和回放開啟IO。
  5. 使用 AudioStreamBasicDescription 結構體描述音頻格式,並使用AudioUnitSetProperty進行設置。
  6. 使用 AudioUnitSetProperty 設置音頻錄製與放播的回調函數。
  7. 分配緩衝區。
  8. 初始化 Audio Unit。
  9. 啟動 Audio Unit。

初始化

初始化看起來像下面這樣。我們有一個 AudioComponentInstance 類型的成員變數,它用於存儲 Audio Unit。

下面的音頻格式用16位表式一個取樣。

#define kOutputBus 0  #define kInputBus 1    // ...      OSStatus status;  AudioComponentInstance audioUnit;    // 描述音頻元件  AudioComponentDescription desc;  desc.componentType = kAudioUnitType_Output;  desc.componentSubType = kAudioUnitSubType_RemoteIO;  desc.componentFlags = 0;  desc.componentFlagsMask = 0;  desc.componentManufacturer = kAudioUnitManufacturer_Apple;    // 獲得一個元件  AudioComponent inputComponent = AudioComponentFindNext(NULL, &desc);    // 獲得 Audio Unit  status = AudioComponentInstanceNew(inputComponent, &audioUnit);  checkStatus(status);    // 為錄製打開 IO  UInt32 flag = 1;  status = AudioUnitSetProperty(audioUnit,                                kAudioOutputUnitProperty_EnableIO,                                kAudioUnitScope_Input,                                kInputBus,                                &flag,                                sizeof(flag));  checkStatus(status);    // 為播放打開 IO  status = AudioUnitSetProperty(audioUnit,                                kAudioOutputUnitProperty_EnableIO,                                kAudioUnitScope_Output,                                kOutputBus,                                &flag,                                sizeof(flag));  checkStatus(status);    // 描述格式  audioFormat.mSampleRate         = 44100.00;  audioFormat.mFormatID           = kAudioFormatLinearPCM;  audioFormat.mFormatFlags        = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;  audioFormat.mFramesPerPacket    = 1;  audioFormat.mChannelsPerFrame   = 1;  audioFormat.mBitsPerChannel = 16;  audioFormat.mBytesPerPacket = 2;  audioFormat.mBytesPerFrame      = 2;    // 設置格式  status = AudioUnitSetProperty(audioUnit,                                kAudioUnitProperty_StreamFormat,                                kAudioUnitScope_Output,                                kInputBus,                                &audioFormat,                                sizeof(audioFormat));  checkStatus(status);  status = AudioUnitSetProperty(audioUnit,                                kAudioUnitProperty_StreamFormat,                                kAudioUnitScope_Input,                                kOutputBus,                                &audioFormat,                                sizeof(audioFormat));  checkStatus(status);      // 設置數據採集回調函數  AURenderCallbackStruct callbackStruct;  callbackStruct.inputProc = recordingCallback;  callbackStruct.inputProcRefCon = self;  status = AudioUnitSetProperty(audioUnit,                                kAudioOutputUnitProperty_SetInputCallback,                                kAudioUnitScope_Global,                                kInputBus,                                &callbackStruct,                                sizeof(callbackStruct));  checkStatus(status);    // 設置聲音輸出回調函數。當speaker需要數據時就會調用回調函數去獲取數據。它是 "拉" 數據的概念。  callbackStruct.inputProc = playbackCallback;  callbackStruct.inputProcRefCon = self;  status = AudioUnitSetProperty(audioUnit,                                kAudioUnitProperty_SetRenderCallback,                                kAudioUnitScope_Global,                                kOutputBus,                                &callbackStruct,                                sizeof(callbackStruct));  checkStatus(status);    // 關閉為錄製分配的緩衝區(我們想使用我們自己分配的)  flag = 0;  status = AudioUnitSetProperty(audioUnit,                              kAudioUnitProperty_ShouldAllocateBuffer,                              kAudioUnitScope_Output,                              kInputBus,                              &flag,                              sizeof(flag));    // 初始化  status = AudioUnitInitialize(audioUnit);  checkStatus(status);

開啟 Audio Unit

OSStatus status = AudioOutputUnitStart(audioUnit);  checkStatus(status);

關閉 Audio Unit

OSStatus status = AudioOutputUnitStop(audioUnit);  checkStatus(status);

結束 Audio Unit

AudioComponentInstanceDispose(audioUnit);

錄製回調

static OSStatus recordingCallback(void *inRefCon,                                    AudioUnitRenderActionFlags *ioActionFlags,                                    const AudioTimeStamp *inTimeStamp,                                    UInt32 inBusNumber,                                    UInt32 inNumberFrames,                                    AudioBufferList *ioData) {        // TODO:      // 使用 inNumberFrames 計算有多少數據是有效的      // 在 AudioBufferList 里存放著更多的有效空間        AudioBufferList *bufferList; //bufferList里存放著一堆 buffers, buffers的長度是動態的。        // 獲得錄製的取樣數據        OSStatus status;        status = AudioUnitRender([audioInterface audioUnit],                               ioActionFlags,                               inTimeStamp,                               inBusNumber,                               inNumberFrames,                               bufferList);      checkStatus(status);        // 現在,我們想要的取樣數據已經在bufferList中的buffers中了。      DoStuffWithTheRecordedAudio(bufferList);      return noErr;  }

播放回調

static OSStatus playbackCallback(void *inRefCon,                                    AudioUnitRenderActionFlags *ioActionFlags,                                    const AudioTimeStamp *inTimeStamp,                                    UInt32 inBusNumber,                                    UInt32 inNumberFrames,                                    AudioBufferList *ioData) {      // Notes: ioData 包括了一堆 buffers      // 儘可能多的向ioData中填充數據,記得設置每個buffer的大小要與buffer匹配好。      return noErr;  }

結束

Audio Unit可以做很多非常棒的的工作。如混音,音頻特效,錄製等等。它處於 iOS 開發架構的底層,特別合適於音影片直播這種場景中使用。

我們今天介紹的只是 Audio Unit眾多功能中的一小點知識,但這一點點知識對於我來說已經夠用了。對於那些想了解更多Audio Unit的人,只好自行去google了。

「知識無窮盡,只取我所需」。這就是我的思想,哈!

希望大家 多多觀注!