iOS音頻播放(三)AudioUnit介紹與實戰

在iOS平臺上,全部的音頻框架底層都是基於AudioUnit實現的。較高層次的音頻框架包括: Media Player、 AV Foundation、OpenAL和Audio Toolbox,這些框架都封裝了AudioUnit,而後提供了更高層次的API(功能更少,職責更單一的接口)。git

當開發者在開發音視頻相關產品的時候,若是對音視頻須要更高程度的控制、性能以及靈活性,或者想要使用一些特殊功能(回聲消除)的時候,能夠直接使用AudioUnit API。蘋果官方文檔中描述,AudioUnit提供了音頻快速的模塊化處理,若是是在如下場景中,更適合使用AudioUnit而不是使用高層次的音頻框架。github

  • 想使用低延遲的音頻I/O(input或者output),好比說在VoIP的應用場景下。
  • 多路聲音的合成而且回放,好比遊戲或者音樂合成樂器的應用。
  • 使用AudioUnit裏面提供的特有功能,好比:回聲消除、Mix兩軌音頻,以及均衡器、壓縮器、混響器等效果器。
  • 須要圖狀結構來處理音頻,能夠將音頻處理模塊組裝到靈活的圖狀結構中,蘋果公司爲音頻開發者提供了這種API。

構建AudioUnit的時候須要制定類型(Type)、子類型(subtype)以及廠商(Manufacture).類型(Type)就是四大類型的AudioUnit的Type;而子類型(subtype)就是該大類型下面的子類型(好比Effect該大類型下面有EQ、Compressor、limiter等子類型);廠商(Manufacture)通常狀況下比較固定,直接寫成kAudioUnitManufacturer_Apple就能夠了。利用以上這三個變量開發者能夠完整描述出一個AudioUnit了,好比使用下面的代碼建立一個RemoteIO類型的AudioUnit:算法

AudioComponentDescription ioUnitDescription;
ioUnitDescription.componentType = kAudioUnitType_Output;
ioUnitDescription.componentSubType = kAudioUnitSubType_RemoteIO;
ioUnitDescription.componentManufacturer = kAudioUnitManufacturer_Apple;
ioUnitDescription.componentFlags = 0;
ioUnitDescription.componentFlagsMask = 0;
複製代碼

上訴代碼構造了RemoteIO類型的AudioUnit描述的結構體,那麼如何使用這個描述來構造真正的AudioUnit呢?有兩種方式:第一種方式是直接使用AudioUnit裸的建立方式;第二種方式是使用AUGraph和AUNode(其實一個AUNode就是對AudioUnit的封裝)來構建。下面就來分別介紹這兩種方式。bash

(1) 裸建立方式

首先根據AudioUnit的描述,找出實際的AudioUnit類型:框架

AudioComponent ioUnitRef = AudioComponentFindNext(NULL,&ioUnitDescription);
複製代碼

而後聲明一個AudioUnit引用:模塊化

AudioUnit ioUnitInstance;
複製代碼

最後根據類型建立這個AudioUnit實例:函數

AudioConponentInstanceNew(isUnitRef,&ioUnitInstance);
複製代碼

(2) AUGraph建立方式

首先聲明而且實例化一個AUGraph:性能

AUGraph processingGraph;
NewAUGraph(&processingGraph);
複製代碼

而後按照AudioUnit的描述在AUGraph中添加了一個AUNode:ui

AUNode ioNode;
AUGraphAddNode(processingGraph,&ioUnitDescription,&isNode);
複製代碼

接下來打開AUGraph,其實打開AUGraph的過程也是間接實例化AUGraph中全部的AUNode。注意,必須在獲取AudioUnit以前打開整個AUGraph,不然,咱們將不能從對應的AUNode中獲取正確的AudioUnit:編碼

AUGraphOpen(processingGraph);
複製代碼

最後在AUGraph中的某個Node裏得到AudioUnit的應用:

AudioUnit ioUnit;
AUGraphNodeInfo(processingGraph,ioNode,NULL,&ioUnit);
複製代碼

AudioUnit的通用參數設置

本節將以RemoteIO這個AudioUnit爲例來說解AudioUnit的參數設置,RemoteIO這個AudioUnit是與硬件IO相關的一個Unit,它分爲輸入端和輸出端(I表明Input,O表明Output)。輸入端通常是指麥克風,輸出端通常是指揚聲器(Speaker)或者耳機。若是須要同時使用輸入輸出,即K歌應用中的耳返功能(用戶在唱歌或者說話的同時,耳機會將麥克風收錄的聲音播放出來,讓用戶可以聽到本身的聲音),則須要開發者作一些設置將它們連起來。

