天天看點

iOS 實時音頻采集與播放

作者:音視訊開發T哥

1、前言

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

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

iOS 實時音頻采集與播放

2、基本概念

在介紹 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 內的一部分。
iOS 實時音頻采集與播放
  • 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)

3、使用流程概要

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

C++音視訊學習資料免費擷取方法:關注音視訊開發T哥,點選「連結」即可免費擷取2023年最新C++音視訊開發進階獨家免費學習大禮包!

4、初始化

初始化看起來像下面這樣。我們有一個 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 Unitstatus = AudioComponentInstanceNew(inputComponent, &audioUnit);
checkStatus(status);// 為錄制打開 IOUInt32 flag = 1;
status = AudioUnitSetProperty(audioUnit, 
                              kAudioOutputUnitProperty_EnableIO, 
                              kAudioUnitScope_Input, 
                              kInputBus,
                              &flag, 
                              sizeof(flag));
checkStatus(status);// 為播放打開 IOstatus = 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);           

5、錄制回調

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;
}           

6、播放回調

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

7、結束

Audio Unit可以做很多非常棒的的工作。如混音,音頻特效,錄制等等。它處于 iOS 開發架構的底層,特别合适于音視訊直播這種場景中使用。

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

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

原文連結:iOS 實時音頻采集與播放