适用于ios的音频单元指南(1)

概述

概览

  • 音频单元提供快速,模块化的音频处理
  • 选择一个设计模式和构建app

音频单元托管基础

音频单元提供快速,模块化的音频处理

  • 在ios中的音频组件
  • 使用标识符指定和获取音频单元
  • 使用scoped 和 elements 指定音频单元的部件
  • 使用属性 配置 音频单元
  • 使用参数和UIKit 为用户提供控制
  • I/O 单元的基本特征

音频处理graph管理音频单元

  • 音频处理graph具有一个完整的 I / O单元
  • 音频处理graph提供线程安全
  • 使用graph 的“pull”获取音频流

渲染回调功能将音频输入到音频单元

  • 理解音频单元的渲染回调函数

音频流格式启用数据流

  • 使用AudioStreamBasicDescription结构
  • 了解设置流格式的位置和方式

去苹果官网找关于 ios的音频指南,搜出来的始终都是macOS的。没有ios方向,看起来很费劲,所以,跑去了Google搜了下,果然搜到了在苹果官网没有搜到的适用于ios的音频单元指南

概述

ios 提供音频处理插件,支持混合,均衡,格式转换和实时输入/输出,用于录制,回放,离线渲染和实时对话,流入VoIP(互联网协议语言)。我们可以从iosapp 动态加载和使用。这些功能很强大并且很灵活,称为音频单元。

音频单元通常在称为音频处理图形上下文工作,如下图。


上图实例中,app通过一个或者多个回调函数将音频发送到图中的第一个音频单元,并对每个音频单元进行控制。I/O 单元的输出(最后的一个音频单元或者其他的音频处理graph)直接连接到输出硬件

概览

由于音频单元构成ios音频堆栈中编程的最底层,因此有效使用它们需要比其他ios音频技术需要更深入的了解。除非我们需要实时播放合成声音,低延迟I/O,或者特定音频单元功能,否则请下查看 Media Player, AV Foundation, OpenAL, or Audio Toolbox frameworks。

音频单元提供快速,模块化的音频处理

直接使用音频单元的两大优势:

  • 出色的响应能力。音频单元渲染的回调函数中使用的是实时优先级线程,因此我们写的代码是非常靠近硬件的。合成乐器和实时同步语音I/O 直接使用音频单元收益最多。

  • 动态重新配置。音频的graph api 允许我们以线程安全的方式动态组装,重新配置和重新排列复杂的音频处理链,同时处理音频。这是ios中唯一提供此功能的音频API.

音频单元的生命周期如下:

  • 1.运行时,获取对动态可链接库的引用,该库定义了我们可以使用的音频单元。
  • 2.实例化音频单元
  • 3.根据类型的需要配置音频单元,以使用app的意图
  • 4.初始化音频单元准备处理音频
  • 5.控制音频单元
  • 6.完成后,取消分配音频单元

音频单元提供非常有用的个性化功能,如立体声声像,混音,音量控制和音频电平测量。托管音频设备可以让我们为app添加此类功能。但是,要获取这些好处,必须获得一系列基本概念,包括音频数据流格式,渲染回调函数和音频单元架构。(这些知识后面讲解)

选择一个设计模式和构建app

托管设计模式的音频单元提供了一个非常灵活的蓝图,可根据app的具体情况进行自定义。每种模式表明:

  • 如何配置I/O。I/O 单元有两个独立的元件,一个接受来自输入硬件的音频,一个将音频发送到输出硬件。每个设计模式都指示应启用哪个或哪些元素。

  • 在音频处理grahp中,必须指定音频数据流格式。 我们必须正确的指定格式以支持音频流。

  • 建立音频单元连接的位置以及附加渲染回调函数的位置。 音频单元连接可以当做桥梁,将音频流格式从一个音频单元的输出传播到另一个音频单元的输出。渲染回调可让我们处理在graph的音频或者操作在但一个样本级别的音频。

无论选择哪种设计模式,构建音频单元托管app的步骤基本相同:

  • 配合app音频会话确保app在系统和设备硬件的上下文正常工作
  • 构建音频处理graph(如何构建往下看)
  • 提供用于控制图形音频单元的用户界面。

音频单元托管基础

ios中所有的音频技术都简历在音频单元之上,如下图。


只有当需要最高程度的控制,性能或者灵活性,或者只需要直接使用音频单元就可以获得特定功能(如声学回音消除)时,直接使用音频但是是正确的选择。

音频单元提供快速,模块化的音频处理

