Audio Processing Graph(AUGraph)完成即時錄音與播放功能

Audio Processing Graph(AUGraph)完成即時錄音與播放功能

iOS中不管是聲音或影像播放、錄音都有提供非常方便的Framework來使用,但這些功能大都只能直接將原來的內容存到檔案或是從檔案中讀取後播放,在一些場合中是必需要即時的處理播放與錄音的動作,像VoIP這類型的或是助聽器功能,它們的共通點都需要將麥克風的內容即時的播放出來,這時就必需要用到較進階的功能AUGraph的使用,接下來會完成一個最簡單的範例展示AUGraph基本功能,並且會在UI上增加靜音開關,這並非是調整硬體音量至最小,而是學習在Callback中改變即時要播放的資料,資料經過處理後,直接模擬類似靜音的功能。

AudioSession

在一開始之前,我要先了解AudioSession,它管理與取得Audio硬體的資訊,並且以SingleTon物件存在:

//-----------start-----------
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
//------------end------------

例如,我們可以知道硬體支援的SampleRate:

//-----------start-----------
NSLog(@"sampleRate %f",[audioSession sampleRate]);
//------------end------------

執行結果:

sampleRate 44100.000000

也可以設定SampleRate,但在這設定的SampleRate如果跟硬體的不同,它會照你設定的SampleRate並使用演算法將原始資料轉換成你的SampleRate,好比用軟體調整照片縮小、放大的感覺,但還是建議直接使用原生的SampleRate處理會來的比較好。

//-----------start-----------
    NSError *error;
    [audioSession setPreferredSampleRate:44100.0 error:&error];
    NSLog(@"preferredSampleRate %f",[audioSession preferredSampleRate]);
//------------end------------

執行結果:

preferredSampleRate 44100.000000

範例中設定了Audio屬性與SampleRate:

//-----------start-----------
    audioSession = [AVAudioSession sharedInstance];
    NSError *error;
    // set Category for Play and Record
    [audioSession setCategory:AVAudioSessionCategoryPlayAndRecord error:&error];
    [audioSession setPreferredSampleRate:(double)44100.0 error:&error];
//------------end------------

我們這次要實作即錄即播,所以將Category設定在AVAudioSessionCategoryPlayAndRecord,下面則是部分支援的Category設定:

AVAudioSessionCategoryAmbient;
AVAudioSessionCategorySoloAmbient;
AVAudioSessionCategoryPlayback;
AVAudioSessionCategoryRecord;
AVAudioSessionCategoryPlayAndRecord;
AVAudioSessionCategoryAudioProcessing;

之後再設定SampleRate為44100.0,這裡輸入的單位為hz,範例中的 AudioSession 只有設定這些,後面就要開始來談AUGraph的設定及功能簡介。

Remote I/O Unit

Remote I/O Unit是屬於Audio Unit其中之一,也是與硬體有關的一個Unit,它分為輸出端與輸入端,輸入端通常為 麥克風 ,輸出端為 喇叭耳機 …等,如果你同時要用輸入與輸出,使用上必需要做一些設定將它與硬體連接起來。

如同上圖所示,Remote I/O Unit又分為Element 0、Element 1,其中Element 0 掌控著輸出端,反之掌控著輸入端,且每個Element又分為Input scope及Output scope,我們必需要將Element 0Output scope喇叭接上,Element 1Input scope麥克風接上,程式段落會是下面這樣:

//-----------start-----------
    UInt32 oneFlag = 1;
    UInt32 busZero = 0;//Element 0
    CheckError(AudioUnitSetProperty(remoteIOUnit,
                                    kAudioOutputUnitProperty_EnableIO,
                                    kAudioUnitScope_Output,
                                    busZero,
                                    &oneFlag,
                                    sizeof(oneFlag)),"couldn't kAudioOutputUnitProperty_EnableIO with kAudioUnitScope_Output");
//------------end------------

上面程式會將喇叭與Element 0接上,所以會指定Element 0(這裡是傳入busZero值)的kAudioUnitScope_Output,並且將其設定oneFlag(傳入1),如果無錯誤產生代表設定成功。

//-----------start-----------
    //
    UInt32 busOne = 1; //Element 1
    CheckError(AudioUnitSetProperty(remoteIOUnit,
                                    kAudioOutputUnitProperty_EnableIO,
                                    kAudioUnitScope_Input,
                                    busOne,
                                    &oneFlag,
                                    sizeof(oneFlag)),"couldn't kAudioOutputUnitProperty_EnableIO with kAudioUnitScope_Input");
//------------end------------

反之,將Element 1的kAudioUnitScope_Input設定值為oneFlag(1),讓麥克風與Element 1確實接上,這些都接上後,可能會有個疑問,Element 0的Input scope要與Element 1的Output scope接上嗎?這樣是不是就可以完成指定功能?答案:不需要的。

