VST3效果器不能只靠main(),因其是需实现IComponent等接口的动态库,由宿主按协议加载;必须继承AudioEffect、重写process()等虚函数,提供GetFactory(),并用引用计数管理生命周期。

为什么直接写 VST3 效果器不能只靠 main()?
因为 VST3 插件不是独立程序,而是遵循 Steinberg 官方 IComponent、IEditController、IAudioProcessor 等接口的动态库(Windows 上是 .dll,macOS 是 .vst3 bundle)。你写的 C++ 代码必须被宿主(如 Cubase、Reaper)按 VST3 协议加载和调用,不实现这些接口,宿主根本识别不了你的插件。
常见错误现象:Plugin not found 或宿主报 Invalid module,往往是因为导出符号不对、缺少 GetFactory() 入口、或未链接 VST3 SDK 的 public.sdk 和 pluginterfaces。
- 必须用
Steinberg::Vst::AudioEffect基类继承并重写关键虚函数,比如process()、setBusArrangements() - 必须提供全局
GetFactory()函数,返回一个实现了IPluginFactory的对象 - 所有接口指针传递都通过
void**+QUERYINTERFACE宏完成,不能裸传this - 不要手动管理插件生命周期(比如 new/delete 插件实例),全部交给 SDK 的
FUnknown引用计数机制
process() 里怎么安全读写音频缓冲区?
VST3 使用非连续、可变通道数的缓冲区结构:Vst::ProcessData 中的 inputs 和 outputs 是 Vst::AudioBusBuffers* 数组,每个 bus 又含 channelBuffers32 或 channelBuffers64(取决于宿主请求的精度)。直接索引 buffer[0] 很容易越界或错用浮点类型。
典型踩坑:process() 被调用时,processData->numSamples 可能为 0(静音段)、或与前次不同(block size 可变);若没检查就 memcpy 或 for-loop,会触发 ASan 报错或爆音。
立即学习“C++免费学习笔记(深入)”;
- 始终先检查
processData->numSamples > 0,否则直接 return - 用
processData->inputParameterChanges读自动化参数,别在process()外部缓存IParamValueQueue指针 - 对每个 input/output bus,遍历
bus->getChannelCount(),再对每通道调用bus->getChannelBuffer(channel)获取真实指针 - 若宿主启用了双精度处理(
processData->processMode == Vst::kRealtime但sampleRate高且插件声明支持),需分支处理float*和double*
void MyEffect::process (ProcessData& data) {
if (data.numSamples <= 0) return;
auto inBuf = data.inputs[0].channelBuffers32;
auto outBuf = data.outputs[0].channelBuffers32;
int32 numChannels = data.inputs[0].getChannelCount();
for (int32 ch = 0; ch < numChannels; ++ch) {
float* in = inBuf[ch];
float* out = outBuf[ch];
for (int32 i = 0; i < data.numSamples; ++i) {
out[i] = in[i] * mGain; // 示例:简单增益
}
}
}
参数如何从 UI 同步到 process()?
VST3 不允许 UI 线程直接改音频线程变量。所有参数变更必须走 IEditController::performEdit() → 宿主排队 → IComponent::notify() → process() 内通过 inputParameterChanges 拉取。跳过这套流程,UI 拖动时音频线程看到的还是旧值,甚至引发数据竞争。
常见错误现象:旋钮拖动时效果无响应、参数跳变、或宿主崩溃——大概率是 UI 直接写了成员变量,或在 performEdit() 里没调用 fireParamChanged()。
- 每个参数需注册唯一
ParamID(如static const ParamID kGainId = 0;),并在getParams()返回的IParameter列表中定义 -
performEdit()中仅做合法性检查,然后调用fireParamChanged(paramID, normalizedValue) - 在
process()开头,遍历data.inputParameterChanges的每个IParamValueQueue,用getPoint()提取时间戳和归一化值,再插值更新本地变量(如mGainSmoothed) - 避免在
performEdit()中做任何耗时操作(如文件 IO、malloc),它运行在 UI 线程
为什么插件加载后没声音或 CPU 占用飙高?
最常被忽略的是总线配置和采样率适配。VST3 默认只声明 1 个输入/输出 bus,且默认是 mono。如果宿主以 stereo 发送音频,但插件没声明 stereo bus,宿主可能静音转发或 fallback 到错误通道映射。
另一个隐蔽问题是采样率变更未响应:setSampleRate() 被调用后,如果你的滤波器系数还用旧的 sampleRate 计算,就会失真甚至振荡;而如果没重置延迟线长度等状态,还会产生点击声。
- 在
MyEffect::setBusArrangements()中显式接受 stereo 输入/输出:inputs[0].setChannelCount(2),并 returnkResultTrue(否则宿主认为不支持该布局) - 重写
setSampleRate(),更新所有依赖采样率的计算(如1.0 / sampleRate、biquad 系数),并清空 IIR 滤波器状态数组 - 在
setupProcessing()中根据processSetup.processMode决定是否启用多线程优化(如 OpenMP),但注意 VST3 已保证process()不会被并发调用,通常无需额外锁 - 调试时打开
Steinberg::Vst::IComponent::canProcessSampleSize(),确保返回kResultTrue对应的SampleSize(kSample32或kSample64)
复杂点在于:VST3 的生命周期比想象中更“松散”——initialize() 和 terminate() 可能被多次调用,process() 可能在不同线程(如离线渲染)中执行。别假设单例、全局静态或线程局部存储一定安全。


















