构建 Audio Unit 应用程序
- [构建 Audio Unit 应用程序](#构建 Audio Unit 应用程序)
-
- 从选择设计模式开始
-
- [I/O Pass Through](#I/O Pass Through)
- [I/O Without a Render Callback Function](#I/O Without a Render Callback Function)
- [I/O with a Render Callback Function](#I/O with a Render Callback Function)
- [Output-Only with a Render Callback Function](#Output-Only with a Render Callback Function)
- 其他设计模式
- 构建应用程序
-
- [配置 audio session](#配置 audio session)
- [指定 audio unit](#指定 audio unit)
- [创建 audio processing graph](#创建 audio processing graph)
- [配置 audio unit](#配置 audio unit)
- 编写并绑定渲染回调函数
- [连接 audio unit nodes](#连接 audio unit nodes)
- 提供用户界面
- [初始化然后开启 audio processing graph](#初始化然后开启 audio processing graph)
- [Debug 小技巧](#Debug 小技巧)
构建 Audio Unit 应用程序
使用 Audio Unit 构建应用程序主要步骤是选择一个设计模式,然后编写代码来实现该模式。
从选择设计模式开始
在 iOS 应用程序中,Audio Unit 有很多基本设计模式。每种模式都有的共同特征:
-
仅仅只有一个 I/O 单元。
-
在整个 audio processing graph 中使用单一的音频流格式,尽管该格式可能存在变化。
-
要求在特定位置设置流格式或部分流格式。
正确设置流格式对于建立音频数据流至关重要。这些模式大多依赖于音频单元连接提供的音频流格式从源到目的地的自动传播。合理利用这种传播的特性可以减少了编写和维护的代码量。同时,必须保证清楚了解每种模式需要如何进行设置。例如,必须在 iPod EQ 单元的输入和输出上设置完整的流格式。
在大多数情况下,设计模式都会使用 AUGraph。虽然可以在不使用 graph 的情况下实现这些模式中的任何一种,但使用 graph 可以简化代码并支持动态重新配置。
I/O Pass Through
I/O Pass Through 模式将传入的音频直接发送到输出硬件,没有处理音频数据的选项。虽然这没有什么实际价值,但基于这种模式构建 Audio Unit 应用程序是验证和巩固对 Audio Unit 概念的理解的好方法。图 2-1 说明了这种模式。
如图所示,音频输入硬件将其流格式强加在 Remote I/O unit 的 input element 的外向一侧。开发者需要指定要在此元素的内侧使用的格式, 音频单元内部将根据需要执行格式转换。为了避免不必要的采样率转换,在定义流格式时最好使用音频硬件的采样率。我们也不必指定 Remote I/O unit 的 output element 的流格式,因为流格式会通过连接从 input element 传给 output element。同理,传给硬件的流将会根据硬件需要完成一次自动转换。
I/O Without a Render Callback Function
可以在 Remote I/O unit 的元素之间添加一个或多个其他音频单元,例如,使用多通道混音器单元将传入的麦克风音频定位在立体声域中,或提供输出音量控制。在这个设计模式中,仍然没有渲染回调函数,如图 2-2 所示。这简化了模式,但限制了其效用。如果没有渲染回调函数,就无法直接操作音频。
在此模式中,可以像在 I/O Pass Through 模式中一样配置 Remote I/O unit 的两个元素。要设置多通道混音器单元,必须在混音器输出上设置流格式的采样率。混音器的输入流格式通过音频单元连接从 Remote I/O unit 的 input element 的输出中传播,自动建立。同样,Remote I/O unit 的 output element 输入范围的流格式由音频单元连接建立,这要归功于混音器单元输出的传播。
在这种模式的任何情况下,每当使用 I/O unit 以外的其他音频单元时,必须设置 kAudioUnitProperty_MaximumFramesPerSlice 属性。与 I/O Without a Render Callback Function 模式一样,无需配置任何音频数据缓冲区。
I/O with a Render Callback Function
通过在 Remote I/O unit 的输入和输出元素之间放置渲染回调函数,可以在传入音频到达输出硬件之前进行操作,例如:使用渲染回调函数来调整输出音量,还可以添加颤音、环调制、回声或其他效果。这种模式如图 2-3 所示。
如图所示,此模式使用 Remote I/O unit 的两个元素。将渲染回调函数附加到 output element 的input scope。当该元素需要另一组音频数据时,系统会触发回调。反过来,回调通过调用 Remote I/O unit 的 input element 的渲染回调函数来获得新的音频数据。
与其他 I/O 模式一样,您必须在 Remote I/O unit 上明确启用输入,因为默认情况下,输入是禁用的。而且无需配置任何音频数据缓冲区。
请注意,当使用渲染回调函数建立从一个音频单元到另一个音频单元的音频路径时,回调取代了音频单元连接。
Output-Only with a Render Callback Function
在最简单的情况下,这种模式涉及一个直接连接到 Remote I/O unit 的 output element 的 input scope 的渲染回调函数,如图 2-4 所示。
可以利用此模式完成复杂的音频结构。例如,将几个声音混合在一起,然后通过设备的输出硬件播放它们。图 2-5 显示了这种情况。
在图中,需要在 iPod EQ 的输入和输出上设置完整的流格式,多通道混音器只需要在其输出上设置正确的采样率。正如前面说到的,完整的音频流格式信息会在传递的过程中自动赋值。
对于每个多通道混合器单元输入,要设置完整的流格式。对于 input 0,需要显式设置它的流格式。对于 input 1,流格式由音频单元连接从 iPod EQ 单元的输出传播。一般来说,必须单独考虑每个音频单元的流格式需求。
其他设计模式
Audio Unit 还有另外两种主要设计模式:
-
Input-only with a Render Callback Function:回调函数由应用程序调用,将音频数据传给 Remote I/O unit 的 input element。然而,在大多数情况下,对于这样的应用程序来说,更好的选择是使用输入音频队列对象(使用 AudioQueueNewInput 函数实例化的 AudioQueueRef 类型),使用音频队列对象提供了更大的灵活性,因为它的渲染回调功能不在实时线程上。
-
Generic Output unit:离线音频处理。与 Remote I/O unit 不同,该音频单元无法连接到设备的音频硬件。当使用它向应用程序发送音频时,它仅仅取决于应用程序调用其渲染方法。
构建应用程序
无论选择哪种设计模式,构建 Audio Unit 应用程序的步骤基本相同:
- 配置 audio session。
- 指定 audio unit。
- 创建 audio processing graph,然后获取 audio unit。
- 配置 audio unit。
- 连接 audio unit nodes。
- 提供用户界面。
- 初始化然后开启 audio processing graph。
配置 audio session
构建 Audio Unit 应用程序的第一步与任何 iOS 音频应用程序的步骤相同:配置音频会话。音频会话是应用程序和硬件交互的中介,它的特征在很大程度上决定了应用程序的音频功能及其与系统其他部分的交互性。首先指定要在应用程序中使用的采样率,如下所示:
objc
self.graphSampleRate = 44100.0; // 单位:赫兹
接下来,使用音频会话对象请求系统使用指定采样率作为设备硬件采样率。这里的目的是避免硬件和应用程序之间的采样率转换。这可以最大限度地提高 CPU 性能和音质,并最大限度地减少功耗。
objc
NSError *audioSessionError = nil;
// 获取 audio session 单例对象
AVAudioSession *mySession = [AVAudioSession sharedInstance];
// 请求当前设备硬件使用的采样率
[mySession setPreferredHardwareSampleRate: graphSampleRate
error: &audioSessionError];
// 设置音频分类,AVAudioSessionCategoryPlayAndRecord 指的是支持音频输入与输出
[mySession setCategory: AVAudioSessionCategoryPlayAndRecord
error: &audioSessionError];
// 激活 audio session
[mySession setActive: YES error: &audioSessionError];
// 激活会话后更新采样率
self.graphSampleRate = [mySession currentHardwareSampleRate];
还可以配置其他硬件特征:音频硬件 I/O 缓冲区持续时间。采样率在 44.1kHz 的延时约为 23ms,相当于每次采集 1024 个采样点。如果 I/O 延迟在应用程序中至关重要,可以设置更短的 duration,低至约 0.005ms(相当于 256 个采样点),如下所示:
objc
self.ioBufferDuration = 0.005;
[mySession setPreferredIOBufferDuration: ioBufferDuration
error: &audioSessionError];
有关如何配置和使用音频会话对象的完整说明,请参阅:Audio Session Programming Guide。
指定 audio unit
在运行时,配置音频会话后,应用程序尚未获取音频单元。可以使用 AudioComponentDescription 结构来得到一个指定的音频单元,然后根据音频单元说明符和选择的设计模式构建一个 audio processing graph。
创建 audio processing graph
在此步骤中,将创建设计模式的骨架。具体来说,有以下几步:
- 实例化 AUGraph 对象,该实例代表 audio processing graph。
- 实例化一个或多个 AUNode 对象,每个对象代表 graph 中的一个音频单元。
- 添加 nodes 到 graph。
- 打开 graph 并且实例化 audio units。
- 获得 audio units 引用。
下面代码显示了如何对包含 Remote I/O unit 和多通道混合器单元的 graph 执行这些步骤。假设已经为每个音频单元定义了 AudioComponentDescription 结构。
objc
AUGraph processingGraph;
NewAUGraph (&processingGraph);
AUNode ioNode;
AUNode mixerNode;
// 已经为每个音频单元定义了 AudioComponentDescription 结构
AUGraphAddNode(processingGraph, &ioUnitDesc, &ioNode);
AUGraphAddNode(processingGraph, &mixerDesc, &mixerNode);
AUGraphAddNode 函数调用使用音频单元说明符 ioUnitDesc 和 mixerDesc。此时,图形被实例化,并拥有您将在应用程序中使用的节点。要打开 graph 并实例化音频单元,请调用 AUGraphOpen:
objc
AUGraphOpen (processingGraph);
然后,通过 AUGraphNodeInfo 函数获取对音频单元实例的引用,如下所示:
objc
AudioUnit ioUnit;
AudioUnit mixerUnit;
AUGraphNodeInfo(processingGraph, ioNode, NULL, &ioUnit);
AUGraphNodeInfo(processingGraph, mixerNode, NULL, &mixerUnit);
ioUnit 和 mixerUnit 变量现在保存对图形中音频单元实例的引用,允许对它们进行配置和互连音频单元。
配置 audio unit
每个 iOS 音频单元都需要自己的配置,这里介绍一些常见的配置:
-
默认情况下,Remote I/O unit 启用输出并禁用输入。如果同时执行 I/O,或仅使用输入,必须相应地重新配置 I/O unit。
-
除 Remote I/O unit 和 Voice-Processing I/O unit 外,所有 iOS 音频单元都需要配置其 kAudioUnitProperty_MaximumFramesPerSlice 属性。此属性确保音频单元准备好生成足够数量的音频数据帧,以响应渲染调用。
-
所有音频单元都需要在输入、输出或两者上定义其音频流格式。
编写并绑定渲染回调函数
对于使用渲染回调函数的设计模式,必须编写这些函数,然后在正确的点添加它们。
可以在音频数据不流动时,使用 audio unit API 立即添加渲染回调:
objc
AURenderCallbackStruct callbackStruct;
callbackStruct.inputProc = &renderCallback;
callbackStruct.inputProcRefCon = soundStructArray;
AudioUnitSetProperty (
myIOUnit,
kAudioUnitProperty_SetRenderCallback,
kAudioUnitScope_Input,
0, // output element
&callbackStruct,
sizeof (callbackStruct)
);
也可以使用 audio processing graph API 以线程安全的方式附加渲染回调,即使在音频数据流动时:
objc
AURenderCallbackStruct callbackStruct;
callbackStruct.inputProc = &renderCallback;
callbackStruct.inputProcRefCon = soundStructArray;
AUGraphSetNodeInputCallback (
processingGraph,
myIONode,
0, // output element
&callbackStruct
);
// ... some time later
Boolean graphUpdated;
AUGraphUpdate (processingGraph, &graphUpdated);
连接 audio unit nodes
在大多数情况下,最好使用 audio processing graph API 中的 AUGraphConnectNodeInput 和 AUGraphDisconnectNodeInput 函数建立或断开音频单元之间的连接。这些函数是线程安全的,避免了显式定义连接的编码开销。
objc
AudioUnitElement mixerUnitOutputBus = 0;
AudioUnitElement ioUnitOutputElement = 0;
AUGraphConnectNodeInput (
processingGraph,
mixerNode, // source node
mixerUnitOutputBus, // source node bus
iONode, // destination node
ioUnitOutputElement // desinatation node element
);
或者,可以使用音频单元属性机制直接建立和断开音频单元之间的连接。要做到这一点,请使用 AudioUnitSetProperty 函数以及 kAudioUnitProperty_MakeConnection 属性。这种方法要求为每个连接定义一个 AudioUnitConnection 结构,作为其属性值。
objc
AudioUnitElement mixerUnitOutputBus = 0;
AudioUnitElement ioUnitOutputElement = 0;
AudioUnitConnection mixerOutToIoUnitIn;
mixerOutToIoUnitIn.sourceAudioUnit = mixerUnitInstance;
mixerOutToIoUnitIn.sourceOutputNumber = mixerUnitOutputBus;
mixerOutToIoUnitIn.destInputNumber = ioUnitOutputElement;
AudioUnitSetProperty (
ioUnitInstance, // connection destination
kAudioUnitProperty_MakeConnection, // property key
kAudioUnitScope_Input, // destination scope
ioUnitOutputElement, // destination element
&mixerOutToIoUnitIn, // connection definition
sizeof (mixerOutToIoUnitIn)
);
提供用户界面
在许多情况下,需要提供一个用户界面,允许用户调整特定的音频单元参数,并在某些特殊情况下调整音频单元属性,比如:要更改 iPod EQ单元的活动均衡曲线,需要更改 kAudioUnitProperty_PresentPreset 属性的值。无论哪种情况,用户界面还应提供有关当前设置的视觉反馈。
初始化然后开启 audio processing graph
在开始音频流之前,必须通过调用 AUGraphInitialize 函数来初始化 audio processing graph。这个关键步骤:
- 通过为每个单元单独自动调用 AudioUnitInitialize 函数来初始化 graph 拥有的音频单元(如果要在不使用 graph 的情况下构建处理链,则必须依次显式初始化每个音频单元)。
- 验证 graph 的连接和音频数据流格式。
- 跨音频单元连接,传播流格式。
objc
OSStatus result = AUGraphInitialize(processingGraph);
// Check for error. On successful initialization, start the graph...
AUGraphStart(processingGraph);
// Some time later
AUGraphStop(processingGraph);
Debug 小技巧
-
通过函数返回值可以检查调用是否成功。
-
注意函数调用之间的依赖性。例如,只有在成功初始化 audio processing graph 后,才能启动它。检查 AUGraphInitialize 的返回值。如果函数成功返回,就可以启动图表。如果失败了,利用 CAShow 函数将 graph 的状态打印到控制台。
-
确保您将每个 AudioStreamBasicDescription 结构初始化为 0,如下所示:
AudioStreamBasicDescription stereoStreamFormat = {0};
。将 ASBD 的字段初始化为 0 可确保没有字段包含垃圾数据(注意:作为类声明中的实例变量,其字段会自动初始化为 0,无需自己初始化它们)。 -
可以将 AudioStreamBasicDescription 结构的字段值打印到 Xcode 控制台,这在开发过程中非常有用。
objc- (void) printASBD: (AudioStreamBasicDescription) asbd { char formatIDString[5]; UInt32 formatID = CFSwapInt32HostToBig(asbd.mFormatID); bcopy(&formatID, formatIDString, 4); formatIDString[4] = '\0'; NSLog (@" Sample Rate: %10.0f", asbd.mSampleRate); NSLog (@" Format ID: %10s", formatIDString); NSLog (@" Format Flags: %10X", asbd.mFormatFlags); NSLog (@" Bytes per Packet: %10d", asbd.mBytesPerPacket); NSLog (@" Frames per Packet: %10d", asbd.mFramesPerPacket); NSLog (@" Bytes per Frame: %10d", asbd.mBytesPerFrame); NSLog (@" Channels per Frame: %10d", asbd.mChannelsPerFrame); NSLog (@" Bits per Channel: %10d", asbd.mBitsPerChannel); }
这种方法可以快速揭示 ASBD 中的问题。