圖中已經有表示,中間這段是我們程式需要做處理的,後面會提到怎麼利用程式將這2個接上,在之前先對程式中關鍵的名稱解釋:

  • remoteIOUnit 取得的Remote I/O Unit的指標會存入remoteIOUnit(自建指標名稱),這需要用到AUGraphNodeInfo取得
  • kAudioOutputUnitProperty_EnableIO 設定的類型為 Enable IO,也是Remote I/O
  • kAudioUnitScope_Output 圖中的Output scope
  • kAudioUnitScope_Input 圖中的Input scope
  • AudioUnitSetProperty 要取得AudioUnit的資料必需要使用這個指定的Function來取得,依照傳入的類型後,Function本身會將設定值傳入傳址的變數中,也就是範例中的&oneFlag

AUGraph (Audio Processing Graph)

AUGraph它的功能是來管理Audio Unit,像下圖看到的示意圖(來源:apple官網):

它可以平衡一些對Audio的處理,像是多輸入需要混音(Mixer)將多輸入聲音資料整合,以及需要處理音訊資料時可以利用它加入一個render的回呼(callback),能夠在音訊資料輸入時,回呼Function中處理音訊資料,這樣在進階音訊處理時相對的方便很多,不需要再管一些設定。

AUGraph使用時必需要先建立一個AUGraph,程式如下:

//-----------start-----------
    AUGraph auGraph;
    CheckError (NewAUGraph(&auGraph),"couldn't NewAUGraph");
    CheckError(AUGraphOpen(auGraph),"couldn't AUGraphOpen");
//------------end------------

建立完成後需要像開檔案一樣開啟它,這裡使用AUGraphOpen,接下來就可以開始使用AUGraph相關設定,在使用與AUGraph有關時會在命名前面加上AUGraph,像是:

  • AUGraphSetNodeInputCallback 設定回呼時會被呼叫的Function
  • AUGraphInitialize 初始化AUGraph
  • AUGraphUpdate 更新AUGraph,當有增加Node或移除時可以執行這將整個AUGraph規則更新
  • AUGraphStart 所有設定都無誤要開始執行AUGraph功能。

Node

Audio Processing Graph中必需要加入功能性的Node才能完成它,範例中會先加入Remote I/O類型的Node,官網中的圖示有使用了Mixer的Node及Remote I/O的Node:

圖示中(來源:apple官網)由Mixer Node的Output與Remote I/O Node的Input相接來玩成混音(Mixer),但範例中會完成單純以Remote I/O為主的功能,所以只加入Remote I/O Node:

//-----------start-----------
    //
    AudioUnit remoteIOUnit;
    AudioComponentDescription componentDesc;
    componentDesc.componentType = kAudioUnitType_Output;
    componentDesc.componentSubType = kAudioUnitSubType_RemoteIO;
    componentDesc.componentManufacturer = kAudioUnitManufacturer_Apple;
    componentDesc.componentFlags = 0;
    componentDesc.componentFlagsMask = 0;
    //
    CheckError (AUGraphAddNode(auGraph,&componentDesc,&remoteIONode),"couldn't add remote io node");
    CheckError(AUGraphNodeInfo(auGraph,remoteIONode,NULL,&remoteIOUnit),"couldn't get remote io unit from node");
//------------end------------

加入Node時必需要先產生元件的描述檔,告知你要加入的Node類型,加入成功後利用Node的資料來取得這個Node的Audio Unit元件,對於Node中的一些細項設定必需要靠取得的Audio Unit元件來設定。前面Remote I/O Unit中看到利用AudioUnitSetProperty設定時必需要指定你要哪個Audio Unit,每一個Node都是一個Audio Unit,都能對它做各別的設定,設定的方式是一樣的,但參數不一定相同,像在設定kAudioUnitType_Mixer時,可以設定它輸入要幾個Channel。

StreamFormat 設定音訊格式

AUGraph中如果不想要做特殊處理,那麼我們就可以將每個Node上的音訊格式設定成一樣,此時我們用一組音訊格式的設定檔將其他的都設定成相同的。

範例中我們從Output scope及Element 0中取得音訊格式,這個是喇叭的支援的音訊格式,我們利用它的設定值將其他的也設定成一樣的,程式中可看到將Element 1的Output scope及Element 0的Input scope都設定成一樣的設定值,如此一來我們的程式在使用時就不需要轉換音訊格式,直接拿音訊資料處理完後直接輸出就行,先回顧一下圖示再來看程式:

程式內容:

//-----------start-----------
    AudioStreamBasicDescription effectDataFormat;
    UInt32 propSize = sizeof(effectDataFormat);
    CheckError(AudioUnitGetProperty(remoteIOUnit,
                                    kAudioUnitProperty_StreamFormat,
                                    kAudioUnitScope_Output,
                                    0,
                                    &effectDataFormat,
                                    &propSize),"couldn't get kAudioUnitProperty_StreamFormat with kAudioUnitScope_Output");

    CheckError(AudioUnitSetProperty(remoteIOUnit,
                                    kAudioUnitProperty_StreamFormat,
                                    kAudioUnitScope_Output,
                                    1,
                                    &effectDataFormat,
                                    propSize),"couldn't set kAudioUnitProperty_StreamFormat with kAudioUnitScope_Output");

    CheckError(AudioUnitSetProperty(remoteIOUnit,
                                    kAudioUnitProperty_StreamFormat,
                                    kAudioUnitScope_Input,
                                    0,
                                    &effectDataFormat,
                                    propSize),"couldn't set kAudioUnitProperty_StreamFormat with kAudioUnitScope_Input");
