iOS底层音频处理初步研究

概述

最近研究了一下iOS底层音效处理的实现方式,感觉像是发现了新大陆。过去项目中都是使用AudioUnit来做单纯的音频采集和播放,有时为了方便,甚至会重复使用采集和播放的AudioUnit对象。但是研究后发现,使用AUGraph来组合不同类型的AudioUnit,能够在音频处理方面玩出各种花样。

AudioUnit

1、简单介绍

AudioUnit是Apple框架中音频方面最底层的API,性能最强,延迟最低,在专业性较强的音频处理方面能够发挥很大的作用。其有两个比较重要的属性,一个是Scope,另一个是Element。

2、Scope

Scope有三种类型,input、output和global,在每个AudioUnit中固定存在,每种类型的Scope都有对应的Element。

3、Element

Element的类型分为input和output,数量则根据AudioUnit的类型而定,比如I/O类型的AudioUnit有1个用于输入和1个用于输出的Element,而mixer类型的AudioUnit有多个用于输入的Element和1个用于输出的Element。

4、AudioUnit、Scope和Element的关系

弄清楚AudioUnit、Scope和Element这三者之间的关系,对于理解和使用AudioUnit极其重要,下面举一个I/O类型AudioUnit的例子来说明。首先看图:

该图展示了一个使用麦克风采集声音,然后将声音进行播放的场景,这种场景在K歌App中十分常见,即用户唱歌,麦克风采集音频,经过App处理,再将音频播放至耳机中。
图中的Element0代表输出,Element1代表输入。从音频数据的流向可以看出,音频从Element1的Output scope传输至App中,App处理完之后再提交给Element0的Input scope,这3个部分是开发者能够接触到的。
而Element1的Input scope代表了麦克风输入,Element0的Output scope代表了扬声器/耳机的输出,这2个部分涉及硬件接口,开发者无法接触具体内容,只能进行开关。
从上述解读中可以看出,Element的类型和数量代表了I/O类型AudioUnit拥有的能力,反映在I/O类型AudioUnit上就是接受外部输入(Element1,麦克风采集声音)和输出数据至外部(Element0,通过扬声器/耳机播放声音)。

5、AudioUnit的类型

AudioUnit的类型分为主类型和子类型,下面介绍几种比较重要的子类型

(1)RemoteIO
RemoteIO所属主类型为kAudioUnitType_Output,通常用于音频播放场景。

(2)VoiceProcessingIO
VoiceProcessingIO所属主类型也是kAudioUnitType_Output,在RemoteIO基础上增加了回声消除和自动增益控制的功能,通常用于音频采集场景。

(3)DynamicsProcessor
DynamicsProcessor所属主类型为kAudioUnitType_Effect。前面提到VoiceProcessingIO类型有自动增益控制的功能,简称AGC。这个AGC有时候会用力过猛,某些情况下会把音量压的很低,这时候,音频的整体动态控制就需要交给DynamicsProcessor来做了,这也是本人最近研究的主要内容。

(4)AUConverter
AUConverter所属主类型为kAudioUnitType_FormatConverter,通常用于音频的格式转换。

(5)MultiChannelMixer
MultiChannelMixer所属主类型为kAudioUnitType_Mixer,通常用于混音的场景。

AUGraph

AUGraph,是AudioToolbox提供的一个重要组件,作为AudioUnit工作的上下文,它能够组合多个AudioUnit,控制多个AudioUnit之间的数据流动,为音频处理功能提供了技术基础。

使用AUGraph组合多个AudioUnit

实时通讯和视频会议的场景下,都会使用VoiceProcessingIO类型的AudioUnit做音频采集,其自带自动增益控制功能,简称AGC。这个AGC有时候会用力过猛,某些情况下(比如对着麦克风大声喊话)会把音量压的很低,脱离正常音量范围。这时候,音频的整体动态控制就需要交给DynamicsProcessor来做了,这也是本人最近研究的主要内容。具体踩坑的过程就不说了,直接上音频采集的流程图:

