1. 程式人生 > >iOS 實時音訊採集與播放

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了。

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

希望大家 多多觀注!