//------------end------------

整個過程到這已經快接近完成,最後只要再設定render callback,後面我們再來看看怎麼設定。

Render Callback

當我們都將硬體與軟體都設定完成後,接下來就要在音訊資料進來時設定一個Callback,讓每次音訊資料從硬體轉成數位資料時都能直接呼叫Callbackle立即處理這些數位資料後再輸出至輸出端,依照Apple提供的結果,Callback是以C語言的Static Function存在的,所以事先就要將這段放在程式的上端,並將內容寫好,如下:

//-----------start-----------
//
static OSStatus PerformThru(
                            void                        *inRefCon,
                            AudioUnitRenderActionFlags  *ioActionFlags,
                            const AudioTimeStamp        *inTimeStamp,
                            UInt32                      inBusNumber,
                            UInt32                      inNumberFrames,
                            AudioBufferList             *ioData)
{
    CDYViewController *THIS=(__bridge CDYViewController*)inRefCon;

    OSStatus renderErr = AudioUnitRender(THIS->remoteIOUnit, ioActionFlags,
                                         inTimeStamp, 1, inNumberFrames, ioData);

:
:
你要處理的程式
:
:

    if (renderErr < 0) {
        return renderErr;
    }


    return noErr;
}

//------------end------------

Function的名稱可以自行訂定,範例上的名稱是 PerformThru ,當呼叫此Callback時,必需要再另外呼叫AudioUnitRender將Remote I/O的輸入端資料讀進來,其中每次資料是以Frame存在的,每筆Frame有N筆音訊資料內容(這與類比轉數位的概念有關,在此會以每筆Frame有N點),2聲道就是乘上2倍的資料量,整個資料都存在例子中的ioData指標中,這裡不先加以說明細節,先就範例需要做說明:

ioData->mBuffers[0].mData
ioData->mBuffers[0].mDataByteSize


  • mDataByteSize mData裡總共有幾筆資料
  • mData 進來Frame的資料,Apple一般會提供1024筆音訊資料,但在處理時還是多注意一下mDataByteSize的值
  • mBuffers[n] n值為0~1,根據音訊Channel而定,雙聲道為0~1,單聲道索引就只有0

關於這些資訊說明都在標頭檔CoreAudio/CoreAudioTypes.h之中,有些還是必需要看內容才能更了解其意義,利用取得的資料你可以當做是聲音處理去做好對應的處理,例如你可以做一個低通濾波的演算法,只能讓設定的頻率以下的資料通過再輸出資料…等,這個過於專業還是留給專業的來,所以範例以最簡單的靜音功能做示範,想要有靜音功能只要將資料內容的值全化為0就完成,程式如下:

//-----------start-----------
        for (UInt32 i=0; i < ioData->mNumberBuffers; i++)
        {
            memset(ioData->mBuffers[i].mData, 0, ioData->mBuffers[i].mDataByteSize);
        }
//------------end------------

利用memset將雙聲道資料都清為0再做輸出,這會放在接下來的範例中。前面完成Callback Function的內容,再來需要將使用的Callback填入AUGraph提供的Callback設定才能發揮:

//-----------start-----------
    AURenderCallbackStruct inputProc;
    inputProc.inputProc = PerformThru;
    inputProc.inputProcRefCon = (__bridge void *)(self);
    CheckError(AUGraphSetNodeInputCallback(auGraph, remoteIONode, 0, &inputProc),"Error setting io output callback");
//------------end------------

宣告AURenderCallbackStruct結構,並將Callback名稱PerformThru填入,其中也將inputProcRefCon填入self,也就是將此Class物件的指標傳入,如此一來就能在Callback取得所有的變數使用,範例中會有個Switch來存入是否要靜音的值就是因為有填入inputProcRefCon才能發揮作用,之後再使用AUGraphSetNodeInputCallback填入就完成,這是以Node基礎的Callback設定方式,從名稱看的出來是Input,也就是在有需要Input時就會呼叫,這點要注意。

執行範例程式

此次範例程式內容很精簡,單純要做實驗或實作一些音訊處理的程式時,可以下載範例程式直接增加程式在Callback中就能完成,希望這個精簡的範例能對初學者帶來幫助,為了呈現Callback的做用,先前有提到了,會利用靜音效果來呈現,所以UI方面增加Switch功能,利用開、關方式操作。

注意:使用程式之前請務必接上耳機操作

程式範例:SimpleRemoteIO

以上為AUGraph操作心得分享,圖示部分擷取Apple官網,著作權為官方所有,其中有很多細節及概念無法說明的很清楚,希望在未來時可以慢慢的補上,有任何不清楚或改正的地方請留下你的寶貴意見。

參考資料

Using Specific Audio Units

Audio Unit Hosting Guide