当我们需要以下功能时候,请直接使用音频单元,而不是更高级api:

  • 具有低延迟的同时音频I/O ,例如用于VoIP应用
  • 响应回放合成声音,例如用于音乐游戏或合成乐器
  • 使用特定的音频单元功能,例如声学回音消除,混合或者音调均衡
  • 一种处理链架构,可以让音频处理ok组合合成灵活的网络。这是ios中唯一的提供此功能的音频API
在ios中的音频组件

ios 提供七种音频单元,按照目的可分为四类,如表所示

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

以上音频单元的标识符如下

Name and description Identifier keys Corresponding four-char codes
Converter unit
支持线性PCM的音频格式转换
kAudioUnitType_FormatConverter
kAudioUnitSubType_AUConverter
kAudioUnitManufacturer_Apple
aufc
conv
appl
iPod Equalizer unit
提供iPod均衡器的功能。
kAudioUnitType_Effect
kAudioUnitSubType_AUiPodEQ
kAudioUnitManufacturer_Apple
aufx
ipeq
appl
3D Mixer unit
支持混合多个音频流,输出声像,采样率转换,等等。
kAudioUnitType_Mixer
kAudioUnitSubType_AU3DMixerEmbedded
kAudioUnitManufacturer_Apple
aumx
3dem
appl
Multichannel Mixer unit
支持将多个音频流混合到单个流中。
kAudioUnitType_Mixer
kAudioUnitSubType_MultiChannelMixer
kAudioUnitManufacturer_Apple
aumx
mcmx
appl
Generic Output unit
支持线性PCM的音频格式转换 可用于启动和停止graph
kAudioUnitType_Output
kAudioUnitSubType_GenericOutput
kAudioUnitManufacturer_Apple
auou
genr
appl
Remote I/O unit
连接到设备硬件以进行输入,输出或同时输入和输出。
kAudioUnitType_Output
kAudioUnitSubType_RemoteIO
kAudioUnitManufacturer_Apple
auou
rioc
appl
Voice Processing I/O unit
具有I / O单元的特性,并为双向通信增加了回声抑制功能。
kAudioUnitType_Output
kAudioUnitSubType_VoiceProcessingIO
kAudioUnitManufacturer_Apple
auou
vpio
appl

注意:ios动态插件架构不支持第三方音频设备。

effect Unit
ios4 提供一个效果单元-ipod均衡器(内置的ipod应用就是用的这个均衡器)。想查看均衡器有哪些,我们可以点击设置->音乐->均衡器设置。是用改设备单元时候,我们需要提供UI界面。改音频单元提供一组预设的均衡器曲线,如低音增强器,流行音乐和口语。

Mixer Units
os 提供了两个混音器单元。3D混音器单元是OpenAL 构建的基础。在大多数情况下,如果需要3D混音器单元的功能,最佳选择是使用OpenAL,它提供了更适合游戏应用的更高级API。实例代码查看oalTouch

多声道混音器单元可以任意说了的单声道或者立体声流提供混音,并带有立体声输出。我们可以打开或关闭每个输入,设置输入增强,或者设置其立体声平移位置。代码可查看mixerHost

I/O Units
ios 提供三个I/O 单元。远程I/O单元是最常用的 。他连接到输入和输出音频硬件,并为我们提供对各个传入和传出音频样本值的低延迟访问。它也提供硬件音频格式和app音频格式之间的格式转换,这是通过包含格式转换单元完成的。有关代码可以查看aurioTouch

语音处理I / O单元通过添加声学回声消除来扩展远程I / O单元,以用于VoIP或语音聊天应用程序。 它还提供自动增益校正,语音处理质量调整和静音。

通用输出单元不连接到音频硬件,而是提供一种机制,用于将处理链的输出发送到您的应用程序。 您通常会使用通用输出单元进行离线音频处理。

Format Converter Unit
iOS 4提供了一个格式转换器单元,通常通过I / O单元间接使用。

在合奏中使用两个音频单元API

ios 中有一个用于直接处理音频单元的API,另一个用于处理音频处理graph的api。我们可以在app 中同时使用这两个api

这两个API 之间存在一些重叠,我们可以根据自己的编程风格自由混合搭配。音频单元API 和音频处理Graph 均提供一下功能:

  • 获取对定义音频单元的动态可链接库的引用
  • 实例化音频单元
  • 互联音频单元并附加渲染回到函数
  • 启动和停止音频流
使用标识符指定和获取音频单元

