AudioUnit

在iOS平臺上,所有的音頻框架底層都是基于AudioUnit實(shí)現(xiàn)的。較高層次的音頻框架包括:MediaPlayer、AVFoundation、OpenAL和AudioToolBox,這些框架都封裝了AudioUnit,然后提供了更高層次的API(功能更少,職責(zé)更單一的接口)。


16a8dc6da3e49765.png

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

  • 想使用低延遲的音頻I/O(input或者output),比如說在VoIP(基于IP的語音傳輸)的應(yīng)用場景下。
  • 多路聲音的合成并且回放,比如游戲和音樂合成器的應(yīng)用。
  • 使用AudioUnit里面提供的特有功能,比如:回聲消除、Mix兩軌音頻、以及均衡器、混響器等效果器。
  • 需要圖狀結(jié)構(gòu)來處理音頻,可以將音頻處理模塊組裝到靈活的圖狀結(jié)構(gòu)中,蘋果公司為音頻開發(fā)者提供了這種API。

1.認(rèn)識AudioSession

在iOS的音視頻開發(fā)中,使用具體API之前都會先創(chuàng)建一個會話,這里也不意外,但在這之前,先來認(rèn)識一下音頻會話(AudioSession),其用于管理與獲取iOS設(shè)備音頻的硬件信息,并且是以單例的形式存在??梢允褂萌缦麓a來獲取AudioSession的實(shí)例:

AVAudioSession *audioSession = [AVAudioSession sharedInstance];

獲得AudioSession的實(shí)例之后,就可以設(shè)置以何種方式使用音頻硬件做哪些處理了,基本的設(shè)置具體如下所示。
1)根據(jù)我們需要硬件設(shè)備提供的能力來設(shè)置類別:

[audioSession setCategory:AVAudioSessionCategoryPlayAndRecord error:&error];

2)設(shè)置I/O的Buffer,Buffer越小則說明延遲越低:

NSTimeInterval bufferDuration = 0.002;
[audioSession serPreferredIOBufferDuration:bufferDuration error:&error];

3)設(shè)置采樣頻率,讓硬件設(shè)備按照設(shè)置的采樣頻率來采集或者播放音頻:

double hwSampleRate = 44100.0;
[audioSession setPreferredSampleRate:hwSampleRate error:&error];

4)當(dāng)設(shè)置完畢所有的參數(shù)之后就可以激活A(yù)udioSession了,代碼如下:

[audioSession setActive:YES error:&error];

2.構(gòu)建AudioUnit

在創(chuàng)建并啟用音頻會話之后,就可以構(gòu)建AudioUnit了。構(gòu)建AudioUnit的時候需要制定類型(Type)、子類型(subtype)以及廠商(Manufacture).類型(Type)就是四大類型的AudioUnit的Type;而子類型(subtype)就是該大類型下面的子類型(比如Effect該大類型下面有EQ、Compressor、limiter等子類型);廠商(Manufacture)一般情況下比較固定,直接寫成kAudioUnitManufacturer_Apple就可以了。利用以上這三個變量開發(fā)者可以完整描述出一個AudioUnit了,比如使用下面的代碼創(chuàng)建一個RemoteIO類型的AudioUnit:

AudioComponentDescription ioUnitDescription;
ioUnitDescription.conponentType = kAudioUnitType_Output;
ioUnitDescription.componentSubType = kAudioUnitSubType_RemoteIO;
ioUnitDescription.componentManufactureer = kAudioUnitManufacturer_Apple;
ioUnitDescription.conponentFlags = 0;
ioUnitDescription.conponentFlagsMask = 0;

上訴代碼構(gòu)造了RemoteIO類型的AudioUnit描述的結(jié)構(gòu)體,那么如何使用這個描述來構(gòu)造真正的AudioUnit呢?有兩種方式:第一種方式是直接使用AudioUnit裸的創(chuàng)建方式;第二種方式是使用AUGraph和AUNode(其實(shí)一個AUNode就是對AudioUnit的封裝)來構(gòu)建。下面就來分別介紹這兩種方式。
(1)裸創(chuàng)建方式
首先根據(jù)AudioUnit的描述,找出實(shí)際的AudioUnit類型:

AudioComponent ioUnitRef = AudioComponentFindNext(NULL,&ioUnitDescription);

然后聲明一個AudioUnit引用:

AudioUnit ioUnitInstance;

最后根據(jù)類型創(chuàng)建這個AudioUnit實(shí)例:

AudioConponentInstanceNew(isUnitRef,&ioUnitInstance);

