【融云分析】iOS 混音之 AVAudioEngine 详解

【融云分析】iOS 混音之 AVAudioEngine 详解

何谓混音?顾名思义就是将多种音频混合在一起。举一个比较好理解的例子,我们常听的流行音乐,其中包含伴奏和原唱,将两种音频混缩处理之后最终就产生了一首流行歌曲,这就是混音。当然在正式讨论混音之前,我们需要先来简单地了解下物理世界中声音和计算机中的数字音频概念。

音频的基本概念

声音是由物体振动产生的,当物体振动时,会产生一个纵向压力波,当这个波到达人耳时,听觉系统会把其解释成声音。既然声音是一种波,频率和振幅就成了描述声音的重要属性(物理世界中,频率和振幅是波的重要属性)。频率是指声源在一秒钟内的振动次数,振幅是表示振动的范围和强度的物理量,声波振幅的大小能够决定音强。

在计算机世界中,声音都被数字化了,我们把它称为数字音频(后面简称音频)。音频有采样率和采样大小(也叫位深),实际上这两个概念和音频输入设备联系紧密,在智能终端中,这个设备被称为麦克风。麦克风的主要工作是将物理声音(也称为模拟信号)转化为数字信号,这个转换过程也被称为模数转换(A/D),数模转换使用的采集定理被称为奈奎斯特定律:在数模转换过程中,当采样率大于信号中最高频率的 2 倍时,采样之后的数字信号就完整的保留了原始信号中的信息。

众所周知,人的听觉频率是 20hz-20khz 之间。为了将声音中的信息完整保存下来,需要将音频设备的采样率设置在 40khz 以上。比如平常听的数字音乐,一般其采样率是 44.1k 或 48k,这保证了较高的音质。音频设备采集到的数据,经过量化编码,最终输出数字信号,也就是 PCM 数据。在量化编码时,采样大小决定了每个样本的最大可表示范围,比如采样大小 16 位,则其表示的最大数值是 65535。

iOS 音频的采集方式

苹果公司为 iOS 提供了非常丰富的多媒体编程接口,以提高原生应用在多媒体方面的用户体验,下图是iOS的多媒体框架图如下:

从上图可以看出,在 iOS 中所有的音频技术都构建于 Audio Unit 之上, 更上层的 Media Player、AV Foundation、OpenAL 和 Audio Toolbox,它们封装了 Audio Unit 为特定任务提供增强生产效率的专门 API。

在调用音频采集 API 之前,先要使用 AVAudioSession 对硬件设备做一些设置,代码如下:

接下来根据上面框架图从上到下讨论音频采集的编程接口,它们和所属框架分别是:

1.AVAudioRecorder;

2.AVCaptureAudioDataOutput;

3.Audio Queue;

4.Audio Unit。

AVFoudation中苹果提供了 AVAudioRecorder 和 AVCaptureAudioDataOutput, AVAudioRecorder 将声音录制到一个文件中,开发者使用它无法操纵录音过程中产生的音频样本数据,只能实现采集声音录制成一个文件。AVCaptureAudioDataOutput 相对更加灵活一些,它要与 AVCaptureSession 结合起来使用,通过代理方法 AVCaptureAudioDataOutput:didOutputSampleBuffer:fromConnection: 会返回 CMSampleBufferRef 类型的音频数据,CMSampleBufferRef 是对 PCM 数据的封装,该类型是对媒体数据的抽象表示,既能表示音频,也可以表示视频,甚至可以存储压缩后的媒体数据。

Audio Queue 由 Audio ToolBox 框架提供,不仅可以录音,还支持播放音频。具体使用方法可以参考 Audio Queue Services Programming Guide

由于 AVAudioEngine 底层技术是 Audio Unit、AU Graph,为了更好的理解后面的知识,下面重点介绍 Audio Unit 的使用方式,对于程序员来说,没有比直接给出代码更好的方式了:

接下来是 InputCallback 的实现:

上面的代码展示了使用Audio Unit 录音需要以下步骤:

1.通过指定 AudioComponentDescription 描述信息,获取 Audio Unit 实例;

2.配置 Audio Unit 的属性,比如开启或者禁用音频总线,输出数据的格式,设置回调函数等;

3.初始化 Audio Unit;

4.启动 Audio Unit。

上述步骤执行完成后,当系统通过麦克风采集到声音数据后会通过回调函数来通知应用程序,也就是上面的 InputCallback。在该函数中使用 AudioUnitRender 获取原始的 PCM 数据,这些数据最终会存储在一个 AudioBufferList 的数据结构中,在上面的代码片段 InputCallback 函数中 BufferList中存储了音频数据。在这之后,可以将数据写入文件,或者编码后通过网络发送。

Audio Unit 的分类和特性

Audio Unit 根据用途不同被分成了 4 大类,分别是:Effect(效果器), Mixer(混音), I/O, Format convert(格式转化)。Effect 和 Format convert 超出了本文的主题,这里不做讨论。

I/O:上面的代码中,使用的RemoteIO unit(kAudioUnitSubType_RemoteIO)是最常用的,它连接着音频硬件设备,提供了对流入和流出的音频样本数据的低延时访问,也提供了音频硬件设备和应用之前音频格式的转化。

Voice-Processing IO Unit 扩展了 RemoteIO,为 VoIP 和语音聊天应用提供了回声抑制功能,也提供了自动增益校正、语音处理质量调整、静音等功能。

Generic Output Unit 也是一种 I/O Unit,不在本文讨论的范畴。