要在运行时查找音频单元,首先在音频组件中描述数据结构中指定类型,子类型和制造商key。无论是使用音频单元还是音频处理graph的API,都执行以下操作

  AudioComponentDescription ioUnitDescription;
    ioUnitDescription.componentType          = kAudioUnitType_Output;
    ioUnitDescription.componentSubType       = kAudioUnitSubType_RemoteIO;
    ioUnitDescription.componentManufacturer  = kAudioUnitManufacturer_Apple;
    ioUnitDescription.componentFlags         = 0;
    ioUnitDescription.componentFlagsMask     = 0;

上述代码只是指定一个音频单元-远程I/O 单元。
要创建通配符描述,需要将一个活多个类型/子类型字段设置为0。例如,要是想匹配所有的I/O 单元,将上述代码的componentSubType设置为0 就可以了。

这里罗列下每个 音频组件 ios中对应的subType

type subType 制造商
kAudioUnitType_Output kAudioUnitSubType_RemoteIO kAudioUnitManufacturer_Apple
kAudioUnitType_MusicDevice kAudioUnitSubType_Sampler
kAudioUnitSubType_MIDISynth
kAudioUnitManufacturer_Apple
kAudioUnitType_MusicEffect kAudioUnitManufacturer_Apple
kAudioUnitType_FormatConverter kAudioUnitSubType_AUConverter
kAudioUnitSubType_Varispeed
kAudioUnitSubType_DeferredRenderer
kAudioUnitSubType_Splitter
kAudioUnitSubType_MultiSplitter
kAudioUnitSubType_Merger
kAudioUnitSubType_NewTimePitch
kAudioUnitSubType_AUiPodTimeOther
kAudioUnitSubType_RoundTripAAC
kAudioUnitSubType_AUiPodTime
kAudioUnitManufacturer_Apple
kAudioUnitType_Effect kAudioUnitSubType_PeakLimiter
kAudioUnitSubType_DynamicsProcessor
kAudioUnitSubType_LowPassFilter
kAudioUnitSubType_HighPassFilter
kAudioUnitSubType_BandPassFilter
kAudioUnitSubType_HighShelfFilter
kAudioUnitSubType_LowShelfFilter
kAudioUnitSubType_ParametricEQ
kAudioUnitSubType_Distortion
kAudioUnitSubType_Delay
kAudioUnitSubType_SampleDelay
kAudioUnitSubType_NBandEQ
kAudioUnitSubType_Reverb2
kAudioUnitSubType_AUiPodEQ'
kAudioUnitManufacturer_Apple
kAudioUnitType_Mixer kAudioUnitSubType_MultiChannelMixer
kAudioUnitSubType_MatrixMixer
kAudioUnitSubType_SpatialMixer
kAudioUnitSubType_SpatialMixer
kAudioUnitManufacturer_Apple
kAudioUnitType_Panner ios不支持 kAudioUnitManufacturer_Apple
kAudioUnitType_Generator kAudioUnitSubType_ScheduledSound
kAudioUnitSubType_AudioFilePlayer
kAudioUnitManufacturer_Apple
kAudioUnitType_OfflineEffect kAudioUnitManufacturer_Apple
kAudioUnitType_MIDIProcessor kAudioUnitManufacturer_Apple

设置描述符后,我们可以使用两个API 中的任意一个获得对指定音频单元(或者一组音频单元)的库的引用。如下

///获取音频单元的引用
    AudioComponent foundIoUnitReference = AudioComponentFindNext (NULL,&ioUnitDescription);
    ///实例化音频单元
    AudioUnit ioUnitInstance;
    AudioComponentInstanceNew (foundIoUnitReference,
                               &ioUnitInstance);

传入NULL 给函数AudioComponentFindNext是告诉此函数使用系统定义的顺序查找与描述匹配的第一个系统音频单元。如果我们在这里传入先前找到的音频单元,则改函数将好到与描述匹配的下一个音频单元。例如,通过重复调用该函数,可以获取对所有I/O单元的引用。
AudioComponentFindNext 函数的结果是对定义音频单元的动态可链接库的引用。将引用传递给AudioComponentInstanceNew函数以实例化音频单元。

我们可以改为使用音频处理grahp 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,一种opaque类型,代表在音频处理graph上下文的对音频单元。在AUGraphNodeInfo的输出中,我们将在ioUnit参数中接受对新的音频单元实例的引用。

我们现在有两个音频单元实例了,接下来我们需要学习如何对其配置了。因此,我们需要了解两个音频单元的特征,scopes和elements。

使用scoped 和 elements 指定音频单元的部件