(2)AUGraph創(chuàng)建方法
首先聲明并且實(shí)例化一個AUGraph:

AUGraph processingGraph;
NewAUGraph(&processingGraph);

然后按照AudioUnit的描述在AUGraph中添加了一個AUNode:

AUNode ioNode;
AUGraphAddNode(processingGraph,&ioUnitDescription,&isNode);

接下來打開AUGraph,其實(shí)打開AUGraph的過程也是間接實(shí)例化AUGraph中所有的AUNode。注意,必須在獲取AudioUnit之前打開整個AUGraph,否則,我們將不能從對應(yīng)的AUNode中獲取正確的AudioUnit:

AUGraphOpen(processingGraph);

最后在AUGraph中的某個Node里獲得AudioUnit的應(yīng)用:

AudioUnit ioUnit;
AUGraphNodeInfo(processingGraph,ioNode,NULL,&ioUnit);

3.AudioUnit的通用參數(shù)設(shè)置

本節(jié)將以RemoteIO這個AudioUnit為例來講解AudioUnit的參數(shù)設(shè)置,RemoteIO這個AudioUnit是與硬件IO相關(guān)的一個Unit,它分為輸入端和輸出端(I代表Input,O代表Output)。輸入端一般是指麥克風(fēng),輸出端一般是指揚(yáng)聲器(Speaker)或者耳機(jī)。如果需要同時使用輸入輸出,即K歌應(yīng)用中的耳返功能(用戶在唱歌或者說話的同時,耳機(jī)會將麥克風(fēng)收錄的聲音播放出來,讓用戶能夠聽到自己的聲音),則需要開發(fā)者做一些設(shè)置將它們連起來。


11386185-b205827ce3a7309a.png

上圖中的RemoteIO Unit分為Element0和Element1,其中Element0控制輸出端,Element1控制輸入端,同時每個Element又分為Input Scope和Output Scope。如果開發(fā)者想要使用揚(yáng)聲器的聲音播放功能,那么必須將這個Unit的Element0的OutputScope和Speaker進(jìn)行連接。而開發(fā)者想要使用麥克風(fēng)的錄音功能,那么必須將這個Unit的Element1的InputScope和麥克風(fēng)進(jìn)行連接。使用揚(yáng)聲器的代碼如下:

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函數(shù)來判斷錯誤并且輸出Could not Connect To Speaker的提示。具體的CheakStatus函數(shù)如下:

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

接下來再來看一下如何啟動麥克風(fēng)的代碼:

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

上面這段代碼就是把RemoteIOUnit的Element1的InputScope連接上麥克風(fēng)。連接成功之后,就應(yīng)該給AudioUnit設(shè)置數(shù)據(jù)格式了,AudioUnit的數(shù)據(jù)格式分為輸入和輸出兩個部分,下面先來看一下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結(jié)構(gòu)體,其實(shí)在iOS平臺做音視頻開發(fā)久了就會知道:不論音頻還是視頻的API都會接觸到很多StreamBasicDescription,該Description是描述音視頻具體格式的。下面就來具體分析一下上述代碼是如何制定格式的。

  • mFormatID參數(shù)可用來制定音頻的編碼格式,此處制定音頻的編碼格式為PCM格式。
  • 接下來是設(shè)置聲音的采樣率、聲道數(shù)以及每個Packet有幾個Frame。
  • mFormatFlags是用來描述聲音表示格式的參數(shù),代碼中的第一個參數(shù)指定每個sample的表示格式是Float格式,這點(diǎn)類似于之前講解的每個sample都是使用2個字節(jié)(SInt16)來表示;然后就是后面的參數(shù)NonInterleaved,字面理解這個單詞的意思是非交錯的,其實(shí)對于音頻來講就是左右聲道是非交錯存放的,實(shí)際的音頻的數(shù)據(jù)會存儲在一個AudioBufferList結(jié)構(gòu)中的變量mBuffers[0]里面,右聲道就會在mBuffers[1]里面;而如果mFormatFlags指定的是Interleaved的話,那么左右聲道就會交錯排列在mBuffers[1]里面。
  • 接下來的mBitsPerChannel表示的是一個聲道的音頻數(shù)據(jù)用多少位來表示,前面已經(jīng)提到過每個采樣時候用Float來表示,所以這里使用8乘以每個采樣的字節(jié)數(shù)來賦值。
  • 最終是參數(shù)mBytesPerFrame和mBytesPerPacket的賦值,這里需要根據(jù)mFormatFlags的值來進(jìn)行分配,如果在NonInterleaved的情況下,就賦值為bytesPerSamele(因為左右聲道是分開存放的),這樣才能表示一個Frame到底有多少個byte。