上圖中的RemoteIO Unit分爲Element0和Element1,其中Element0控制輸出端,Element1控制輸入端,同時每一個Element又分爲Input Scope和Output Scope。若是開發者想要使用揚聲器的聲音播放功能,那麼必須將這個Unit的Element0的OutputScope和Speaker進行鏈接。而開發者想要使用麥克風的錄音功能,那麼必須將這個Unit的Element1的InputScope和麥克風進行鏈接。使用揚聲器的代碼以下:

OSStatus status = noErr;
UInt32 oneFlag = 1;
UInt32 busZero = 0;// Element 0
status = AudioUnitSetProperty(remoteIOUnit,kAudioOutputUnitProperty_EnableIO,kAudioUnitScope_output,busZero,&oneFlag,sizeof(oneFlag));
CheckStatus(status,@"Could not Connect To Speaker",YES);
複製代碼

上面這段代碼就是把RemoteIOUnit的Element0的OutputScope鏈接到Speaker上,鏈接過程會返回一個OSStatus類型的值,可使用自定義的CheckStatus函數來判斷錯誤而且輸出Could not Connect To Speaker的提示。具體的CheakStatus函數以下:

static void CheckStatus(OSStatus status,NSString *message,BOOL fatal)
{
      if(status != noErr)
      {
              char fourCC[16];
              *(UInt32 *)fourCC = CFSwapInt32HostToBig(status);
              fourCC[4] = '\0';
              if(isprint(fourCC[0]) && isprint(fourCC[1]) && isprint(fourCC[2]) && isprint(fourCC[3]))
                    NSLog(@"%@:%s",message,fourCC);
              else
                    NSLog(@"%@:%d",message,(int)status);
              if(fatal)
                    exit(-1);
      }
}
複製代碼

接下來再來看一下如何啓動麥克風的代碼:

UInt32 busOne = 1; // Element 1
AudioUnitSetProperty(remoteIOUnit,kAudioOutputUnitProperty_EnableIO,kAudioUnitScope_input,busOne,&oneFlag,sizeof(oneFlag));
複製代碼

上面這段代碼就是把RemoteIOUnit的Element1的InputScope鏈接上麥克風。鏈接成功以後,就應該給AudioUnit設置數據格式了,AudioUnit的數據格式分爲輸入和輸出兩個部分,下面先來看一下Audio Stream Format的描述:

UInt32 bytesPerSample = sizeof(Float32);
AudioStreamBasicDescription asbd;
bzero(&asbd,sizeof(asbd));
asbd.mFormatID = kAudioFormatLinearPCM;
asbd.mSampleRate = _sampleRate;
asbd.mChannelsPerFrame = channels;
asbd.mFramesPerPacket = 1;
asbd.mFormatFlags = kAudioFormatFlagsNativeFloatPacked | kAudioFormatFlagIsNonInterleaved;
asbd.mBitsPerChannel = 8*bytesPerSample;
asbd.mBytesPerFrame = bytePerSample;
asbd.mBytesPerPacket = bytesPerSamele;
複製代碼

上面這段代碼展現瞭如何填充AudioStreamBasicDescription結構體,其實在iOS平臺作音視頻開發久了就會知道:不論音頻仍是視頻的API都會接觸到不少StreamBasicDescription,該Description是描述音視頻具體格式的。下面就來具體分析一下上述代碼是如何制定格式的。

  • mFormatID參數可用來制定音頻的編碼格式,此處制定音頻的編碼格式爲PCM格式。
  • 接下來是設置聲音的採樣率、聲道數以及每一個Packet有幾個Frame。
  • mFormatFlags是用來描述聲音表示格式的參數,代碼中的第一個參數指定每一個sample的表示格式是Float格式,這點相似於以前講解的每一個sample都是使用2個字節(SInt16)來表示;而後就是後面的參數NonInterleaved,字面理解這個單詞的意思是非交錯的,其實對於音頻來說就是左右聲道是非交錯存放的,實際的音頻的數據會存儲在一個AudioBufferList結構中的變量mBuffers[0]裏面,右聲道就會在mBuffers[1]裏面;而若是mFormatFlags指定的是Interleaved的話,那麼左右聲道就會交錯排列在mBuffers[1]裏面。
  • 接下來的mBitsPerChannel表示的是一個聲道的音頻數據用多少位來表示,前面已經提到過每一個採樣時候用Float來表示,因此這裏使用8乘以每一個採樣的字節數來賦值。
  • 最終是參數mBytesPerFrame和mBytesPerPacket的賦值,這裏須要根據mFormatFlags的值來進行分配,若是在NonInterleaved的狀況下,就賦值爲bytesPerSamele(由於左右聲道是分開存放的),這樣才能表示一個Frame到底有多少個byte。