音频单元 是由scopes和elements 组成的,见下图。当我们调用函数来配置和控制音频单元时,我们需要指定scopes 和element的标识符。


scope 是音频单元内的编程上下文。这些上下文是不可以互相嵌套的。我们可以使用AudioUnitScope(<AudioUnitPropertyies.h>)枚举中的常量指定要定位的范围。

elements是嵌套在音频单元scope内的编程上下文。当元素是输入或者输出scope的一部分时,它相当于物理音频设置中的信号bus(总线)-因此有时称为bus(总线)。这两个术语-element和bus-在音频单元编程中完全相同。本文档在强调信号流时使用bus,强调音频单元的特定功能方面使用element

这里搞清楚点,element 是scope 一部分,但是相同的 element 存在多个scope中的。

我们可以通过设置 0来作为索引的值来指定element和bus。如果设置全局的scope的属性和参数的时候,我们需要设置element 的值是0

因为Global scope 只有一个element 0(element output)。

刚开始看到上面的这句话不是很理解,反复看翻译文档。算是理解大概了解了这句话的意思了
其实可以这么理解,scope 可以包含多个element元素,有时候我们需要设置scope 的属性和参数,有时候需要设置element 的属性和参数,而设置属性函数AudioUnitSetProperty每次需要指定scope 和element ,但是设置scope的属性和参数的值的时候是不需要element的,因此我们就把element 所在的位置设置成0就行了。
例如 kAudioUnitProperty_ElementCount 是用来配置混音器单元的element 的数量的,这就算是scope的属性了,因此,设置element 参数所在位置是0
而kAudioOutputUnitProperty_EnableIO 是用来开启I/O 的,是针对元素的,因此element 就要根据需要自己选择位置了。

上图说明了音频单元的基本架构,其中输入和输出上的elements数量相同。然而,各种音频单元的架构有所不同。例如,混音器单元可能有多个输入元件但是具有一个输出元件。尽管体系结构有这些变化,但是我们还是可以把这里学到的知识扩展到其他音频单元上。
Global scope 使用于整个音频单元,不与任何特定的音频流相关联。他只有一个element,命名element 0.有些属性(例如每个切片的最大帧数kAudioUnitProperty_MaximumFramesPerSlice)只能在
Global scope 使用。

kAudioUnitProperty_MaximumFramesPerSlice 该属性只能设置在global Scope 上。

input和output scopes 直接参与通过音频单元移动一个或多个音频流。正如我们期望的一样,音频input的scope 并从output scope。
属性和参数可以应用到整个input 和output的scope上,例如element 数量属性(kAudioUnitProperty_ElementCount),我们可以通过该属性设置input 和output scope 上的element数量
还有一些其他属性和参数,例如 开启关闭I/O 属性(kAudioOutputUnitProperty_EnableIO)和音量属性(kMultiChannelMixerParam_Volume),可以应用于scope中的特定element上。

这里想说的是属性和参数,一部分可以修饰scope, 另一份也可以用来修饰scope中的element。

使用属性 配置 音频单元

音频单元属性是一个键值对,可以用来配置音频单元。属性的key是具有关联标识符的唯一整数,例如kAudioUnitProperty_MaximumFramesPerSlice= 14.Apple 保留0到63999之间的属性键。

每个属性的值都是指定的数据类型,并具有指定的读写访问权限,可以参考 Audio Unit Properties Reference.设置音频单元的属性,可以使用函数AudioUnitSetProperty。下列代码显示该函数的用法。

 OSStatus result = AudioUnitSetProperty (
                                            ioUnit,
                                            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)
                                            );
    

下面是我们在音频单元开发中经常使用的一些属性,可以看mixerHost代码来熟悉下列属性。

  • kAudioOutputUnitProperty_EnableIO 用于在I/O 单元上启动和禁止输入和输出。默认启用输出,关闭输入。
  • kAudioUnitProperty_ElementCount,用于配置混音器单元上的输入元素的数量
  • kAudioUnitProperty_MaximumFramesPerSlice 用于指定音频单元应准备响应于渲染调用而产生的音频数据的最大帧数。对应大多数音频设备,在大多数情况下,必须按照参考文档的说明来设置此属性。如果不这样设置,屏幕锁定时候音频将停止。
  • kAudioUnitProperty_StreamFormat 用于指定特定音频单元输入或者输出总线的音频流数据格式

只有音频单元未初始化时,才能设置大多数属性值。这些属性不应该由用户更改。但是,有些属性( iPod EQ unit上的kAudioUnitProperty_PresentPreset属性)和语音处理I/O单元的kAUVoiceIOProperty_MuteOutput属性,在播放的音频时会被更改。