AUGraph的创建使用NewAUGraph函数完成,添加节点使用AUGraphAddNode函数来完成。每个节点都会有一个对应的AudioUnit对象,要关联各个AudioUnit对象,使用AUGraphConnectNodeInput函数连接各个节点即可。

这里有3点需要注意:
1、主类型为Effect的AudioUnit,输入输出的数据格式需要是32位float量化的,负责音频采集的Unit的音频参数中需要配置该格式。
2、DynamicsProcessor需要48K采样率的音频,负责音频采集的Unit的音频参数中需要配置48K采样率。
3、在DynamicsProcessor后又添加了一个RecordUnit节点,但是Element类型不同,主要是为了驱动整个音频链路,后面会详细说明。

AUGraph和AudioUnit使用注意事项

研究AUGraph和AudioUnit功能的过程可以说十分的艰难,踩坑无数。Apple关于这方面的文档写的很差,很多重要的内容没有说明,许多很重要的隐式规则需要去实践才能发现。同时国内的许多教程只是对于表面知识进行了简单的阐述,没有体现出对其内容的深刻理解,许多关键资料只有在国外技术论坛上才能找到。所以下面总结了一些关键的注意事项:

1、Effect类型的AudioUnit,仅支持32位float进行量化的数据。

2、格式转换类型的AudioUnit,不支持32位float32量化的音频数据,需要16位int量化的音频数据。

3、AudioUnit的音频链路的驱动方式。
(1)不同于常规的数据链路,AudioUnit的音频处理链路不是由链路的起点来推动的,而是由链路的终点来进行驱动的,采集和播放都是这样。
(2)网络上常见的教程中将I/O类型的Unit作为链路的最后一个节点,整个流程由该Unit来驱动。
(3)一些做格式转换或音效的Unit(比如DynamicsProcessor),并不具备驱动链路的能力。要使整个音频链路能够运行,还需要在链路的最后增加一个具备驱动能力的Unit。所以在之前的流程图中,需要在DynamicsProcessor后又添加了一个RecordUnit节点,来驱动整个音频链路。

4、同一个AUGraph中只能包含一个I/O类型的AudioUnit,若包含多个,AUGraph初始化时会报错。所以之前的流程图中,用于驱动的Unit复用了采集音频的Unit,Element设置为0。

5、AUGraphSetNodeInputCallback函数中将Scope设置为Input scope,且不能修改,因此要设置RecordUnit的采集回调时还是要用AudioUnitSetProperty函数。

6、若调用AUGraphConnectNodeInput进行了连接,那么destNode对应的AudioUnit通过AudioUnitSetProperty设置的RenderCallback回调就不会触发。

7、AudioUnitAddRenderNotify函数,注册某个AudioUnit渲染的回调。可以在某个AudioUnit被要求渲染前触发,flag为pre-render;可以在该AudioUnit完成其渲染操作后触发,flag为post-render。该回调函数不受AUGraphConnectNodeInput的影响,可以解决第6点中存在的问题。

8、使用蓝牙耳机进行采集时:若设置了AVAudioSessionPortOverrideSpeaker,则音频帧长与使用内置麦克风或有线耳机时效果一样;若未设置AVAudioSessionPortOverrideSpeaker,则音频帧长是使用内置麦克风或有线耳机时的3倍,数据量也是3倍,会超出AudioUnit的最大处理量。采集链路终点的前一个节点需要配置kAudioUnitProperty_MaximumFramesPerSlice来适配数据量过大的情况。

9、正常情况下,采集Unit使用VPIO类型,播放Unit使用RemoteIO类型,这样的组合只有在AudioSession的mode设置了VoiceChat、GameChat或VideoChat时才能工作;同时使用VoiceChat模式时,存在有线耳机插拔之后,扬声器音量变小的问题,确定是系统bug,在Apple官方开发者论坛中有说明;使用GameChat和VideoChat时不会出现该问题,因此非游戏App中推荐使用VideoChat模式。

结语

经过简单的探索,我深刻地体会到了AudioToolbox框架功能的强大,可以看出目前实践的内容只触及了冰山一角,还有更多与音频处理的内容值得深入挖掘。