audioUnit混音

demo地址,AudioMusicMixer这个target。

使用AudioUnitGraph来实现一个混音功能,受到官方混音例子的影响,做了一个不同输入源到不同声道的效果,如左边放音乐、右边放录音。

这个demo为了认识两点:1. AUGraph 2.audioUnit自带的混音。

AUGraph 是什么?

graph是图形的意思,它是指一个处理音频的组件组成的功能网络。比如录音组件、播放组件、混音组件、特效等,把它们组合在一起,构成一个音频数据处理的流程,可以不是线性的,那么就成了2维的图。通过对各种组件的自由组合,几乎可以完成你想要的任何需求。

如果了解滤镜,那么和这个网络结构也是类似的。

这个demo使用AUGraph构建一个流程:3个输入源,两个音频文件和一个录音(remoteIO的audioUnit),提供数据给mixer,每个输入源可以调整声道和声音大小。

流程类似
IOWithoutRenderCallback_2x.png

构建AUGraph

NewAUGraph(&processingGraph);
...
status = AUGraphAddNode(processingGraph, &playDesc, &recordPlayNode);
...
status = AUGraphAddNode(processingGraph, &mixerDesc, &mixerNode);
status = AUGraphOpen(processingGraph);

NewAUGraph新建,然后不断通过AUGraphAddNode添加节点,也就是一个处理组件。最后AUGraphOpen打开。

AUGraphAddNode的3个参数分别是:要添加的AUGraph、节点性质描述和节点变量。

属性描述使用AudioComponentDescription对象,对于录音和播放都使用:

    playDesc.componentType = kAudioUnitType_Output;
    playDesc.componentSubType = kAudioUnitSubType_RemoteIO;

而混音组件是:

    mixerDesc.componentType = kAudioUnitType_Mixer;
    mixerDesc.componentSubType = kAudioUnitSubType_MultiChannelMixer;

当然还有其他类型的混音组件,目前只研究了这个。

获取AudioUnit

开启之后,使用status = AUGraphNodeInfo(processingGraph, recordPlayNode, NULL, &recordPlayUnit);获取node对应的AudioUnit。可以使用audioUnit的大量功能函数来做复杂的处理。

在node之间建立连接
status = AUGraphConnectNodeInput(processingGraph, mixerNode, 0, recordPlayNode, 0);

参数分别是:AUGraph变量、前一个node、前一个node的element索引、后一个node、后一个node的element索引。

每个node都可能有多个输入输出流,每个对应一个element,可以理解为机器的连接线之类的。上面的这段代码就是:把mixerNode的element0输出连接到recordPlayNode的element0。

使用AUGraphConnect的好处是不需要我们编程处理数据了,两个node之间连接好之后,系统会处理它们之间的数据传输。mixerNode是负责混音的节点,recordPlayNode即负责播放也负责录音(remoteIO的audioUnit固定两个element,一个录音一个播放),它的element0负责播放,所以最后一个参数传了0。而对于kAudioUnitSubType_MultiChannelMixer类型混音节点,输入可能有多个,但输出是一个,即element0。

所以上面这段代码的实际作用是:把混音结束后的音频流输出给播放组件。

设置音频格式

AUGraphConnect可以建立连接后让系统处理,但对于更复杂的需求,还需要自己来手动处理音频数据。

在这之前要先设定音频格式,为了简便,固定3个输入源:索引0是第一个音频文件,1是录音数据,2是第二个音频文件。

