#include "core/namespaces.h" // reaper_bridge.cpp — see reaper_bridge.h. The DAW-facing edge; keep it thin. #include "shell/instrument/reaper_bridge.h" #include #include "core/instrument/map/bridge_marshal.h" #include "core/capture/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 // 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 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. Channel-derived (Phase V, V4): the accessor returns "reasampler" (stable) or // "reasampler_beta" (beta), matching whatever the extension wrote. 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; setProjExtState_ = nullptr; getTrackGuid_ = nullptr; guidToString_ = 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 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( reaper->getReaperApi("GetProjExtState")); enumProjExtState_ = reinterpret_cast( 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( reaper->getReaperApi("EnumProjects")); // pS-usage: the (prefix-guarded) usage publish write + the track-identity pair the // usage record stamps. All degrade to null gracefully — an old REAPER just never // publishes usage (the extension then protects by bank references only). setProjExtState_ = reinterpret_cast( reaper->getReaperApi("SetProjExtState")); getTrackGuid_ = reinterpret_cast( reaper->getReaperApi("GetTrackGUID")); guidToString_ = reinterpret_cast( reaper->getReaperApi("guidToString")); return getProjExtState_ != nullptr; } std::optional 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(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; 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 buf(static_cast(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(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 } bool ReaperBridge::writeUsageExtState(const std::string& usageKey, const std::string& value) { if (!setProjExtState_ || !hostApp_) return false; // STRUCTURAL read-only-bank guard: this module writes usage keys and nothing else. // A non-"rsusage_" key is a programming error upstream — refuse rather than widen // the instrument's write surface (banks/view/tail/assign stay extension-owned). const std::string prefix = kProjExtUsageKeyPrefix; if (usageKey.compare(0, prefix.size(), prefix) != 0) return false; auto* reaper = static_cast(hostApp_); void* proj = reaper->getReaperParent(3); // null = current project (same as reads) // SetProjExtState returns "the size of the state for this extname" (SDK ~6288) — // after storing our non-empty value the namespace state is necessarily > 0, so a // <= 0 return means the write did not land. Reported to the caller (the publish // path retries on the next reload tick); a silently-dropped record would leave the // instance's holds unprotected. const int rv = setProjExtState_(proj, kProjExtNamespace(), usageKey.c_str(), value.c_str()); // Deliberately NO MarkProjectDirty: a usage change always accompanies a component- // state change that already dirties the project; an idempotent load-time republish // must not flag an untouched project as modified. return rv > 0; } std::string ReaperBridge::currentTrackGuid() { if (!hostApp_ || !getTrackGuid_ || !guidToString_) return {}; auto* reaper = static_cast(hostApp_); void* track = reaper->getReaperParent(1); // the hosting MediaTrack* if (!track) return {}; // no track context (unusual host state) void* guid = getTrackGuid_(track); if (!guid) return {}; char buf[64] = {0}; // guidToString's documented destNeed64 contract guidToString_(guid, buf); return std::string(buf); } 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 buf(4096, '\0'); enumProjects_(-1, buf.data(), static_cast(buf.size())); return projectDirOfRpp(std::string(buf.data())); } } // namespace reasampler::vst