检查属性是否可用或者访问值以及观察值的变化更改,使用下列函数

  • AudioUnitGetPropertyInfo 检查属性是否可用;如果返回yes,那么会返回知道数据大小以及是否可以更改该值
  • AudioUnitGetProperty ,AudioUnitSetProperty 获取和设置属性的值
  • AudioUnitAddPropertyListener, AudioUnitRemovePropertyListenerWithUserData 安装或者删除回调函数以监视属性值的更改
使用参数和UIKit 为用户提供控制

音频单元参数是用户可调节的设置,可在音频单元产生音频时改变。实际上,大多数参数(例如音量或者立体声平移位置)的意图是音频单元正在执行的时候进行的调整。

与音频单元属性类似,音频单元参数是键值对。key是被适用的音频单元定义。它始终是个枚举常量,例如kMultiChannelMixerParam_Pan=2,对于音频单元是唯一的,但是全局不一定唯一。

与属性值不同,每个参数值都是相同的类型:32位 float。值的取值范围及其表示的度量单位由音频单元的参数实现决定。

设置和获取参数值使用下列函数

I/O 单元的基本特征

I/O 单元是app应用中常用的音频单元,而且在几个方面也是很特殊的。因此,我们需要了解I/O 单元的基本特性才能获取更好的音频单元编程。

I/O 音频单元有两个元素


虽然这两个elements是音频单元的组成部分,但是我们的app还是需要将他们视为独立的实体。 打个比方,我们可以根据需要使用kAudioOutputUnitProperty_EnableIO 属性来独立启用或者禁用每个element。

可以这么想,把I/O 音频单元相当于,输入音频单元和输出音频单元封装在一起了。

在 I/O 单元中的Element 1 直接连接到设备上的音频输入硬件,在图中是麦克风表示。element 1 和麦克风通过input scope 具体怎么连接,我们是不需要关心的。我们第一次获取的数据是来自output scope 的element 1 。

同理 element 0 直接连接在音频的输出硬件,例如图中的扬声器。数据传输也是首先经过input scope 中的 element 0 到达output scope 的 element0 ,在传递给扬声器的。

使用音频单元时,我们经常听到I/O单元的两个element ,而不是他们的编号是名称:

  • input element是 element 1(助记符设备:但是input的字母i具有类似数字1的外观)
  • output element 是 element 0(助记符设备:output的字母o具有类似于0 的外观)

上图,每个element 都有个一个input scope 和 output scope。因此,描述I/O 单元的这些部分可能有点混乱。例如,我们可说在一个同步的I/O APP中,我们从 element 1 通过output scope接受音频,并将音频通过input scope 传递给element 0。

最后,I/O 单元是唯一能够在音频处理graph中启动和停止音频流的音频单元。通过这种方式,I/O 音频单元负责音频单元应用中的音频流。

音频处理graph管理音频单元

音频处理graph是coreFoundation风格的opaque类型 AUGraph,用于构建和管理音频单元处理链。graph可以利用多个音频单元和多个渲染回调函数的功能,允许我们创建几乎任何我们能想象的音频处理解决方案。

AUGraph类型是线程安全的;他使我们能够动态的重新配置处理链。例如,我们可以安全的插入均衡器,甚至可以在播放音频时交换混音器输入的不同渲染的回调函数。事实上,AUGraph类型仅在ios中使用,用于在音频应用程序中执行此类动态重新配置。

音频处理graph API使用另一个不透明类型AUNode来表示图形上下文的单个音频单元。当使用graph的时候,我们通常与AUnode 交互,这里AUnode 是作为音频单元的代理。

当我们将grahp放在一起的时候,我们必须配置每个音频单元。因此,我们必须通过音频单元api直接与音频单元进行交互。AUNode本身是不可以配置的。

我们还可以把AUNode作为element增加到 grahp中。

总的来说,构建音频处理graph 需要三个任务

  • 将node 增加到 grahp中
  • 直接配置由node表示的音频单元
  • 互联node
音频处理graph具有一个完整的 I / O单元

无论您是在进行录制,回放还是同步I / O,每个音频处理grahp都有一个I / O单元。I / O单元可以是iOS中的任何一个,具体取决于您的应用程序的需求。

graph 可以通过AUGraphStart 和AUGraphStop 函数启动和停止音频流。反过来,这些函数可以通过调用AudioOutputUnitStart 和AudioOutputUnitStop 函数将开始或者停止消息传递给I/O单元。

