1. 介绍
iOS提供音频处理插件,支持混合、均衡、格式转换和实时输入/输出,用于录音、回放、离线渲染和实时对话,如VoIP (Voice over Internet Protocol)。您可以动态地从iOS应用程序中加载和使用(即托管)这些强大而灵活的插件,即audio units。
Audio units 通常在一个称为音频处理图(audio processing graph)的封闭对象上下文中工作,如图所示。在这个例子中,你的app通过一个或多个回调函数将音频发送到图中的第一个音频单元,并对每个音频单元进行单独控制。I/O unit 的输出(这个或任何音频处理图中的最后一个音频单元)直接连接到输出硬件。
因为 audio units 是 iOS音频编程堆栈的最底层,所以比起其他iOS音频技术,你需要更深入地理解它们。
除非你需要实时播放合成的声音,低延迟的I/O(输入和输出),或特定的音频单元功能,否则首先考虑的应该是 Media Player, AV Foundation, OpenAL, 或者 Audio Toolbox frameworks这些技术。这些更高层次的技术可以代替您使用音频单元,并提供重要的附加功能,如 Multimedia Programming Guide 所述
Audio Units Provide Fast, Modular Audio Processing(音频单元提供快速,模块化音频处理)
直接使用音频单元的两个最大优势是:
- 优秀的响应能力。因为你可以访问音频单元渲染回调函数中的实时优先级线程,所以你的音频代码尽可能接近金属。合成乐器和实时同步语音I/O从直接使用音频单元中获益最多。
- 动态重新配置。音频处理图形API是围绕 AUGraph 不透明类型构建的,它允许你在处理音频的同时,以线程安全的方式动态地组装、重新配置和重新安排复杂的音频处理链。这是iOS中唯一提供此功能的音频API。
音频单元的生命周期如下:
- 在运行时,获取一个动态可链接库的引用,该库定义了您想要使用的音频单元
- 实例化音频单元
- 根据所需的类型配置音频单元,满足你的需求
- 初始化音频单元,使其准备好处理音频
- 开始音频流
- 控制音频单元
- 完成后,释放音频单元
音频单元提供了非常有用的个人功能,如立体声平移、混合、音量控制和音频电平测量。托管音频单元可以让你将这些功能添加到你的应用程序中。然而,为了获得这些好处,你必须了解一些基本概念,包括音频数据流格式、渲染回调函数和音频单元架构。
Choosing a Design Pattern and Constructing Your App(选择一个设计模式并构建你的应用)
音频单元托管设计模式提供了一个灵活的蓝图来定制应用的细节。每个模式都表示:
- 如何配置I/O单元。I/O单元有两个独立的元素,一个从输入硬件接收音频,一个将音频发送到输出硬件。每个设计模式都指示应该启用哪个或哪些元素。
- 其中,在音频处理图中,必须指定音频数据流格式。您必须正确指定支持音频流的格式。
- 在哪里建立音频单元连接以及在哪里添加渲染回调函数。音频单元连接是一种正式的构造,它将流格式从一个音频单元的输出传播到另一个音频单元的输入。回调允许您将音频输入到 graph 中,或在 graph 中的单个示例级别上操作音频
无论你选择哪种设计模式,构建音频单元托管应用的步骤基本上是相同的:
- 配置应用程序音频会话,以确保应用程序在系统和设备硬件环境下正确工作
- 构造一个音频处理图(audio processing graph)。这个多步骤的过程利用了你在音频单元托管基础中学到的一切
- 提供一个用户界面来控制图形的音频单元
熟悉这些步骤,以便将它们应用到您自己的项目中。
Get the Most Out of Each Audio Unit(充分利用每个音频单元)
本文的大部分内容将告诉你所有iOS音频单元都具有重要的共同属性。例如,你的应用程序需要在运行时指定和加载音频单元,然后正确地指定它的音频流格式。
同时,每个音频单元都有某些独特的功能和要求,从要使用的正确音频样本数据类型到正确行为所需的配置。了解每个音频单元的使用细节和具体功能,这样你就知道,例如,何时使用3D Mixer单元,何时使用Multichannel Mixer。
How to Use This Document
如果你想在开始你的项目之前有一个坚实的概念基础,请先阅读 Audio Unit Hosting Fundamentals。本章解释api背后的概念。继续阅读 Constructing Audio Unit Apps,学习如何为你的项目选择设计模式和构建应用程序的工作流。
如果你有一些音频单元的经验,只是想要特定类型的细节,你可以从 Using Specific Audio Units 开始
基本参考文档包括以下内容:
-
Audio Unit Properties Reference 描述可用于配置每种音频单元类型的属性
-
Audio Unit Parameters Reference 描述可用于控制每种音频单元类型的参数。
-
Audio Unit Component Services Reference 描述用于访问音频单元参数和属性的API,并描述各种音频单元回调函数。
-
Audio Component Services Reference 描述了在运行时访问音频单元和管理音频单元实例的API。
-
Audio Unit Processing Graph Services Reference 描述用于构造和操作音频处理图的API,这些图是动态可重构的音频处理链。
-
Core Audio Data Types Reference 描述托管音频单元所需的数据结构和类型。
2. Audio Unit Hosting 基础
iOS中的所有音频技术都建立在音频单元之上,如图所示。这里展示的高级技术 Media Player, AV Foundation, OpenAL, 和 Audio Toolbox 都是包装的音频单元,为特定任务提供专用的api
在项目中只有当你需要最高程度的控制、性能或灵活性时,或者当你需要一个只有直接使用音频单元才能获得的特定功能 (如回声消除) 时,直接使用音频单元才是正确的选择,否则请选用其他api
Audio Units Provide Fast, Modular Audio Processing(音频单元提供快速,模块化音频处理)
当你需要以下内容之一时,直接使用音频单元,而不是通过更高层次的api:
- 低延迟的同步音频I/O(输入和输出),如VoIP (Voice over Internet Protocol)应用程序
- 对合成声音的响应式播放,如音乐游戏或合成乐器
- 使用一种特定的音频单元功能,如回声消除、混音或音调均衡
- 一个处理链架构,让您将音频处理模块组装到灵活的网络中。这是iOS中唯一提供此功能的音频API。
Audio Units in iOS
iOS提供7个音频单元,按用途分为4类
| Purpose | Audio units |
|---|---|
| Effect | iPod Equalizer |
| Mixing | 3D Mixer Multichannel Mixer |
| I/O | Remote I/O Voice-Processing I/O Generic Output |
| Format conversion | Format Converter |
注意:iOS动态插件架构不支持第三方音频单元。也就是说,唯一可用于动态加载的音频单元是由操作系统提供的。
1. Effect Unit
iOS 4提供了一个效果单元,iPod均衡器,相同的均衡器使用内置的iPod应用程序。要查看iPod应用程序的用户界面为这个音频单元,去设置> iPod > EQ。当使用这个音频单元,你必须提供自己的UI。这个音频单元提供了一套预设的均衡曲线,如低音助推器,流行,和口头的词。
2. Mixer Units
iOS提供了两个混合器单元。3D Mixer单元是OpenAL建立的基础。在大多数情况下,如果你需要3D Mixer单元的功能,你最好的选择是使用OpenAL,它提供了一个更高级的API,非常适合游戏应用程序。
多通道混频器单元提供任何数量的单声道或立体声流的混合,与立体声输出。您可以打开或关闭每个输入,设置其输入增益,并设置其立体声平移位置。有关如何使用此音频单元的演示,请参见示例代码项目audio Mixer (MixerHost)。
3. I/O Units
iOS提供3个I/O单元。Remote I/O 单元是最常用的。它连接到输入和输出音频硬件,为您提供低延迟访问单个传入和传出音频样本值。它提供了硬件音频格式和应用程序音频格式之间的格式转换,通过包含的格式转换器单元来实现
Voice-Processing I/O单元通过添加用于VoIP或语音聊天应用程序的回声消除来扩展远程I/O单元。它还提供自动增益校正,语音处理质量的调整和静音。
Generic Output单元不连接音频硬件,而是提供了一种将处理链的输出发送到应用程序的机制。您通常会使用Generic Output单元进行离线音频处理。
4. Format Converter Unit
iOS 4提供了一个格式转换器单元,通常通过I/O单元间接使用。
Use the Two Audio Unit APIs in Concert(同时使用两个音频单元api)
iOS有一个用于直接处理 audio units 的API,还有另一个用于处理 audio processing graphs。当你在应用中使用音频单元时,你会同时使用这两个api。
- 要直接使用 audio units (配置和控制它们),请使用 Audio Unit Component Services Reference 中描述的方法
- 要创建和配置 audio processing graph (音频单元的处理链),请使用 Audio Unit Processing Graph Services Reference 中描述的方法
这两个api之间有一些重叠,您可以根据自己的编程风格自由混合和匹配。audio unit API 和 audio processing graph API 分别提供以下功能:
- 获取对定义音频单元的动态链接库的引用
- 实例化音频单元
- 连接音频单元和附加渲染回调函数
- 启动和停止音频流
本文档提供了使用这两种API的代码示例,但重点关注 audio processing graph API。如果需要在代码中的两个API之间进行选择,请使用graph API,除非您有特定的理由不这样做。你的代码会更紧凑,更容易阅读,更易于支持动态重新配置 (见 Audio Processing Graphs Provide Thread Safety )
Use Identifiers to Specify and Obtain Audio Units(使用标识符来指定和获取音频单元)
要在运行时找到音频单元,首先在音频组件描述数据结构中指定其 type、subtype 和 manufacturer keys。无论使用audio unit 还是 audio processing graph API,都要做到这一点
Listing 1-1 Creating an audio component description to identify an audio unit
AudioComponentDescription ioUnitDescription;
ioUnitDescription.componentType = kAudioUnitType_Output;
ioUnitDescription.componentSubType = kAudioUnitSubType_RemoteIO;
ioUnitDescription.componentManufacturer = kAudioUnitManufacturer_Apple;
ioUnitDescription.componentFlags = 0;
ioUnitDescription.componentFlagsMask = 0;
这个描述精确地指定了一个音频单元—Remote I/O unit。这个key和iOS中其他音频单元key都列在 Identifier Keys for Audio Units 中。注意,所有iOS音频单元在componentManufacturer字段中都使用 kAudioUnitManufacturer_Apple。
要创建通配符描述,请将一个或多个 type / subtype 字段设置为0。例如,要匹配所有I/O单元,将设置 componentSubType = 0
有了描述之后,您可以使用两个api中的任意一个来获得指定音频单元(或音频单元集)的库引用。音频单元API如下所示
Listing 1-2 Obtaining an audio unit instance using the audio unit API
AudioComponent foundIoUnitReference = AudioComponentFindNext (
NULL,
&ioUnitDescription
);
AudioUnit ioUnitInstance;
AudioComponentInstanceNew (
foundIoUnitReference,
&ioUnitInstance
);
-
将 NULL 传递给 AudioComponentFindNext 的第一个参数,告诉该函数使用系统定义的顺序找到匹配描述的第一个系统音频单元。如果在此参数中传递先前找到的音频单元引用,该函数将定位与描述匹配的下一个音频单元。例如,这种用法允许您通过反复调用 AudioComponentFindNext 来获得对所有I/O单元的引用
-
AudioComponentFindNext 调用的第二个参数引用清单1-1中定义的音频单元描述。
-
AudioComponentFindNext 函数的结果是对定义音频单元的动态可链接库的引用。将引用传递给 AudioComponentInstanceNew 函数来实例化音频单元,如清单1-2所示。
你可以使用 audio processing graph API 来实例化音频单元。清单1-3显示了如何做到这一点
Listing 1-3 Obtaining an audio unit instance using the audio processing graph API
// Declare and instantiate an audio processing graph
AUGraph processingGraph;
NewAUGraph (&processingGraph);
// Add an audio unit node to the graph, then instantiate the audio unit
AUNode ioNode;
AUGraphAddNode (
processingGraph,
&ioUnitDescription,
&ioNode
);
AUGraphOpen (processingGraph); // indirectly performs audio unit instantiation
// Obtain a reference to the newly-instantiated I/O unit
AudioUnit ioUnit;
AUGraphNodeInfo (
processingGraph,
ioNode,
NULL,
&ioUnit
);
此代码清单介绍了 AUNode,这是一种不透明的类型,表示 audio processing graph 上下文中的音频单元。在 AUGraphNodeInfo 函数调用的输出中,您在 ioUnit 参数中接收到对新的音频单元实例的引用
AUGraphAddNode 调用的第二个参数引用清单1-1中定义的音频单元描述。
获得音频单元实例后,可以对其进行配置。为此,您需要了解两个音频单元的特性,scopes 和 elements
Use Scopes and Elements to Specify Parts of Audio Units(使用 Scopes 和 Elements 来指定音频单元的部分)
一个音频单元的各个部分被组织成 scopes 和 elements,如图1-2所示。当调用一个函数来配置或控制一个音频单元时,您可以指定 scopes 和 elements 来标识该函数的特定目标。
Figure 1-2 Audio unit scopes and elements
scope 是音频单元中的编程上下文。尽管 global scope 的名称可能暗示了其他情况,但这些上下文从来不是嵌套的。通过使用 Audio Unit Scopes 枚举中的常量来指定目标scope。
element 是嵌套在 audio unit scope 内的编程上下文。当一个 element 是输入或输出 scope 的一部分时,它类似于物理音频设备中的信号总线—因此有时称为 bus。这两个术语—element 和 bus—在音频单元编程中指的是完全相同的东西。本文档在强调信号流时使用 bus,在强调音频单元的特定功能方面使用 elements,如I/O单元的输入和输出 elements (见 Essential Characteristics of I/O Units)
可以通过 zero-indexed 整数值指定 element (或 bus)。如果设置一个应用于整个 scope 的属性或参数,则将 element 值指定为0。
上图1-2说明了音频单元的一种常见架构,其中输入和输出的 elements 数量是相同的。然而,不同的音频单元使用不同的架构。例如,一个混频器单元可能有几个输入 elements ,但只有一个输出 elements 。您可以将在这里学到的关于 scope 和 elements 的知识扩展到任何音频单元,尽管架构中存在一些变化
global scope,如图1-2的底部所示,应用于作为一个整体的音频单元,而不与任何特定的音频流相关联。它只有一个 element,即 element 0。有些属性,比如每个片的最大帧数(kAudioUnitProperty_MaximumFramesPerSlice),只应用于 global scope。
输入和输出 scopes 直接参与通过音频单元移动一个或多个音频流。如您所料,音频从输入 scope 进入,从输出 scope 离开。属性或参数可以作为一个整体应用于输入或输出 scope,例如 element 计数属性 (kAudioUnitProperty_ElementCount)。其他属性和参数,如启用I/O属性(kAudioOutputUnitProperty_EnableIO)或卷参数(kMultiChannelMixerParam_Volume),应用于范围内的特定 element
Use Properties to Configure Audio Units(使用属性配置音频单元)
音频单元 property 是一个 key-value 键值对,可以用来配置音频单元。属性的键是具有相关助记符标识符的唯一整数,例如 kAudioUnitProperty_MaximumFramesPerSlice = 14。Apple保留从0到63999的属性键。在Mac OS X中,第三方音频单元使用这个范围以上的key
每个属性的值都是指定的数据类型,并具有指定的读/写访问权限,如 Audio Unit Properties Reference 中所述。要在任何音频单元上设置任何属性,可以使用一个灵活的函数: AudioUnitSetProperty。清单1-4显示了该函数的典型用法,注释突出显示了如何指定 scope 和 element,以及指示属性的键和值。
Listing 1-4 Using scope and element when setting a property
UInt32 busCount = 2;
OSStatus result = AudioUnitSetProperty (
mixerUnit,
kAudioUnitProperty_ElementCount, // the property key
kAudioUnitScope_Input, // the scope to set the property on
0, // the element to set the property on
&busCount, // the property value
sizeof (busCount)
);
以下是你在音频单元开发中会经常用到的一些属性。通过阅读它的参考文档来熟悉它们:
- kAudioOutputUnitProperty_EnableIO, 用于启用或禁用I/O输入或输出。默认情况下,输出是启用的,输入是禁用的。
- kAudioUnitProperty_ElementCount, 例如,用于配置 mixer unit 上的输入 elements 数量
- kAudioUnitProperty_MaximumFramesPerSlice,为了指定音频数据的最大帧数,音频单元应准备用于响应渲染调用而产生。对于大多数音频单元,在大多数情况下,您必须按照参考文档中描述的那样设置此属性。如果你不这样做,你的音频将在屏幕锁定时停止
- kAudioUnitProperty_StreamFormat,用于指定特定音频单元输入或输出 bus 的音频流数据格式
大多数属性值只能在音频单元未初始化时设置。用户不打算更改这些属性。但是,有些,比如iPod EQ单元的 kAudioUnitProperty_PresentPreset 属性,以及 Voice-Processing I/O 单元的 kAUVoiceIOProperty_MuteOutput 属性,是打算在播放音频时更改的
要发现属性的可用性,访问其值,并监视其值的更改,可以使用以下函数:
- AudioUnitGetPropertyInfo,查明某属性是否可用; 如果是,则给出其值的数据大小以及是否可以更改该值
- AudioUnitGetProperty, AudioUnitSetProperty,获取或设置属性的值
- AudioUnitAddPropertyListener,AudioUnitRemovePropertyListenerWithUserData,安装或移除回调函数以监视对属性值的更改
Use Parameters and UIKit to Give Users Control
音频单元参数用户是可配置的,当音频单元产生音频时,该设置可以改变。事实上,大多数参数(如音量或立体声平移位置)的目的是实时调整音频单元正在执行的处理。
与音频单元属性一样,音频单元参数也是键值对。key是由它所适用的音频单元定义的。它总是一个枚举常量,例如 kMultiChannelMixerParam_Pan = 2,它对音频单元是唯一的,但不是全局唯一的。
与属性值不同,每个参数值都具有相同的类型: 32位浮点数。一个值的允许范围,以及它所代表的度量单位,是由参数的音频单元的实现确定的。这些和其他方面的参数在 Audio Unit Parameters Reference
要获取或设置参数值,请使用以下函数之一,音频单元组件服务参考中详细描述了这些函数:
为了让用户控制一个音频单元,让他们通过用户界面访问它的参数。首先从UIKit框架中选择一个合适的类来表示参数。对于一个 on/off 特性,例如 Multichannel Mixer单元的kMultiChannelMixerParam_Enable 参数,你可以使用一个UISwitch对象。对于一个持续变化的特征,比如由 kMultiChannelMixerParam_Pan 参数提供的立体平移位置,你可以使用UISlider对象
Essential Characteristics of I/O Units(I/O单元的基本特征)
一个I/O单元包含两个 elements,如图1-3所示。
Figure 1-3 The architecture of an I/O unit
尽管这两个 elements 是一个音频单元的一部分,但你的应用将它们视为独立的实体。例如,您可以根据应用程序的需要使用enable I/O属性 (kAudioOutputUnitProperty_EnableIO) 来独立地启用或禁用每个element。
I/O单元的 Element 1 直接连接到设备上的音频输入硬件,在图中由麦克风表示。这个硬件连接 ( element 1 的 Input scope ) 对您来说是不透明的。对从输入硬件输入的音频数据的第一次访问是在 element 1 的 Output scope 内。
类似地,I/O单元的 element 0 直接连接设备上的音频输出硬件,如图1-3所示,由扬声器表示。您可以将音频传递到 element 0 的 Input scope,但其 Output scope 是不透明的。
在处理音频单元时,你经常会听到I/O单元的两个 elements 不是通过数字而是通过名称来描述:
-
Input element 是 element 1 ( input 中的字母 i 的外观与数字1相似)
-
Output element 是 element 0 ( 单词 output 的字母 O 的外观类似于数字0)
如图1-3所示,每个 element 本身都有一个 Input scope 和一个 Output scope 。出于这个原因,描述I/O单元的这些部分可能会有点混乱。例如,您可以说,在一个同步I/O应用程序中,您从 Input element 的 Output scope 接收音频,并将音频发送到 Output element 的 Input scope
最后,I/O单元是 audio processing graph 中唯一能够启动和停止音频流的音频单元。通过这种方式,I/O单元负责音频单元应用程序中的音频流
Audio Processing Graphs Manage Audio Units
audio processing graph 是一种 Core Foundation风格的不透明类型,AUGraph,您可以使用它来构建和管理音频单元处理链。graph 可以利用多个音频单元和多个渲染回调函数的功能,允许您创建几乎任何您能想到的音频处理解决方案
AUGraph 类型为音频单元添加了线程安全性: 它允许你动态地重新配置处理链。例如,当音频正在播放时,您可以安全地插入一个均衡器,或者甚至为一个混音输入交换一个不同的渲染回调函数。实际上,AUGraph类型提供了iOS中唯一用于在音频应用中执行这种动态重新配置的API。
audio processing graph API 使用另一种不透明的类型 AUNode 来表示 graph 上下文中的单个音频单元。当使用 graph 时,您通常将 nodes 作为其包含的音频单元的代理与它们交互,而不是直接与音频单元交互
然而,当将 graph 组合在一起时,你必须配置每个音频单元,为此你必须通过 audio unit API 直接与音频单元交互。音频单元节点本身是不可配置的。通过这种方式,构造一个 graph 需要您使用这两个api,如在 Use the Two Audio Unit APIs in Concert 中所解释的那样
您还可以通过定义node来表示完整的音频处理 subgraph,将 AUNode 实例作为复杂graph中的element使用。在这种情况下, subgraph 末尾的I/O单元必须是Generic Output单元—不连接设备硬件的I/O单元的一种类型。
大体来说,构建 audio processing graph 需要三个任务:
- 向 graph 添加 nodes
- 直接配置由 nodes 表示的音频单元
- 连接 nodes
有关这些任务和 audio processing graph 生命周期其余部分的详细信息,参考Constructing Audio Unit Apps。有关API的完整描述,请参阅 Audio Unit Processing Graph Services Reference
An Audio Processing Graph Has Exactly One I/O Unit(一个音频处理图有一个I/O单元)
每个 audio processing graph 都有一个I/O单元,无论您是在进行录音、播放还是同时I/O。I/O单元可以是iOS中可用的任意一种,这取决于你的应用程序的需求。关于I/O单元如何在各种使用场景中适合 audio processing graph 的架构,请参见 Start by Choosing a Design Pattern
Graphs 让你通过 AUGraphStart 和 AUGraphStop 函数来启动和停止音频流。这些函数依次通过调用其 AudioOutputUnitStart 或 AudioOutputUnitStop 函数向I/O单元传递启动或停止消息。这样,graph 的I/O单元负责 graph 中的音频流
Audio Processing Graphs Provide Thread Safety(音频处理图形提供线程安全性)
audio processing graph API 采用了 ”to-do list“ 的隐喻来提供线程安全性。此API中的某些函数将一个工作单元添加到更改列表中,以便稍后执行。在您指定了一组完整的更改之后,您可以要求 graph 来实现它们。
以下是 audio processing graph API 支持的一些常见的重新配置,以及它们的相关功能:
- 添加 或 删除 音频单元 nodes ( AUGraphAddNode, AUGraphRemoveNode )
- 添加 或 删除 nodes 之间的连接 ( AUGraphConnectNodeInput, AUGraphDisconnectNodeInput)
- 连接渲染回调函数到音频单元的输入bus ( AUGraphDisconnectNodeInput )
让我们来看一个重新配置正在运行的 audio processing graph 的示例。例如,您已经构建了一个包含Multichannel Mixer单元和Remote I/O单元的 graph,用于两个合成声音的混合回放。您将声音输入mixer的两个输入bus。mixer 输出转到 I/O单元的输出 element,然后转到输出音频硬件。该架构如图1-4所示。
Figure 1-4 A simple audio processing graph for playback
现在,假设用户想将均衡器插入到两个音频流中的其中一个。要做到这一点,在其中一个声音的输入和它要进入的 mixer 输入之间添加iPod EQ单元,如图1-5。
Figure 1-5 The same graph after inserting an equalizer
完成这个实时重新配置的步骤如下:
- 通过调用 AUGraphDisconnectNodeInput 从 mixer 单元的 Input 1 断开 “beats sound” 回调
- 将包含iPod EQ单元的音频单元node添加到graph中。通过使用 AudioComponentDescription 结构体指定iPod EQ单元,然后调用AUGraphAddNode来实现这一点。在这一点上,iPod EQ单元被实例化,但没有初始化。它属于 graph,但尚未参与音频流。
- 配置和初始化iPod EQ单元。在这个例子中,这需要一些东西:
- 调用 AudioUnitGetProperty 函数从 mixer 输入中检索流格式 ( kAudioUnitProperty_StreamFormat )
- 调用两次 AudioUnitSetProperty 函数,一次是在iPod EQ单元的输入上设置流格式,第二次是在输出上设置流格式。(有关如何配置iPod EQ单元的完整描述,请参见Using Effect Units)
- 调用 AudioUnitInitialize 函数为iPod EQ单元分配资源,并准备它处理音频。这个函数调用不是线程安全的,但是当iPod EQ单元还没有积极参与音频处理图形(因为您还没有调用AUGraphUpdate函数)时,您可以(而且必须)在这一点上执行它。
- 通过调用AUGraphSetNodeInputCallback将“beats sound”回调函数附加到iPod EQ的输入。
在前面的列表中,步骤1、2和4——都是 AUGraph* 函数调用—被添加到图graph的 “to-do” 列表中。调用AUGraphUpdate来执行这些挂起的任务。在成功返回AUGraphUpdate 函数时,graph已被动态重新配置,iPod EQ已到位并处理音频。
Audio Flows Through a Graph Using “Pull”(使用 ”pull“ 将音频流通过graph)
在 audio processing graph 中,消费者在需要更多音频数据时调用提供者。对音频数据的请求有一个流,这个流与音频流的方向相反。原理如图1-6所示。
Figure 1-6 The pull mechanism of audio data flow
对一组数据的每个请求都称为呈现调用,或者,称为 pull。该图以灰色的“control flow”箭头表示渲染调用。渲染调用请求的数据更确切地说是一组音频样本帧 (参见 frame)
反过来,响应渲染调用而提供的一组音频样本帧被称为 slice。(参见 slice )。提供 slice 的代码被称为渲染回调函数,在Render Callback Functions Feed Audio to Audio Units中描述。
在图1-6中,pull 的过程如下:
- 在您调用 AUGraphStart 函数之后,虚拟输出设备将调用 Remote I/O单元的 Output element 的回调。该调用请求处理过的音频数据帧的slice
- Remote I/O单元的回调函数在其输入缓冲区中查找要处理的音频数据,以满足调用。如果有数据等待处理,Remote I/O单元将使用它。否则,如图所示,它会调用应用连接到输入的回调函数。在这个例子中,Remote I/O单元的输入连接到 effect单元 的输出。所以,I/O单元 pulls effect单元,请求slice音频帧。
- effect单元的行为就像Remote I/O单元一样。当它需要音频数据时,它从输入连接中获取。在这个例子中,effect单元 pull 应用的渲染回调函数。
- 你的应用程序的渲染回调函数是 pull 的最终接收者。它将请求的帧提供给effect单元。
- effect单元 处理应用的渲染回调提供的slice。然后,effect单元将先前(在步骤2中)请求的已处理数据提供给 Remote I/O单元
- Remote I/O单元处理由effect单元提供的slice。然后,Remote I/O单元将最初请求(在步骤1中)的已处理slice提供给虚拟输出设备。这完成了一个pull的循环
Render Callback Functions Feed Audio to Audio Units(渲染回调函数提供音频到音频单元)
为了从磁盘或内存提供音频到音频单元输入bus,使用符合AURenderCallback原型的渲染回调函数来传递它。当音频单元输入需要另一个样本帧slice时,它会调用你的回调函数,如在Audio Flows Through a Graph Using Pull中所述
编写回调函数的过程可能是设计和构建音频单元应用程序中最具创造性的方面。这是你以任何你能想象和编码的方式产生或改变声音的机会。
同时,渲染回调有一个必须遵守的严格性能要求。渲染回调存在于一个实时优先级线程上,后续的渲染回调将在该线程上异步到达。您在渲染回调中所做的事情是有时间限制的。如果当下一个回调到来时,你仍然在处理前一个渲染调用,那么你会在声音中得到一个间隙。因此,您不能在渲染回调函数中使用锁、分配内存、访问文件系统或网络连接,或以其他方式执行耗时的任务。
Understanding the Audio Unit Render Callback Function(理解音频单元渲染回调函数)
清单1-5显示了符合 AURenderCallback 原型的渲染回调函数的头。本节将依次介绍每个参数的用途和使用方法。
Listing 1-5 A render callback function header
static OSStatus MyAURenderCallback (
void *inRefCon,
AudioUnitRenderActionFlags *ioActionFlags,
const AudioTimeStamp *inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList *ioData
) { /* callback body */ }
-
inRefCon 参数指向你在将回调附加到音频单元输入时指定的编程上下文 (参见Write and Attach Render Callback Functions)。这个上下文的目的是为回调函数提供它需要的任何音频输入数据或状态信息,以计算给定渲染调用的输出音频
-
ioActionFlags 参数允许回调当没有音频需要处理时为音频单元提供一个提示。例如,如果你的应用是一把合成吉他,而用户当前没有在弹奏某个音符,在回调函数体中使用如下语句:
*ioActionFlags |= kAudioUnitRenderAction_OutputIsSilence; -
inBusNumber 参数表示调用回调的音频单元总线,允许您根据此值在回调中进行分支。此外,当附加回调到一个音频单元时,你可以为每个总线指定不同的上下文(inRefCon)。
-
inNumberFrames 参数指示在当前调用中请求回调提供的音频样本帧的数量。你在ioData参数中提供这些帧给缓冲区
-
ioData 参数指向在调用回调函数时必须填充的音频数据缓冲区。放入这些缓冲区的音频必须符合调用回调的总线的音频流格式。
如果对回调的特定调用保持沉默,则显式地将这些缓冲区设置为0,例如使用memset函数。
图1-7在ioData参数中描述了一对非交错立体声缓冲区。使用图中的element来可视化回调需要填充的ioData缓冲区的细节
Figure 1-7 The ioData buffers for a stereo render callback function
Audio Stream Formats Enable Data Flow(音频流格式启用数据流)
在单个示例级别处理音频数据时,就像使用音频单元时一样,仅指定表示音频的正确数据类型是不够的。单个音频样本值中的位的布局是有意义的,所以像Float32或UInt16这样的数据类型表现力不够。在本节中,你将了解Core Audio的解决方案。
Working with the AudioStreamBasicDescription structure(使用AudioStreamBasicDescription结构)
Listing 1-6 The AudioStreamBasicDescription structure
struct AudioStreamBasicDescription {
Float64 mSampleRate;
UInt32 mFormatID;
UInt32 mFormatFlags;
UInt32 mBytesPerPacket;
UInt32 mFramesPerPacket;
UInt32 mBytesPerFrame;
UInt32 mChannelsPerFrame;
UInt32 mBitsPerChannel;
UInt32 mReserved;
};
typedef struct AudioStreamBasicDescription AudioStreamBasicDescription;
因为名字AudioStreamBasicDescription很长,所以它通常被缩写为ASBD。要为ASBD的字段定义值,编写类似于清单1-7所示的代码。
Listing 1-7 Defining an ASBD for a stereo stream
size_t bytesPerSample = sizeof (AudioUnitSampleType);
AudioStreamBasicDescription stereoStreamFormat = {0};
stereoStreamFormat.mFormatID = kAudioFormatLinearPCM;
stereoStreamFormat.mFormatFlags = kAudioFormatFlagsAudioUnitCanonical;
stereoStreamFormat.mBytesPerPacket = bytesPerSample;
stereoStreamFormat.mBytesPerFrame = bytesPerSample;
stereoStreamFormat.mFramesPerPacket = 1;
stereoStreamFormat.mBitsPerChannel = 8 * bytesPerSample;
stereoStreamFormat.mChannelsPerFrame = 2; // 2 indicates stereo
stereoStreamFormat.mSampleRate = graphSampleRate;
首先,确定表示一个音频样本值的数据类型。这个例子使用AudioUnitSampleType定义的类型,这是大多数音频单元推荐的数据类型。在iOS中,AudioUnitSampleType被定义为一个8.24定点整数。清单1-7中的第一行计算类型中的字节数;当定义ASBD的一些字段值时,需要这个数字,如您在清单中所见。
接下来,仍然参考清单1-7,声明一个AudioStreamBasicDescription类型的变量,并将其字段初始化为0,以确保没有字段包含垃圾数据。不要跳过这个归零步骤
现在定义ASBD字段值。为mFormatID字段指定kAudioFormatLinearPCM。音频单元使用未压缩音频数据,所以这是在处理音频单元时使用的正确格式标识符
接下来,对于大多数音频单元,为mFormatFlags字段指定kAudioFormatFlagsAudioUnitCanonical metaflag。这个标志在CoreAudio.framework/CoreAudioTypes.h 中定义如下:
kAudioFormatFlagsAudioUnitCanonical = kAudioFormatFlagIsFloat |
kAudioFormatFlagsNativeEndian |
kAudioFormatFlagIsPacked |
kAudioFormatFlagIsNonInterleaved
这个 metaflag 负责为类型 AudioUnitSampleType 的 linear PCM 样本值中的位指定所有的布局细节。
某些音频单元采用一种非典型的音频数据格式,样例需要不同的数据类型,mFormatFlags 字段需要不同的一组标志。例如,3D Mixer单元需要音频样本值的UInt16数据类型,并且需要ASBD的 mFormatFlags 字段设置为kAudioFormatFlagsCanonical。当使用特定的音频单元时,要注意使用正确的数据格式和正确的格式标志。(参见 Using Specific Audio Units )
继续到清单1-7,接下来的四个字段进一步指定了示例帧中的位的组织和含义。设置这些字段- mbytesperpacket, mBytesPerFrame, mFramesPerPacket和mBitsPerChannel字段-根据你使用的音频流的性质。要了解这些字段的含义,请参考 AudioStreamBasicDescription 结构的文档
根据流中的通道数设置 ASBD 的 mChannelsPerFrame 字段 ,1用于单声道音频,2用于立体声,等等。
最后,根据你在整个应用中使用的采样率设置mSampleRate字段。了解 Understanding Where and How to Set Stream Formats 重要性。Configure Your Audio Session 说明了如何确保您的应用程序的采样率匹配音频硬件采样率。
您可以使用 CAStreamBasicDescription.h 文件(/Developer/Extras/CoreAudio/PublicUtility/) 中提供的c++实用方法,而不是像您在这里看到的那样逐个字段指定ASBD字段。特别是,查看 SetAUCanonical 和 SetCanonical c++方法。这些指定了在给定三个因素的情况下获得ASBD字段值的正确方法:
- 流是用于I/O (SetCanonical) 还是用于 音频处理 (SetAUCanonical)
- 您希望流格式表示多少个通道
- 是要交错流格式还是非交错流格式
不管你是否在你的项目中包含CAStreamBasicDescription.h文件来直接使用它的方法,苹果建议你研究该文件来学习正确的方法来使用 AudioStreamBasicDescription结构。
关于如何修复与音频数据流格式相关的问题,请参阅 Troubleshooting Tips
Understanding Where and How to Set Stream Formats(了解在哪里和如何设置流格式)
您必须在audio processing graph中的临界点设置音频数据流格式。在其他方面,系统设置格式。还有一些时候,音频单元连接将流格式从一个音频单元传播到另一个音频单元。
iOS设备上的音频输入和输出硬件具有系统确定的音频流格式。这些格式总是未压缩的, linear PCM 格式和交错。系统将这些格式施加到 audio processing graph 中I/O单元向外的一侧,如图1-8所示
Figure 1-8 Where to set audio data stream formats
在图中,麦克风代表输入音频硬件。系统确定输入硬件的音频流格式,并将其施加到 Remote I/O单元的输入element的 Input scope。
同样,图中的扬声器表示输出音频硬件。系统决定输出硬件的流格式,并将其施加到Remote I/O单元的输出element 的 Output scope。
您的应用程序负责在I/O单元element的面向内部的一侧建立音频流格式。I/O单元执行应用程序格式和硬件格式之间的任何必要转换。您的应用程序还负责设置流格式,在graph中需要它们的任何地方。在某些情况下,例如在图1-8中的Multichannel Mixer unit 的输出中,您只需要设置格式的一部分—特别是采样速率。从选择一个设计模式开始,教你如何为各种类型的音频单元应用程序设置流格式。Using Specific Audio Units 列出了每个iOS音频单元的流格式要求。
音频单元连接的一个关键特征,如图1-8所示,是连接将音频数据流格式从源音频单元的输出传播到目标音频单元的输入。这是一个关键点,因此需要强调:流格式的传播是通过音频单元连接发生的,并且只在一个方向上—从源音频单元的输出到目标音频单元的输入。
利用格式传播。它可以显著减少您需要编写的代码量。例如,当连接多通道Mixer单元的输出到 Remote I/O单元进行回放时,您不需要设置I/O单元的流格式。它是通过音频单元之间的连接来适当设置的,基于 mixer 的输出流格式(见图1-8)。
流格式传播发生在 audio processing graph 生命周期中的一个特定点上—即初始化时。参见 Initialize and Start the Audio Processing Graph
您可以非常灵活地定义应用程序音频流格式。但是,只要可能,就使用硬件正在使用的采样率。当您这样做时,I/O单元不需要执行采样率转换。这将使能源消耗最小化(这是移动设备的一个重要考虑因素),并使音频质量最大化。要了解如何使用硬件采样率,请参见 Configure Your Audio Session
3. 构建 Audio Unit Apps
现在你已经理解了音频单元是如何工作的,就像在 Audio Unit Hosting Fundamentals 中所解释的那样,你已经为构建应用的音频单元部分做好了准备。主要步骤是选择一个设计模式,然后编写代码来实现该模式。
Start by Choosing a Design Pattern
在iOS应用中音频单元有 6 种基本设计模式。首先选择最能代表你希望应用如何处理音频的模式。当您学习每个模式时,请注意它们的共同特征。每一个模式:
- 只有一个 I/O 单元
- 在整个 audio processing graph 中使用单一的音频流格式—尽管该格式可以有变体,例如单声道和立体声流输入混音单元
- 要求您在特定位置设置流格式或流格式的部分
正确设置流格式是建立音频数据流的必要条件。这些模式中的大多数依赖于音频流格式从源到目标的自动传播,如音频单元连接所提供的那样。尽可能地利用这种传播,因为它减少了编写和维护的代码量。与此同时,请确保您了解需要在何处设置流格式。例如,您必须在iPod EQ单元的输入和输出上设置完整的流格式。对于所有iOS音频单元流格式要求,请参考 Using Specific Audio Units
在大多数情况下,本章中的设计模式使用了 audio processing graph ( AUGraph类型 )。您可以在不使用 graph 的情况下实现这些模式中的任何一种,但是使用 graph 可以简化代码并支持动态重新配置,如 Audio Processing Graphs Manage Audio Units 中所述
1. I/O Pass Through
I/O pass-through 模式将传入的音频直接发送到输出硬件,没有选项来处理音频数据。尽管这并没有什么实用价值,但基于这种模式构建音频单元托管应用是验证和巩固你对音频单元概念理解的好方法。该模式如图2-1所示。
Figure 2-1 Simultaneous I/O pass through
从图中可以看到,音频输入硬件将其流格式施加到 Remote I/O单元 的 Input element 的向外的一端。反过来,您可以指定希望在该element的向内一端使用的格式。音频单元根据需要进行格式转换。为了避免不必要的采样率转换,请确保在定义流格式时使用音频硬件采样率
Input element 在默认情况下是禁用的,所以请确保启用它
图2-1所示的模式利用了两个 Remote I/O elements 之间的音频单元连接。具体来说,您不需要在音频单元的 output element 的 input scope 上设置流格式。音频单元连接将传递您为 input element 指定的格式
Output element 向外的一侧采用音频输出硬件的流格式,output element 根据需要为输出音频执行格式转换
使用此模式,您不需要配置任何音频数据缓冲区
2. I/O Without a Render Callback Function
在Remote I/O单元的element之间添加一个或多个其他音频单元可以让你构建一个更有趣的应用程序。例如,你可以使用一个Multichannel Mixer单元来定位传入的麦克风音频在立体声领域或提供输出音量控制。在这种设计模式下,仍然没有回调函数,如图2-2所示。这简化了模式,但限制了它的效用。如果没有渲染回调函数,你就没有办法直接操纵音频。
Figure 2-2 Simultaneous I/O without a render callback function
在此模式中,与在 pass-through 模式中一样,配置 Remote I/O单元的两个元素。要设置Multichannel Mixer单元,您必须在Mixer output 上设置流格式的采样率,如图2-2所示。
mixer的输入流格式是通过 Remote I/O单元的 Input element 的输出,通过音频单元连接自动建立的。类似地,Remote I/O单元 Output element 的 input scope 的流格式是通过音频单元连接建立的,这要感谢 mixer单元 output 的传递。
在此模式的任何实例中,实际上,无论何时除了I/O单元之外使用其他音频单元,您必须设置kAudioUnitProperty_MaximumFramesPerSlice 属性,如 Audio Unit Properties Reference 中所述
3. I/O with a Render Callback Function
通过在Remote I/O单元的 input 和 output element 之间放置回调函数,您可以在传入音频到达输出硬件之前对其进行操作。在一个非常简单的情况下,您可以使用渲染回调函数来调整输出音量。然而,你可以添加颤音、铃声调制、回声或其他效果。通过利用 Accelerate框架 (参见*Accelerate Framework Reference*) 中提供的傅里叶变换和卷积函数,您的可能性是无限的。该模式如图2-3所示。
Figure 2-3 Simultaneous I/O with a render callback function
从图中可以看到,该模式使用了 Remote I/O单元的两个element,就像本章前面的模式一样。将呈现回调函数附加到 output element 的 input scope。当该element需要另一组音频样本值时,它会调用回调。反过来,您的回调通过调用Remote I/O单元的 input element 的呈现回调函数来获得新的示例。
就像其他I/O模式一样,您必须显式地在 Remote I/O单元上开启输入模块,因为输入在默认情况下是禁用的。对于其他I/O模式,您不需要配置任何音频数据缓冲区。
请注意,当您使用渲染回调函数建立从一个音频单元到另一个音频单元的音频路径时,就像在此模式中一样,回调将取代音频单元连接
4. Output-Only with a Render Callback Function
选择这种模式用于音乐游戏和合成器—你正在生成声音并需要最大响应的应用程序。最简单的是,这个模式涉及一个直接连接到 Remote I/O单元 output element 的 input scope 的呈现回调函数,如图2-4所示。
Figure 2-4 Output-only with a render callback function
你也可以使用同样的模式去构建带有更复杂音频结构的应用。例如,您可能希望生成几种声音,将它们混合在一起,然后通过设备的输出硬件播放它们。如图2-5所示。在这里,模式采用了一个 audio processing graph 和两个额外的音频单元,一个Multichannel Mixer和一个iPod EQ。
Figure 2-5 A more complex example of output-only with a render callback function
在图中,请注意iPod EQ要求您在输入和输出上设置完整的流格式。另一方面,Multichannel Mixer只需要在其输出上设置正确的采样率。然后,完整的流格式通过音频单元连接从混频器的输出传播到远程I/O单元的输出元素的输入范围。这些使用细节以及使用各种iOS音频单元的其他细节将在Using Specific Audio Units中进行描述
对于每个Multichannel Mixer单元输入,如图2-5所示,完整的流格式设置。对于 input 0,您显式地设置它。对于 input 1 格式通过iPod EQ单元的输出连接的音频单元传播。通常,您必须单独考虑每个音频单元的流格式需求
5. Other Audio Unit Hosting Design Patterns
还有另外两种主要的音频单元设计模式。录音 或 分析音频,创建一个只有 input 和渲染回调函数的应用程序。回调函数由您的应用程序调用,然后它反过来调用 Remote I/O单元的 input element 的呈现方法。然而,在大多数情况下,像这样的应用程序更好的选择是使用输入音频队列对象(类型为AudioQueueRef,使用 AudioQueueNewInput 函数实例化),如 Audio Queue Services Programming Guide 中解释的那样。使用音频队列对象提供了更大的灵活性,因为它的渲染回调函数不是在实时线程上。
若要执行离线音频处理,请使用 Generic Output unit。与Remote I/O单元不同,此音频单元不连接到设备的音频硬件。当您使用它发送音频到您的应用程序时,它取决于您的应用程序调用其呈现方法。
Constructing Your App
无论你选择哪种设计模式,构建音频单元托管应用的步骤基本上是相同的:
- 配置您的 audio session
- 指定 audio units
- 创建 audio processing graph,然后获取 audio units
- 配置 audio units
- 连接 audio units nodes (音频单元节点)
- 提供 ui
- 初始化并启动 audio processing graph
1. Configure Your Audio Session
构建音频单元应用程序的第一步与任何iOS音频应用程序的步骤相同 :配置音频会话。音频会话的特征很大程度上决定了应用程序的音频功能及其与系统其余部分的交互性。首先,指定您想在应用程序中使用的采样率,如下所示:
self.graphSampleRate = 44100.0; // Hertz
接下来,使用音频会话对象请求系统使用您喜欢的采样率作为设备硬件采样率,如清单2-1所示。这里的目的是为了避免硬件和应用程序之间的采样率转换。这将最大化CPU性能和声音质量,并最小化电池消耗
Listing 2-1 Configuring an audio session
NSError *audioSessionError = nil;
AVAudioSession *mySession = [AVAudioSession sharedInstance]; // 1
[mySession setPreferredHardwareSampleRate: graphSampleRate // 2
error: &audioSessionError];
[mySession setCategory: AVAudioSessionCategoryPlayAndRecord // 3
error: &audioSessionError];
[mySession setActive: YES // 4
error: &audioSessionError];
self.graphSampleRate = [mySession currentHardwareSampleRate]; // 5
上面几行代码的作用如下:
- 为您的应用程序获取对单例音频会话对象的引用
- 请求硬件的采样率。系统可能处理请求,也可能不理睬,这取决于设备上的其他音频活动
- 请求您想要的音频会话类别。这里指定的 “play and record” 类别支持音频输入和输出
- 请求激活您的音频会话
- 音频会话激活后,根据系统提供的实际采样率更新自己的采样率变量
您可能需要配置另一个硬件特性:音频硬件 I/O buffer 时间。在44.1 kHz采样率下,默认持续时间约为23毫秒,相当于1024个采样的片大小。如果I/O延迟在你的应用中非常重要,你可以请求一个更短的持续时间,降低到 0.005毫秒 (相当于256个样本),如下所示:
self.ioBufferDuration = 0.005;
[mySession setPreferredIOBufferDuration: ioBufferDuration
error: &audioSessionError];
有关如何配置和使用音频会话对象的完整说明,请参见 Audio Session Programming Guide
2. Specify the Audio Units You Want
在运行时,在你的音频会话配置代码运行后,你的应用程序还没有获得音频单元。您可以通过使用AudioComponentDescription 结构体来指定您想要的每个组件。请参见 Use Identifiers to Specify and Obtain Audio Units 来了解如何做到这一点。每个iOS音频单元的标识符键都列在 Identifier Keys for Audio Units
如果有了音频单元说明符,您就可以根据所选择的模式构建 audio processing graph
3. Build an Audio Processing Graph
在这一步中,您将创建本章第一部分中解释的设计模式的框架。具体地说
- 实例化一个 AUGraph 不透明类型。该实例表示音频处理图 (audio processing graph)
- 实例化一个或多个 AUNode 不透明类型,每个类型代表 graph 中的一个音频单元
- 将 nodes 添加到 graph 中
- 打开 graph 并实例化音频单元
- 获取对音频单元的引用
清单2-2显示了如何为包含 Remote I/O单元和Multichannel Mixer单元的 graph 执行这些步骤。它假设您已经为每个音频单元定义了一个 AudioComponentDescription 结构体
Listing 2-2 Building an audio processing graph
AUGraph processingGraph;
NewAUGraph (&processingGraph);
AUNode ioNode;
AUNode mixerNode;
AUGraphAddNode (processingGraph, &ioUnitDesc, &ioNode);
AUGraphAddNode (processingGraph, &mixerDesc, &mixerNode);
AUGraphAddNode 函数调用使用了音频单元说明符 ioUnitDesc 和 mixerDesc。此时,graph 将被实例化并拥有你将在应用中使用的 nodes。要打开 graph 并实例化音频单元,请调用AUGraphOpen:
AUGraphOpen (processingGraph);
然后,通过 AUGraphNodeInfo 函数获取对音频单元实例的引用,如下所示:
AudioUnit ioUnit;
AudioUnit mixerUnit;
AUGraphNodeInfo (processingGraph, ioNode, NULL, &ioUnit);
AUGraphNodeInfo (processingGraph, mixerNode, NULL, &mixerUnit);
ioUnit 和 mixerUnit 变量现在包含了对 graph 中音频单元实例的引用,允许您配置并互连音频单元。
4. Configure the Audio Units
每个iOS音频单元都需要自己的配置,如 Using Specific Audio Units 所述。然而,有些配置非常常见,所有iOS音频开发者都应该熟悉它们。
默认情况下,Remote I/O 单元启用了输出,禁用了输入。如果你的应用程序同时执行I/O,或者只使用输入,你必须相应地重新配置 I/O单元。详细信息,请参见 Audio Unit Properties Reference 中的 kAudioOutputUnitProperty_EnableIO 属性。
所有iOS音频单元,除了Remote I/O 和 Voice-Processing I/O units,需要他们的kAudioUnitProperty_MaximumFramesPerSlice 属性配置。此属性确保音频单元已准备好产生足够数量的音频数据帧以响应渲染调用。详细信息,请参见 Audio Unit Properties Reference 中的 kAudioUnitProperty_MaximumFramesPerSlice
所有音频单元都需要它们的音频流格式定义在输入,输出,或两者。有关音频流格式的解释,请参见 Audio Stream Formats Enable Data Flow。关于不同iOS音频单元的特定流格式要求,请参见 Using Specific Audio Units
5. Write and Attach Render Callback Functions
对于使用回调函数的设计模式,必须编写这些函数,然后将它们附加到正确的位置。Render Callback Functions Feed Audio to Audio Units 描述了这些回调函数做什么,并解释了它们是如何工作的
当音频不流动时,您可以通过使用音频单元API立即附加一个回调,如清单2-3所示。
Listing 2-3 Attaching a render callback immediately
AURenderCallbackStruct callbackStruct;
callbackStruct.inputProc = &renderCallback;
callbackStruct.inputProcRefCon = soundStructArray;
AudioUnitSetProperty (
myIOUnit,
kAudioUnitProperty_SetRenderCallback,
kAudioUnitScope_Input,
0, // output element
&callbackStruct,
sizeof (callbackStruct)
);
您可以使用 audio processing graph API,以线程安全的方式附加渲染回调,即使音频是流动的。清单2-4显示了如何做到这一点。
Listing 2-4 Attaching a render callback in a thread-safe manner
AURenderCallbackStruct callbackStruct;
callbackStruct.inputProc = &renderCallback;
callbackStruct.inputProcRefCon = soundStructArray;
AUGraphSetNodeInputCallback (
processingGraph,
myIONode,
0, // output element
&callbackStruct
);
// ... some time later
Boolean graphUpdated;
AUGraphUpdate (processingGraph, &graphUpdated);
6. Connect the Audio Unit Nodes
在大多数情况下,使用 audio processing graph API 中的 AUGraphConnectNodeInput 和 AUGraphDisconnectNodeInput 函数在音频单元之间建立或断开连接是最好的,也是更容易的。这些函数是线程安全的,避免了显式定义连接(不使用graph时必须这样做)的编码开销
清单2-5显示了如何使用音频处理图API将 mixer 节点的输出连接到 I/O单元输出元素的输入
Listing 2-5 Connecting two audio unit nodes using the audio processing graph API
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 属性,如清单2-6所示。这种方法需要为每个连接定义一个 AudioUnitConnection 结构作为其属性值。
Listing 2-6 Connecting two audio units directly
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)
);
7. Provide a User Interface
在构建应用程序的这一点上,音频单元(通常是音频处理图)已经完全构建和配置好了。在很多情况下,你会想要提供一个用户界面来让你的用户调整音频行为。您可以定制用户界面,以允许用户调整特定的音频单元参数,在某些特殊情况下,还可以调整音频单元属性。无论哪种情况,用户界面都应该提供关于当前设置的视觉反馈
Use Parameters and UIKit to Give Users Control 解释了构建用户界面让用户控制参数值的基础知识
8. Initialize and Start the Audio Processing Graph
在启动音频流之前,必须通过调用 AUGraphInitialize 函数初始化音频处理图。关键步骤如下:
- 通过自动地为每个音频单元单独调用AudioUnitInitialize函数来初始化图表所拥有的音频单元。(如果你想构建一个处理链而不使用图表,你就必须依次显式初始化每个音频单元。)
- 验证 graph 的连接和音频数据流格式
- 跨音频单元连接传递流格式
清单2-7显示了如何使用 AUGraphInitialize
Listing 2-7 Initializing and starting an audio processing graph
OSStatus result = AUGraphInitialize (processingGraph);
// Check for error. On successful initialization, start the graph...
AUGraphStart (processingGraph);
// Some time later
AUGraphStop (processingGraph);
Troubleshooting Tips(故障排除技巧)
当Core Audio函数提供返回值时,捕获该值并检查成功或失败。如果出现故障,可以使用Xcode的调试特性,详见*Xcode Debugging Guide*。如果在你的应用中使用Objective-C方法,比如配置音频会话,那么同样可以利用error参数。
注意函数调用之间的依赖关系。例如,只有在成功初始化 audio processing graph 之后,才能启动它。检查 AUGraphInitialize 的返回值。如果函数成功返回,则可以开始 graph。如果失败了,确定是哪里出了问题。检查初始化之前的所有音频单元函数调用是否成功返回
第二,如果 graph 初始化失败,利用 CAShow 函数。这个函数将graph的状态打印到Xcode控制台。
确保你将每个 AudioStreamBasicDescription 结构体初始化为0,如下所示:
AudioStreamBasicDescription stereoStreamFormat = {0};
将ASBD的字段初始化为0可以确保没有字段包含垃圾数据。(在外部存储中声明数据结构时—例如,在类声明中作为实例变量—其字段会自动初始化为0,而不需要您自己初始化它们)
要将 AudioStreamBasicDescription 结构的字段值打印到Xcode控制台(这在开发过程中非常有用),请使用如清单2-8所示的代码。
Listing 2-8 A utility method to print field values for an AudioStreamBasicDescription structure
- (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中的问题
当为音频单元流格式定义ASBD时,请确保您遵循了 Using Specific Audio Units 中的“推荐的流格式属性”和“流格式说明”。不要偏离这些建议,除非你有特殊的理由。