iOS 實時音頻採集與播放
- 2020 年 4 月 2 日
- 筆記
前言
在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)
使用流程概要
- 描述音頻元件(kAudioUnitType_Output/kAudioUnitSubType_RemoteIO /kAudioUnitManufacturerApple)
- 使用 AudioComponentFindNext(NULL, &descriptionOfAudioComponent) 獲得 AudioComponent。AudioComponent有點像生產 Audio Unit 的工廠。
- 使用 AudioComponentInstanceNew(ourComponent, &audioUnit) 獲得 Audio Unit 實例。
- 使用 AudioUnitSetProperty函數為錄製和回放開啟IO。
- 使用 AudioStreamBasicDescription 結構體描述音頻格式,並使用AudioUnitSetProperty進行設置。
- 使用 AudioUnitSetProperty 設置音頻錄製與放播的回調函數。
- 分配緩衝區。
- 初始化 Audio Unit。
- 啟動 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了。
「知識無窮盡,只取我所需」。這就是我的思想,哈!
希望大家 多多觀注!