这里主要是参考VST3.6.5的文档…主要是做一下翻译吧。
主要是英文水平比较烂…看一句忘两句的。惨兮兮的,还是中文好。
Basic Conception —— 基本概念
首先,先看看文档给出的基本概念。
VST 3 音频效果器 主要由两个部分组成:一个处理器部分 和 一个编辑控制器部分。
相应的接口:
处理器:Steinberg::Vst::IAudioProcessor + Steinberg::Vst::IComponent
控制器:Steinberg::Vst::IEditController
VST 3 的设计方式建议通过实现两个组件的方式完全分离 处理器 和 编辑控制器。对这两部分的划分需要一些额外的努力来实现。
但是这种分离使宿主能够在不同的上下文环境中运行每个组件。它甚至可以在不同计算机上运行它们。
另一个好处是,当涉及自动化时,参数更改可以被分离。
在处理这些变化时,需要以一种准确的方式传输,而GUI部分可以用更低的频率更新,并且可以通过延迟补偿或其他处理偏移量的结果来改变。
支持这种分离设置的插件需要在 处理器组件的类信息(Steinberg::PClassInfo2::classFlags) 中设置 Steinberg::Vst::kDistributable 标志。
当然,也并不是所有的插件都可以支持这个功能,例如,当它深深地依赖于不能轻易移动到另一台计算机的资源时,那就是不支持的。
所以,当这个标志没有被设置的时候,宿主不能尝试以任何方式分离组件。
有一种不推荐的方法:
可以在一个组件类中同时实现处理部分和控制器部分,当然,这是不推荐的!
在创建 Steinberg::Vst::IAudioProcessor后,宿主尝试查询Steinberg::Vst::IEditController接口,并在成功时将它作为控制器使用。
请注意:
宿主不需要实例化 VST插件 的 控制器 部分来处理它。VST插件应该准备好在 控制器 没有被实例化时进行处理。
Initialize —— 初始化
Steinberg::Vst::IComponent(处理器) 和 Steinberg::Vst::IEditController(编辑控制器) 都是从类 Steinberg::IPluginBase 派生出来的。
这个接口的目的是初始化组件并在它被销毁之前终止它的操作。
传递给 Steinberg::IPluginBase::initialize 的参数是 Steinberg::Vst::IHostApplication.
在初始化调用之前,宿主不应该调用其他函数,除了需要被调用的 Steinberg::Vst::IComponent::setIoMode 和 可以在初始化之前调用的 Steinberg::Vst::IComponent::getControllerClassId.
Creation and Initialize from Host point of view —— 从宿主角度创建和初始化
这是一个宿主实现的示例,它创建一个给定classID的插件的组件,及其关联的控制器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
//------------------------------------------------------------------------ ... Vst::IComponent* processorComponent; Vst::IEditController* editController; IPluginFactory* factory; ... // factory already initialized (after the library is loaded, see validator for example) ... // create its component part tresult result = factory->createInstance (classID, Vst::IComponent::iid, (void**)&processorComponent); if (processorComponent && (result == kResultOk)) { // initialize the component with our host context (note: initialize called just after creatInstance) res = (processorComponent->initialize (gStandardPluginContext) == kResultOk); // try to create the controller part from the component // for Plug-ins which did not succeed to separate component from controller :-( if (processorComponent->queryInterface (Vst::IEditController::iid, (void**)&editController) != kResultTrue) { FUID controllerCID; // ask for the associated controller class ID (could be called before processorComponent->initialize ()) if (processorComponent->getControllerClassId (controllerCID) == kResultTrue && controllerCID.isValid ()) { // create its controller part created from the factory result = factory->createInstance (controllerCID, Vst::IEditController::iid, (void**)&editController); if (editController && (result == kResultOk)) { // initialize the component with our context res = (editController->initialize (gStandardPluginContext) == kResultOk); // now processorComponent and editController are initialized... :-) } } } } //------------------------------------------------------------------------ |
Extensions —— 扩展
实现这些基本接口的组件的功能可以通过许多可选接口进行扩展,只有在需要扩展的时候才需要实现。
处理器的扩展:
- Steinberg::Vst::IConnectionPoint
- Steinberg::Vst::IUnitData
- Steinberg::Vst::IProgramListData
编辑控制器的扩展:
- Steinberg::Vst::IConnectionPoint
- Steinberg::Vst::IMidiMapping
- Steinberg::Vst::IUnitInfo
Persistence —— 持久性
宿主在项目文件和预设文件中存储和恢复 处理器 和 编辑控制器的完整状态。
- Steinberg::Vst::IComponent::getState + Steinberg::Vst::IComponent::setState 存储和恢复DSP模型
- Steinberg::Vst::IEditController::getState + Steinberg::Vst::IEditController::setState 存储和恢复与处理器无关的GUI设置(如滚动位置等)
- 恢复:当状态恢复时,宿主将处理器状态传递给处理器和控制器 (Steinberg::Vst::IEditController::setComponentState). 宿主必须始终将该状态传递给处理器。控制器必须将参数同步到这个状态(但不能执行任何IComIComponentHandler回调)。恢复状态后,宿主将重新扫描参数(以询问控制器的方式)来更新其实体表示。
The Processing Part —— 处理部分
处理部分由两个相关的接口组成。
分离的原因是不仅要使用音频插件的基本接口 Vst::IComponent, 还要使用其他的媒体(例如:以后的视频处理)
因此 Vst::IAudioProcessor 接口表示处理组件的音频特定部分。
让我们仔细看看这些概念。
Steinberg::Vst::IComponent
关联编辑控制器:为了使宿主能够创建相应的编辑控制器,处理组件必须提供匹配的类别ID。宿主使用模块的类工厂创建控制器组件。(Steinberg::Vst::IComponent::getControllerClassId)
Steinberg::Vst::BusInfo —— 总线可以理解为一个 “数据通道集合” 。它描述了插件的数据输入和数据输出。一个VST组件可以定义任意数量的总线,但是这个数字绝对不能改变。宿主通过激活和关闭总线来进行总线的动态使用。但是组件必须定义支持的总线最大数量,并且定义哪些是默认激活的。可以处理多条总线的宿主允许用户激活最初不活动的总线。
路由信息:
当插件支持多个 I/O 总线时,宿主可能知道总线之间的关系。
宿主对 事件输入通道 与 音频输出总线 的关系尤其感兴趣。(为了将MIDI音轨与音频通道相关联)
Steinberg::Vst::IAudioProcessor
设置:处理器必须先配置,然后才能开始处理。仅当处理器出于非活动状态时才允许配置(Steinberg::Vst::IComponent::setActive).
- 流程设置:通知处理器在处理激活时不能改变的参数。(Steinberg::Vst::ProcessSetup)
- 动态扬声器排列: 宿主可以尝试改变音频总线的通道数量。默认情况下,扬声器排列由插件定义。为了将插件调整到使用不同扬声器布置的环境,宿主可以尝试改变它。(Steinberg::Vst::IAudioProcessor::setBusArrangements)
处理器配置完成后,必须激活。调用激活表示所有配置已完成。
除此之外,处理器还处于“处理状态”,在宿主实际开始执行处理调用之前,必须通过调用 IAudioProcessor::setProcessing(true) 来发信号。
当宿主不再执行处理时,必须在最后一次处理调用之后调用 IAudioProcessor::setProcessing(false)
具体看以下工作流程图:
Note: IAudioProcessor::setProcessing()
- may be call from realtiem Processing Thread (must be lock-free)!
- plugin has to reset its inner state (for example to clean its delay buffers in order to have a defined state when the processing starts again).
图略长。
最后有一个注意事项,翻译如下:
- 调用可能来自实时的处理现场(必须是无锁的)
- 插件必须重置其内部状态(例如:为了再次进行处理时具有定义的状态,清除延迟缓冲器)
处理:
Steinberg::Vst::IAudioProcessor::process 是实现实际处理的方法。所需处理的任何数据将作为参数 Steinberg::Vst::ProcessData 传递给它。
这是必要的,因为处理通常在单独的线程中执行,这是避免线程同步问题的简单方法。
块大小:处理是以块进行的,在 Steinberg::Vst::IAudioProcessor::setupProcessing 中设置一个块中要处理的最大样本数。处理块中实际样本数在过程调用中传输,可能与调用不同,但是它必须是介于 1 和 maxSamplesPerBlock(每块最大采样数) 之间的值。
音频缓冲器:对于插件定义的任何音频总线,宿主必须提供缓冲数据 – 即使是不活动的总线。总线是通过索引来寻址的,所以没有缓冲器总线就会混淆这些索引。实际上数据缓冲区可以是NULL.(参见: Steinberg::Vst::AudioBusBuffers)
- 请注意,channelBuffers32 或者 channelBuffers64 缓冲区指针对于输入和输出可能是相同或不同的:在处理函数中必须考虑这一点(例如如果输入和输出缓冲区是相同的,则在处理之前不重置输出)。对于多输入或多输出(仪器插件的情况),所有输出(或输入)可以共享相同的缓冲区!
- 重要: 为了从宿主到插件刷新参数,宿主可以调用Steinberg::Vst::IAudioProcessor::process ,参数不带缓冲区(Steinberg::Vst::AudioBusBuffers 的 numInputs , numOutputs 和 numSamples 用零值传递).只有当宿主需要发送参数更改并且没有处理被调用时,flush才会发生。
参数与自动化:在处理器中通过调用 Steinberg::Vst::IParameterChanges 和 Steinberg::Vst::IParamValueQueue 这两个接口来传输任何参数更改。由于GUI交互的结果与自动化的传输方式完全相同,因此简单的参数更改也会发生。
上下文状态:对于每个处理块,宿主应该提供关于其状态的信息。
事件:Steinberg::Vst::IEventList
The Editing Part —— 编辑部分
编辑控制器负责插件的GUI方面,它的标准接口是Steinberg::Vst::IEditController.
宿主必须为名为 Steinberg::Vst::IComponentHandler 的控制编辑器提供一个回调接口。处理程序对于宿主和处理器的通信至关重要。
视图:控制器可以选择定义一个编辑器视图。 Steinberg::Vst::IEditController::createView 方法允许宿主通过一个ID字符串来指定视图的类型。目前唯一定义的类型是“编辑器”(Steinberg::Vst::ViewType::kEditor),但是未来版本可能会有变化(例如“setup”)
参数:控制器负责参数的管理。对插件GUI中由用户交互引起的任何参数更改都必须正确地报告给 Steinberg::Vst::IComponentHandler. 宿主负责将更改传输到处理器。为了使自动化的记录相应的工作,有必要按预期的顺序调用 beginEdit, performEdit 和 endEdit.
使用新接口IComponentHandler2(自VST 3.1以来),插件(来自UI)可以在编写自动化时通过在宿主上使用相同的时间戳对参数进行分组,包括一组 beginEdit/performEdit/endEdit函数(参见IComponentHandler) startGroupEdit 和 finishGroupEdit。
插件结构:如果插件由离散的功能部件组成,编辑控制器应该通过实现 Steinberg::Vst::IUnitInfo 接口来发布这个结构和属于每个部分的参数。
Communication between the components —— 组件之间的通信
两个VST3组件(处理器和控制器)需要一种通信方式。它是由宿主来处理这个任务。
Standard Communication —— 标准通信
所有标准数据都是使用上面列出的基本接口在处理器和控制器之间传输的(如果参数更改)。
在创建处理器和控制器之后,宿主将从处理器状态设置控制器组件状态。(这已经改变了以前的SDK,在那里它被认为是创建之后控制器和处理器是同步的)
当宿主设置一个新的处理器状态时(Steinberg::Vst::IComponent::setState),这个状态总是被传送给控制器(Steinberg::Vst::IEditController::setComponentState) 。然后控制器必须同步到这个状态并调整它的参数。
当控制器向宿主发送参数更改时,宿主通过将新值作为 Steinberg::Vst::IParameterChanges 传递给处理器来进行同步。
处理器也可以向宿主传出参数的更改(Steinberg::Vst::ProcessData::outputParameterChanges). 这些更改通过Steinberg::Vst::IEditController::setParamNormalized 调用传递给编辑控制器。
Private Communication —— 专用通信
对宿主保密的数据可以通过消息的方式传输。通信接口是:
- Steinberg::Vst::IConnectionPoint:宿主在处理器和控制器之间建立连接
- Steinberg::Vst::IMessage:表示要发送给对方的消息
- Steinberg::Vst::IAttributeList:属于消息的属性列表
请注意:处理器到控制器的消息在进程调用期间不得发送!因为这个发送的速度可能不够快,导致打破了实时处理。这些任务应该在一个单独的计时器线程中处理。
Initialization of communication from Host point of view —— 宿主初始化通信
这是宿主实现组价和控制器部件连接和同步的一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// ... // the component and the controller parts are previously be created and initialized (see above) // ... if (editController) { // set the host handler // the host set its handler to the controller editController->setComponentHandler (myHostComponentHandler); // connect the 2 components Vst::IConnectionPoint* iConnectionPointComponent = nullPtr; Vst::IConnectionPoint* iConnectionPointController = nullPtr; processorComponent->queryInterface (Vst::IConnectionPoint::iid, (void**)&iConnectionPointComponent); editController->queryInterface (Vst::IConnectionPoint::iid, (void**)&iConnectionPointController); if (iConnectionPointComponent && iConnectionPointController) { iConnectionPointComponent->connect (iConnectionPointController); iConnectionPointController->connect (iConnectionPointComponent); } // synchronize controller to component by using setComponentState MemoryStream stream; // defined in "public.sdk/source/common/memorystream.h" stream.setByteOrder (kLittleEndian); if (processorComponent->getState (&stream) == kResultTrue) { stream.rewind (); editController->setComponentState (&stream); } // now processorComponent and editController parts are connected and synchronized...:-) } |
请注意,您不能依赖处理器组件和编辑控制器之间直接连接的实现细节!