至此,我們就完全構(gòu)造好了這個BasicDescription結(jié)構(gòu)體,下面將這個結(jié)構(gòu)體設(shè)置給對應(yīng)的AudioUnit,代碼如下:

AudioUnitSetProperty(remoteIOUnit,kAudioOutputUnitProperty_StreamFormat,kAudioUnitScope_output,1,&asbd,sizeof(asbd));

4.AudioUnit的分類

介紹完了AudioUnit的通用設(shè)置之后,本節(jié)就來介紹一下AudioUnit的分類。iOS按照AudioUnit的用途將AudioUnit分為五大類型,本節(jié)將從全局的角度出發(fā)來認(rèn)識各大類型以及其下的子類型,并且還會介紹他們的用途,以及對應(yīng)參數(shù)的意義。

(1)Effect Unit

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

  • 均衡效果器:子類型是kAudioUnitSubType_NBandEQ,主要作用是為聲音的某些頻帶增強(qiáng)或者減弱能量,該效果器需要制定多個頻帶,然后為各個頻帶設(shè)置帶寬設(shè)置寬度以及增益,最終將改變聲音在頻域上的能量分布。
  • 壓縮效果器:子類型是kAudioUnitSubType_DynamicsProcessor,主要作用是當(dāng)聲音較小的時候可以提高聲音的能量,當(dāng)聲音的能量草果設(shè)置的閾值時,可以降低聲音的能量,當(dāng)然應(yīng)合理的設(shè)置作用時間、釋放時間以及觸發(fā)值,使得最終可以將聲音在時域上的能量壓縮到一定范圍之內(nèi)。
  • 混響效果器:子類型是kAudioUnitSubType_Reverb2,對于人聲處理來講這是非常重要的效果器,可以想象自己身處在一個空房子中,如果有非常多的反射聲和原始聲疊加在一起,那么從聽感上可能會更有震撼力,但是同時原始聲音也會變得更加模糊,原始聲音的細(xì)節(jié)會被遮蓋住,所以混響的設(shè)置的大小對于不同的人來講會很不一致,可以根據(jù)自己的喜好來進(jìn)行設(shè)置。
    Effect Unit下最長使用的就是這三種效果器,當(dāng)然其下還有很多子類型的效果器,像高通(HighPass)、低通(LowPass)、帶通(BandPass)、延遲(Delay)、壓限(Limiter)等效果器,大家可以自行嘗試一下,感受一下各自的效果。

(2)Mixer Units

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

  • 3D Mixer:該效果器在移動設(shè)備上是無法使用的,僅僅在OS X上可以使用,所以這里不做介紹。
  • MultiChannelMixer:子類型是kAudioUnitSubType_MultiChannelMixer,它是多路聲音混音的效果器,可以接收多路音頻的輸入,還可以分別調(diào)整每一路音頻的增益與開關(guān),并將多路音頻合并一路,該效果器在處理音頻的圖狀結(jié)構(gòu)中非常有用。

(3)I/O Units

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

  • RemoteIO:子類型是kAudioUnitSubType_RemoteIO,從名字上可以看出,這是用來采集音頻和播放音頻的,其實(shí)當(dāng)開發(fā)者的應(yīng)用場景中要使用麥克風(fēng)及揚(yáng)聲器的時候會用到該AudioUnit.
  • Generic Output:子類型是kAudioUnitSubType_GenericOutput,當(dāng)開發(fā)者需要進(jìn)行離線處理,或者說在AUGraph中不適用Speaker(揚(yáng)聲器)來驅(qū)動整個數(shù)據(jù)流,而是希望使用一個輸出(可以放入內(nèi)存隊列或者進(jìn)行磁盤I/O操作)來驅(qū)動數(shù)據(jù)時,就使用該類型。

(4)Format Converter Units

