feat(vst): stand up VST3 instrument spike (S1) — skeleton, IPlugView↔LICE editor, REAPER bridge read

Vendor Steinberg VST3 SDK (v3.7.9_build_61); add reasampler_vst.vst3 as a second, additive build artifact with pure editor-geometry + bridge-marshal helpers under CTest.
This commit is contained in:
2026-07-26 15:29:54 -04:00
parent 5595ba42f9
commit 3c2a7c45b2
17 changed files with 1256 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
// reaper_bridge.cpp — see reaper_bridge.h. The DAW-facing edge; keep it thin.
#include "reaper_bridge.h"
#include <vector>
#include "bridge_marshal.h"
// The VST3 base types must be included before REAPER's VST3 interface header, which
// uses FUnknown / CStringA / uint32 / DECLARE_CLASS_IID / PLUGIN_API from
// pluginterfaces/base — all in namespace Steinberg.
#include "pluginterfaces/base/funknown.h"
#include "pluginterfaces/base/ftypes.h"
// REAPER's VST3-side bridge interface (vendored). IReaperHostApplication is what REAPER
// passes (as an IHostApplication) to IComponent::initialize; it exposes getReaperApi
// (resolve-by-name) and getReaperParent (host context). The header uses UNQUALIFIED
// Steinberg types (FUnknown, CStringA, uint32, FUID, DECLARE_CLASS_IID, PLUGIN_API), so
// it must be pulled into the Steinberg namespace — the same way REAPER's own VST3
// examples include it.
namespace Steinberg {
#include "reaper_vst3_interfaces.h"
} // namespace Steinberg
// DECLARE_CLASS_IID in the REAPER header only DECLARES IReaperHostApplication::iid; some
// TU must DEFINE it. We do it here — this is the only place that queries for the
// interface (FUnknownPtr uses the iid), so the definition lives with its sole use.
DEF_CLASS_IID(Steinberg::IReaperHostApplication)
// The "reasampler" ext-state namespace + bank key. Kept in sync with persist.h by
// value (the extension writes them); we only READ here, so we duplicate the two string
// constants rather than pull the whole REAPER-facing persist.h into the VST artifact.
// If persist.h's kProjExtNamespace / kProjExtBanksKey ever change, these must follow —
// they are the shared wire contract between the extension (writer) and instrument
// (reader). VERIFY against persist.h.
namespace {
constexpr const char* kReasamplerNamespace = "reasampler";
}
namespace reasampler::vst {
bool ReaperBridge::connect(Steinberg::FUnknown* context) {
getProjExtState_ = nullptr;
enumProjExtState_ = nullptr;
hostApp_ = nullptr;
if (!context) return false;
// Query the host context for REAPER's bridge interface. In a non-REAPER host this
// query fails and we stay unconnected — the instrument still loads.
Steinberg::FUnknownPtr<Steinberg::IReaperHostApplication> reaper(context);
if (!reaper) return false;
hostApp_ = reaper.get();
// Resolve the ext-state functions by name. getReaperApi returns the same function
// pointers the extension resolves via rec->GetFunc; a null return means the symbol
// is unavailable (very old REAPER) — degrade gracefully.
getProjExtState_ = reinterpret_cast<GetProjExtStateFn>(
reaper->getReaperApi("GetProjExtState"));
enumProjExtState_ = reinterpret_cast<EnumProjExtStateFn>(
reaper->getReaperApi("EnumProjExtState"));
return getProjExtState_ != nullptr;
}
std::optional<std::string> ReaperBridge::readReasamplerExtState(const std::string& key) {
if (!getProjExtState_ || !hostApp_) return std::nullopt;
// Fetch the host project (getReaperParent(3) — project). Reads that live "reasampler"
// ext-state against the ACTIVE project the instrument was instantiated in, so it
// follows project switches for free (D6).
auto* reaper = static_cast<Steinberg::IReaperHostApplication*>(hostApp_);
void* proj = reaper->getReaperParent(3);
// A null project is legitimate (e.g. instantiated before a project context exists);
// REAPER treats null as the current project for these calls, so we pass it through
// rather than bailing — but if the read yields nothing the caller sees nullopt.
// GetProjExtState writes into a caller buffer; size it generously for a JSON blob
// and let the pure decoder interpret the result. The buffer is NUL-terminated by
// REAPER on success.
std::vector<char> buf(64 * 1024, '\0');
const int rv = getProjExtState_(proj, kReasamplerNamespace, key.c_str(), buf.data(),
static_cast<int>(buf.size()));
// Marshal the raw result through the pure decoder (handles the absent-key case).
return decodeGetProjExtState(rv, std::string(buf.data()));
}
} // namespace reasampler::vst