音频处理graph提供线程安全

音频处理graph API 使用“to do list”来提供线程安全。此api中的某些函数将工作音频单元添加到稍后要执行的更改列表中。当我们指定完整的更改集后,我们可以请求graph实现他们。

以下是音频处理 graph API支持的一些常见的重新配置以及相关功能:

让我们看一个重新配置正在运行的音频处理grahp 示例
假设我们构建了一个包含多声道混音器单元和远程 I/O单元的graph,用于混合播放两个两个合成的声音。我们将声音发送到混频单元的两个input bus。让混频单元的输出连接到I/O单元的输出元件,然后出入到输出音频硬件。



现在,假设用户想要将均衡器插入两个音频流之中一中。因此,在其中一个声音的输入和他进入的混音器输入之间添加一个iPod EQ 单元。如下图


完成此时时重新配置的步骤如下:

  • 1 通过调用AUGraphDisconnectNodeInput断开混音器单元 input 1 架子鼓的回调
  • 2.将包含iPod EQ单元的音频单元节点添加到graph中。通过AudioComponentDescription结构指定iPod EQ单元,然后调用AUGraphAddNode函数执行此操作。此时,iPod EQ单元已实例化但未初始化。他被graph所有,但是尚未参加音频流程
  • 3.配置和初始化iPod的EQ单元。在这个例子中,这需要做一些事情:

1 ·调用AudioUnitGetProperty函数从混音器的input 中检索流格式(kAudioUnitProperty_StreamFormat)
2.调用AudioUnitSetProperty 函数两次,一次在iPod EQ单元的input中设置该流格式,第二次在output上设置它。(意思是输入通过该单元不要改变流的格式)
3 调用AudioUnitInitialize函数为 iPod EQ单元分配资源并准备处理音频。这个函数调用不是线程安全的,但是当 iPod EQ单元没有主动参加音频处理graph时,我们可以(必须)在序列中的这个时候执行它,这个时候还没有调用AUGraphUpdate。

  • 4 通过调用AUGraphSetNodeInputCallback 函数将架子鼓回调函数附加到iPod EQ 的input上。

上面 第一步,第二步,第四步中所有的AUGraph*开头的函数都是被增加到了“to-do”列表中了。调用AUGraphUpdate来执行这些挂起的任务,AUGraphUpdate函数成功返回后,就代表grahp以动态重新配置了。

使用graph 的“pull”获取音频流

在音频处理graph中,消费者在需要更多的音频数据时调用提供者。有一些请求音频数据流,该流程是与音频流相反的方向进行。


对一些列数据的请求称作渲染调用,或者非正式的成为 pull。该表表示渲染调用的是灰色控制流箭头。渲染调用请求的数据更恰当的称为一组音频样本帧(frame)

反过来,相应于渲染调用而提供的一组音频样本帧被称为片(slice)。提供切片的代码称为渲染回调函数。
上图的具体过程:

  • 1当调用AUGraphStart函数之后,虚拟输出设备将调用Remote I/O 单元的output element。该调用请求一个处理过的音频数据帧的一个slice。
  • 2.远程I / O单元的渲染回调函数在其输入缓冲区中查找要处理的音频数据,以满足渲染调用。如果有数据等待处理,那么远程I / O单元使用它。否则,如图所示,他会调用app连接到其输入的渲染回调。在该实例中,远程I / O单元的的input(element 0的 input scope)连接到其effect 音频单元的output(element 0的output scope)。因此远程I / O单元 pull effect 音频单元,要求音频帧的slice。
  • 3.effect 单元与远程I / O单元的行为一致。当他需要音频数据时,他从input 连接获取它。在上述实例中,effect 单元将启动app的渲染回调函数。
  • 4app的渲染回调函数是pull的最终接受者。它将pull的帧提供给effect 单元。
  • 5.effect单元处理程序渲染回调的提供的slice。effcet单元然后将先前请求的处理数据提供给(在步骤二中)提供给远程I/O单元
  • 6 远程I / O单元处理effect单元提供的slice。然后,远程I / O单元将最初请求的处理过的slice(在步骤一中)提供给虚拟输出设备完成一个周期。

渲染回调功能将音频输入到音频单元

从磁盘或者内存向音频单元 input bus 提供音频,需要使用AURenderCallback原型的渲染回调函数。音频单元 input 在需要另一样本帧的slice的时候,调用该回调。

编写渲染回调函数的过程可能是设计和构建音频单元应用程序最具创造性的方面了。这里可以以任何可想象和编码的方式生成或者改变声音的机会。

