简介VST SDK 3.6.14 Build-24 是Steinberg官方于2019年11月发布的VST3插件开发工具包面向音频插件开发者、音乐软件厂商及独立开发团队用于在数字音频工作站DAW中构建均衡器、压缩器、合成器等专业音频效果器。该版本重点优化了插件延迟补偿、多通道处理与64位精度支持并带有VST2兼容接口便于旧工程迁移。压缩包约86.17MB目前已有202人学习下载。SDK内部包含C接口定义IAudioProcessor、IEditController等从音频数据流处理、参数映射到用户控件接口均有覆盖随附的示例工程可帮助理解效果器与乐器插件的基本骨架配套API参考文档对每个类、函数和常量给出说明另有Windows、macOS、Linux下的构建脚本和免DAW调试用的VSTPluginExample测试宿主。这些内容为开发者提供了一条从接口认知到编译调试的完整路径无论是做单机效果器还是跨宿主商用插件都可从中获得扎实的起点。1. 为什么 2019 年的 vst-sdk 3.6.14 到今天还有人翻出来用vst-sdk 3.6.14 这个版本号对做过 VST 插件开发的人来说并不陌生。它是 VST2 协议走向尾声时的一个稳定构建日期停在 2019-11-29build-24 是内部计数不是功能号。你翻出这个 zip多半是三种情况手上有老插件源码要在新机器上重新编译宿主还在用 VST2 接口加载老插件或者想绕过 VST3 那套复杂的组件生命周期先弄个能出声的最小插件理解音频处理链路。这个版本解决的是“存量维护”和“快速原型”两类问题。VST2 接口本质上就是一个 C 结构体加一堆函数指针比 VST3 的模块化组件模型简单一个量级三小时能把一个带音量旋钮的插件跑进宿主。适合刚接触插件开发的初学者也适合需要维护十几年老工程的工程师。它到今天没有被彻底淘汰不是因为技术先进而是因为协议足够薄、足够稳值得把它的边界和坑一次讲透。2. 解开 vst-sdk_3.6.14_build-24_2019-11-29.zip包内布局与先读哪三个文件2.1 解压这个 zip平台不一样命令不一样拿到vst-sdk_3.6.14_build-24_2019-11-29.zip先别急着双击。Windows 和 macOS 解压同一个 zip 的结果可能不一样问题出在权限位和隐藏文件上。macOS 上用 Finder 自带的归档工具解压经常把 POSIX 权限弄丢后续编译时头文件明明在编译器却说找不到。我一般用ditto它能保留权限和资源分支。cd ~/dev ditto -x -k vst-sdk_3.6.14_build-24_2019-11-29.zip . chmod -R urX vst-sdk_3.6.14_build-24_2019-11-29Windows 上没 ditto直接用tar或者 PowerShell 的Expand-Archive都行。chmod -R urX这步在 macOS 上不是可有可无老 zip 里部分头文件权限可能只有读权限追加执行权限后目录才能被正常遍历。解压完成后根目录名就是 zip 去掉扩展名后面所有 include 路径都基于这个根目录。解包后的目录结构有个固定套路先记下这张表后面编译时经常要回来找文件目录作用pluginterfaces/vst2.x/协议头文件aeffect.h、aeffectx.h、vstfxstore.h任何 VST2 工程都绕不开public.sdk/source/vst2.x/AudioEffectX类源码插件主继承体系在这里public.sdk/samples/vst2.x/官方示例工程Win/mac 的工程文件都在这里public.sdk/source/common/平台工具和线程辅助按需编译vstgui/老式 GUI 库VST3 时代被重新设计这个版本里可以先跳过这个包是 SDK 源码包不是预编译库。很多人第一次用会误以为要链接某个.a或.lib文件实际不是。你要做的是把public.sdk/source/vst2.x/audioeffectx.cpp这个文件直接加进你的工程一起编译剩下需要哪个工具文件再按 include 链补齐。SDK 本身不提供二进制这是 VST2 时代的惯例也避免了 ABI 不一致问题。2.2 打开 aeffect.hVST2 插件本质是一个 C 结构体pluginterfaces/vst2.x/aeffect.h是整个 VST2 协议的核心一次读明白后面所有问题都好解。它定义了一个叫AEffect的结构体你的插件在宿主眼里就是这块内存。字段不多但每个都重要magic固定为kEffectMagic数值是0x56737450对应 ASCII 字符VstP。宿主加载时先校验它不对直接拒绝。uniqueID插件的唯一标识四字符码比如MyGn宿主用它区分插件身份。numParams、numPrograms参数个数和预置个数。宿主对插件的自动化、工程保存都建立在参数索引上。dispatcher函数指针宿主和插件之间所有命令都走这里。打开、关闭、设置采样率、设置块大小、MIDI 事件全是 opcode。processReplacing音频处理主函数输入输出浮点指针和块大小插件在这里逐样本处理音频。resume、suspend宿主告诉插件开始/停止处理音频。读aeffect.h时不要跳过注释它把每个 opcode 的调用时机都写清楚了。比如effSetSampleRate和effSetBlockSize一定在resume之前到达这个顺序在写插件时需要依赖。2.3 再读 aeffectx.h 和 vstfxstore.h扩展开关与状态存储aeffectx.h是aeffect.h的扩展包定义了effProcessEvents、effGetParamLabel、effSetSampleRate、effMainsChanged这些 opcode 的常量以及 MIDI 事件结构VstEvents。如果你的插件要接收 MIDI事件注入就走这里。注意一个细节VST2 的 MIDI 和音频是两条通道processReplacing只管音频MIDI 事件通过dispatcher里的effProcessEvents在音频块之前到达时序上必须处理干净否则音符对不上。vstfxstore.h管状态存储。VST2 插件把自身状态存成二进制块交给宿主写进工程文件。读完这个文件你才明白programsAreChunks的作用插件把整个状态打成一个 chunk宿主存储时不用理解内容恢复时原样还给你。这就是很多人说的“后悔药”机制老工程里特别讲究这个因为宿主版本升级后参数列表对不上只有 chunk 能兜底。这三个文件的阅读顺序建议是aeffect.h先建立整体模型aeffectx.h看扩展能力vstfxstore.h最后看因为它的应用场景比较窄只在做预置管理时需要。新手不用一上来啃完先能编译过再回头补。3. 在 macOS 上用 Xcode 编译 3.6.14最小插件工程与 -bundle 命令3.1 先写一个能出声的最小增益插件SDK 自带的例子工程往往带了很多历史包袱Windows 和 Mac 的工程文件散落各处直接打开容易迷路。我更推荐自己新建一个工程只放一个.cpp文件把核心逻辑写清楚。下面是一个最小增益插件属于能放进宿主跑通的最小可用代码// MyGain.cpp #include cmath #include cstring #include public.sdk/source/vst2.x/audioeffectx.h class MyGain : public AudioEffectX { public: MyGain(audioMasterCallback hostCallback) : AudioEffectX(hostCallback), fGain(0.5f) { setNumInputs(2); setNumOutputs(2); setUniqueID(MyGn); canProcessReplacing(); } void setParameter(VstInt32 index, float value) override { if (index 0) fGain value; } float getParameter(VstInt32 index) override { return fGain; } void getParameterName(VstInt32 index, char* text) override { if (index 0) std::strcpy(text, Gain); } void processReplacing(float** inputs, float** outputs, VstInt32 sampleFrames) override { for (int ch 0; ch 2; ch) { const float* in inputs[ch]; float* out outputs[ch]; for (int i 0; i sampleFrames; i) { float s in[i] * fGain; if (std::fabs(s) 1e-15f) s 0.0f; // 防 denormal out[i] s; } } } VstIntPtr close() override { delete this; return 1; } private: float fGain; }; AEffect* VSTPluginMain(audioMasterCallback hostCallback) { if (!hostCallback) return nullptr; if (hostCallback(nullptr, audioMasterVersion, 0, 0, nullptr, 0) 1) return nullptr; MyGain* gain new MyGain(hostCallback); return gain-getAEffect(); }代码逻辑分三段构造函数里把输入输出通道数设为 2声明这个插件支持processReplacing这种实时处理模式processReplacing里按双通道循环逐样本把输入乘上增益值最后的VSTPluginMain是宿主的入口函数它先和宿主做版本协商再创建插件实例并返回AEffect*。audioMasterVersion这个协商很关键。宿主回调在处理器的加载早期会被调用一次如果返回 0说明宿主太老不支持当前插件版本直接返回空指针是最安全的做法。delete this放在close()里是 VST2 的常见写法宿主最终通过effClose命令释放插件对象谁 new 谁 delete 在这里变成插件自己 delete 自己。3.2 Xcode 工程配置与命令行等价物Xcode 里新建一个 macOS 的 Command Line Tool 工程然后把输出类型改成 Bundle 是可行的但更容易的是直接看命令行。它把关键配置都摊开了SDKvst-sdk_3.6.14_build-24_2019-11-29 mkdir -p MyGain.vst/Contents/MacOS clang -stdgnu14 -isysroot $(xcrun --sdk macosx --show-sdk-path) \ -I$SDK -fPIC -O2 -c MyGain.cpp -o MyGain.o clang -stdgnu14 -isysroot $(xcrun --sdk macosx --show-sdk-path) \ -I$SDK -fPIC -O2 -c $SDK/public.sdk/source/vst2.x/audioeffectx.cpp \ -o audioeffectx.o clang -bundle -arch x86_64 -arch arm64 \ -o MyGain.vst/Contents/MacOS/MyGain MyGain.o audioeffectx.o-stdgnu14不是随便选的。3.6.14 的头文件在 C17 模式下会触发register保留字报错降到 C14 是成本最低的解法。-bundle是 macOS 特有的 Mach-O 类型宿主用dlopen加载插件普通可执行文件没这个能力。最后的-arch x86_64 -arch arm64编出通用二进制Apple Silicon 原生跑Intel 老机器也能加载。头文件路径只指到 SDK 根目录代码里#include public.sdk/source/vst2.x/audioeffectx.h才能对上。audioeffectx.cpp是必须一起编译的它是所有AudioEffectX派生类的基类实现不编它链接必失败。3.3 自己编就是编不过的两个玄学点第一个玄学点是签名。本地编出的 bundle 在装了 Gatekeeper 的新 macOS 上第一次被宿主加载时可能被拦。开发机上最简单的处理是加一个 ad-hoc 签名codesign --force --deep --sign - MyGain.vst这不会影响功能只是让系统知道这是个本地构建产物不是网上下载的来路不明文件。第二个玄学点是 bundle 目录结构。老文档里有时只写“生成 .vst 文件”实际老宿主扫描的是MyGain.vst/Contents/MacOS/目录下的可执行文件。漏掉这层目录很多宿主扫不到插件但这跟代码没关系是打包路径不对。血泪经验先确认.vst的目录结构再去怀疑编译器。4. 在 Windows 上用 VS2019 编译 3.6.14.def 导出与 resource 编译的坑4.1 导出方式为什么 Windows 下没有 .def 就找不到入口Windows 上的 VST2 插件本质是 DLL宿主通过GetProcAddress查找入口函数。区别在于入口名必须是裸符号VSTPluginMain而 C 编译器默认会输出修饰名类似?VSTPluginMainYAPEAU...宿主不认识。解决方式有两种源码里加extern C或者提供.def文件。老 SDK 示例工程两种都有我更推荐.def因为可以顺带导出一个别名; MyGain.def EXPORTS VSTPluginMain main VSTPluginMainmain VSTPluginMain这行是给老宿主准备的。有些宿主只认main作为入口比如几十年前的 VST 1.0 时代遗留的扫描器。加了这行别名兼容性直接拉满代价只是.def文件多一行字。4.2 Visual Studio 命令行的完整编译序列VS2019 里建工程时别选“动态链接库”模板它生成一堆 Windows 框架代码跟 VST2 完全无关。直接建空工程然后在“开发者命令提示符”里手动编译或者把下面的命令写进批处理cl /nologo /c /MD /O2 /EHsc /I. MyGain.cpp cl /nologo /c /MD /O2 /EHsc /I. vst-sdk_3.6.14_build-24_2019-11-29\public.sdk\source\vst2.x\audioeffectx.cpp rc /fo MyGain.res MyGain.rc link /nologo /DLL /OUT:MyGain.dll /DEF:MyGain.def MyGain.obj audioeffectx.obj MyGain.res/MD指定动态链接 CRT这是必须的。插件在宿主进程里运行如果插件和宿主各带一份不同的 C 运行时静态库malloc和delete跨模块分配释放就会踩内存。/EHsc开 C 异常/O2开优化/DEF指定导出定义。资源文件MyGain.rc里放版本信息宿主扫描器能看到文件描述和版本号VS_VERSION_INFO VERSIONINFO FILEVERSION 3,6,14,0 PRODUCTVERSION 3,6,14,0 BEGIN BLOCK StringFileInfo BEGIN BLOCK 040904e4 BEGIN VALUE FileDescription, MyGain VST2 Plugin VALUE ProductName, MyGain END END END注意这里FILEVERSION是插件的版本号不是 SDK 版本号别照抄成 3.6.14 就完事。这个资源不是加载必需的但没有它宿主管理器里插件信息会显示成空白。4.3 x64 与 32 位的选择一台机器决定了你是不是白干Windows 工程最容易翻车的地方是目标平台。老 SDK 的示例工程默认是 Win32你在 VS2019 里打开时如果直接编译出来的就是 32 位 DLL。现在主流宿主全是 64 位32 位插件要么扫不到要么加载直接崩。这个坑的隐蔽性在于编译过程完全正常没有任何报错。解决方式配置管理器里新建 x64 平台或者在命令行里确认编译环境。命令行编译前先确认用的是vcvars64.bat而不是vcvars32.bat。编译完再验证一次dumpbin /headers MyGain.dll | findstr machine输出里应该出现AA64或x64如果看到x86说明编译环境错了。这一步检查成本不到十秒能省掉后面进宿主扫不到插件的半小时排查时间。这个习惯我现在还保留着每次跨平台编译完都先查一下目标架构再谈功能测试。5. vst-sdk 3.6.14 避坑记录编译期、链接期与运行期最常见的翻车点5.1 Xcode 15 报 register is reserved现象、原因、解决现象新装 Xcode 后编译 3.6.14 的头文件报错error: register is reserved位置在audioeffectx.h附近。原因Xcode 15 默认使用 C17 标准而老 SDK 头文件里还残留着register关键字这在 C17 里已经是保留字不再是存储类说明符。解决Build Settings 里把C Language Dialect改成GNU14或C14命令行编译时加-stdgnu14。如果工程里其他代码必须用 C17可以用-Dregister把register替换为空副作用极小。5.2 macOS 解压后 include 找不到头文件现象、原因、解决现象头文件搜索路径配置完全正确但编译器报aeffect.h: No such file or directory。原因Finder 自带的解压工具没有保留 zip 里的 POSIX 权限目录变成了只读不可遍历状态。解决用ditto -x -k重新解压然后执行chmod -R urX。注意X是大写它只给目录加执行权限不碰普通文件。如果问题依旧检查 include 路径里的文件名大小写Aeffect.h和aeffect.h在 macOS 默认文件系统下是不同文件。5.3 Windows 宿主报“无法定位程序输入点”的现象、原因、解决现象DLL 复制进宿主插件目录扫描时报无法定位程序输入点 VSTPluginMain宿主直接拒绝加载。原因源码里的VSTPluginMain没有包extern C编译器输出修饰名宿主按裸符号找找不到。解决函数定义前加extern C并且配.def文件确保导出名是VSTPluginMain顺带导出main别名给老宿主。检查方式命令行执行dumpbin /exports MyGain.dll看导出表里是否有VSTPluginMain和main。5.4 宿主扫到了插件一加载就闪退现象、原因、解决现象宿主能识别插件但加载的瞬间进程崩溃或者拖动窗口时直接退出。原因最常见的是运行时库不匹配插件用了/MT静态运行时而宿主用动态运行时跨 DLL 边界传递内存后释放崩溃。另一类原因是插件里有依赖宿主回调的全局静态对象宿主还没准备好接口就被构造函数调用了。解决Windows 编译统一用/MD插件里不要用全局对象做复杂初始化把资源获取全部挪到VSTPluginMain里等宿主回调就绪后再 new 插件实例。排查时用 VS 的调试器附加宿主进程在VSTPluginMain和processReplacing下断点看崩溃堆栈落在哪个模块这一步能定位九成问题。5.5 processReplacing 里没处理 denormalCPU 空转 30%现象、原因、解决现象插件在宿主里跑起来后 CPU 占用异常高播放暂停后依然居高不下但听感上好像没区别。原因音频流静音时浮点运算迭代出大量非规格化数denormalCPU 处理这些数比正常浮点慢几十倍。解决采样循环里对接近零的样本直接清零或者在处理开头打开 CPU 的 FTZ/DAZ 标志位#include xmmintrin.h _mm_setcsr(_mm_getcsr() | 0x8040);0x8040是 FTZFlush To Zero和 DAZDenormals Are Zero两个标志位合成的掩码设置后 CPU 遇到非规格化数直接当零处理不再走慢速路径。这个操作放到processReplacing开头或者插件构造函数里执行一次就行不用每个采样块都设。这不是玄学是音频插件行业里公认的优化习惯老 SDK 的示例代码没写新人基本都要踩一遍。6. 不装宿主也能验证插件写一个 200 行伪宿主把 AEffect 跑起来6.1 伪宿主加载流程与关键代码每次改完插件都开宿主验证效率太低。我习惯写一个几十行的伪宿主直接在命令行加载插件喂一段测试音频看输出是否合理。macOS 下用dlopenWindows 下换成LoadLibrary和GetProcAddress逻辑完全一样。#include cmath #include cstdio #include dlfcn.h #include vst-sdk_3.6.14_build-24_2019-11-29/pluginterfaces/vst2.x/aeffect.h VstIntPtr hostCallback(AEffect* e, VstInt32 opcode, VstInt32 index, VstIntPtr value, void* ptr, float opt) { if (opcode audioMasterVersion) return 1; // 版本协商必须回正数 return 0; } int main() { void* handle dlopen(MyGain.vst/Contents/MacOS/MyGain, RTLD_NOW); if (!handle) { printf(dlopen: %s\n, dlerror()); return 1; } auto entry (AEffect* (*)(audioMasterCallback))dlsym(handle, VSTPluginMain); AEffect* fx entry(hostCallback); if (!fx || fx-magic ! kEffectMagic) { printf(bad magic\n); return 1; } fx-dispatcher(fx, effOpen, 0, 0, nullptr, 0); fx-dispatcher(fx, effSetSampleRate, 0, 0, nullptr, 44100.0f); fx-dispatcher(fx, effSetBlockSize, 0, 512, nullptr, 0); fx-dispatcher(fx, effMainsChanged, 0, 1, nullptr, 0); float in[512], out[512]; for (int i 0; i 512; i) in[i] std::sin(i * 0.01f); float* ins[2] { in, in }; float* outs[2] { out, out }; fx-processReplacing(fx, ins, outs, 512); printf(out[0]%f, out[511]%f\n, out[0], out[511]); fx-dispatcher(fx, effMainsChanged, 0, 0, nullptr, 0); fx-dispatcher(fx, effClose, 0, 0, nullptr, 0); dlclose(handle); return 0; }伪宿主里最容易忽略的就是audioMasterVersion回调。插件在VSTPluginMain里做了版本协商如果回调返回 0插件直接拒绝加载你会看到一个“加载成功但入口返回空”的假象。回调里对版本查询返回 1是对插件最基本的手动关怀。加载成功后真正要看的输出是out[0]和out[511]增益是 0.5输入正弦波峰值是 1.0输出峰值应该在 0.5 左右如果全是 0 或者出现大数值回头查processReplacing的通道循环和指针偏移。6.2 再验证一层输入输出能否 in-place 共用同一块缓冲区VST2 的宿主有权要求插件支持 in-place 处理也就是输出缓冲区可能和输入缓冲区是同一个指针。很多插件在单独分配的输出缓冲上跑得好好的一到 in-place 就坏数据。伪宿主里加一行就能测出来fx-processReplacing(fx, ins, outs, 512); // outs[0] ins[0] in把outs[0]直接指向in处理完对比in的前几个样本是否还符合预期。如果代码里用了额外临时缓冲这里大概率翻车。这个检查做完再进 DAW 做最终测试我这些年的习惯都是这样先命令行跑通再开宿主先验证协议字段再验证音质。希望帮到你。本文还有配套的精品资源点击获取