简介:VST SDK 3.6.14 Build-24是Steinberg于2019年11月发布的VST3插件开发套件,面向音频软件开发者,用于在Windows、macOS与Linux上构建与宿主DAW兼容的音频效果器、合成器等插件。压缩包为zip格式,大小约86.17MB;上游未提供内部文件总数与类型明细。已有202人浏览/下载。该SDK的核心内容包括:定义插件与宿主交互的C++类接口(如IAudioProcessor、IEditController)、可直接改写的示例工程、完整的API参考文档、跨平台构建脚本,以及便于脱离DAW验证插件功能的测试宿主VSTPluginExample。VST3特性方面,涵盖多通道处理、64位精度支持、参数自动化和延迟补偿,同时保留VST2兼容途径。对希望进入音乐软件开发领域的个人或团队而言,这份官方工具包是理解插件架构并着手开发均衡器、压缩器乃至复杂合成器的实用起点。 做音频插件开发的,应该都见过这种命名格式的压缩包:vst-sdk_3.6.14_build-24_2019-11-29.zip。这是一份VST SDK的归档版本,来自Steinberg官方,版本号3.6.14,构建号24,打包日期2019年11月29日。可能很多人第一反应是“这都哪年的老古董了”,但恰恰是这类“老古董”,至今还跑在一堆商业插件和硬件厂商的工具链里。如果你正在维护老项目、接手别人的插件源码,或者单纯想搞明白VST2和VST3在底层到底有什么不一样,这个版本的SDK是个非常合适的观察样本。
这篇文章我会从实际使用的角度,拆一遍这个SDK的目录结构、工程搭建、效果器实现和调试分发,不做教科书式的API罗列,只讲我在项目里真正用到的部分,以及那些官方文档不会告诉你的坑。
1. 3.6.14到底是个什么版本:VST2时代的最后体面
先说版本背景。SDK版本号3.x延续了VST2.4规范时期的命名习惯,3.6.14不是VST3的版本号,而是整套SDK发行包的版本号,里面同时包含VST2.4和VST3的接口定义。这一点很多人会搞混——看到“3.6.14”以为是VST3的迭代版本,其实VST3 SDK当时早就走独立的2.x、3.x路线了,而VST2.4的接口定义在很长一段时间内都保持稳定,所以SDK的主版本号更多反映的是打包节奏,不是接口变更幅度。
Steinberg在2018年宣布停止向新用户授权VST2 SDK,同时允许已授权用户继续使用现有版本。这意味着3.6.14(以及之后少数几个维护构建)实际上是绝大多数商业团队拿到的最后一批合法VST2版本。说得直白一点,2019年之后发布的新插件如果还带VST2格式,底层用得最多、最稳妥的SDK来源之一就是这个3.6.14。我见过不少工作室的老项目,直到今天编译配置里还锁着这个版本,不是不想升级,而是升级VST3涉及音色包格式、UI框架、参数通信等多层改动,风险太大,不如冻结SDK版本。
这个构建号也不是随便写的。build-24对应的是2019-11-29当天的代码状态,修复了之前几个版本里AudioEffect的析构顺序问题和部分平台下VST2.4的MIDI事件解析异常。如果你是从更早的3.6.13升上来的,升级理由很充分;如果你是从3.5.x直接跳到3.6.14,那要注意接口上有些细微变化,后面我会提到。
2. 解压之后先别写代码:目录结构和工程骨架
把压缩包解开,你会看到VST3 SDK这个根目录。里面最重要的三个部分不是随便排列的:
pluginterfaces/:核心接口定义,VST2的aeffect.h、aeffectx.h在这里,VST3的vst/ivstcomponent.h、vst/ivstaudioeffect.h等也在这里。这一层只管接口描述,不涉及具体实现。public.sdk/:提供给开发者使用的实现骨架,包括source/vst2.x/下的audioeffect.cpp、audioeffectx.cpp,以及VST3那边的source/vst/。做VST2插件,主要工作就是继承这里的AudioEffect类。base/(或vstgui4/、cmake/等目录):基础工具库和构建脚本。
先讲VST2。
public.sdk/source/vst2.x/目录下有audioeffect.cpp这类封装好的基类。这个基类把VST2.4的C风格接口AEffect封装成C++虚函数,你在子类里重写processReplacing、setParameter、getParameter这些虚函数,SDK负责处理和宿主之间的C接口转换。这样做的好处是插件代码可以写得非常直观,几乎感知不到C接口的存在。
工程骨架我是这样搭的,用的CMake:
cmake_minimum_required(VERSION 3.12) project(MyGain VERSION 1.0.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(VST2_SDK_ROOT "/path/to/vst-sdk_3.6.14") set(VST3_SDK_ROOT "${VST2_SDK_ROOT}/VST3 SDK") add_library(mygain SHARED src/MyGainProcessor.cpp src/MyGainProcessor.h ${VST3_SDK_ROOT}/public.sdk/source/vst2.x/audioeffect.cpp ${VST3_SDK_ROOT}/public.sdk/source/vst2.x/audioeffectx.cpp ) target_include_directories(mygain PRIVATE ${VST3_SDK_ROOT} ${VST3_SDK_ROOT}/public.sdk/source/vst2.x ) # Windows导出符号,关键 target_compile_definitions(mygain PRIVATE BUILDING_VST2_DLL VST2SDK_EXPORT_SYMBOLS ) set_target_properties(mygain PROPERTIES PREFIX "" ) # macOS下需要修改导出符号 if(APPLE) set_source_files_properties( ${VST3_SDK_ROOT}/public.sdk/source/vst2.x/audioeffect.cpp PROPERTIES COMPILE_FLAGS "-fvisibility=hidden" ) target_link_options(mygain PRIVATE -Wl,-exported_symbol,_VSTPluginMain ) endif() # Windows下指定DLL导出 if(WIN32) target_link_options(mygain PRIVATE /EXPORT:VSTPluginMain) endif()这里有个细节值得注意:VSTPluginMain是宿主动态加载时的入口函数,不同平台下的导出方式完全不同。Windows用/EXPORT链接器选项直接导出,macOS用-exported_symbol限制符号可见性。很多人第一次编插件时发现Windows下能跑、macOS下加载不了,多半就是macOS的符号可见性没设置对。SDK默认源码里带了一些导出宏,但我在多个编译器版本下实测,最稳的还是自己配置导出选项。
还有一点,CMake里PREFIX ""很重要。Windows下如果不设置,生成的DLL会叫libmygain.dll,宿主扫描插件目录时看到lib开头虽然不一定会跳过,但很多插件管理工具会识别异常。去掉前缀,生成mygain.dll就正常了。
接着看VST3。
VST3的工程结构比VST2复杂不少。VST3插件不是一个简单的DLL导出入口就能跑起来的,它要求实现IPluginFactory、IComponent、IEditController等多个接口,SDK里提供了一个模板工厂public.sdk/source/main/pluginfactory.cpp(在较新版本里位置略有变化,但3.6.14里是这样的),你通过宏注册自己的Processor和Controller类。
我用的是SDK自带的工厂方案:
#include "public.sdk/source/main/pluginfactory.h" #include "mygainprocessor.h" #include "mygaincontroller.h" #define stringPluginName "MyGain" BEGIN_FACTORY_DEF(stringCompanyName, stringCompanyWeb, stringCompanyEmail) DEF_CLASS2(INLINE_UID_FROM_FUID(kMyGainProcessorUID), PClassInfo::kManyInstances, kVstAudioEffectClass, stringPluginName, Vst::kDistributable, Vst::PlugType::kFx, "1.0.0", kVstVersionString, MyGainProcessor::createInstance) DEF_CLASS2(INLINE_UID_FROM_FUID(kMyGainControllerUID), PClassInfo::kManyInstances, kVstComponentControllerClass, stringPluginName "Controller", Vst::kDistributable, Vst::PlugType::kFx, "1.0.0", kVstVersionString, MyGainController::createInstance) END_FACTORYkMyGainProcessorUID和kMyGainControllerUID是自定义的GUID,必须自己生成,不能照抄示例。我吃过大亏:以前图省事,复制官方示例的GUID后改了个名字,结果在Cubase里倒是能加载,但换到另一个宿主就识别成官方自带的插件了,参数列表全乱,折腾了一下午才发现是GUID冲突。写个脚本生成随机GUID,然后固定到代码里,这是最保险的做法。
工程结构上,VST3一般会拆成Processor和Controller两个类,前者干音频处理的活儿,后者管参数和UI。这样做的意义在于,宿主可以把Processor放在实时音频线程上跑,把Controller放在UI线程上跑,两者通过IEditController::setParamNormalized之类的接口传递参数。VST3相比VST2最大的架构变化就在这:VST2的参数读写和UI是全在一起的,宿主改参数和音频线程要互相锁,处理不好就是爆音甚至死锁。VST3把这两个环节拆开,等于在架构层面消灭了一整类并发问题。
3. 手写一个增益插件:从processReplacing到参数平滑
不管VST2还是VST3,写一个最简单的增益插件都是最快的入门路径。我用VST2来演示核心逻辑,因为VST2的代码量最精简,概念也直观。
先看Processor的核心:
class MyGain : public AudioEffectX { public: MyGain(audioMasterCallback audioMaster) : AudioEffectX(audioMaster, 1, 1) , m_gain(0.5f) , m_smoothGain(0.5f) { setNumInputs(2); // 立体声输入 setNumOutputs(2); // 立体声输出 setUniqueID('MyGn'); // 插件唯一ID,4字符 canProcessReplacing(); } ~MyGain() override {} void setParameter(Vst::Int32 index, float value) override { if (index == 0) m_gain = value; } float getParameter(Vst::Int32 index) override { return index == 0 ? m_gain : 0.0f; } void getParameterLabel(Vst::Int32 index, char* label) override { if (index == 0) vst_strncpy(label, "", kVstMaxParamStrLen); } void getParameterDisplay(Vst::Int32 index, char* text) override { if (index == 0) float2string(m_gain * 100.0f, text, kVstMaxParamStrLen); } void getParameterName(Vst::Int32 index, char* text) override { if (index == 0) vst_strncpy(text, "Gain", kVstMaxParamStrLen); } void processReplacing(float** inputs, float** outputs, Vst::Int32 sampleFrames) override { float* in1 = inputs[0]; float* in2 = inputs[1]; float* out1 = outputs[0]; float* out2 = outputs[1]; const float gain = m_gain; const float smoothFactor = 0.001f; // 平滑系数 for (Vst::Int32 i = 0; i < sampleFrames; ++i) { // 一阶低通平滑,避免参数突变产生爆音 m_smoothGain += (gain - m_smoothGain) * smoothFactor; out1[i] = in1[i] * m_smoothGain; out2[i] = in2[i] * m_smoothGain; } } private: float m_gain; float m_smoothGain; // 平滑后的增益值 };这个代码里最值得讲的其实是smoothFactor那一段。很多新手写增益插件就是直接把m_gain乘上去,结果在DAW里快速拖动旋钮时,声音会出现“滋滋”的爆音。原因是:GAIN参数每次变化是突变的(step函数),突变会让波形产生非连续点,在频域上表现为宽带噪声。平滑的目标是把步进信号变成指数逼近信号,消除非连续点。
smoothFactor的选择有个经验值参考:数值越大,跟随越快但平滑效果越差;数值越小,平滑越彻底但拖泥带水。对44.1kHz采样率,0.001级别大概有20-30毫秒的过渡时间,听感上是“跟手但不炸”,这个值可以直接抄作业。如果你做的是压缩器这类的动态处理器,平滑可以再快一点,0.01左右;如果是失真的drive参数,平滑要更慢,不然会有“颗粒感”。
processReplacing这个名字也有历史原因。VST2.4之前是process,直接原地处理输入输出缓冲,后来为了支持非破坏性处理,才加了Replacing版本——把处理结果写到输出缓冲而不是覆盖输入。现在宿主基本只调processReplacing,process已经名存实亡。SDK的canProcessReplacing()就是告诉宿主:“我有Replacing能力,你放心吧。”
VST2里还应该实现getChunk和setChunk,用于在DAW工程保存时持久化插件状态。如果你不实现,宿主会用默认的参数序列化,这在参数少时没问题,但一旦你有多个参数、或者有非参数状态(比如预设名、波形表),就必须自己实现。3.6.14版本的SDK里,官方示例AGain已经提供了getChunk/setChunk的参考实现,直接照着改成自定义结构就行。
示例代码如下:
Vst::Int32 MyGain::getChunk(void** data, bool isPreset) { // 把m_gain写进一个float数组 static float chunk[1]; chunk[0] = m_gain; *data = chunk; return sizeof(chunk); } Vst::Int32 MyGain::setChunk(void* data, Vst::Int32 byteSize, bool isPreset) { if (byteSize >= sizeof(float)) { float* chunk = static_cast<float*>(data); m_gain = chunk[0]; m_smoothGain = m_gain; // 加载预设时直接跳到目标值 } return byteSize; }注意m_smoothGain = m_gain这行:加载预设时应该跳过平滑过程,直接到达目标值。如果不这么做,加载工程后插件会从上一个平滑位置缓动到新预设值,造成几毫秒的非预期瞬态。这个坑非常隐蔽,我是在给一个鼓组插件写预设管理器时踩到的,音频轨都开始播放了,VU表还在慢慢爬,一看就是平滑没被重置。
VST3的Processor部分与之对应的是process方法,签名是tresult PLUGIN_API process(ProcessData& data)。ProcessData里包含了输入输出缓冲、参数变化队列(IParameterChanges)、事件列表(IEventList)、过程精度(sampleSize)等,结构比VST2的裸指针+采样数要丰富得多。VST3的参数变化是面向块(block)的,ProcessData::inputParameterChanges里每个队列存了该块内的所有参数变化点,你可以精确到某个采样位置去处理参数更新,这个能力VST2没有——VST2只能在一整块处理开始前更新参数。
4. 同一份代码编译出VST2和VST3:CMake双格式方案
如果你只需要VST2,直接编码太方便了。但现实是,2020年之后越来越多宿主开始逐步弱化VST2支持,一些DAW甚至在新版本里把VST2功能标记为“遗留”。如果你想要插件同时支持VST2和VST3,用一套共享算法代码、分别编译成两种格式,是当前最主流的做法。
我常用的CMake方案长这样:
# 公共算法库,不依赖SDK add_library(mygain_core STATIC src/MyGainCore.cpp src/MyGainCore.h ) target_include_directories(mygain_core PUBLIC src) # VST2插件 add_library(mygain_vst2 SHARED src/vst2/MyGainVST2.cpp ${VST3_SDK_ROOT}/public.sdk/source/vst2.x/audioeffect.cpp ${VST3_SDK_ROOT}/public.sdk/source/vst2.x/audioeffectx.cpp ) target_link_libraries(mygain_vst2 PRIVATE mygain_core) target_compile_definitions(mygain_vst2 PRIVATE BUILDING_VST2_DLL) # VST3插件 add_library(mygain_vst3 SHARED src/vst3/MyGainProcessor.cpp src/vst3/MyGainController.cpp src/vst3/MyGainFactory.cpp ${VST3_SDK_ROOT}/public.sdk/source/vst/vstaudioeffect.cpp ${VST3_SDK_ROOT}/public.sdk/source/vst/vstcomponent.cpp ${VST3_SDK_ROOT}/public.sdk/source/vst/vstcomponentbase.cpp ${VST3_SDK_ROOT}/public.sdk/source/vst/vsteditcontroller.cpp ${VST3_SDK_ROOT}/public.sdk/source/main/pluginfactory.cpp ${VST3_SDK_ROOT}/public.sdk/source/main/dllmain.cpp ) target_link_libraries(mygain_vst3 PRIVATE mygain_core)核心算法放在独立的mygain_core静态库里,VST2和VST3各自只写一层薄薄的适配代码。这样做最大的好处是:算法逻辑只维护一份,接口层各写各的,改算法不会牵动接口,编译哪个格式都不会意外破坏另一个。
有几个细节值得专门说:
第一,VST3必须链接dllmain.cpp。Windows下VST3插件的DllMain由这个文件提供,它负责调用InitDll和ExitDll,同时管理模块引用计数。如果你忘了链接它,生成的插件在宿主里能注册但加载时会无声无息地失败——只有通过调试器才能看到根本没进到工厂函数。
第二,VST3的插件文件后缀是.vst3而不是.dll。Windows上要让CMake生成.vst3后缀的动态库,需要这样设置:
set_target_properties(mygain_vst3 PROPERTIES PREFIX "" SUFFIX ".vst3" )不设置SUFFIX的话,生成的是.dll,大多数宿主扫描不到。macOS上还要把BUNDLE_EXTENSION设为vst3,并配置MACOSX_BUNDLE_BUNDLE_NAME等属性。3.6.14这个SDK的CMake脚本里有现成模板,但模板是为SDK自己的示例工程准备的,直接拷过来通常会带一些不需要的平台判断,我一般自己写,干净利落。
第三,VST2和VST3的插件ID格式不一样。VST2是4字符码(如'MyGn'),VST3是128位FUID字符串。我见过有人图省事,直接用同一串ASCII字符转成两种格式,结果VST3的FUID在实际使用中因为不够随机,和别人的插件撞车。VST3的FUID应该用SDK提供的GUIDGenerator之类工具生成,生成后固定写死。
双格式还有一个隐藏好处:可以在同一个DAW里同时挂VST2和VST3版本做对比,快速定位问题是出在格式适配层还是核心算法层。我调试的一些诡异问题,比如“在Reaper里正常但在Cubase里参数跳变”,用这个招很快就能锁定是VST2适配参数单位的问题,而不是算法本身。
5. 宿主加载不了插件时,从哪下手排查
写好了插件,双击DAW扫描,结果扫描列表里一片红,或者干脆没出现。这种排查过程我经历了太多次,把经验按优先级整理如下。
Windows平台
- 先确认插件是64位还是32位。现在绝大多数DAW都是64位,但不少老工具链默认编出32位DLL。VS工程里检查
Platform是否为x64,CMake检查CMAKE_SIZEOF_VOID_P是否为8。位数不匹配是VST2时代最高频的加载失败原因,没有之一。 - 检查依赖的运行时库。如果你用VS编译,默认可能要装对应版本的VC++ Redistributable。SDK本身不需要额外运行时,但如果你链接了其他第三方库(比如某些GUI框架),它们自带的DLL要放在插件同目录或者系统PATH里,否则DLL加载会因找不到依赖而失败。用
Dependencies(旧版Dependency Walker的替代品)打开生成的插件DLL,能直接看到缺哪些依赖。 - VST2扫描目录要放对。大多数宿主在设置里指定了VST/VST3插件目录,不同宿主默认路径不一样。最笨但最有效的方法:把生成的DLL同时拷到宿主指定的VST目录和系统常用VST目录(比如
C:\Program Files\Common Files\VST3,不过这是VST3的路径),逐个试。
macOS平台
- 签名问题是大头。如果你在本地编译且没有做签名,macOS的
codesign校验会挡住加载。开发阶段可以用codesign --force --deep --sign -做ad-hoc签名,至少能让本机DAW正常加载。正式分发前需要Developer ID签名,这个绕不过去。 - VST3的bundle结构要完整。VST3在macOS上是一个
.vst3的bundle,里面有Contents/MacOS/和Contents/Info.plist。如果没有正确设置MACOSX_BUNDLE相关CMake属性,生成的可能是一堆散文件而不是一个完整bundle,宿主自然识别不了。
加载后立刻崩溃
排除位数和签名问题后,插件加载时崩溃多半出在工厂注册或UID上。先用宿主自带的插件验证工具跑一下:macOS有auval(针对AU格式),VST3有一种官方验证工具叫Validator(在SDK的tools/目录下)。3.6.14里VST3 SDK/Validation目录下就有Validator的源码,编译出来用命令行跑,它能不带UI地创建插件实例、扫描参数、测试音频处理,崩溃时会直接告诉你崩在哪个环节,比在DAW里黑盒调试高效得多。
Validator的使用示例:
./Validator "MyGain.vst3"如果输出里有Scan of component failed,多半是工厂注册UID或类描述符写错了;如果是某些特定测试崩,比如ProcessTest崩,那就是Processor实现里的问题;如果是ParameterTest崩,就要检查getParameterString之类的函数有没有越界写。Validator的输出是定位问题的第一手依据,不要跳过。
之前我遇到过一个十分隐蔽的情况:插件在Windows上稳定运行,在macOS上只要加载工程就崩。Validator提示StreamTest失败,定位到是getChunk/setChunk里用了#pragma pack(1)压过的结构体,在x86_64下因为对齐方式差异导致读取越界。这种不同平台字节对齐导致的问题,只有Validator这类工具能快速暴露出来。
6. 关于分发你需要提前知道的几件事
插件写完了,编译通过,Validator也全绿,接下来就是分发。这一步有很多坑跟SDK本身无关,但SDK版本会决定你踩坑的方式。
版本号不要太随意。VST3的版本号(kVersionString)会和插件的兼容性绑定,宿主如果发现你在同一GUID下改了版本号,可能会重置用户设置。我见过有开发者小版本每天变,用户反馈“升级一次设置丢一次”,折腾了很久才找到原因。3.6.14这个SDK自带的版本管理很稳定,但如果你没有在工厂注册里固定"1.0.0"这样的字符串,DAW就用自己的启发式算法去处理了,结果不可预期。
发布前跑一次Validator全量测试。刚说了Validator很关键,但要注意全量测试的输出量很大,建议生成一份完整日志存档,以免用户报错后无法追溯。很多第三方渠道(比如Plugin Boutique)在审核插件时会要求提供兼容性测试报告,Validator的输出就是最省事的证明材料。
多宿主实测。我强烈建议至少在Cubase、Reaper、Studio One、FL Studio里各加载一次,跑一个预设切换和参数自动化。不同宿主对VST3的调用方式有微小差异——比如有的宿主按块大小4096调process,有的宿主块小到128,有的宿主在暂停时还会传空块。这些差异只有实测才能暴露。3.6.14这个版本的VST3接口相对成熟,但还是避免不了个别宿主对某些扩展接口(如IEditController2)支持不全的情况。
说到最后,我自己现在还在VST2岗位上维护项目的经验是:如果只是维护老插件,3.6.14完全够用,不需要硬上新版;如果计划新开VST3项目,直接用这个SDK也没问题,接口足够稳定。真正要留心的不是SDK版本老不老,而是你有没有把UID、签名、Validator验证这些“看不见的环节”都理顺。先把这几点处理好,后面的开发顺畅度会高很多。
本文还有配套的精品资源,点击获取