Overwrite
个人理解的名词解释:
- Audio Unit:音频单元,通常指一个音频单元实例,或者 Audio Unit 技术。
- Audio Component:音频组件,指音频单元(Audio Unit)类的类型。
- Audio Processing Graph:音频处理图,即音频图。
- Audio Node:音频结点,在音频处理图中担任音频单元的标识,在音频处理图执行打开操作后间接实例化对应的音频单元
- Element:音频单元中的具有特定功能的元件
- Input element:特指连接音频输入硬件(如麦克风)的元件
- Output element:特指连接音频输出硬件(如扬声器)的元件
- Bus:与 element 的概念相同,不过在强调信号流的时候使用
- Scope:音频单元中音频流的端口或范围
- Input scope:音频流的输入端口
- Output scope : 音频流的输出端口
- Global scope:音频单元的全局范围
- 音频流流向:Input scope -> Output scope
如上图所示,AudioUnit是iOS中音频最底层的API,仅在高性能,专业处理声音的需求下使用.
1. Audio Unit 提供快速、模块化的音频处理
使用场景
以最低延迟的方式同步音频的输入输出,如VoIP应用
手动合成音视频,如音乐游戏、乐器音乐合成软件
使用特定的 Audio Unit 功能,如回声消除、混音、音调均衡
将多种音频处理模块灵活组装应用
1.1 Audio Unit 的用途
用途 | Audio Unit |
---|---|
音效(Effect) | iPod Equalizer |
音频混合(Mixing) | 3D Mixer、Multichannel Mixer |
音频输入输出(I/O) | Remote I/O、Voice-Processing I/O、Generic Output |
格式转换(Format conversion) | Format Converter |
-
Effect Unit
- iPod Equalizer:提供一组预设的均衡曲线,如低音增强(Bass Booster)、流行(Pop)和口语(Spoken Word)等等
-
Mixer Units
3D Mixer:OpenAL底层构建的基础,如果需要3D Mixer unit特性,建议直接使用OpenAL,因为它提供了很多封装好的功能强大的API。
Multichannel Mixer:为多个音频提供混音功能,而且支持不同声道声音混合,最后以立体声输出。你可以单独打开或关闭其中一个输入音频的声音、调节音量、播放速度等等。总的来说,这就是一个多音频输入,单音频输出的 Audio Unit。
-
I/O Units
(常用)Remote I/O:它直接连接输入和输出的音频硬件,以低延迟的方式访问单个接收或发出的音频采样数据。并且提供了硬件音频格式到应用设置音频格式的格式转换功能(Format Converter)。
Voice-Processing I/O:通过声学的回声消除拓展了Remote I/O unit,常用于VoIP或语音通信的应用。它还提供了自动增益校正、语音处理质量调整和静音功能。
Generic Output:它不连接任何音频硬件而是提供一个将处理链的输出传递给应用程序的途径,通常用于离线音频处理。
-
Format Converter Unit
Format Converter:在 I/O Units 中间接使用于的音频格式转换部分。
Converter unit:支持转换线性PCM音频数据类型(linear PCM)
1.2 Audio Unit 的两套 API
iOS 中操控 Audio Unit 的有两套API,一套是用于直接操控单个 Audio Unit,另一套通过音频处理图(audio processing graphs)的方式操控多个Audio Unit。
Audio Unit API
-
Audio Graph API
两套API有部分相同的功能的函数,开发者在使用的时候可以混合使用:
获取 Audio Units 的动态可链接库的引用
实例化 Audio Units
连接 Audio Units 并注册回调函数
启动和停止音频流
1.3 获取 Audio Units 实例
1 - 1 创建音频组件描述来标识一个 Audio Unit
AudioComponentDescription ioUnitDescription;
ioUnitDescription.componentType = kAudioUnitType_Output;
ioUnitDescription.componentSubType = kAudioUnitSubType_RemoteIO;
ioUnitDescription.componentManufacturer = kAudioUnitManufacturer_Apple;
ioUnitDescription.componentFlags = 0;
ioUnitDescription.componentFlagsMask = 0;
AudioComponentDescription
:一个用来描述并唯一标识一个音频组件的结构体
标识字段(Identifier keys):
- componentType:类型,audio unit 的主要功能
- componentSubType:子类型,audio unit 的详细功能
- componentManufacturer:制造商,一般都是
kAudioUnitManufacturer_Apple
- componentFlags & componentFlagsMask:提供位移枚举的方式标识特定描述,一般都设0忽略
更多 Identifier keys 见 Identifier Keys for Audio Units
1 - 2 获取 Audio Unit 对象实例
Audio Unit API
AudioComponent foundIoUnitReference = AudioComponentFindNext (
NULL,
&ioUnitDescription
);
AudioUnit ioUnitInstance;
AudioComponentInstanceNew (
foundIoUnitReference,
&ioUnitInstance
);
AudioComponent
类型:音频组件类型,本质是一个指定特定音频组件的类指针。一个单独的音频组件,可用于实例化多个相同类型的音频单元实例。
AudioComponentFindNext
函数:该函数用于在系统设备可用组件中查找最相近的组件,并将其返回其类指针。可用组件的顺序可能会根据系统或者Audio Unit 的版本进行变化,详情见kAudioComponentRegistrationsChangedNotification
- 第一个参数(
inComponent
):设置为NULL表示使用系统定义的顺序查找第一个匹配的音频组件。如果你将上一个使用的音频组件传给该参数,则该函数将从这个组件开始继续寻找下一个与之描述匹配的音频组件。根据可用列表的顺序进行实例化,有助于性能提升。 - 第二个参数(
inDesc
):音频组件描述结构体地址,用做组件查找的依据。 - 返回值:符合条件的音频组件,即音频单元类指针。
AudioComponentInstanceNew
函数:该函数用于创建音频组件实例,即一个音频单元(audio unit)。
- 第一个参数(
inComponent
):音频组件,用于告诉系统需要的音频单元类型,该参数不可为空。 - 第二个参数(
outInstance
):音频单元实例,用于接收实例化的音频单元。 - 返回值(
OSStatus
):结果状态返回值,无错误返回 noErr(即0),否则返回错误码。
Audio Graph API
// 声明并实例化一个音频处理图
AUGraph processingGraph;
NewAUGraph(&processingGraph);
// 添加音频单元结点到图中,然后实例化这些结点
AUNode ioNode;
AUGraphAddNode (
processingGraph,
&ioUnitDescription,
&ioNode
);
// 间接执行音频单元的实例化
AUGraphOpen (processingGraph);
// 获取新实例化的 I/O 单元引用
AudioUnit ioUnit;
AUGraphNodeInfo (
processingGraph,
ioNode,
NULL,
&ioUnit
);
NewAUGraph
函数:实例化一个音频处理图
AUGraphAddNode
函数:通过音频组件描述添加一个音频结点到音频处理图中。
AUGraphOpen
函数:打开指定的音频处理图(头文件中表示音频单元在此时并没有初始化,该操作并不会有资源被申请?)。
AUGraphNodeInfo
函数:返回指定音频结点的信息。
- 输入参数(
inGraph
&inNode
):音频处理图与输入结点 - 输出参数(
outDescription
&outAudioUnit
):音频组件描述与音频单元,设置NULL
表示不获取该信息。 - 返回值(
OSStatus
):函数的执行成功与错误码。
1.4 Audio Units 的 Scopes 与 Elements
一个音频单元由以下的Scopes
和Elements
组成:
[图片上传中...(IO_unit_2x.png-b104de-1593881466577-0)]
-
scope
:音频单元内部的一个编程上下文,本文主要称之为音频流动的“端口”或“范围”。-
Input scope
和Output scope
直接参与音频数据流通过音频单元的过程。让音频数据从Input scope
进入,并从Global scope
离开。可以这两个scope
中可以配置一些属性和参数,例如``kAudioUnitProperty_ElementCount、
kAudioOutputUnitProperty_EnableIO和
kMultiChannelMixerParam_Volume`等等。-
Input scope
:音频输入端口,在此处都需要向Element
输入音频数据; -
Output scope
:音频输出端口,在此处都需要将Element
的音频数据输出到其他地方;
-
-
Global scope
:全局范围并没有与Element
嵌套,用于配置音频单元中与输入输出概念无关的属性,例如kAudioUnitProperty_MaximumFramesPerSlice
等。
-
-
element
:音频单元内部一个嵌套在Scope
中的编程上下文,本文称之为“元件”或“总线”。- 嵌套在
Input scope
和Output scope
的Element
类似一个在物理音频设备上的信号总线。因此在文档中'element'
和'bus'
都代表上图中的ElementX
。一般在强调信号流时使用'bus'
,在强调音频单元的特定功能方面时使用'element'
。 - 对于
Input Output scope
中的Element
都是从0开始索引的;对于Global scope
直接用0索引。 - 上图只是表示一个普通音频单元的内部架构,但是对于不同音频单元会有不同的内部结构,比如一个音频混合单元,它会有多个输入的
element
,一个输出的element
。
- 嵌套在
1.5 Audio Units 的属性配置
一个音频单元的属性是通过一个键值对来设置的,其中键是通过一个唯一的整数(UInt32
)来标识的。苹果预留了0 ~ 63999
的区间标识属性键,剩下的区间可提供给第三方音频单元使用。我们可以通过AudioUnitSetProperty
函数进行属性配置,调用方式由以下代码块所示。
UInt32 busCount = 2;
OSStatus result = AudioUnitSetProperty (
mixerUnit, // 属性设置目标音频单元
kAudioUnitProperty_ElementCount, // 属性键
kAudioUnitScope_Input, // 属性设置所在域
0, // 属性设置所在元件
&busCount, // 属性值地址
sizeof(busCount) // 属性值的字节大小
);
常用属性:
-
kAudioOutputUnitProperty_EnableIO
:用于在 I/O Unit 上启用或禁用输入或输出。默认输出已启用,输入已禁用。 -
kAudioUnitProperty_ElementCount
:配置mixer unit上的输入elements
的数量 -
kAudioUnitProperty_MaximumFramesPerSlice
:为了指定音频数据的最大帧数,音频单元应该准备好响应于回调函数调用而产生。对于大多数音频设备,在大多数情况下,您必须按照参考文档中的说明设置此属性。如果不这样做,屏幕锁定时您的音频将停止。 -
kAudioUnitProperty_StreamFormat
:指定特定音频单元输入或输出总线的音频流数据格式。
大多数属性只能在音频单元没有初始化时指定,但是某些特定属性可以在音频单元运行时设置,如``Voice-Processing I/O unit的
kAUVoiceIOProperty_MuteOutput静音功能、
iPod EQ unit的
kAudioUnitProperty_PresentPreset`当前模式功能。
属性相关函数
-
AudioUnitGetPropertyInfo
:用于判断指定音频单元中该属性是否可用,可用则提供值的大小。 -
AudioUnitGetProperty
、AudioUnitSetProperty
:获取、设置属性。 -
AudioUnitAddPropertyListener
、AudioUnitRemovePropertyListenerWithUserData
:监听、移除监听特定属性。
1.6 Audio Units 的参数配置
相比与音频单元属性,音频单元参数在音频单元工作的过程中都是可以调整的。音频单元参数也是以键值对的方式表示的。
- 键是以枚举值的形式展示,在同一种类的音频单元中枚举值是唯一的,但在所有音频单元中的有重复。
- 值统一都是32位的浮点数(Float32)类型的。值的允许范围以及含义由音频单元的实现确定,详情见以下文档(Audio Unit Parameters Reference)。
参数相关函数(其使用方式类似属性获取与设置)
-
AudioUnitGetParameter
:获取指定参数值 -
AudioUnitSetParameter
:设置指定参数值
我们可以通过UIKit
中的UISlider
和UISwitch
控件在音频单元工作的过程中改变其参数值,达到用户交互的效果。
1.7 I/O Units 的基本特性
I/O Units 是一个十分常用的音频单元,它在许多地方都比较特别。它包含了两个element
,内部结构如下图所示:
I/O Unit 的两个elements
是两个独立的实体,在使用的时候我们需要通过设置属性(kAudioOutputUnitProperty_EnableIO
)单独启用和禁用element
。
-
Element 1
:Input scope
部分是对开发者不透明的,它直接连接着音频输入的硬件设备(麦克风),可在其Output scope
部分获取音频数据。一般 I/O unit 的Element 1
又称为input element
。 -
Element 0
:Ouput scope
部分是对开发者不透明的,它直接连接着音频输出的硬件设备(扬声器),可在其Input scope
部分传入音频数据。一般 I/O unit 的Element 0
又称为output element
。
因此 I/O unit 在音频处理图中担任着音频流处理的起始点和终点,拥有开启和关闭音频流的能力。
2. Audio Processing Grapha 管理 Audio Units
Audio Processing Grapha 即一个音频处理图,是 Core-Foundation风格的数据结构——AUGrapha。通过 AUGrapha 我们可以构建和管理一个音频处理链条。一个音频处理图可以利用多个音频单元和多个呈现回调函数创建任何你所想象的音频处理方案。
-
AUGraph
:音频图,本质是一个音频图的指针类型,该类保证了线程安全。例如播放音频时,可以保证安全地插入一个均衡器(equalizer)或者在混合器(mixer)输入端更换回调函数。事实上,AUGraph
提供了iOS平台上用于音频应用程序的动态配置API。- 更多:在构建一个音频图的时候,必须配置好图中的所有音频单元,AUGraph相关的API并不能完全胜任,因此需要同时使用音频图和音频单元两套API。
-
AUNode
:音频结点,本质是一个SInt32
类型,在音频图中的标识一个独立的音频单元。在配置使用音频图时,为了可读性,一般使用一个音频结点来代表图中的包含的音频单元,而不是直接使用音频单元。- 更多:音频结点除了可以代表一个音频单元外,还能代表音频图里面的子图,但是I/O unit连接的子图必须只能使用 Generic Output unit 而不是 I/O unit。因为一个物理设备最多只能只能连接一个音频单元。
总的来说,构建音频处理图需要以下三步:
- 添加音频结点到音频图中
- 通过音频结点获取音频单元,直接配置音频单元的属性参数
- 将音频结点连接起来
2.1 Audio Processing Graphs 中的 I/O Unit
无论是录音、回放和同步I/O流,所有音频处理图都有一个 I/O unit。音频图中的 I/O unit 负责音频流的输入和输出,其他音频单元负责其他音频流处理工作。
- 音频图通过
AUGraphStart
和AUGraphStop
方法启动和停止音频流 - 通过
AudioOutputUnitStart
和AudioOutputUnitStop
方法传达启动和停止信息给I/O unit
2.2 Audio Processing Graphs 线程安全
音频处理图API提供一个“to-do list”保存需要做的操作,在开发者配置完成后再告诉音频图去实现。以下是音频处理图支持的一些常见重新配置以及相关功能:
-
AUGraphAddNode
、AUGraphRemoveNode
:添加移除音频结点 -
AUGraphConnectNodeInput
、AUGraphDisconnectNodeInput
:添加移除音频结点之间的连接 -
AUGraphSetNodeInputCallback
:设置连接音频单元输入总线(input bus)的回调函数
下面将以一个音频合成播放的音频处理图重新配置为例,讲述音频处理图运行中的线程安全。首先构建一个音频处理图包含 Multichannel Mixer unit 和 Remote I/O unit,用于播放合成两种输入源的混音效果。运行中的音频处理图中,开发者将两个输入源的数据送给 Mixer unit 的 input bus,mixer 的输出端连接着 I/O unit 的output element
,最终将音频传给硬件输出。音频处理图如下:
现在,在其中一个音频流中插入一个“音频均衡器”,则音频处理图如下:
以下是完成重新配置的步骤:
- 通过调用
AUGraphDisconnectNodeInput
断开mixer unit
从input 1
的“鼓声”回调。 - 通过配置“音频组件描述”,然后调用
AUGraphAddNode
将一个包含一个包含iPod EQ unit
的音频结点到音频图中的音频结点添加到音频图中。(此时,iPod EQ unit
已具有实例化对象但未被初始化,新增的音频结点也只是存在音频图中并未参与音频流) - 配置和初始化
iPod EQ unit
- 调用
AudioUnitGetProperty
函数从Mixer unit
的输入端获取当前使用的流格式(kAudioUnitProperty_StreamFormat
) - 调用
AudioUnitSetProperty
函数两次,分别设置iPod EQ unit
输入端和输出端的流格式 - 调用
AudioUnitInitialize
函数为iPod EQ unit
分配内存和准备处理音频使用。(注:这个函数是线程不安全的,需要当iPod EQ unit
尚未主动参与进音频处理图时,即没有调用AUGraphUpdate
函数前使用。
- 调用
- 通过调用
AUGraphSetNodeInputCallback
将“鼓声”回调函数添加到iPod EQ unit
的input
端。
上面1,2,4步使用AUGraph
开头的函数,都会被添加到的任务执行列表(“to-do list”)中。然后通过调用AUGraphUpdate
执行这些未开始任务。如果成功返回,则代表音频图已经被动态重新配置并且iPod EQ unit
也已经就位正在处理音频数据。
2.3 通过 Graph "pull"音频流
在音频处理图的音频流流动类似生产者消费者模式,消费者在需要更多音频数据时通知生产者。请求音频数据流的方向与音频流提供的方向正好相反。
对一组音频数据的每个请求称为渲染调用(render call),也称为拉流(pull)。该图灰色“控制流”箭头表示为拉流操作。拉流请求的数据本质是一组音频样本帧(audio sample frames),一组音频样本帧也称为一个切片(slice)。提供切片的代码称为渲染回调函数( render callback function)。下列是拉取音频流的步骤:
- 调用
AUGraphStart
函数后,虚拟输出设备调用Remote I/O unit
上output element
的渲染回调函数请求一片处理过的音频数据帧。 -
Remote I/O unit
的回调函数在其输入缓冲区中查找要处理的音频数据去满足渲染请求。如果有数据则直接传递,否则,调用连接其输入端的回调函数。上图中Remote I/O unit
的输入端连接一个effect unit
的输出端,则I/O unit
从effect unit
中拉流,请求一片音频数据帧。 -
effect unit
的行为与Remote I/O unit
一样。当它需要音频数据时,便从输入连接中获取它。上图中,effect unit
从应用程序的回调函数中获取音频数据。 - 应用程序的回调函数最终接收了这个拉流请求,在函数中提供
effect unit
需要的音频帧数据。 -
effect unit
从应用程序的回调函数中获取的音频数据,然后按照步骤2中要求提供音频数据给Remote I/O unit
-
Remote I/O unit
将从effect unit
提供的音频数据,按照步骤1中的要求提供给虚拟输出设备,完成了一个拉流周期。
3. 通过回调函数将音频传递给 Audio Units
为了将音频数据从内存或磁盘中传递到音频单元的input bus
,需要使用实现一个渲染回调函数填充数据并通过AURenderCallback
属性配置音频单元的属性。这样当音频单元需要一片音频帧数据时候,该回调函数就会被调用。渲染回调函数给了我们操作音频数据很高的自由度,我们可以在回调函数以任何方式创建或改变音频数据。与此同时,渲染回调函数处于实时优先级线程上,后续的函数调用都是异步的。因此,我们在渲染回调函数中处理时间是有限的,如果在下一次渲染调用到达时,上一个回调函数还没运行完成,那么你的音频结果将会出现缺口,导致你的结果是不连续的。因此,不能在渲染回调函数中执行线程锁、分配内存、访问文件系统或网络连接等耗时任务。
以下是渲染回调函数的详细说明:
static OSStatus MyAURenderCallback (
void *inRefCon,
AudioUnitRenderActionFlags *ioActionFlags,
const AudioTimeStamp *inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList *ioData
) { /* callback body */ }
-
inRefCon
:表示注册回调函数时传递的指针,一般可传当前对象实例。因为回调函数是C语言形式,无法直接访问本类中属性与方法,所以将实例化对象传入可以间接调用当前对象中属性与方法。 -
ioActionFlags
:表示音频渲染处理的行为,本质是一个位移枚举,告诉 Audio Unit 使用不同的音频渲染方式。比如在一个乐器音乐合成应用中,用户当前没有需要播放的音符,则在回调函数中使用*ioActionFlags |= kAudioUnitRenderAction_OutputIsSilence;
。 -
inTimeStamp
:表示调用回调函数的时间,可用作音频同步的时间戳。它是一个AudioTimeStamp
结构体,每次mSampleTime
字段的值都会根据inNumberFrames
参数中的数字递增。例如,如果你的应用是音序器或鼓机,则可以使用mSampleTime
的值来调度声音。 -
inBusNumber
:表示调用回调函数的audio unit bus
,可通过该值在回调函数中进行分支操作。另外,当音频单元注册回调函数时,可以为每个bus
指定不同的inRefCon
。 -
inNumberFrames
:表示回调函数中的需要填充的音频帧数。 -
ioData
:表示需要填入的音频数据缓冲,该音频数据缓冲的结构必须与当前所在bus
的的音频流格式一致。如果需要在渲染回调中实现静音功能,则需要通过memset
函数将ioData
的buffers
都设为0。
下图描述的是ioData
参数中的一对非交错(noninterleaved)立体声缓冲区:
4. 音频流格式启用数据流
在一个音频样本数据中,二进制位的布局是有含义的,这不是单纯用Float32
和UInt16
数据类型可以表达的。音频单元(audio unit
)可以使用音频组件描述(AudioComponentDescription
)来表达,音频流格式则使用音频流基本描述(AudioStreamBasicDescription
,即ASBD
)来表达。
// ASBD 结构体
struct AudioStreamBasicDescription {
Float64 mSampleRate;
UInt32 mFormatID;
UInt32 mFormatFlags;
UInt32 mBytesPerPacket;
UInt32 mFramesPerPacket;
UInt32 mBytesPerFrame;
UInt32 mChannelsPerFrame;
UInt32 mBitsPerChannel;
UInt32 mReserved;
};
typedef struct AudioStreamBasicDescription AudioStreamBasicDescription;
// 定义一个立体声 ASBD
// AudioUnitSampleType => 8.24 fixed-point integer => SInt32
size_t bytesPerSample = sizeof(AudioUnitSampleType);
AudioStreamBasicDescription stereoStreamFormat = {0};
stereoStreamFormat.mSampleRate = graphSampleRate;
stereoStreamFormat.mFormatID = kAudioFormatLinearPCM; // 未压缩的音频数据
/*
kAudioFormatFlagsAudioUnitCanonical = kAudioFormatFlagIsFloat |
kAudioFormatFlagsNativeEndian |
kAudioFormatFlagIsPacked |
kAudioFormatFlagIsNonInterleaved
kAudioFormatFlagsAudioUnitCanonical = kAudioFormatFlagIsSignedInteger |
kAudioFormatFlagsNativeEndian |
kAudioFormatFlagIsPacked |
kAudioFormatFlagIsNonInterleaved | (kAudioUnitSampleFractionBits << kLinearPCMFormatFlagsSampleFractionShift)
*/
stereoStreamFormat.mFormatFlags = kAudioFormatFlagsAudioUnitCanonical;
stereoStreamFormat.mBytesPerPacket = bytesPerSample;
stereoStreamFormat.mFramesPerPacket = 1;
stereoStreamFormat.mBytesPerFrame = bytesPerSample;
stereoStreamFormat.mChannelsPerFrame = 2; // 2 indicates stereo
stereoStreamFormat.mBitsPerChannel = 8 * bytesPerSample;
stereoStreamFormat.mReserved = 0;
-
mSampleRate
:采样率,每秒钟音频流中的样本帧数 -
mFormatID
:格式标识,大体的数据格式类型 -
mFormatFlags
:格式配置,位移枚举,指定具体的数据格式 -
mBytesPerPacket
:每个音频包中的字节数 -
mFramesPerPacket
:每个音频包的帧数- 未压缩音频:一个音频包只有一帧的数据
- 压缩音频:一个音频包是一块压缩好的数据,比如一个ACC音频包有1024个样本帧
-
mBytesPerFrame
:每一帧的字节数- 非交错型数据(
non-interleaved
):每一帧只是包含一个声道数据 - 交错型数据(
interleaved
):每一帧只是包含多个声道数据
- 非交错型数据(
-
mChannelsPerFrame
:声道数,每一帧的声道数 -
mBitsPerChannel
:位深,每一个声道的二进制数 -
mReserved
:保留的?,表示填塞数据结构强制8字节对齐
注:音频流格式在创建的时候需要将事先初始化为0,即不包含任何数据,否则可能会出bug。
音频处理图中必须在关键点设置好音频数据格式,其他点系统将会设置自动格式。iOS 设备上的音频输入和输出硬件具有系统确定的音频流格式,该格式始终是未压缩的,采用交错的线性 PCM 格式。
- 绿色方块:系统根据硬件事先设置好了音频流格式
- 蓝色方块:开发者设置音频流格式的关键点
- 红色方块:开发者设置采样率的位置