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使用範例。
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了。
“知識無窮盡,只取我所需”。這就是我的思想,哈!
希望大家 多多觀注!