for (int i = 0; i<MixerInputSourceCount; i++) {
        if ([[self.audioChannelTypes objectForKey:@(i)] integerValue] == AUGraphMixerChannelTypeStereo) {
            sourceStreamFmts[i] = *([[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32
                                                                         sampleRate:44100
                                                                           channels:2
                                                                        interleaved:NO].streamDescription);
        }else{
            sourceStreamFmts[i] = *([[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32
                                                                         sampleRate:44100
                                                                           channels:1
                                                                        interleaved:YES].streamDescription);
        }
    }

MixerInputSourceCount是输入源数量,根据设置的声道类型,来确定音频格式。两种格式的区别只是声道和interleaved这个属性。

在双声道时设为2,左边或右边单声道设为1。interleaved这个单词是"交错,交叉存取"的意思,这个在设为NO的时候,AudioBufferList包含两个AudioBuffer,每个负责一个声道的数据,而设为YES时,是一个AudioBuffer,两个声道的数据混在一起的。跟视频数据如YUV里面的plane的概念类似。

左右声道分开的好处是,可以单独的填充左边或右边的声音,比如把音频文件1的数据都只填充到第一个AudioBuffer里,那只有左边有声音。

mixer设置多个输入源
    UInt32 inputCount = MixerInputSourceCount;
    status = AudioUnitSetProperty(mixerUnit, kAudioUnitProperty_ElementCount, kAudioUnitScope_Input, 0, &inputCount, sizeof(inputCount));

然后给每个输入源设置回调和输入格式:

for (int i = 0; i<inputCount; ++i) {
        AURenderCallbackStruct mixerInputCallback;
        mixerInputCallback.inputProc = &mixerDataInput;
        mixerInputCallback.inputProcRefCon = (__bridge void*)self;
        
        status = AUGraphSetNodeInputCallback(processingGraph, mixerNode, i, &mixerInputCallback);
        
        status = AudioUnitSetProperty(mixerUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, i, &mixStreamFmt, sizeof(AudioStreamBasicDescription));
    }

这里变量i代表着输入源的索引,也是element的索引。在文档里,element和bus是同一个东西,都是指一个完整的数据流处理环境(context),和输入输出流是对应的。

构建音频读取器

自己写的TFAudioFileReader类,内部使用ExtAudioFile来读取,因为这个系统组件自带转码,而且还可以转采样率,非常好用。

开启关闭

status = AUGraphInitialize(processingGraph);

在open之后,初始化。

然后就可以使用AUGraphStart(processingGraph);AUGraphStop(processingGraph);控制开启关闭。

输入回调

开启了AUGraph之后,mixer节点会不断的从它的输入源输入数据,方式就是AUGraphSetNodeInputCallback设置的回调函数。

而mixer输出部分不需要我们操心了,连接建立后,播放系统会处理。

static OSStatus mixerDataInput(void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData){
    
    AUGraphMixer *mixer = (__bridge AUGraphMixer *)(inRefCon);
    
    //inBusNumber为输入源的索引,根据这个值来从不用源获取音频数据
    if (inBusNumber == FirstAudioFileIndex) {
        
        [mixer readAudioFile:0 numberFrames:inNumberFrames toBuffer:ioData];
        
    }else if (inBusNumber == RecordUnitSourceIndex){
        
        [mixer readRecordedAudio:ioActionFlags timeStamp:inTimeStamp numberFrames:inNumberFrames toBuffer:ioData];
    }else if (inBusNumber == SecondAudioFileIndex){
        [mixer readAudioFile:1 numberFrames:inNumberFrames toBuffer:ioData];
    }
    
    return 0;
}

inBusNumber这里用了bus这个名词,其实还是指element,或说输入输出流。使用这个索引确定是哪个输入流,从不同的源获取数据。

实现输入源独立的声道控制

回调函数里的参数ioData是分配了内存的,只要把需要的数据填充进去就好了。

interleaved影响的就是这里的这个ioData的格式。

双声道时,直接调用AudioUnitRender,把ioData穿进去赋值。

跟我设想稍微不同的是,这样读取出来的ioData只有第一个AudioBuffer有数据,即ioData->mBuffers[1].mData打印出来都是0,虽然录音的audioUnit也是设置了双声道的,效果就是录音只有左边有声音。简便起见,直接把第一个的数据赋值到第二个。

但声道的时候,就只填充一个AudioBuffer,比如只想在左边,就只填充ioData->mBuffers[0]。然后把另一个AudioBuffer的数据全部抹掉。

        if (channelType == AUGraphMixerChannelTypeLeft) {
            bufList.mBuffers[0] = ioData->mBuffers[leftChannelIndex]; //只填充左声道数据
            memset(ioData->mBuffers[rightChannelIndex].mData, 0, ioData->mBuffers[rightChannelIndex].mDataByteSize);
        }
        ...
调整音量
AudioUnitSetParameter(mixerUnit, kMultiChannelMixerParam_Volume, kAudioUnitScope_Input, (UInt32)index, volume, 0);

index是输入源的索引,kAudioUnitScope_Input表示调节的是输入声音。

可继续

  • 把输入源封装成一个类,可以是文件、是录音、是网络数据流等,然后混音可以自由的组合和拆解各个输入源。不仅调节音量,或者还可以加上变调等,就跟使用滤镜处理图像一样。

  • 混音输出可以加一个实时输出到文件,给mixer组件加一个renderCallback就可以拿到数据,然后可以输出到文件或者推送到服务器都没问题。

  • 录音和混音之间加一个缓冲区,为了简便,是在mixer需要数据的时候调用AudioUnitRender,但混音需求数据的频率和录音输出数据的频率不一定一致,会导致某些数据丢失。

  • 在其他iphone或mac试一下双声道录音是否可以得到两个声道数据不同。否则双声道没有意义了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容