前言
相关文章:
使用VideoToolbox硬编码H.264
使用VideoToolbox硬解码H.264
使用AudioToolbox编码AAC
使用AudioToolbox播放AAC
HLS点播实现(H.264和AAC码流)
HLS推流的实现(iOS和OS X系统)
iOS在线音频流播放
Audio Unit播放PCM文件
Audio Unit录音(播放伴奏+耳返)
Audio Unit播放aac/m4a/mp3等文件
Audio Unit和ExtendedAudioFile播放音频
前文介绍了AudioUnit的录音/播放、AudioConvert进行音频转换、ExtendedAudioFile进行音频文件的读/写,其中AudioUnit的初始化都是通过AudioComponentInstanceNew
实现,实际工程中更多使用的是AUGraph的方式进行AudioUnit的初始化。
本文尝试用AUGraph来管理RemoteI/O Unit和Mixer Unit,实现录音、伴奏播放、人声和伴奏混合的功能。
正文
1、概念介绍
AUGraph连接一组 audio unit 之间的输入和输出,构成一张图,同时也为audio unit 的输入提供了回调。AUGraph抽象了音频流的处理过程,子结构可以作为一个AUNode嵌入到更大的结构里面进行处理。AUGraph可以遍历整个图的信息,每个节点都是一个或者多个AUNode,音频数据在点与点之间流通,并且每个图都有一个输出节点。输出节点可以用来启动、停止整个处理过程。
每个AudioUnit都有Input, Output 和 Global 三个域。
input输入域是音频流进入unit的入口,output输出域是音频流离开unit的出口,global全局域则代表整个unit。
输入域和输出域都有若干个bus/element,比如说mixer unit有多个输入bus,只有一个输出bus;而splitter unit则有一个输入bus,有多个输出的bus。
注意的是,bus和channel不是一个东西,一个是音频流,一个是音频流的格式。
比如说Remote I/O Unit的输入域的inputBus是来自麦克风的音频流,其音频格式是双声道。
2、具体流程
- 1、初始化文件流和AVAudioSession,分配buffer;
- 2、新建AUGraph,并添加两个AUNode,一个是RemoteI/O Unit的节点,一个是Mixer Unit的节点。
添加AUNode的节点有两个步骤,先通过AUGraphAddNode
添加节点,再通过AUGraphNodeInfo
获取节点对应的AudioUnit。 - 3、建立两个AUNode的联系,
AUGraphConnectNodeInput
通过把Mixer Unit的outputBus的输出作为RemoteI/O Unit的outputBus的输入;
(这里需要注意,不是RemoteI/O的inputBus 的输入,因为RemoteI/O Unit的inputBus的输入是麦克风)
同时设置好RemoteI/O Unit的输入和输出格式、Record的回调函数; - 4、调用
AUGraphInitialize
初始化AUGraph,然后通过AUGraphStart
开始整个AUGraph;
在AUGraph开启后,麦克风收到录制数据后调用kAudioOutputUnitProperty_SetInputCallback
的回调,把麦克风的数据回调给APP;
Mixer Unit还会通过之前kAudioUnitProperty_SetRenderCallback
设置好的回调,要求APP填充两个inputBus的输入;
在Mixer Unit处理好数据之后,会按照之前AUGraphConnectNodeInput
设置的,把数据发送给Remote I/O Unit;
Remote I/O Unit再把数据发送给扬声器。
3、音频流解析
如下,是整个demo的音频流向:
伴奏文件被读取到内存,再被送到MixUnit的inputBus0;
麦克风录取到音频数据,送到Remote I/O Unit的inputBus,存到内存中,再被送到MixUnit的inputBus1;
MixUnit混合两个inputBus的数据,通过outputBus输出到Remote I/O Unit的outputBus中;
Remote I/O Unit再把outputBus的数据发送个扬声器。
遇到的问题
1、AUGraphNodeInfo
无法初始化AudioUnit
实际运行时,报错是AudioUnitSetProperty
方法,返回了-50的错误码。
检查错误码,是AudioUnitSetProperty
的audio unit参数为空。
往上回溯,定位到AUGraphNodeInfo
没正确初始化传入的audio unit参数,导致audio unit为空,并且当时没有报错,直到AudioUnitSetProperty
时才报错。
经过仔细检查,发现是
AUGraphOpen
方法被遗漏。
必须先打开AUGraph,才进行获取AudioUnit的操作。
2、AUGraphSetNodeInputCallback
给RemoteI/O Unit设置回调无效
如下,给RemoteI/O Unit设置回调可以用AudioUnitSetProperty
方法修改kAudioOutputUnitProperty_SetInputCallback设置回调,但尝试用AUGraphSetNodeInputCallback
对RemoteI/O Unit节点添加回调的时候,发现没法正常调用回调函数。
AURenderCallbackStruct recordCallback;
recordCallback.inputProc = RecordCallback;
recordCallback.inputProcRefCon = (__bridge void *)self;
// CheckError(AUGraphSetNodeInputCallback(auGraph, outputNode, INPUT_BUS, &recordCallback), "record callback set fail"); // 这个不行,因为scope不一致
CheckError(AudioUnitSetProperty(outputUnit, kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Output, INPUT_BUS, &recordCallback, sizeof(recordCallback)), "set property fail");
AUGraphSetNodeInputCallback 默认是inputScope,如果在input bus的inputScope修改属性,会造成异常现象;
3、kAudioOutputUnitProperty_SetInputCallback 和 kAudioUnitProperty_SetRenderCallback 混淆
- kAudioUnitProperty_SetRenderCallback 是audio unit需要数据,向Host请求数据;
- kAudioOutputUnitProperty_SetInputCallback是audio unit通知Host数据已经就绪,可以通过
AudioUnitRender
拉取数据;
AudioUnitRender
的解释是:Initiates a rendering cycle for an audio unit.
下图阐释了AudioUnit是如何通过AudioUnitRender
去Pull音频流数据
4、AUGraphConnectNodeInput的BUS参数设置错误
AUGraphConnectNodeInput(auGraph, mixNode, OUTPUT_BUS, outputNode, OUTPUT_BUS)
,从字面看是把mixNode的输出作为outputNode的输入。
但是在bus的参数设置上,为什么Remote I/O Unit的bus不是inputBus?
因为Remote I/O Unit有输入域有两个Bus,inputBus对应的是麦克风的输入,outputBus对应的是app发送给Remote I/O Unit的数据。
这里Mixer Unit是把人声和伴奏混合后,输出给Remote I/O Unit,相当于app发送数据给Remote I/O Unit,所以这里应该填outputBus。
总结
demo中仍然存在问题,因为两个unit结构混乱:
麦克风=>I/O Unit=>APP=>MixUnit
文件=>APP=>MixUnit
然后再是MixUnit=>I/O Unit=>扬声器
其中,I/O Unit既指向MixUnit,同时MixUnit又指向I/O Unit。
更好的实现方案,用一个Unit来实现录音,再用另外一个Unit进行播放,形成 RecordUnit=>MixUnit=>PlayUnit这样的结构会更加漂亮。
这个设想就交由你去实现了!
demo的代码点击这里,书写不易,不如来个喜欢支持下↓↓
附录
Core Audio Tips
Audio Unit Properties Reference PDF
Audio Unit Hosting Guide for iOS