在iOS平臺上,全部的音頻框架底層都是基於AudioUnit實現的。較高層次的音頻框架包括: Media Player、 AV Foundation、OpenAL和Audio Toolbox,這些框架都封裝了AudioUnit,而後提供了更高層次的API(功能更少,職責更單一的接口)。git
當開發者在開發音視頻相關產品的時候,若是對音視頻須要更高程度的控制、性能以及靈活性,或者想要使用一些特殊功能(回聲消除)的時候,能夠直接使用AudioUnit API。蘋果官方文檔中描述,AudioUnit提供了音頻快速的模塊化處理,若是是在如下場景中,更適合使用AudioUnit而不是使用高層次的音頻框架。github
構建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
首先根據AudioUnit的描述,找出實際的AudioUnit類型:框架
AudioComponent ioUnitRef = AudioComponentFindNext(NULL,&ioUnitDescription);
複製代碼
而後聲明一個AudioUnit引用:模塊化
AudioUnit ioUnitInstance;
複製代碼
最後根據類型建立這個AudioUnit實例:函數
AudioConponentInstanceNew(isUnitRef,&ioUnitInstance);
複製代碼
首先聲明而且實例化一個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);
複製代碼
本節將以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是描述音視頻具體格式的。下面就來具體分析一下上述代碼是如何制定格式的。
至此,咱們就徹底構造好了這個BasicDescription結構體,下面將這個結構體設置給對應的AudioUnit,代碼以下:
AudioUnitSetProperty(remoteIOUnit,kAudioOutputUnitProperty_StreamFormat,kAudioUnitScope_output,1,&asbd,sizeof(asbd));
複製代碼
介紹完了AudioUnit的通用設置以後,本節就來介紹一下AudioUnit的分類。iOS按照AudioUnit的用途將AudioUnit分爲五大類型,本節將從全局的角度出發來認識各大類型以及其下的子類型,而且還會介紹他們的用途,以及對應參數的意義。
類型是kAudioUnitType_Effect,主要提供聲音特效處理的功能。其子類型及用途說明以下。
類型是kAudioUnitType_Mixer,主要提供Mix多路聲音的功能。其子類型及用途以下。
類型是kAudioUnitType_Output,它的用途就像分類的名字同樣,主要提供的就是I/O的功能。其子類型及用途說明以下。
類型是kAudioUnitType_FormatConverter,主要用於提供格式轉換的功能,好比:採樣格式由Float到SInt16的轉換、交錯和平鋪的格式轉換、單雙聲道的轉換等,其子類型及用途說明以下。
類型是kAudioUnitType_Generator,在開發中咱們常用它來提供播放器的功能,其子類型及用途說明以下。
實際的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/…)是代碼。