S4 Tier 0: the bank plays — VST3 marshals MIDI to the S3 core, reads the live bank + resolves WAV the M4 way, mono downmix, lock-free load handoff, LICE sample-pick

This commit is contained in:
2026-07-26 16:43:04 -04:00
parent b0fc052113
commit 0cde457224
24 changed files with 1239 additions and 302 deletions
+39 -17
View File
@@ -5,6 +5,8 @@
#include <vector>
#include "bridge_marshal.h"
#include "capture_paths.h" // projectDirOfRpp (shared M4 project-dir derivation)
#include "ext_keys.h" // kProjExtNamespace (shared wire contract)
// The VST3 base types must be included before REAPER's VST3 interface header, which
// uses FUnknown / CStringA / uint32 / DECLARE_CLASS_IID / PLUGIN_API from
@@ -27,21 +29,17 @@ namespace Steinberg {
// 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";
}
// The "reasampler" ext-state namespace is the SHARED wire contract between the
// extension (writer) and this instrument (reader); it lives in ext_keys.h (pure,
// REAPER-free) — reasampler::kProjExtNamespace — so the two artifacts read one symbol
// and cannot drift. The S1 spike duplicated it locally; that duplication is retired.
namespace reasampler::vst {
bool ReaperBridge::connect(Steinberg::FUnknown* context) {
getProjExtState_ = nullptr;
enumProjExtState_ = nullptr;
enumProjects_ = nullptr;
hostApp_ = nullptr;
if (!context) return false;
@@ -58,6 +56,10 @@ bool ReaperBridge::connect(Steinberg::FUnknown* context) {
reaper->getReaperApi("GetProjExtState"));
enumProjExtState_ = reinterpret_cast<EnumProjExtStateFn>(
reaper->getReaperApi("EnumProjExtState"));
// EnumProjects(-1, ...) yields the active project AND its .rpp path — the same call
// persist.cpp uses, so the instrument derives the project directory identically.
enumProjects_ = reinterpret_cast<EnumProjectsFn>(
reaper->getReaperApi("EnumProjects"));
return getProjExtState_ != nullptr;
}
@@ -74,15 +76,35 @@ std::optional<std::string> ReaperBridge::readReasamplerExtState(const std::strin
// 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()));
// GetProjExtState writes into a caller buffer; the bank blob can be large (many
// samples), so grow the buffer until the value fits rather than risk a silent
// truncation — mirrors persist.cpp's getProjExtStateString growing strategy. The
// return value is the value length; if it fits strictly inside the buffer it is
// complete, else grow and retry up to a 16 MB ceiling.
for (int cap = 1 << 16; cap <= (1 << 24); cap <<= 2) {
std::vector<char> buf(static_cast<std::size_t>(cap), '\0');
const int rv = getProjExtState_(proj, kProjExtNamespace, key.c_str(),
buf.data(), cap);
if (rv <= 0) return std::nullopt; // absent / empty key
std::string s(buf.data());
if (static_cast<int>(s.size()) + 1 < cap) {
return decodeGetProjExtState(rv, s);
}
// else: possibly truncated -> grow and retry.
}
return std::nullopt; // pathologically large (>16 MB) — give up rather than loop
}
// Marshal the raw result through the pure decoder (handles the absent-key case).
return decodeGetProjExtState(rv, std::string(buf.data()));
std::string ReaperBridge::activeProjectDir() {
if (!enumProjects_) return {};
// idx=-1 is the current project tab; the out-buffer receives the full .rpp path,
// EMPTY for a never-saved project. Same call + convention as persist.cpp; the pure
// projectDirOfRpp turns the .rpp path into the project directory (parent, forward-
// slashed) and keeps an unsaved project's empty path empty (no default-location
// fallback — the tool's invariant).
std::vector<char> buf(4096, '\0');
enumProjects_(-1, buf.data(), static_cast<int>(buf.size()));
return projectDirOfRpp(std::string(buf.data()));
}
} // namespace reasampler::vst