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