類型是kAudioUnitType_FormatConverter,主要用于提供格式轉(zhuǎn)換的功能,比如:采樣格式由Float到SInt16的轉(zhuǎn)換、交錯和平鋪的格式轉(zhuǎn)換、單雙聲道的轉(zhuǎn)換等,其子類型及用途說明如下。

  • AUConverter:子類型是kAudioUnitSubType_AUConverter,格式轉(zhuǎn)換效果器,當(dāng)某些效果器對輸入的音頻格式由明確的要求時,或者開發(fā)者將音頻數(shù)據(jù)輸入給一些其他的編碼器進(jìn)行編碼,又或者開發(fā)者想使用SInt16格式的PCM裸數(shù)據(jù)在其他CPU上進(jìn)行音頻算法計算等的場景下,就需要這個ConverterNode了。下面來看一個比較典型的場景,我們自定義一個音頻播放器,由FFmpeg解碼出來的PCM數(shù)據(jù)是SInt16格式的,因此不能直接輸出給RemoteIO Unit,最終才能正常播放出來。
  • Time Pinch:子類型是kAudioUnitSubType_NewTimePitch,即變速變調(diào)效果器,可以對聲音的音高、速度進(jìn)行調(diào)整。

(5)Generator Units

類型是kAudioUnitType_Generator,在開發(fā)中我們經(jīng)常使用它來提供播放器的功能,其子類型及用途說明如下。

  • AudioFilePlayer:子類型是kAudioUnitSubType_AudioFilePlayer,在AudioUnit里面,如果我們的輸入不是麥克風(fēng),而希望其實(shí)一個媒體文件。需要注意的是,必須在初始化AUGraph之后,再去配置AudioFilePlayer的數(shù)據(jù)源以及播放范圍等屬性,否則就會出現(xiàn)錯誤,其實(shí)數(shù)據(jù)源還是會調(diào)用AudioFile的解碼功能,將媒體文件中的壓縮數(shù)據(jù)解壓成為PCM裸數(shù)據(jù),最終再交給AudioFilePlayer Unit進(jìn)行后續(xù)處理。

5.構(gòu)造一個AUGraph

實(shí)際的K歌應(yīng)用中,會對用戶發(fā)出的聲音進(jìn)行處理,并且立即給用戶一個耳返(在50ms之內(nèi)將聲音輸出到二級中,讓用戶可以聽到)。那么如何讓RemoteIOUnit利用麥克風(fēng)采集出來的聲音,經(jīng)過中間效果器的處理,最終輸出到Speaker中播放給用戶呢?下面就來介紹一下如何以AUGraph的方式將聲音采集、聲音處理以及聲音輸出的整個過程管理起來。


16a8dc6ddfa36bc8.png

首先要知道數(shù)據(jù)可以在通道中傳遞是由最右端Speak(RemoteIO Unit)來驅(qū)動的,它會向其上一級——AUNode要數(shù)據(jù),然后它的前一級繼續(xù)向前一級要數(shù)據(jù),并最終從RemoteIOUnit的Element1(即麥克風(fēng))中要數(shù)據(jù),這樣就可以將數(shù)據(jù)按相反的方向一級一級地傳遞下去,最終傳遞到RemoteIOUnit的Element0(即Speaker)并播放給用戶聽到。當(dāng)然你想離線處理的時候應(yīng)該由誰來進(jìn)行驅(qū)動呢?其實(shí)在進(jìn)行離線處理的時候應(yīng)該使用Mixer Unit大類型下面子類型為Generic Output的AudioUnit來做驅(qū)動端。那么這些AudioUnit或者說AUNode是如何進(jìn)行連接的呢?有兩種方式,第一種方式是直接將AUNode連接起來;第二種方式是通過回調(diào)的方式將AUNode連接起來。
(1)直接連接的方式

AUGraphConnectNodeInput(mPlayerGraph,mPlayerNode,0,mPlayerIONode,0);

將Audio File Player Unit和RemotelIO Unit直接連接起來,當(dāng)Remote Unit需要播放數(shù)據(jù)的時候,就會調(diào)用AudioFilePlay Unit來獲取數(shù)據(jù),這樣就把這兩個AudioUnit連接起來了。
(2)回調(diào)的方式

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

這段代碼首先是構(gòu)造一個AURenderCallBack的結(jié)構(gòu)體,并制定一個回調(diào)函數(shù),然后設(shè)置給RemoteIO Unit的輸入端,當(dāng)RemoteIO Unit需要數(shù)據(jù)輸入的時候就會回調(diào)該回調(diào)函數(shù),回調(diào)函數(shù)代碼如下:

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

該回調(diào)函數(shù)主要完成兩件事情:第一件事情是去Mixer Unit里面要數(shù)據(jù),通過調(diào)用AudioUnitRender的方式來驅(qū)動Mixer Unit獲取數(shù)據(jù),得到數(shù)據(jù)之后放入ioData中,從而填充回到方法中的參數(shù),將Mixer Unit與RemoteIO unit連接了起來;第二件事情則是利用ExtAudioFile將這段聲音編碼并寫入本地磁盤的一個文件中。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容