至此,咱們就徹底構造好了這個BasicDescription結構體,下面將這個結構體設置給對應的AudioUnit,代碼以下:

AudioUnitSetProperty(remoteIOUnit,kAudioOutputUnitProperty_StreamFormat,kAudioUnitScope_output,1,&asbd,sizeof(asbd));
複製代碼

AudioUnit的分類

介紹完了AudioUnit的通用設置以後,本節就來介紹一下AudioUnit的分類。iOS按照AudioUnit的用途將AudioUnit分爲五大類型,本節將從全局的角度出發來認識各大類型以及其下的子類型,而且還會介紹他們的用途,以及對應參數的意義。

(1) Effect Unit

類型是kAudioUnitType_Effect,主要提供聲音特效處理的功能。其子類型及用途說明以下。

  • 均衡效果器:子類型是kAudioUnitSubType_NBandEQ,主要做用是爲聲音的某些頻帶加強或者減弱能量,該效果器須要制定多個頻帶,而後爲各個頻帶設置帶寬設置寬度以及增益,最終將改變聲音在頻域上的能量分佈。
  • 壓縮效果器:子類型是kAudioUnitSubType_DynamicsProcessor,主要做用是當聲音較小的時候能夠提升聲音的能量,當聲音的能量草果設置的閾值時,能夠下降聲音的能量,固然應合理的設置做用時間、釋放時間以及觸發值,使得最終能夠將聲音在時域上的能量壓縮到必定範圍以內。
  • 混響效果器:子類型是kAudioUnitSubType_Reverb2,對於人聲處理來說這是很是重要的效果器,能夠想象本身身處在一個空房子中,若是有很是多的反射聲和原始聲疊加在一塊兒,那麼從聽感上可能會更有震撼力,可是同時原始聲音也會變得更加模糊,原始聲音的細節會被遮蓋住,因此混響的設置的大小對於不一樣的人來說會很不一致,能夠根據本身的喜愛來進行設置。 Effect Unit下最長使用的就是這三種效果器,固然其下還有不少子類型的效果器,像高通(HighPass)、低通(LowPass)、帶通(BandPass)、延遲(Delay)、壓限(Limiter)等效果器,你們能夠自行嘗試一下,感覺一下各自的效果。

(2) Mixer Units

類型是kAudioUnitType_Mixer,主要提供Mix多路聲音的功能。其子類型及用途以下。

  • 3D Mixer:該效果器在移動設備上是沒法使用的,僅僅在OS X上可使用,因此這裏不作介紹。
  • MultiChannelMixer:子類型是kAudioUnitSubType_MultiChannelMixer,它是多路聲音混音的效果器,能夠接收多路音頻的輸入,還能夠分別調整每一路音頻的增益與開關,並將多路音頻合併一路,該效果器在處理音頻的圖狀結構中很是有用。

(3) I/O Units

類型是kAudioUnitType_Output,它的用途就像分類的名字同樣,主要提供的就是I/O的功能。其子類型及用途說明以下。

  • RemoteIO:子類型是kAudioUnitSubType_RemoteIO,從名字上能夠看出,這是用來採集音頻和播放音頻的,其實當開發者的應用場景中要使用麥克風及揚聲器的時候會用到該AudioUnit.
  • Generic Output:子類型是kAudioUnitSubType_GenericOutput,當開發者須要進行離線處理,或者說在AUGraph中不適用Speaker(揚聲器)來驅動整個數據流,而是但願使用一個輸出(能夠放入內存隊列或者進行磁盤I/O操做)來驅動數據時,就使用該類型。

(4) Format Converter Units

類型是kAudioUnitType_FormatConverter,主要用於提供格式轉換的功能,好比:採樣格式由Float到SInt16的轉換、交錯和平鋪的格式轉換、單雙聲道的轉換等,其子類型及用途說明以下。

  • AUConverter:子類型是kAudioUnitSubType_AUConverter,格式轉換效果器,當某些效果器對輸入的音頻格式由明確的要求時,或者開發者將音頻數據輸入給一些其餘的編碼器進行編碼,又或者開發者想使用SInt16格式的PCM裸數據在其餘CPU上進行音頻算法計算等的場景下,就須要這個ConverterNode了。下面來看一個比較典型的場景,咱們自定義一個音頻播放器,由FFmpeg解碼出來的PCM數據是SInt16格式的,所以不能直接輸出給RemoteIO Unit,最終才能正常播放出來。
  • Time Pinch:子類型是kAudioUnitSubType_NewTimePitch,即變速變調效果器,能夠對聲音的音高、速度進行調整。

