内容简介:使用AudioUnitGraph来实现一个混音功能,受到官方混音例子的影响,做了一个不同输入源到不同声道的效果,如左边放音乐、右边放录音。这个 demo 为了认识两点:1. AUGraph 2.audioUnit 自带的混音。
demo地址 ,AudioMusicMixer这个target。
使用AudioUnitGraph来实现一个混音功能,受到官方混音例子的影响,做了一个不同输入源到不同声道的效果,如左边放音乐、右边放录音。
这个 demo 为了认识两点:1. AUGraph 2.audioUnit 自带的混音。
AUGraph 是什么?
graph是图形的意思,它是指一个处理音频的组件组成的功能网络。比如录音组件、播放组件、混音组件、特效等,把它们组合在一起,构成一个音频数据处理的流程,可以不是线性的,那么就成了2维的图。通过对各种组件的自由组合,几乎可以完成你想要的任何需求。
如果了解滤镜,那么和这个网络结构也是类似的。
这个 demo 使用AUGraph构建一个流程:3个输入源,两个音频文件和一个录音(remoteIO的audioUnit),提供数据给mixer,每个输入源可以调整声道和声音大小。
流程类似
构建 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
表示调节的是输入声音。
以上所述就是小编给大家介绍的《audioUnit混音》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
C# 6.0本质论
[美] Mark Michaelis(马克·米凯利斯)、[美] Eric Lippert(埃里克·利珀特) / 周靖、庞燕 / 人民邮电出版社 / 2017-2-1 / 108
这是C#领域中一部广受好评的名作,作者用一种易于理解的方式详细介绍了C#语言的各个方面。全书共有21章和4个附录(其中哟2个附录从网上下载),介绍了C#语言的数据类型、操作符、方法、类、接口、异常处理等基本概念,深入讨论了泛型、迭代器、反射、线程和互操作性等高级主题,还介绍了LINQ技术,以及与其相关的扩展方法、分部方法、Lambda表达式、标准查询操作符和查询表达式等内容。每章开头的“思维导图”......一起来看看 《C# 6.0本质论》 这本书的介绍吧!