// reaper_bridge.cpp — see reaper_bridge.h. The DAW-facing edge; keep it thin. #include "shell/instrument/reaper_bridge.h" #include #include #include #include "core/instrument/map/bridge_marshal.h" #include "core/wire/ext_state_read.h" // readProjExtStateGrowing (grow-loop policy) #include "core/capture/capture_paths.h" // projectDirOfRpp (shared project-dir derivation) #include "ext_keys.h" // kProjExtNamespace (shared wire contract) // VST3 base types must be included before REAPER's VST3 interface header, which uses // unqualified Steinberg types (FUnknown, CStringA, uint32, DECLARE_CLASS_IID, PLUGIN_API). #include "pluginterfaces/base/funknown.h" #include "pluginterfaces/base/ftypes.h" // REAPER's VST3-side bridge interface (vendored): IReaperHostApplication is the // IHostApplication REAPER passes to IComponent::initialize, exposing getReaperApi // (resolve-by-name) and getReaperParent (host context). Pulled into namespace Steinberg // (the header's unqualified types), 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 the iid; this is the only TU that // queries for the interface, so the DEFINE lives with its sole use. DEF_CLASS_IID(Steinberg::IReaperHostApplication) // The ext-state namespace is the shared wire contract with the extension — ext_keys.h's // kProjExtNamespace() (pure, REAPER-free), channel-derived so both artifacts read one // symbol and cannot drift. namespace reasampler::vst { using capture::projectDirOfRpp; using instrument::map::decodeGetProjExtState; bool ReaperBridge::connect(Steinberg::FUnknown* context) { getProjExtState_ = nullptr; enumProjExtState_ = nullptr; enumProjects_ = nullptr; setProjExtState_ = nullptr; getTrackGuid_ = nullptr; guidToString_ = nullptr; namedCommandLookup_ = nullptr; mainOnCommandEx_ = nullptr; getCursorPositionEx_ = nullptr; timeMapGetTimeSigAtTime_ = nullptr; hostApp_ = nullptr; if (!context) return false; // 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(); // getReaperApi returns the same function pointers the extension resolves via // rec->GetFunc; a null return means the symbol is unavailable (very old REAPER). getProjExtState_ = reinterpret_cast( reaper->getReaperApi("GetProjExtState")); enumProjExtState_ = reinterpret_cast( reaper->getReaperApi("EnumProjExtState")); // EnumProjects(-1, ...) yields the active project + its .rpp path — same convention // the persist shell uses, so the instrument derives the project directory identically. enumProjects_ = reinterpret_cast( reaper->getReaperApi("EnumProjects")); // The (prefix-guarded) usage publish write + the track-identity pair it stamps. All // degrade to null gracefully — an old REAPER never publishes usage. setProjExtState_ = reinterpret_cast( reaper->getReaperApi("SetProjExtState")); getTrackGuid_ = reinterpret_cast( reaper->getReaperApi("GetTrackGUID")); guidToString_ = reinterpret_cast( reaper->getReaperApi("guidToString")); // The bake crossing: resolve the extension's action id by name, then fire it against // this instance's own project. All degrade to null gracefully — an unresolvable pair // simply leaves the bake affordance disabled. namedCommandLookup_ = reinterpret_cast( reaper->getReaperApi("NamedCommandLookup")); mainOnCommandEx_ = reinterpret_cast( reaper->getReaperApi("Main_OnCommandEx")); getCursorPositionEx_ = reinterpret_cast( reaper->getReaperApi("GetCursorPositionEx")); timeMapGetTimeSigAtTime_ = reinterpret_cast( reaper->getReaperApi("TimeMap_GetTimeSigAtTime")); return getProjExtState_ != nullptr; } std::optional ReaperBridge::readReasamplerExtState(const std::string& key) { if (!getProjExtState_ || !hostApp_) return std::nullopt; // getReaperParent(3) reads the live "reasampler" ext-state against the active project // the instrument was instantiated in, so it follows project switches for free. A null // project is legitimate (REAPER treats it as the current project) — pass it through // rather than bailing; a fruitless read still yields nullopt to the caller. auto* reaper = static_cast(hostApp_); void* proj = reaper->getReaperParent(3); // The bank blob can be large, so grow the buffer until it fits rather than risk a // silent truncation. The shared wire::readProjExtStateGrowing loop keeps this bridge // read and the extension's persist/usage reads from drifting. const auto read = wire::readProjExtStateGrowing( [&](char* buf, int cap) { return getProjExtState_(proj, kProjExtNamespace(), key.c_str(), buf, cap); }); if (read.status != wire::GrowingExtStateRead::Status::Complete) return std::nullopt; // absent / empty key, or pathologically large (>16 MB) return decodeGetProjExtState(read.apiReturn, read.value); } bool ReaperBridge::writeGuarded(const std::string& key, const std::string& value, const char* requiredPrefix) { // getProjExtState_ is required too: without it the write could not be proven, and an // unprovable write may not be reported as a landed one. if (!setProjExtState_ || !getProjExtState_ || !hostApp_) return false; // Read-only-BANK guard: this module writes the two sanctioned per-instance prefixes // and nothing else. Any other key is refused rather than widening the instrument's // write surface (banks/view/tail/assign stay extension-owned). const std::string prefix = requiredPrefix; if (key.compare(0, prefix.size(), prefix) != 0) return false; auto* reaper = static_cast(hostApp_); void* proj = reaper->getReaperParent(3); // null = current project (same as reads) // The return is DELIBERATELY discarded: it describes the whole extname's state, not // this key, so testing it was a guard that could not fire for the bake publish // (wire::extStateWriteLanded owns the reasoning). The read-back below is the proof. setProjExtState_(proj, kProjExtNamespace(), key.c_str(), value.c_str()); const auto back = wire::readProjExtStateGrowing([&](char* buf, int cap) { return getProjExtState_(proj, kProjExtNamespace(), key.c_str(), buf, cap); }); if (back.status == wire::GrowingExtStateRead::Status::Overflow) return false; // could not check -> report unconfirmed, never a claimed success return wire::extStateWriteLanded( value, back.status == wire::GrowingExtStateRead::Status::Complete ? std::optional(back.value) : std::nullopt); } bool ReaperBridge::writeUsageExtState(const std::string& usageKey, const std::string& value) { return writeGuarded(usageKey, value, kProjExtUsageKeyPrefix); } bool ReaperBridge::writeBakeExtState(const std::string& bakeKey, const std::string& value) { return writeGuarded(bakeKey, value, kProjExtBakeKeyPrefix); } int ReaperBridge::lookupCommand(const std::string& commandName) { if (!namedCommandLookup_ || commandName.empty()) return 0; return namedCommandLookup_(commandName.c_str()); } bool ReaperBridge::extensionActionAvailable(const std::string& commandName) { return mainOnCommandEx_ != nullptr && lookupCommand(commandName) != 0; } bool ReaperBridge::invokeExtensionAction(const std::string& commandName) { if (!mainOnCommandEx_ || !hostApp_) return false; const int command = lookupCommand(commandName); if (command == 0) return false; auto* reaper = static_cast(hostApp_); // getReaperParent(3), not the focused tab — the ask is that an instance in a background // tab bakes into ITS OWN project's bank (see the header on what that does and does not // guarantee). void* proj = reaper->getReaperParent(3); // flag 0 is the conventional "no modifier" value; the header documents no other, and // the extension's hookcommand ignores it. mainOnCommandEx_(command, 0, proj); return true; } double ReaperBridge::projectTempoBpm() { if (!timeMapGetTimeSigAtTime_ || !getCursorPositionEx_ || !hostApp_) return 0.0; auto* reaper = static_cast(hostApp_); void* proj = reaper->getReaperParent(3); int num = 0, denom = 0; double tempo = 0.0; timeMapGetTimeSigAtTime_(proj, getCursorPositionEx_(proj), &num, &denom, &tempo); // The meter is read but not applied: the note model's beat is a quarter note by // ruling (core/instrument/note/CLAUDE.md), so the undivided BPM is the right one. return tempo; } 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 is empty for a never-saved // project. projectDirOfRpp keeps that empty (no default-location fallback). std::vector buf(4096, '\0'); enumProjects_(-1, buf.data(), static_cast(buf.size())); return projectDirOfRpp(std::string(buf.data())); } } // namespace reasampler::vst