(5) Generator Units

類型是kAudioUnitType_Generator,在開發中咱們常用它來提供播放器的功能,其子類型及用途說明以下。

  • AudioFilePlayer:子類型是kAudioUnitSubType_AudioFilePlayer,在AudioUnit裏面,若是咱們的輸入不是麥克風,而但願其實一個媒體文件。須要注意的是,必須在初始化AUGraph以後,再去配置AudioFilePlayer的數據源以及播放範圍等屬性,不然就會出現錯誤,其實數據源仍是會調用AudioFile的解碼功能,將媒體文件中的壓縮數據解壓成爲PCM裸數據,最終再交給AudioFilePlayer Unit進行後續處理。

構造一個AUGraph

實際的K歌應用中,會對用戶發出的聲音進行處理,而且當即給用戶一個耳返(在50ms以內將聲音輸出到二級中,讓用戶能夠聽到)。那麼如何讓RemoteIOUnit利用麥克風採集出來的聲音,通過中間效果器的處理,最終輸出到Speaker中播放給用戶呢?下面就來介紹一下如何以AUGraph的方式將聲音採集、聲音處理以及聲音輸出的整個過程管理起來。

首先要知道數據能夠在通道中傳遞是由最右端Speak(RemoteIO Unit)來驅動的,它會向其上一級——AUNode要數據,而後它的前一級繼續向前一級要數據,並最終從RemoteIOUnit的Element1(即麥克風)中要數據,這樣就能夠將數據按相反的方向一級一級地傳遞下去,最終傳遞到RemoteIOUnit的Element0(即Speaker)並播放給用戶聽到。固然你想離線處理的時候應該由誰來進行驅動呢?其實在進行離線處理的時候應該使用Mixer Unit大類型下面子類型爲Generic Output的AudioUnit來作驅動端。那麼這些AudioUnit或者說AUNode是如何進行鏈接的呢?有兩種方式,第一種方式是直接將AUNode鏈接起來;第二種方式是經過回調的方式將AUNode鏈接起來。

(1) 直接鏈接的方式

AUGraphConnectNodeInput(mPlayerGraph,mPlayerNode,0,mPlayerIONode,0);
複製代碼

將Audio File Player Unit和RemotelIO Unit直接鏈接起來,當Remote Unit須要播放數據的時候,就會調用AudioFilePlay Unit來獲取數據,這樣就把這兩個AudioUnit鏈接起來了。

(2) 回調的方式

AURenderCallbackStruct renderProc;
renderProc.inputProc = &inputAvailableCallback;
renderProc.inputProcRefCon = (__bridge void *)self;
AUGraphSetNodeInputCallback(mGraph,ioNode,0,&finalRenderProc);
複製代碼

這段代碼首先是構造一個AURenderCallBack的結構體,並制定一個回調函數,而後設置給RemoteIO Unit的輸入端,當RemoteIO Unit須要數據輸入的時候就會回調該回調函數,回調函數代碼以下:

static OSStatus renderCallback(void *inRefCon,AudioUnitRenderActionFlags *ioActionFlags,const AudioTimeStamp *inTimeStamp,UInt32 inBusNumber,UInt32 inNumberFrames,AudioBufferList *ioData)
{
      OSStatus result = noErr;
      _unsafe_unretained AUGraphRecoder *THIS = (__bridge AUGraphRecorder *)inRefCon;
      AudioUnitRender(THIS->mixerUnit,ioActionFlags,inTimeStamp,0,isNumberFrames,ioData);
      result = ExtAudioFileWriteAsync(THIS->finalAudiofile,inNumberFrames,ioData);
      return result;
}
複製代碼

該回調函數主要完成兩件事情:第一件事情是去Mixer Unit裏面要數據,經過調用AudioUnitRender的方式來驅動Mixer Unit獲取數據,獲得數據以後放入ioData中,從而填充回到方法中的參數,將Mixer Unit與RemoteIO unit鏈接了起來;第二件事情則是利用ExtAudioFile將這段聲音編碼並寫入本地磁盤的一個文件中。

示例代碼

這裏(github.com/Nicholas86/…)是代碼。

相關文章
相關標籤/搜索