这里,渲染回调必须遵守严格的性能要求。渲染回调存在于实时优先级的线程上,后续渲染调用将异步到达。(相当于定时触发该函数)我们在渲染回调函数体重所做的工作是在这个受限时间的环境中进行的。如果在下一个渲染调用到达时,如果回调扔在渲染上一个渲染调用,那么声音就会产生间隙。因此,在改函数中,我们不能采用锁,分配内存,访问文件系统或者网络连接,或者以其他方式在渲染回调函数的主题中执行耗时间的任务。

回调函数不能执行耗时任务,否则会产生声音间隙。就是断续声音

理解音频单元的渲染回调函数

下列代码显示符合AURenderCallback原型函数

static OSStatus MyAURenderCallback (
    void                        *inRefCon,
    AudioUnitRenderActionFlags  *ioActionFlags,
    const AudioTimeStamp        *inTimeStamp,
    UInt32                      inBusNumber,
    UInt32                      inNumberFrames,
    AudioBufferList             *ioData
) { /* callback body */ }

当我们将渲染回调函数绑定到input上的时候, inRefCon 参数指向的是我们指定的编程上下文。上下文的目的可以是一下两个中的一个,第一个是提供给回调函数音频输入数据,第二个是提供给回调函数需要计算给定渲染调用的输出音频的状态信息

ioActionFlags 参数允许回调向调用音频单元提供没有要处理的音频的提示。例如,如果app是合成吉他并且用户当前没有播放音符,那么执行此操作。在要为其输出静默的回调调用期间,执行下列语句:

*ioActionFlags |= kAudioUnitRenderAction_OutputIsSilence;

当想要产生静音时,还必须将ioData参数指向的缓冲区设置为0。
inTimeStamp参数表示调用回调的时间。他包含一个AudioTimeStamp 结构,mSampleTime 字段是一个样本帧计数器。在每次调用回调时候,mSampleTime字段的值将增加inNumberFrames参数汇总的数字。
inBusNumber 参数指示调用回调的音频单元的bus,允许我们根据该值在回调内进行分支。此外,在讲回调附加到音频单元时,我们可以为每个bus 指定不同的上下文。
inNumberFrames参数指示要求回调在当前调用中提供的音频样本帧的数量。我们可以将这些帧提供给ioData参数中的缓冲区
ioData参数指向回调在调用时必须填充的音频数据缓冲区。放入这些缓冲区的音频必须符合调用回调的bus的音频流格式。

如果正在特定的回调调用播放静音,请将这些缓冲区显示设置设置为0,使用memset函数

下图描绘了ioData参数中的一对非交织立体声缓冲区。使用图中的element可视化回调需要填充的ioData缓冲区的详细信息。


image.png

音频流格式启用数据流

在单个样本级别处理音频数据时,与使用音频单元一样,仅指定正确的数据类型来表示音频是不够的。单个音频的采样率的布局具有重要意义,因此像Float32 或者Uint16这样的数据类型不够表达。在本小节中,我们将了解下Core Audio对此问题的解决方案。

使用AudioStreamBasicDescription结构

在应用中或者应该与硬件之间移动音频值的货比是AudioStreamBasicDescription结构。

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的代码

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定点类型。上述实例的第一行计算了类型中的字节数;在定义ASBD的某些字段值时需要该数字。
接下来,声明一个AudioStreamBasicDescription类型变量,字段初始化为0,确保没有字段包含垃圾数据。不要跳过这个归零操作;否则以后肯定会遇到麻烦。
现在定义ASBD字段值。为mFormatID字段指定kAudioFormatLinearPCM。音频单元使用未压缩的音频数据。这是格式标识符。
接下来,对于大多数音频单元,为mFormatFlags字段指定kAudioFormatFlagsAudioUnitCanonical元标志。此标志在CoreAudio.framework / CoreAudioTypes.h定义如下

kAudioFormatFlagsAudioUnitCanonical = kAudioFormatFlagIsFloat |
                                kAudioFormatFlagsNativeEndian |
                                     kAudioFormatFlagIsPacked |
                             kAudioFormatFlagIsNonInterleaved