Mixer:这里只讨论 Multichannel Mixer Unit,它提供了混合多个单声道或者立体声音频流,可以关闭或者开启任何一个输入总线,设置输入增益等。

了解了 Audio Unit 分类,下面讨论下 Audio Unit 的结构,如下图所示:

上图展示了一个 Audio Unit 的通用结构,它由 Scope(域) 和 Element(元素)组成,这里的 Element 也被称为音频总线 (bus),其中 1 表示 Input,0表示Output,1 和 0 分别跟 Input 和 Output 的首字母十分相似。域分为 Input、Output 和 Global,Input 和 Ouput 分别代表一个总线的输入输出部分,Global 只有一个总线,那就是 0 总线,有些属性只被应用于 Global 域。上面代码片段的属性配置部分使用了这些概念。

另外,上面的 Input Element 和 Output Element 的数量是相等的,然而不同的 Audio Unit 使用了不同结构,例如一个 Mixer Unit 可能有若干个 Input Element,但是只有一个 Output Element,尽管有不同的结构的 Audio Unit,仍然可以将 Scope Element 延伸到任意的 Audio Unit。

AU Graph

此外iOS 还提供了 AU Graph 创建和管理 Audio Unit 的处理链,AU Graph 具有管理多个 Audio Unit 的能力,使用 AU Node 来表示在 AU Graph 中的独立 Audio Unit,在 Au Graph 中通常与 Au Node 交互,而不是与 Audio Unit 交互,AU Node 可以视为 Audio Unit 的代理。从数据结构的角度来理解,AU Graph 代表一张有向图,其中每一个 Audio Unit 表示图中的一个结点,Audio Unit 总线之间彼此建立的连接,可以认为是结点之间的边,并且这个连接是有方向的,这样节点和边就构成了一个有向图。

构建 AU Graph 总体来说需要做三件事:

1.向图中添加节点;

2.配置由节点表示的Audio Unit;

3.连接节点。

这里举一个简单的例子,比如要实现耳返功能,思路就是把 Audio Unit 输入总线的输出部分和输出总线的输入部分相连,示例代码如下:

运行上面的代码,就可以在扬声器中听到自己讲话的声音了。上面的代码构建了如下处理链图:

这里需要注意的是,一个 AU Graph 中只能添加一个 I/O 类型的Unit。请看下面的代码:

上面的代码在第二次添加就失败了,返回 kAUGraphErr_OutputNodeErr(-10862)的错误,这个错误码文档有详细描述,这里就不赘述了。

AVAudioEngine 背后的机制

基于前面 Audio Unit、AU Graph 代码实践,不难理解 AVAudioEngine 背后的机制。每个 AVAudioEngine 实例中有一个 AU Graph 对象中,AU Graph 对象中有一个 RemoteIO audiounit,RemoteIO Unit 的输入总线和输出总线分别被封装成了AVAudioEngine的 InputNode 和 OutputNode。这里 InputNode 和 OutputNode 不能直接创建,必须通过 AVAudioEngine 来获取,这样从语法上限制了向 AU Graph 中添加多个 I/O 类型的 Audio Unit,同时这两个 Node 共享同一个 Audio Unit。

下面代码是对上面结论的验证:

运行上面的代码会发现所有的 Assert 都会顺利通过,不仅如此我们可以知道 InputNode 和 OutputNode 本身不是同一个对象,但是它们都封装了同一个 I/O 类型的 Audio Unit,并且这个 Audio Unit 的子类型是 RemoteIO。如果对 InputNode 调用 setVoiceProcessingEnabled(true)方法,AVAudioEngine 会将其对应的 Audiounit 的 subType 切换成了 VoiceProcessingIO 类型。

根据上面的知识,可以得出 AVAudioEngine 是对Audio Unit 和 Au Graph 封装,其将麦克风采集(InputNode),扬声器播放(OutputNode),音频混合(MainMixer)等功能抽象成 AVAudioNode,使用 AU Graph 的 AUGraphConnectNodeInput 将节点的输入输出相互连接从而构成一个音频处理链。

AVAudioEngine 实现混音

本文前面已经介绍了 Audio Unit 采集音频,AU Graph 构建音频处理链,以及探索了 AudioEngine 背后的基本原理。这样再使用 AVAudioEngine 会觉得非常容易理解,甚至会觉得 API 就应该设计成这样。下面用 AVAudioEngine 实现一个将音频文件和麦克风的声音混合最后输出到扬声器的功能,可以看出 AVAudioEngine 让代码变得更加简洁并且富有表现力。具体代码如下:

AVAudioEngine 大幅减少了代码量,使用很少的代码就可以实现混音的复杂任务。

总结

本文主要介绍了声音的物理性质、数字音频数模转换的理论知识、iOS 的声音采集方式、Audio Unit和AU Graph的编程概念。然后使用 AVAudioEngine 实现混音功能,同时深挖了 AVAudioEngine 背后的机制。Audio Unit 提供了快速、模块化的音频处理机制,具备低延时的音频I/O、回声消除、混音、均衡等功能。AU Graph 的处理链体系结构,可将音频模块组装成灵活网络。结合它们可以封装出简洁易用的 AVAudioEngine。在 iOS 应用开发中使用这些技术处理音频任务有很多优势,比如可以轻松让程序获得最佳性能,大幅降低开发成本等。当然处理 Audio Unit 之外,还可以通过混音算法来实现混音,几乎所有的混音算法都是通过对输入的音频数据做线性叠加衍生出的,比如平均值法,自适应加权等。感兴趣的读者可以比较下这些算法实现的效果和 Audio Unit的差异。

点击获取更多技术干货

       

标签: , ,