博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

用c++设计音效插件 :4 AU编程指南

Posted on 2022-06-17 22:17  pencilCool  阅读(300)  评论(0编辑  收藏  举报

翻译自: https://learning.oreilly.com/library/view/designing-audio-effect/9780429954313/xhtml/Ch04.xhtml

目前有两种不同的AU插件规范:v2和v3。AUv3规范从根本上说是针对iOS编程的,而AUv2仍然是非iOS应用的首选API。还有一个AU桥,可以帮助将v2版本包装成v3插件。在本书中,我们将为非iOS应用使用V2插件API,因为这是所有AU DAW都会加载的插件类型。这两个版本都遵循我们在第二章学习的音频插件范式。你可以从苹果公司免费下载AU SDK。

4.1 设置AU SDK

AU SDK仅由两个文件夹组成。AUPublic和PublicUtility。这两个文件夹包含了所有的AU SDK文件。与其他SDK不同的是,它们不包含示例代码。你可以只下载这两个文件夹,也可以从https://developer.apple.com/library/content/samplecode/sc2195/Introduction/Intro.html,下载示例项目(包含同样的两个文件夹)。你也可以从www.willpirkle.com/Downloads/AU_SDK.zip,只获取SDK文件。没有任何库需要构建;安装完这两个文件夹后,你就完成了。

4.1.1 AU示例项目

包含SDK文件夹的苹果文档档案中也包含了一组样本项目。在这些项目中,AU SDK文件夹(AUPublic和PublicUtility)被嵌入到样本项目的外部文件夹中;最好把它们留在那里,只需把这两个SDK文件夹复制到你最终的插件开发文件夹中。这里有FX、合成器和发电机(振荡器)插件的样本项目,以及其他的。只要打开每个项目里面的Xcode项目文件,然后构建它们;注意,这与样本代码打包的SDK文件夹的位置有严格的依赖关系。在尝试编写你自己的插件之前,请确保你能正确构建样本代码。

4.1.2 AU文档

AU的API文档并不包含在SDK中,而是以网页和网站的形式在互联网上完全实现。你可能希望下载并缓存完整的文档,以防你需要在互联网有限的地区工作。AU的文档一般都很好;但是,你有时可能会遇到过时的信息或不工作的链接。AU的编程指南特别好,包括关于样本代码的细节、例子,以及一些涉及CoreAudio、CoreFoundation和其他框架的操作系统特定部分的链接。文档的主要页面可以在https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioUnitProgrammingGuide/Introduction/Introduction.html。

4.2 AU结构和剖析

AU插件只为MacOS编写。这意味着你必须使用Xcode和MacOS来编译AU插件,而不考虑你的插件框架(ASPiK,JUCE,等等)。AU插件是打包在MacOS捆绑包中的组件。捆绑包是一个文件夹的特殊名称,它以文件的形式出现在用户面前。如果你右击一个包(文件)并选择 "显示包内容",那么你可以看到包的文件、文件夹和子文件夹。AAX和VST在MacOS和Windows中都已转为捆绑概念(Windows中的捆绑也是一个伪装成文件的文件夹)。

从开发的一开始,你就需要决定该插件是否支持自定义GUI。AU插件的独特之处在于,信号处理和GUI对象必须作为单独的产品被独立编译;而在其他API中,这些对象被编译成一个产品。一个AU Xcode项目必须被设置为专门独立编译这两个组件,产生两个输出产品:AU插件的后缀为.component,Cocoa GUI视图工厂的后缀为.bundle,如图4.1a所示。构建完成后,你会把GUI组件移到AU包中。这在编译器中是作为构建后的操作来处理的,如图4.1b所示,不过你也可以手动完成这个移动。最后,插件文件(比如GoldenFlanger.component)被复制或移动到最终的目标文件夹,如图4.1c所示。对于开发来说,你可以手动完成这个最后的移动,或者作为后期构建的操作。如果你使用的是ASPIK,那么所有这些复制和移动都会自动为你处理,而且编译器项目也会相应地设置好。如果你不是,请查阅你的插件框架文档以了解这些细节。如果你是从头开始设计你的插件,那么你必须自己处理这些细节。

图4.1:(a)AU插件项目编译了两个输出文件,即AU组件和Cocoa GUI包。(b) Cocoa bundle被移到AU组件中。(c) 最后,单个捆绑包被移到AU库目标文件夹中。

AU信号处理部分是用C++编写的,作为派生自AU基类之一的对象,其方式与AAX和VST插件相同,并在第2章中描述。然而,GUI产品必须被设计成使用Objective-C编程语言的Cocoa对象。此外,GUI对象必须被设计成一个类工厂,在用户每次打开它时提供GUI的实例。如果你不打算实现一个自定义的GUI,那么你的插件可以写成一个没有GUI组件的单一C++对象。现在,好消息来了:如果你使用ASPiK来开发你的插件,你将使用VSTGUI4库来开发GUI对象,它是用本地C++编写的,包括你决定实现的任何自定义视图对象(见plugingui.h和plugingui.cpp,它们实现了VSTGUI4对象)。你不需要为一行Objective-C代码而烦恼! 这是由于VSTGUI4架构师非常聪明的编程而实现的。他们使用Objective-C运行时编程API来注册和包装C++ GUI对象为Objective-C。Objective-C运行时API是专门设计来允许其他面向对象的编程组件被注册和包装成Cocoa对象。如果你从头开始设计一个AU插件,那么你也需要直接用Objective C编写GUI工厂和视图对象。

4.2.1 AU基类

你的AU处理对象是用C++编写的,根据你的最终产品目标,它来自于几个AU基类之一。这与所有其他插件的API是一致的。这些选择如下。

AUEffectBase:用于有一个音频输入和一个音频输出流的FX插件(每个流可能包含多个通道),不支持MIDI。
AUMIDIEffectBase:用于有一个音频输入和一个音频输出流的FX插件,它也想接收或产生MIDI信息。
MusicDeviceBase:用于没有音频输入流的合成器插件,有一个渲染合成器音符的音频输出流,并能接收或产生MIDI信息。
所有的ASPiK AU插件项目都是使用AUMIDIEffectBase类创建的,这样FX插件就可以根据自己的意愿发送和接收MIDI信息。这对侧链有影响,我们将在后面讨论,因为所有的ASPiK插件都可以选择为用户公开一个侧链。所有这三个基类都是从名为AUBase的母类派生出来的。因此,它们都有许多共同的功能和编程范式。在这本FX 中,如果你决定从头开始创建插件,你会想使用AUEffectBase或AUMIDIEffectBase类。

4.2.2 MacOS的捆绑ID

MacOS中的每个捆绑包都有一个独特的字符串,称为捆绑包ID。苹果公司有关于命名这个字符串的建议,以及在你准备商业销售你的产品时向苹果公司注册它。通常情况下,你将你的公司和产品名称嵌入到捆绑ID中。唯一的主要限制是,你不应该使用"-"字符来连接字符串组件,而应该使用句点符号。例如,mycompany.au.golden-flanger是不合法的,而mycompany.au.golden.flanger是可以的。此外,因为你的AU项目生成了两个产品,最终会合并在一起,你需要确保这两个产品的捆绑ID值相匹配。这些都是在Xcode项目的信息面板中设置的,它实际上只是显示了两个基础info.plist文件中的信息。如果你有任何问题,请查阅苹果文档。如果您使用ASPiK来生成您的AU项目,那么捆绑ID值是为您创建的,并在编译器项目中的Info.plist预处理器定义中设置。你可以在项目的构建设置面板中查看或改变这个值。当我们讨论AU如何根据用户的要求创建你的插件GUI时,我们将了解更多关于bundle ID的信息。

4.2.3 AU编程注意事项

AU插件是用C++编写的,使用Xcode。然而,与AAX和VST不同,AU插件需要依赖于操作系统的框架和编程范式。如果你不熟悉CoreFoundation对象,如CFArrayRef、CFStringRef、CFMutableArrayRef等,那么AU插件的一些代码就会显得很陌生。如果你想从头开始写AU插件,或者你打算在AU编程中花费时间和精力,超出你的第三方框架实现的范围,那么你可能想在苹果CoreFoundation对象上投入一些时间。带有自定义图形用户界面的AU插件所需的框架包括CoreAudio、CoreMIDI、CoreFoundation、QuartzCore、AudioUnit、AudioToolbox、Accellerate和OpenGL。后两者是可选的,用于快速DSP算法(Accellerate)和VSTGUI4(OpenGL)。你不需要成为这些方面的专家,就可以用ASPiK或其他插件框架创建插件;但是,在某些时候,你可能想做一些定制,这至少需要在框架中进行一些探索。

与AAX和VST一样,有大量的C++类型定义可以重命名标准数据类型,所以你应该总是右键单击并要求编译器带你到你不认识的定义。大多数的定义都很简单,应该是为了使代码更容易阅读,尽管有时它们会产生相反的效果。绝大多数AU基类函数都返回错误或成功代码,它们实际上只是整数,被类型化为ComponentResult和OSStatus。成功代码是noErr,常见的失败代码是kAudioUnitErr_InvalidParameter和kAudioUnitErr_InvalidProperty。

该插件是一个DLL,需要入口点(主机将调用的函数名称),这些入口点定义在导出的符号文件中,后缀为.exp。其中一个是与info.plist入口相匹配的AU工厂函数。一般来说,你的插件框架会定义这些;你不需要花时间去处理它们,除非你想从头开始编写插件。

在实例化过程中,该插件将被反复查询,以询问有关其属性和能力的信息。有一个很大的标准查询列表;如果你愿意,你可以设置断点并观察其动作。在某些情况下,这是你的插件对其他查询作出真实回答或你的插件暴露出字符串列表参数的直接结果。主机还将查询插件的工厂预设,插件在一个连续的内存块中创建工厂预设。当用户选择其中一个预设时,主机将通知该插件,以便它可以相应地设置参数。我们将很快讨论预置。

AU插件对象的C++名称除了必须是一个合法的C++类名称外没有其他限制--你可以重复使用同一个AU插件对象的名称。然而,正如第4.7.1节所指出的,Cocoa视图工厂和视图对象的命名需要你注意,必须是唯一的。

在AU编程中,你还会看到许多对作用域和元素的引用,它们被传递到许多AU插件函数中。作用域指的是在AU插件中使用或访问某个东西的上下文。AU插件架构定义了八个作用域,其中前三个我们用于FX插件。

全局:与插件作为一个整体的各个方面有关。
输入:与进入AU的数据有关
输出:与离开AU的数据有关
组:与渲染音符有关(合成)。
部件:与多音色合成器有关
note:与音符有关
layer和layerItem:与组中的合成器层有关。
元素是作用域的子组件,从零开始的整数索引。对于输入和输出作用域,它们对应于硬件总线。一个全局作用域有一个而且只有一个元素,即元素0。

4.3 描述。插件描述字符串

你的插件描述字符串包含在Xcode主视图的 "Info "标签中。这反映了相关的info.plist文件中的信息;你可以编辑该文件或编译器项目本身,我推荐这样做。有第二个info.plist文件用于Cocoa视图工厂,但它不包含任何关于音频插件的信息;你通常只需要确保AU插件和Cocoa视图包的ID值相匹配,因为Cocoa包被嵌入到AU包内。一个典型的Xcode AU插件项目会有类似于图4.2的信息面板。这个项目被命名为GoldenFlanger,它包括两个目标,一个是AU插件,另一个是Cocoa GUI。你可以按照自己的意愿来命名这些目标。

图4.2中的插件打包信息包括以下内容。

bundle identifier:插件的bundle(包)的唯一标识符字符串
可执行名称:最终插件文件的名称(bundle/folder);这里的插件被打包为GoldenFlanger.component
更重要的是字典对象,它包含了更多至关重要的插件描述字符串。

  • 制造商:一个定义制造商的四字代码;这里我们的虚构公司是PluggCo,四字代码为PLCO
  • 子类型:一个四字代码,在你的公司的插件集中确定该插件;你可能会在测试过程中调整这个字符串的值(对于我们最初的构建,我们将使用FLNG,代表镶边器)。

图4.2: 一个AU插件项目的Xcode信息面板。关键的字符串用粗体显示;为了简洁起见,有些信息被省略了。

  • type: 四个字符的代码,用于识别AU插件的类型,可以是下列之一。

    • aufx:一个源自AUEffectBase的音频效果插件
    • aumf:具有MIDI功能的音频效果,来自AUMIDIEffectBase。
    • aumu:具有MIDI功能的软件合成器,来自于MusicDeviceBase。
  • name:这个字段很关键,因为它包括插件的名称和制造商,因为你希望它们出现在DAW的插件选择菜单中;用冒号把插件制造商和DAW的插件名称字符串分开,如 "PlugCo: Golden Flanger"。

  • 工厂函数:插件的入口点名称,在导出的符号文件中设置后缀为.exp

请特别注意名称字段:这允许你选择你希望DAW为用户展示的确切的名称字符串;这意味着这个名称字符串可以完全独立于插件项目或包的名称。

4.4 描述。插件选项/特性

插件选项包括音频输入/输出、MIDI、侧链、延迟和尾随时间。对于AU插件来说,这些都是在插件构造函数中,或者在你覆盖的其他基类函数中,以编程方式完成的。

4.4.1 侧链输入

当你从适当的基类中派生出AU插件对象时,它将自动为你设置音频输入和输出总线的数量。插件对象的构造器调用的第一个函数是AUBase::CreateElements( ),它将设置基本的音频I/O总线。之后,你可以修改音频I/O的细节。如果你想暴露一个二级输入作为一个侧链,你必须明确地添加它并命名它。这很简单,只需要三行代码。

SetBusCount(kAudioUnitScope_Input, 2);
SafeGetElement(kAudioUnitScope_Input, 0)->SetName(CFSTR("Main Input"));
SafeGetElement(kAudioUnitScope_Input, 1)->SetName(CFSTR("Sidechain"));

第一行将输入总线的数量调整为两个,而下面几行则对每个母线进行命名。音频母线0是默认的音频输入,母线1是侧链输入;你不能改变这些名称,尽管你可以改变名称字符串。如果你的插件不需要侧链,你可以完全省略这段代码,因为CreateElements函数调用将处理基本装配。在这里我们应该讨论一个小小的注意事项,它涉及到使用AUMIDIEffectBase类。虽然这个基类看起来与普通的AUEffectBase相同,但加入了MIDI,它不允许为用户暴露出一个侧链。由于许多侧链插件可能也想接收MIDI信息,你可以通过将AU字典中的类型设置为aufx来解决这个问题,尽管该插件是从MIDI效果基类派生出来的,但这种方式略显不理想。这将允许DAW为这个AU插件类型暴露一个侧链(不要告诉别人)。如果你的插件需要MIDI,但不需要侧链输入,那么你可以指定更合法的aumf作为插件类型。

4.4.2 延迟

如果你的插件包括一个延迟,给音频信号增加一个必要的固定延迟,比如第20章的PhaseVocoder或第18.7节的look-ahead compressor,你可以为AU插件主机说明这一点。框架会在你的插件对象上调用函数GetLatency( )来查询它的这个特性。如果你的插件包括一个固定的延迟,你可以重写这个函数并返回延迟值。与AAX和VST不同,AU的延迟是以秒为单位报告的,而不是以样本为单位。这意味着很可能AU插件的延时值会随着采样率的变化而变化。因此,你将需要根据采样率来调整延迟值。对于一个固定的(与采样率无关的)延时,这个函数的实现很简单;例如,如果延时是1毫秒,你可以简单地写。

virtual Float64 GetLatency() {return 0.001;}

对于一个基于样本的延迟,随着采样率的变化而变化,你可以返回一个变量来代替。

virtual Float64 GetLatency() {return latencyInSeconds;}

变量latencyInSeconds是我们为该插件声明的一个成员变量(当然你可以随心所欲地重命名)。我们将在两个不同的函数中重新计算这个值,其中采样率可能发生变化,我们将在后面的章节中介绍。

4.4.3 尾音时间

混响和延时插件通常需要一个尾音时间,主机通过在音频播放停止后将零值的音频样本泵入插件来实现,允许混响尾音或延时重复和消退。当你的插件被加载时,主机会查询你的插件,要求提供尾音时间的信息。如果你的插件不需要尾音时间,那么你可以忽略这些功能。有两个AUBase函数一起工作:第一个查询插件,看它是否想要一个音频尾音;如果插件返回真,那么第二个函数被调用,询问尾音时间(秒)。例如,如果你的插件想要一个5秒的音频尾音,你将实现这两个函数,如下所示。

virtual bool SupportsTail() {return true;}
virtual Float64 GetTailTime() {return 5.0;}

4.4.4 自定义图形用户界面

在实例化过程中,AU主机会反复询问插件的各种信息,统称为音频单元属性。有许多属性是你的插件可能希望支持的:例如,主机可能会询问插件的 "最重要 "的参数,作为概览提供。如果该插件实现了一个自定义的Cocoa GUI,那么它需要对音频单元属性kAudioUnitProperty_CocoaUI的查询做出noErr的回答,我们很快就会提到。

4.4.5 工厂预置和状态保存/加载

AU和VST支持工厂预置,这些预置在编译插件时被编码,所以它们被 "内置 "到C++对象中,永远不能被改变。有两个支持预置的函数,如果不使用第三方插件框架,你将需要覆盖和完成。第一个函数是对预设列表的查询,它被打包成AUPreset结构,它对预设名称字符串和预设索引号进行编码。

typedef struct AUPreset {
      SInt32      presetNumber;
      CFStringRef presetName;
} AUPreset;

AUPresets以静态声明的数组形式返回,这意味着该字符串数组必须在插件的生命周期内持续存在。数组作为一个函数参数返回,函数本身返回成功/错误代码。

ComponentResult GetPresets(CFArrayRef *outData) const

你可以直接把数组写成一个全局变量,如:。

static AUPreset kPresets[3] = {
{0, CFSTR("Factory Preset")},
{1, CFSTR("Really Loud Flanger")},
{2, CFSTR("Crazy Resonance")}};

另一个选择是,如果在编译时不知道预设名称和数量,则在运行时动态分配内存来存储阵列。当用户选择一个工厂预设时,插件通过函数被通知,它被传递给所选工厂预设的AUPreset结构。

OSStatus NewFactoryPresetSet(const AUPreset& inNewFactoryPreset)

该函数对预设信息进行解码,然后根据它以前存储的预设数据来设置AU插件的参数。

当用户保存DAW会话或创建自己的预置时,AU主机将处理数据的读写(也称为序列化),所以插件不需要直接做什么。然而,只有AU参数将用标准机制来保存。AU基类具有两个用于加载和保存状态信息的函数,称为SaveState和RestoreState:如果你有某种需要保存或加载预设的自定义信息,而且超出了插件的参数状态,你需要覆盖这些函数并使用CFMutableDictionaryRef对象实现你自己的保存和加载功能。

4.5 初始化。定义插件参数

在AU中,一个插件参数总是编码并代表参数的实际值。例如,如果你有一个音量控制的范围是-60到+12dB,而用户把它调整到-3dB,那么这个参数将存储-3的值,并把这个值传送给你的插件。如果你想把数据写入GUI参数(例如,用户加载预置),那么你也要把实际的-3值传给它。这与VST3不同,VST3在GUI和插件之间移动数据时使用[0.0, 1.0]范围内的归一化值。AU参数使用一个整数值作为控制ID,所以你使用这个数值访问参数。

与其他所有的API一样,AU定义了一个插件参数对象,允许以线程安全的方式调整参数。对于AU来说,这个对象是一个ParameterMapEvent,它代表了参数的值,也允许斜坡式的参数,以实现参数的平滑化。参数总是表示为32位浮点值,不管它们与哪个GUI控件相联系。它们使用一个AudioUnitParameterID类型的ID值来访问,这只是一个无符号的32位整数,我在本书中称之为控件ID。AU有两个内部选项来存储你的参数:std::vector和std::map。你可以选择将你的参数声明为索引的,也就是说,控制ID值是零索引的,没有间隙(0, 1, 2, 3, ... N),在这种情况下,基类将使用一个向量来存储参数。对于非索引的参数,也就是默认的操作,则使用map来代替。对于随机访问,向量的速度稍快。然而,AAX和VST并没有这个选项。我们希望有允许ID值有间隙的灵活性(例如根据ID的数值来编码一些信息),所以我们将使用非索引版本。

你使用一个名为Globals( )的辅助函数来获取和设置插件参数,该函数可以访问存储的参数列表,然后你只需调用获取或设置函数,传递控制ID值和数据的变量。要将ID为42的参数设置为0.707的值,请调用set函数。

Globals()->SetParameter(42, 0.707);

要获得同一个参数的值,你可以调用get函数,该函数将值返回到你声明的一个变量中。

double auParameter = Globals()->GetParameter(42);

在初始化阶段,定义或暴露插件参数给AU主机的方法由三部分组成。在插件构造函数中,你通过调用SetParameter( )函数创建一个参数,并传递给它一个控制ID值。由于参数不存在,那么就会为你创建一个,所以没有实际的创建函数需要处理。例如,假设一个插件有两个参数,属性如下。

Parameter 1
type: linear, numeric
control ID = 0
name = “Volume”
units = “dB”
min/max/default = −60/+12/−3


Parameter 2
type: string list
control ID = 42
name = “Channel”
string list: “stereo, left, right”

在构造函数中,你将有效地创建它们。

Globals()->SetParameter(0, −3); // volume = 0, default = −3dB
Globals()->SetParameter(42, 0); // channel = 42, default = item 0

构造函数完成后,基类将反复查询你的插件,要求提供关于每个新创建的参数的更多信息。该查询函数为

ComponentResult GetParameterInfo(AudioUnitScope inScope,
                                  AudioUnitParameterID inParameterID,
                                  AudioUnitParameterInfo& outParameterInfo)


范围和参数ID值被送入函数中(范围代表参数是全局的,与整个AU有关,或者它是专门的--我们所有的AU参数都是全局的)。这些信息以AudioUnitParameterInfo结构返回,该结构编码如下。

为用户显示的名称字符串
单位(例如,dB或Hz;可以留空)
最小、最大、和默认的参数值
类型:有索引的参数为用户显示零索引的字符串列表,而无索引的参数代表典型的数字参数
对于我们的参数,你会对传入的参数ID值进行解码,然后填入outParameterInfo成员。对于连续参数,我们应用自定义单位来填写我们自己的字符串,而对于字符串列表参数,我们通知这个参数是kAudioUnitParameterUnit_Indexed,这标志着主机需要调用另一个函数来接收字符串列表。

if(inParameterID == 0)
{
          // --- VOLUME --- //
          CFStringRef name = CFStringCreateWithCString(NULL, “Volume””,
                                                     kCFStringEncodingASCII);
          AUBase::FillInParameterName (outParameterInfo, name, false);
          // --- custom, set units
          CFStringRef units = CFStringCreateWithCString(NULL, “dB””,
                                                      kCFStringEncodingASCII);
          outParameterInfo.unit = kAudioUnitParameterUnit_CustomUnit;
          outParameterInfo.unitName = units;
          // --- set min and max
          outParameterInfo.minValue = −60.0;
          outParameterInfo.maxValue = +12.0;
          outParameterInfo.defaultValue = −3.0;
}
else if(inParameterID == 42)
{
          // --- CHANNEL --- //
          CFStringRef name = CFStringCreateWithCString(NULL, “Channel””,
                                                     kCFStringEncodingASCII);
          AUBase::FillInParameterName (outParameterInfo, name, false);
          // --- set indexed to get query for string list
          outParameterInfo.unit = kAudioUnitParameterUnit_Indexed;
          // --- set min and max
          outParameterInfo.minValue = 0;

          outParameterInfo.maxValue = 2;
          outParameterInfo.defaultValue = 0;
}

最后,对于任何你在GetParameterInfo( )函数中表示为索引的参数,基类将调用另一个函数,以便你可以提供用于显示的字符串列表,你以CoreFoundation数组的形式传回。

ComponentResult GetParameterValueStrings(AudioUnitScope inScope,
                                    AudioUnitParameterID   inParameterID,
                                    CFArrayRef*            outStrings)

为了回复我们的参数2的查询,我们将实现下图所示的函数。注意,我们用逗号分隔的版本创建字符串,然后用逗号分隔的列表创建数组--这是因为将字符串列表存储为逗号分隔很容易。(查看CoreFoundation文档中的其他各种CFStringCreateArray函数)。

if(inParameterID == 42)
{
      // --- convert the list into an array
      CFStringRef enumList = CFStringCreateWithCString(NULL,
                              “stereo, left right”, kCFStringEncodingASCII);
      CFStringRef comma CFSTR(",");
      CFArrayRef strings = CFStringCreateArrayBySeparatingStrings(NULL,
                                                       enumList, comma);
      // --- create the array COPY
      *outStrings = CFArrayCreateCopy(NULL, strings);
}

4.5.1 线程安全的参数访问

为了对参数进行线程安全的访问,你可以使用上面描述的两个函数。

Globals()->GetParameter(…)
Globals()->SetParameter(…)

4.5.2 初始化。定义插件的通道I/O支持

一个AU插件通过函数SupportedNumChannels( )声明它的音频通道I/O能力,该函数在初始化阶段被调用。该插件在AUChannelInfo指针数组中返回其通道I/O支持,并将可能的音频I/O组合的数量作为返回变量。

UInt32 SupportedNumChannels(const AUChannelInfo** outInfo)

AUChannelInfo结构很简单,包含了保持输入和输出通道数量的变量。

typedef struct AUChannelInfo {
            SInt16 inChannels;
            SInt16 outChannels;
} AUChannelInfo;

在所有的API中,AU对音频通道的支持是最不具体的:你只需声明插件支持的所有通道数组合。例如,AAX和VST都定义了两个不同版本的7.1环绕声音频:一个用于索尼,另一个用于DTS。AU不区分这两种情况,在这种情况下,只报告输入通道数和输出通道数的数值为8(7+1=8)。

4.5.3 初始化。通道数和采样率信息

当主机想把插件置于初始化状态时,AU函数Initialize( )被调用。该函数的基类版本处理基本的音频通道支持信息--它调用SupportedNumChannels函数作为这个过程的一部分。它还设置了一些低级别的音频流信息,并执行一些基本的初始化操作。你的AU插件应该覆盖这个方法,首先调用基类,然后再做其他工作。对于我们的FX插件,我们得到当前DAW会话的采样率,并调整任何与速率有关的变量,如我们的插件延迟值。记住,AU要求以秒为单位报告延迟,而不是以样本为单位。你可以通过查询音频流描述来获得当前的采样率和文件比特深度,这最终导致了一个包含音频流信息的结构,被恰当地命名为AudioStreamBasicDescription,你通过调用辅助函数GetStreamFormat( )来获得它。

struct AudioStreamBasicDescription
{
          Float64             mSampleRate;
          AudioFormatID       mFormatID;
          AudioFormatFlags    mFormatFlags;
          UInt32              mBytesPerPacket;
          UInt32              mFramesPerPacket;
          UInt32              mBytesPerFrame;
          UInt32              mChannelsPerFrame;
          UInt32              mBitsPerChannel;
          UInt32              mReserved;
};

你可以像这样获得采样率的权限。

Float64 sampleRate = GetOutput(0)->GetStreamFormat().mSampleRate;

由于各种音频流格式属性(包括交织或非交织),有一系列的辅助函数将流信息转化为通道数、比特深度和其他你可能需要的数据。这些被定义在CAStereamBasicDescription.h中,包括SampleWordSize( ),你可以用它来计算音频流的比特深度。

UInt32 bitDepth = GetOutput(0)->GetStreamFormat().SampleWordSize()*8;

如果你的插件由于某种原因动态地改变了音频通道数,这就是你处理部分操作的地方(记住,这个函数最终调用SupportedNumChannels)。这里显示了一个实现的例子;注意基类函数是在函数的顶部调用的。

ComponentResult Initialize()
{
           // --- call base class FIRST
           ComponentResult result = AUEffectBase::Initialize();

           if(result == noErr)
           {
                // --- do your initialization here
                Float64 sampleRate = GetOutput(0)->GetStreamFormat().mSampleRate;
           // --- do more stuff …
           }
              return result;
}

4.6 缓冲区处理周期

AU缓冲区的处理周期遵循与AAX和VST相同的范式。一个库存的AU插件会被自动假设为独立地处理通道,并且处理函数会对每个音频通道调用一次。在第2章中,我们讨论了为了使我们的插件设计尽可能的通用,我们希望一次处理所有的音频缓冲区,把它们分成若干帧,这样我们就可以在同一时间访问同一采样周期的所有通道数据。这在现有的AU通道处理范式中是不可能的。然而,AU插件可以覆盖产生这些原始单声道的低级函数,并直接访问缓冲区,其方式与AAX和VST完全相同。这允许你写处理代码,在不同的API之间是可移植的。我们希望覆盖的两个函数名为Render和ProcessBufferLists,如图4.3所示。

图4.3: AU缓冲区的处理周期被分成两个函数,Render和ProcessBufferLists;每个功能块的伪代码显示在右边。

你只需要覆盖Render函数来获得对额外的侧链输入缓冲区的访问;你拿起这些缓冲区的指针并存储它们。ProcessBufferLists函数在Render之后立即被调用,如果你的插件使用侧链输入,你就使用侧链缓冲区的指针来执行你的音频信号处理。使用缓冲区列表处理函数使得处理过程在所有的API中是相同的。正如我们在第2章中所看到的,所有的API都传输单独的、去交错的通道缓冲区,每个音频通道一个缓冲区。

我们可以把缓冲区的处理分成三个部分,完全按照图4.3中的继承。

从GUI控制变化中更新参数,根据需要烹饪变量。
使用更新的信息处理音频数据。
写出输出的参数信息(仪表、信号图等)。
这种模式与AU参数相同,除了一个注意事项:没有办法确定一个GUI参数自上一个缓冲区处理周期以来是否发生了变化。换句话说,没有布尔 "脏 "标志来表示参数被改变了。尽管你的插件可以覆盖低级别的参数函数来试图规避这一点(不推荐),但线程安全要求我们接受这样一个事实:我们有可能需要在每个缓冲进程周期开始时更新和重新处理插件中的每个参数。这不一定是坏事,因为用户可以在DAW会话中对每一个插件的参数进行自动化设置,这样它们在每个缓冲处理周期都处于变化之中。这代表了一种最坏的情况,与硬件设计者所遵循的模式并不一样--例如,硬件合成器可能会认为每个补丁参数都可能在每个采样间隔上被调制。

一种选择是访问每个参数,并将其与上一个处理周期的存储值进行比较。然后,你可以减去这两个值,以试图确定是否有什么变化--由于四舍五入,你通常会减去这两个值,并将它们与一些小的四舍五入的delta值进行比较,如0.0001。这里的一个论点是,你在吃CPU周期来执行比较。然而,如果一个参数有一个非常复杂的、CPU密集型的烹饪功能,那么我们真的只想在确定它发生变化时更新参数。在样本书项目中,我们一般都会编写烹饪函数,首先检查参数是否发生了变化,然后只在参数发生变化时才烹饪数据,将这一任务卸载到插件内核中,脱离插件外壳代码。还有一些粒度方法,在这些方法中,你只在一些粒度上更新内部参数,而这是通过计算采样周期来完成的。最终,由你来决定如何处理这个操作。对于非常简单的、具有短而简单的烹饪功能的插件来说,通常比较简单的做法是在缓冲处理周期开始时盲目地更新和重新计算所有参数。

4.6.1 处理:从GUI控件中更新插件参数

对于AU来说,获取GUI参数值是简单而直接的:你只需调用我们在第4.5节中讨论过的参数的获取函数。你通常会写一个for-loop,每次简单地访问参数,并抓取每个参数的当前值。然后你需要调用烹饪函数来处理参数的变化。这在图4.3中的ProcessBufferLists函数中显示。你的插件框架应该有这个内置的缓冲区处理(对于ASPiK,请看preProcessAudioBuffers( ) 函数)。当然,如果你从头开始推出你自己的AU插件,那么你将需要自己处理这个。

4.6.2 处理:重置算法和为流媒体做准备

所有的音频插件都需要为下一个音频流事件做准备,正如我们在第二章看到的那样。对于延迟和过滤插件来说,这通常涉及到将旧的数据从缓冲区中冲出,并将线性或循环缓冲区指针重新设置到顶部。其他插件可能需要为每个新的音频运行重置状态变量:例如,第20章中的一些PhaseVocoder算法需要跟踪不同的快速傅里叶变换(FFT)频段的运行相位,这些值也同样需要在每个新的音频流事件开始时被重置或清除。请注意,这也包括用户和DAW之间的互动:例如,即使是音频流,大多数DAW允许用户重新定位音频播放位置,这可能需要重新设置内部变量和状态。AU为这种操作提供了复位功能。在ASPiK中,我同时使用Reset和Initialize函数来检查和更新采样率,以及冲刷缓冲区和重置变量。原因来自于经验:如果你把一个AU插件加载到Apple Logic、Apple AULab和Reason®中,然后设置断点来观察主机和基类与插件的交互,你会发现对这些函数的调用有三种不同的序列。因此,除了基类的调用,我们的Reset和Initialize函数在本质上是相同的。这里显示的是Reset函数;Initialize函数除了调用AUMIDIEffectBase::Initialize而不是AUBase::Reset外,其他都是一样的。

ComponentResult AUFXPlugin::Reset(AudioUnitScope inScope,
                                        AudioUnitElement inElement)
{
          // --- reset the base class
          ComponentResult result = AUBase::Reset(inScope, inElement);
          if(result == noErr)
          {
                // --- do your initialization here
                Float64 sampleRate = GetOutput(0)->GetStreamFormat().mSampleRate;
                // --- do more stuff …
          }
          return result;
}

4.6.3 处理:访问音频缓冲区

AU与AAX和VST的不同之处在于,侧链缓冲区是在音频信号处理的单独函数中访问的,也就是图4.3中的Render函数。该函数的参数包括包含绝对采样位置的音频时间戳结构,其他可选的计时信息,如SMPTE和字时钟数据,以及在即将调用ProcessBufferLists时需要处理的帧数。

OSStatus Render(AudioUnitRenderActionFlags& ioActionFlags,
                      const AudioTimeStamp& inTimeStamp,
                      UInt32                inNumberFrames)

如果你在构建过程中设置了一个侧链输入,那么你可以按以下方法访问它并获得其通道数。首先,你需要通过调用函数HasInput( )并传递侧链通道(1)来检查侧链是否实际存在--如果不存在,那么你可以跳过,调用基类方法。

bool scAvailable = false;
try {scAvailable = HasInput(1);}
catch (…) {scAvailable = false;}
if(!scAvailable)
      return AUEffectBase::Render(ioActionFlags, inTimeStamp, inNumberFrames);

如果侧链存在,你就用函数GetInput( )访问它,然后用PullInput( )函数获得样本数和缓冲区指针。这需要Render的一些参数,而这些参数对ProcessBufferLists来说是不可用的,因此才把侧链的访问放在一个单独的函数中。

// --- get and check side chain input
AUInputElement* SCInput = GetInput(1);
if(!SCInput) return AUEffectBase::Render(…);
AudioBufferList* scBufferList = nullptr;
if(SCInput->PullInput(ioActionFlags, inTimeStamp, 1, inNumberFrames) ==
   noErr) {
    int sidechainChannelCount = SCInput->NumberChannels();
    scBufferList = &(SCInput->GetBufferList());
}

音频信号的处理是在ProcessBufferLists函数中完成的,包括三个部分。

获取输入和输出通道的计数。
获取输入和输出缓冲区的指针。
获取帧数,并设置一个for-loop来处理从输入到输出的情况。
该函数及其参数如下。

OSStatus ProcessBufferLists(AudioUnitRenderActionFlags& ioActionFlags,
                                  const AudioBufferList&     inBuffer,
                                  AudioBufferList&           outBuffer,
                                  UInt32           inFramesToProcess)

输入和输出缓冲区的指针包含在AudioBufferList引用中,而inFramesToProcess变量包含帧数。AudioBufferList是一个允许访问单个通道缓冲区的结构,其索引与我们在第四章看到的通道I/O交错相同。对于一个立体声缓冲区,索引=0是左声道,索引=1是右声道。为了获取通道计数和缓冲区指针,你可以这样写(为了简洁,省略了计数和指针验证)。

SInt16 auNumInputs = GetInput(0)->GetStreamFormat().NumberChannels();
SInt16 auNumOutputs = GetOutput(0)->GetStreamFormat().NumberChannels();

为了从AudioBufferLists中得到左右缓冲区的指针,你需要将数据块指针(mData,它被声明为void)转换成float。你可以这样写。

float* inputL = (float*)inBuffer.mBuffers[0].mData;
float* inputR = (float*)inBuffer.mBuffers[1].mData; // 1 = right

float* outputL = (float*)outBuffer.mBuffers[0].mData;
float* outputR = (float*)outBuffer.mBuffers[1].mData; // 1 = right

现在你有了指针和帧数,你可以设置一个循环来处理这些信息(也许还可以使用侧链)。一个简单的音量插件可能是这样编码的;这里我们处理立体声输入/立体声输出。

// --- loop over frames and process
for(int i=0; i<inFramesToProcess; i++){
          // --- apply gain control to each sample
          outputL[i] = volume * inputL[i];
          outputR[i] = volume * inputR[i];
}

要使用侧链缓冲区列表中的信息,你只需以与其他信息相同的方式对待它。

float* sideaChainInputL = (float*)scBufferList.mBuffers[0].mData;
float* sideaChainInputR = (float*)scBufferList.mBuffers[1].mData;


4.6.4 处理:写入输出参数

如果你的插件将数据写入输出的VU表或某种自定义视图(见第21章),那么你也会在这里做。所有的处理完成后,你将把输出信息写到需要接收它的适当的GUI参数上。下面的代码将从每个输出缓冲区获取第一个输出样本的幅度(绝对值),并写入连接到适当参数索引值的VU表。

Globals->SetParameter(meterL_ID, fabs(outputL[0]);
Globals->SetParameter(meterR_ID, fabs(outputR[0]);


4.7 AU/GUI连接

我们看到,作为插件描述的一部分,我们可以通过对GetPropertyInfo查询的响应来表明AU插件实现了一个自定义的Cocoa GUI。具体来说,我们需要为属性标识符kAudioUnitProperty_CocoaUI返回一个成功代码,但我们也需要对函数参数稍作调整,以便为将来的调用做准备。做这一切的代码在这里。注意,如果我们不支持该属性,用ID值解码,我们就调用基类实现来返回。

ComponentResult GetPropertyInfo(AudioUnitPropertyID inID,
                                      AudioUnitScope      inScope,
                                      AudioUnitElement    inElement,
                                      UInt32&             outDataSize,
                                      Boolean&            outWritable)
{
     if (inScope == kAudioUnitScope_Global)
     {
        if(inID == kAudioUnitProperty_CocoaUI)
        {
            outWritable = false;
            outDataSize = sizeof(AudioUnitCocoaViewInfo);
            return noErr;
        }
     }
     return AUEffectBase::GetPropertyInfo(inID, inScope, inElement,
                                                outDataSize, outWritable);
}

在这一点上,主机认为我们有一个GUI要显示,所以我们有几个问题要处理。

当用户打开GUI时,创建并显示它。
当用户关闭GUI时销毁它。
建立一个AU事件监听系统,以线程安全的方式将Cocoa GUI正确地连接到底层的AU参数。
当用户打开GUI时,主机将查询插件的两个信息:Cocoa GUI类工厂的名称和AU包的路径,称为包的URL。当你创建你的AU项目时,你创建并命名Cocoa GUI类工厂。如果你使用的是ASPiK,类工厂的名字将为你生成,包括GUID装饰以保证唯一性。如果你使用的是另一个插件框架,请查阅该文档。如果你是自己开发的,那么你就自己起这个名字。获取bundle路径并不那么简单,需要多次调用CoreFoundation的帮助器,以找出最终的路径。下图是基本代码。当然,你可以得到与本书配套的完整项目代码。返回代码 "fnfErr "是一个 "未找到文件 "的代号。注意,视图工厂名称和捆绑路径被放入一个AudioUnitCocoaViewInfo结构中,在outData参数中被传回。

ComponentResult GetProperty(AudioUnitPropertyID inID,
                                  AudioUnitScope    inScope,
                                  AudioUnitElement  inElement,
                                  void*             outData)
{
    if (inScope == kAudioUnitScope_Global)
    {
       // --- find the UI associated with this AU
       if(inID == kAudioUnitProperty_CocoaUI)
       {
          // --- Look for a resource in the main bundle
          CFBundleRef bundle = CFBundleGetBundleWithIdentifier(…)
          if(bundle == NULL) return fnfErr;

          // --- get the path
          CFURLRef bundleURL = CFBundleCopyResourceURL(…)
          if(bundleURL == NULL) return fnfErr;
          CFStringRef className = “CocoaGUIFactory_234324”;
          // --- pack data into AudioUnitCocoaViewInfo structure
          AudioUnitCocoaViewInfo Info = {bundleURL, {className}};
          *((AudioUnitCocoaViewInfo *)outData) = cocoaInfo;
          return noErr;
        }
     }
     return AUEffectBase::GetProperty(inID, inScope, inElement, outData);
}

在这之后的操作将高度依赖于你的插件框架,或者你自己的Objective-C代码,如果你是从头开始设计的话。因为这些细节都是不同的,对于ASPiK用户来说,在代码和用户手册中都有完整的记录,我们将在这里省略大部分的细节。请参考你的框架文档。

然而,任何试图实现AU图形界面的框架都需要处理两个AU和Cocoa特有的问题。

Cocoa的扁平命名空间
AU的事件监听器系统

4.7.1 Cocoa的扁平命名空间

Cocoa不使用命名空间。这就自动产生了GUI对象的名称冲突问题--这意味着两个插件设计者可能会不小心把他们的Cocoa视图工厂、Cocoa视图和Cocoa GUI对象命名得很相似。例如,设计者可能都将他们的视图对象命名为AUPluginView。当用户试图在一个DAW会话中使用两个插件时,这将导致问题,通常表现为其中一个插件打开了错误的GUI(这取决于用户在DAW中实例化插件的顺序)。在命名GUI视图工厂、视图和内部对象时必须非常小心。苹果公司建议用设计者的姓名缩写或其他符号来装饰对象的名称。为了克服这个问题,VSTGUI4架构师使用了一个在运行时(插件实例化时)实现的对象名称装饰方案,并基于当前的日期和时间。这解决了GUI对象命名的问题,但没有解决Cocoa视图工厂或其创建的视图对象的命名问题。如果你使用ASPiK,当你用CMake生成编译器项目时,会为你创建一个GUID(见第2章),并用于(除其他外)装饰Cocoa视图工厂和对象名称,通过设计保证你不会有任何命名空间问题。如果你从头开始设计你的插件,那么你需要自己解决命名空间的问题,如果使用其他第三方框架,你应该查阅文档以了解他们如何规避这个问题。

4.7.2 AU的事件监听系统

AU规范不遗余力地将GUI与插件参数分离和隔离,使系统以线程安全的方式运行。在AU参数的深处有一个原子变量访问,确保数据永远不会处于半生不熟的状态(见第二章)。然而,有一个完全独立的机制用于将参数值移入和移出GUI,称为AU事件监听器系统。实现AU事件监听器系统所需的所有代码都在GUI对象的内部。当然,这将取决于你的插件框架:实现的细节都会不同。在ASPiK中,我们用C++做所有这些工作,因为我们使用的是VSTGUI4。其他框架或从头开始建立的插件可能会用Objective-C实现事件监听器系统。

然而,基本的想法在所有的实现中是一致的。它涉及两个方向的信息流。

当用户调整GUI控件时,我们在参数设置操作中使用AU事件监听器将控件变化信息传送到AU参数中。
如果用户加载一个预置,自动运行,或打开一个保存的DAW会话,AU插件处于一个特定的状态,GUI需要更新适当的信息,这是通过一个事件调度操作完成的;在自动运行的情况下,GUI通过一系列的事件调度调用被动画化(控件自己移动)。
实现AU事件监听器系统有四个组成部分。为了实现它,GUI对象必须保持两个成员变量:一个是对GUI所连接的AU插件的引用;另一个是事件监听器的引用,在参数设置操作中使用。这有点意思:为了隔离这两个组件,其中一个(GUI)必须持有另一个的引用。GUI对象可能会声明这两个变量,如下所示。

AudioUnit          m_AU;
AUEventListenerRef AUEventListener;

4.7.2.1 事件列表调度回调函数

当异步参数变化被发送到 GUI 时,例如在预设加载后或自动化运行时,需要一个老式的 C 型回调函数。这个函数位于GUI对象的代码之外。你可以随心所欲地命名它,但它必须有正确的参数设置才能使用。在ASPiK中,我们把它命名为EventListenerDispatcher,它的定义是这样的。

void EventListenerDispatcher(void *inRefCon, void *inObject,
                                   const AudioUnitEvent *inEvent,
                                   UInt64 inHostTime, Float32 inValue)

如果你习惯于使用回调函数,那么void参数应该是熟悉的。当GUI对象设置AU事件监听器系统时,它传入一个指向自身的指针(this指针)。当回调函数被调用时,这个指针被传递到回调中,在inRefCon变量中被隐藏为一个void。然后,在函数中我们可以解开指针并使用它--例如,为了响应预设的加载或自动化而移动一个控件的位置。被传递的对象的确切类型取决于框架和GUI对象的设计。void* inObject指向与事件相关的东西,它可能是一个控制指针,或者与我们之前使用的这个指针一样。在任何情况下,AudioUnitEvent的输入值被用来解码与GUI控件相连的参数的控制ID,inValue表示未规范化的、实际的AU参数值,需要反映在GUI控件中。为了实现回调函数,你可以写出如下内容,其中GUI的类名是PluginGUI,它包含一个名为dispatchAUControlChange的成员函数,用来根据参数的实际值移动控件的位置;当然,这个函数的细节取决于GUI对象的实现。PluginGUI对象的这个指针在创建AU事件监听器时被传入AU事件监听器(见第4.8节)。

void EventListenerDispatcher(void *inRefCon, void *inObject,
                                   const AudioUnitEvent *inEvent,
                                   UInt64 inHostTime, Float32 inValue)
{
        // --- un-cloak the void* to reveal the PluginGUI object
        PluginGUI *pEditor = (PluginGUI*)inRefCon;
        if(!pEditor) return;
        // --- get the control ID from the event’s parameter
        UInt32 controlID = inEvent->mArgument.mParameter.mParameterID;
        // --- use the control ID and actual value to move the GUI control
        pEditor->dispatchAUControlChange(controlID, inValue);
}

4.7.2.2 创建AU事件监听器

在初始化过程中,GUI对象必须创建主事件监听器组件,然后它为连接到GUI控件的每一个参数添加一个事件。你使用AUEventListenerCreate函数来设置监听器。参数包括事件调度器回调函数的名称,一个特定对象的指针,作为inRefCon值传入该回调函数,一些在CoreFoundation中关于消息循环的信息,一个指向GUI对象上面声明的AUEventListenerRef的指针,最后还有一些关于事件系统的粒度的信息。在你想要/需要这个系统有多精确方面,你有一点余地。下面是这个创建函数的一个例子,重要的参数用粗体表示。你可以看到回调函数的名字和这个指针是前两个参数,而AUEventListenerRef的成员变量在最后。

const Float32 AUEVENT_GRANULARITY = 0.0005;
const Float32 AUEVENT_INTERVAL = 0.0005;
enum {kBeginEdit, kEndEdit, kValueChanged, kNumEventTypes};

__Verify_noErr(AUEventListenerCreate(EventListenerDispatcher,
                                                  this,
                                                  CFRunLoopGetCurrent(),
                                                  kCFRunLoopDefaultMode,
                                                  AUEVENT_GRANULARITY,
                                                  AUEVENT_INTERVAL,
                                                  &AUEventListener));

创建了AU事件监听器后,你需要添加你的GUI希望得到通知的事件。有三种主要的事件类型需要你支持。

开始参数变化:用户点击一个控件开始移动它。
参数值变化:用户正在改变控件的值。
结束参数变更:用户释放控件,并完成了变更。
开始和结束参数变化事件对于确保自动化正常工作是很重要的,因为它需要知道控件何时被调整(想想你最喜欢的DAW的自动化记录系统--你触摸控件,移动它,然后释放它)。当用户移动控件时,或当自动化系统向控件回放信息时,或当预设被加载时,值变化事件将被触发。这意味着我们需要为每个插件参数设置三种特定的事件类型。添加事件是相当直接的;这使用AudioUnitParameter结构,它包含AU事件参考的变量,参数的控制ID,范围和元素。范围总是全局的,它只有一个索引=0的元素。 每个事件类型都是用AUEventListenerAddEventType函数添加的,它还需要一个特定对象的指针来传递给调度器回调。我们再次使用这个指针。

假设我们已经为一个简单的音量控制旋钮声明了一个AU参数。这个参数的控制ID已经被预定义为VOLUME_CTRL。要为这个控制设置三种事件类型,你可以写。

// --- to hold the event
AudioUnitEvent auEvent;
// --- parameter information
AudioUnitParameter parameter = {m_AU, VOLUME_CTRL,
                                      kAudioUnitScope_Global,
                                      0}; // 0 = global element
// --- attach parameter
auEvent.mArgument.mParameter = parameter;
// --- add the begin change gesture (aka “touch”)
auEvent.mEventType = kAudioUnitEvent_BeginParameterChangeGesture;
__Verify_noErr(AUEventListenerAddEventType(AUEventListener,
                                                 this,
                                                 &auEvent));
// --- add the end change gesture (aka “release”)
auEvent.mEventType = kAudioUnitEvent_EndParameterChangeGesture;
__Verify_noErr(AUEventListenerAddEventType(…);
// --- add the value change event
auEvent.mEventType = kAudioUnitEvent_ParameterValueChange;
__Verify_noErr(AUEventListenerAddEventType(…);

4.7.2.3 销毁事件监听器

当GUI对象被销毁时,它必须首先销毁它所创建的事件监听器。这是通过调用AUListenerDispose来完成的,它需要我们在创建时使用的保存的AUEventListenerRef。

__Verify_noErr(AUListenerDispose(AUEventListener));

4.8 销毁/终止

AU插件很容易被销毁。所有的底层清理工作都在基类中为你完成,因为你的AU对象的析构器被调用。你的AU插件对象只需要释放任何动态声明的资源,你可以在对象的析构器中完成。AU插件对象不是一个类工厂,一旦该对象被销毁,它就永远消失了。你不需要担心状态或其他类工厂的细节。

4.9 检索AU主机信息

AU为你的插件提供了一个查询主机信息的机制。这在三个插件的API中是独一无二的,因为主机信息是在每个缓冲区处理周期中传递给插件的。主机信息包括关于当前音频播放的数据,如音频文件的绝对采样位置(当前缓冲区处理周期中输入音频缓冲区的第一个采样的索引),DAW会话的每分钟节拍(BPM)和时间符号设置,以及关于音频传输的信息(是否正在播放或停止,或是否正在发生循环)。并非所有的AU主机都支持每一个可能的主机信息,其中一些是出了名的不正确:例如,在用户第一次启动音频流后,Apple Logic会报告传输器处于播放模式,无论音频是否真的在播放;它有一个 "粘性 "布尔标志。所以,你需要小心解释其中的一些信息。然而,绝对的采样位置和时间,以及主机的BPM和时间特征信息,在所有的AU客户端上都是非常明确的。你的插件可以在任何时候调用AU主机上的三个函数来获取这些信息。对于ASPiK来说,我们在每个缓冲区处理周期开始时进行这些调用,这样插件的核心对象在其内部处理开始时就有了更新的信息。这三个函数如下。

CallHostBeatAndTempo:获取当前BPM和节拍值
CallHostMusicalTimeLocation:获得时间特征、到下一拍的采样数等。
CallHostTransportState:获取传输状态(播放或停止,在Apple Logic中是出了名的错误)和循环信息
每个函数的参数都是以传递指针的方式实现的,所以你必须先声明局部变量来保存信息。例如,要获得BPM和当前节拍值,你可以使用CallHostBeatAndTempo。

// --- return variables
Float64 outCurrentBeat = 0.0;
Float64 outCurrentTempo = 0.0;
OSStatus status = CallHostBeatAndTempo(&outCurrentBeat,
&outCurrentTempo);
if(status == noErr)
{
        hostInfo->dBPM = outCurrentTempo;
        hostInfo->dCurrentBeat = outCurrentBeat;
}


你可以查看Apple AU的文档,了解其他函数的细节。如果你使用的是ASPiK,所有来自这三个函数调用的信息都会被你的插件核心所利用,并在每个缓冲区处理周期中被更新。否则,请咨询你的插件框架,看看有哪些信息可供你使用。

4.10 验证你的插件
一旦你的插件编译完成,你需要在试图将其加载到DAW中进行测试之前对其进行验证。对于Apple Logic来说,验证是至关重要的--如果你试图加载你的插件,但它崩溃了,或者没有通过Logic的AU验证,那么它就会被列入 "黑名单",DAW就不会再加载它。MacOS包含一个叫auval的AU验证程序,你可以从终端调用。auval和Apple Logic都使用我们在第4.3节中讨论的制造商和子类型的两个四字代码,在你的插件描述中选择组件进行测试。如果Apple Logic将你的插件列入黑名单(因为你忽略了先运行auval),那么你将需要改变这四个字符代码中的至少一个,通常是子类型,以欺骗Apple Logic,使其认为这是一个不同的插件。主要的规则是:花时间运行auval--最终它将为你节省痛苦和头痛。要运行auval,你需要知道你的插件所使用的AU类型代码,这取决于它所衍生的基类。这与你在插件描述中设置的类型代码相同:aufx、aumf或aumu(见4.3节)。验证一个aufx插件的语法是。

auval -v aufx

是你的四个字符的插件代码,是你的四个字符的公司名称。对于第4.3节中的插件描述,你应该输入。

auval -v aufx FLNG PLCO

验证将成功或失败;它将显示一个非常详细的打印结果,包括所有运行的测试以及哪些测试通过和失败。它还会给你一些关于一些失败的提示,但如果插件验证失败,在许多情况下,你将需要回去做一些调查工作。问题是,如果你的构造函数是畸形的,你可能永远无法通过实例化,这将使调试变得困难--当然,Apple Logic会预先验证你的插件。你可以在www.willpirkle.com/forum/,获得关于弄清auval问题的帮助和更多信息。浏览以前的用户问题或发表你自己的问题--论坛上欢迎所有问题。没有什么愚蠢的问题!

4.11 使用ASPiK来创建AU插件

在设计这些插件时,没有什么可以替代工作的代码;但是,代码所需要的书本空间是令人望而却步的。即使你不想使用ASPiK PluginKernel进行开发,你仍然可以在第8章中列出的项目中使用这些代码。

4.12 Bibliography

Apple Computers, Inc. 2018. The Audio Unit Programming Guide, https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioUnitProgrammingGuide/Introduction/Introduction.html, Accessed August 1, 2018.

Pirkle, W. 2015. Designing Software Synthesizers in C++, Chap. 2. New York: Focal Press.