此标记负责指定AudioUnitSampleType类型的线型PCM样本值中位的所有布局详细信息。
某些音频单元采用非典型音频数据格式,需要不同的样本数据类型和mFormatFlags字段的不同标志集。例如3D混音器单元需要UInt16数据类型作为其音频样本值,并要求将ASBD的mFormatFlags字段设置为kAudioFormatFlagsCanonical。使用特定音频设备时,请小心使用正确的数据格式和正确的格式标志。可以参考Using Specific Audio Units.
继续看上面代码,接下来的四个字段进一步制定了样本帧中位的组织和含义。根据您正在使用的音频流的性质,设置这些字段-mBytesPerPacket, mBytesPerFrame, mFramesPerPacket和 mBitsPerChannel 。每个字段的具体含义可以看AudioStreamBasicDescription
设置ASBD的mChannelsPerFrame字段来设置流的声道数量,1是单声道 2是立体声。
最后,跟着您在整个app使用的采样率设置mSampleRate字段。Understanding Where and How to Set Stream Formats解释了避免采样率转换的重要性。 Configure Your Audio Session确保应用程序的采样率与音频硬件采样率相匹配。
我们也可以使用CAStreamBasicDescription.h ((/Developer/Extras/CoreAudio/PublicUtility/))中提供的C++ 实用程序方法而不是这里指定的ASBD字段。特别是 SetAUCanonical 和 SetCanonical 方法。下列指明数学ASBD字段的正确方法

  • 流是用于I/O (SetCanonical)还是用于音频处理 (SetAUCanonical)
  • 希望流格式有多少通道
  • 希望流格式是交错还是非交错

无论是否再项目中包含CAStreamBasicDescription.h 文件以直接使用其方法,Apple 建议我们研究该文件以了解使用AudioStreamBasicDescription的正确方法。

了解设置流格式的位置和方式

我们必须在音频处理graph的关键点设置音频数据流格式。在其他非关键点,系统帮助我们设置流格式。在其他的非关键点,音频单元连接将流数据从一个单元传递到另一个单元。

ios设置上的音频输入和输出硬件具有系统确定的音频流格式。这些格式始终是未压缩的,采用线型PCM格式,并且是交错的。系统在音频处理graph中将这些格式强制放在I/O 单元的朝外侧。

这里还是需要强调下 音频输入和输出硬件的音频流格式是确定好的,格式是始终是未压缩的,采用线型PCM格式,并且是交错的

这里有个朝外侧什么意思呢?我们知道麦克风始终连接的是I/O 音频单元的input element 上,所以可以理解为input element 所在的input scope 是朝外侧。同理 output elment 所在的output Scope也是朝外侧。

在图中,麦克风代表输入音频硬件。系统确定输入硬件的音频流格式,并且经过 远程I/O 单元的input scope 连接到 input element上

类似的,图中的扬声器代表输出音频硬件。系统确定输出硬件的流格式,并经过远程I/O单元的 output scope 的output element 发送给扬声器。

app负责在I/O单元 element 的向内侧建立音频流格式。I/O单元在我们的app格式和硬件格式之间执行必要的转换。我们的app还负责在上图中的任何其他位置设置流格式。在某些情况下,例如,上图中的多通道混音器单元的输出 ,我们只需要设置格式的一部分-特别是采样率。Start by Choosing a Design Pattern可以显示为各种类型的音频单元应用设置流格式的位置。 Using Specific Audio Units列出每个ios音频单元的流格式要求。

如图,音频单元连接的一个关键特性是连接音频数据流格式从其源音频单元的output 传播到目标音频单元的input 。这是一个关键点,因此需要强调的是:流格式传播通过音频单元连接,传播方向只能从源音频单元的输出到目标音频单元的输入进行。

output 可以理解为output Scope 中的 element 0 和element 1
input 可以理解为input scope 中的 element 0 和 element 1
这里也需要注意,不是所有音频单元都可以连接到 element 0 和element 1的。 例如,混音器单元有多个input element 和一个输出 element 。混音器的 input scope 的output 是不可以连接其他音频单元的output

利用格式传播。这可以显著的减少我们需要编写的代码量。例如,将多声道混音器单元的输出连接到远程I/O单元进行播放时,无需为I/O单元设置流格式。它根据混音器的输出流格式通过音频单元之间的连接进行适当设置。

流格式的传播发生在音频处理grahp的生命周期的特定点-即初始化的时候。可以参考Initialize and Start the Audio Processing Graph.

我们可以非常灵活的定义应用程序音频流格式。但是,只要有可能,请使用硬件使用的采样率。执行此操作时候,I/O单元无需执行采样率转换。这最大限度的减少了能源使用-这是移动设备中的一个重要考虑因素-并且最大化了音频质量。了解如何使用硬件采样率,可以参考Configure Your Audio Session.


mixerHost
audio unit ios
参考文章
音频单元
Audio Unit Component Services

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容