// reaper_bridge.h — the REAPER VST-host bridge. Thin shell: resolves REAPER API functions // by name over the host context and reads the live "reasampler" project ext-state. The // fiddly decode lives in bridge_marshal (pure). // // Bridge mechanism: REAPER passes an IHostApplication as `context` to // IComponent::initialize; querying it for IReaperHostApplication yields getReaperApi // (resolve a REAPER API function pointer by name) and getReaperParent(3) (the host // ReaProject*; 1=track, 2=take, 3=project, 4=fxdsp, 5=trackchan) — not VST2 hostcb opcodes. #pragma once #include #include #include "pluginterfaces/base/funknown.h" namespace reasampler::vst { // Wraps the REAPER host bridge for a single plugin instance. Constructed cheaply; // connect() must be called with the initialize() context before any read. All reads // degrade to nullopt (never crash) when the host is not REAPER or a symbol is absent — // the instrument must load in non-REAPER hosts too, just without live state. class ReaperBridge { public: ReaperBridge() = default; // Binds to the host (`context` is the FUnknown* IComponent::initialize hands us). // Returns true when the host is REAPER and the ext-state API resolved; safe to call // with a null or non-REAPER context (returns false). bool connect(Steinberg::FUnknown* context); bool isConnected() const { return getProjExtState_ != nullptr; } // Reads a "reasampler" ext-state value by key from the host's active project. // Returns nullopt when unconnected, unresolvable, or the key is absent. // // NOT REAL-TIME SAFE (allocates + calls into REAPER): audio-thread callers MUST NOT // invoke this. The instrument reads on the main/UI thread and hands a snapshot to // the process path. std::optional readReasamplerExtState(const std::string& key); // The active project's directory (forward-slashed, no trailing slash) — the same // convention persist uses to place the bank alongside the .rpp. Empty for an unsaved // project or when unconnected. Not RT-safe. std::string activeProjectDir(); // The instrument's TWO sanctioned ext-state write surfaces, each accepting exactly one // key prefix and refusing every other key. That structural refusal is what keeps the // read-only-BANK invariant intact — banks/view/tail/assign stay unwritable from here — // and neither payload is bank state. Neither is RT-safe: the call sites are the // off-audio-thread reload path and the editor's UI tick. // // Both return true iff the key READ BACK as exactly the value written (an empty value // is a clear, which lands as an absent-or-empty key). SetProjExtState's own return // cannot answer that — wire::extStateWriteLanded owns why, and testing it here was a // guard that could never fire. A `false` does not distinguish a write that was never // issued (unconnected host, refused prefix) from one that did not take or could not be // checked; a caller must not name one of the three. // // Neither marks the project dirty. A usage change always rides a component-state change // that already does; a bake request is transient and is cleared in the same tick. // THIS INSTANCE's usage record. `usageKey` MUST carry the "rsusage_" prefix // (ext_keys.h's usageKeyFor). bool writeUsageExtState(const std::string& usageKey, const std::string& value); // THIS INSTANCE's resample-bake request. `bakeKey` MUST carry the "rsbake_" prefix // (ext_keys.h's bakeKeyFor). An empty value clears the key. bool writeBakeExtState(const std::string& bakeKey, const std::string& value); // --- Extension action invocation (the bake crossing) ------------------------------ // // `commandName` is the NamedCommandLookup spelling — the registered command_id string // with a leading underscore, which the registration string itself does not carry. // Whether the extension is loaded AND has registered this action. Used to paint the // affordance: an unavailable action must read Disabled, never enabled-then-refusing. bool extensionActionAvailable(const std::string& commandName); // Fires the action with THIS INSTANCE's own project tab (getReaperParent(3)) as // Main_OnCommandEx's `proj`, rather than leaving it to whichever tab is focused. What // REAPER then makes current for the action's duration is NOT verified in the DAW, so // this is a request, not a guarantee — the extension side re-derives which project a // request came from and refuses one it cannot safely land. Returns false when the action // is unregistered; the invocation itself reports nothing, so a caller learns the result // from the state the action wrote, never from here. Must NOT be called from a mouse // handler: it runs the extension's whole bake landing synchronously, and the action // re-points this instance. bool invokeExtensionAction(const std::string& commandName); // The effective project tempo (BPM, quarter notes per minute) at the edit cursor of // this instance's own project. 0.0 when unconnected or unresolvable — the caller // refuses rather than substituting a tempo (no hardcoded rates or tempos in src/). double projectTempoBpm(); // The canonical GUID string of the track hosting this FX instance (same rendering as // the extension's track_guid::guidString, so usage records compare byte-equal // against its live-FX enumeration). Empty when unconnected or no track context (the // usage reader then falls back to any-instance liveness). Not RT-safe. std::string currentTrackGuid(); private: // Resolved REAPER API function pointers (by name via getReaperApi). Signatures // verified against reaper_plugin_functions.h. using GetProjExtStateFn = int (*)(void* proj, const char* extname, const char* key, char* valOutNeedBig, int valOutNeedBig_sz); using EnumProjExtStateFn = bool (*)(void* proj, const char* extname, int idx, char* keyOut, int keyOut_sz, char* valOut, int valOut_sz); // EnumProjects(-1, projfnOut, sz) -> active project + its .rpp path. idx=-1 (current // tab) follows the active project, same convention as the persist shell. using EnumProjectsFn = void* (*)(int idx, char* projfnOut, int projfnOut_sz); // Used ONLY by the two prefix-guarded writers — see the read-only-bank note there. using SetProjExtStateFn = int (*)(void* proj, const char* extname, const char* key, const char* value); using NamedCommandLookupFn = int (*)(const char* commandName); using MainOnCommandExFn = void (*)(int command, int flag, void* proj); using GetCursorPositionExFn = double (*)(void* proj); using TimeMapGetTimeSigAtTimeFn = void (*)(void* proj, double time, int* numOut, int* denomOut, double* tempoOut); // Opaque-pointer signatures so the header stays SDK-type-free; the GUID* is passed // straight through, never dereferenced here. using GetTrackGuidFn = void* (*)(void* tr); using GuidToStringFn = void (*)(const void* g, char* destNeed64); void* hostApp_ = nullptr; // IReaperHostApplication* (opaque here; used in .cpp) GetProjExtStateFn getProjExtState_ = nullptr; EnumProjExtStateFn enumProjExtState_ = nullptr; EnumProjectsFn enumProjects_ = nullptr; SetProjExtStateFn setProjExtState_ = nullptr; GetTrackGuidFn getTrackGuid_ = nullptr; GuidToStringFn guidToString_ = nullptr; NamedCommandLookupFn namedCommandLookup_ = nullptr; MainOnCommandExFn mainOnCommandEx_ = nullptr; GetCursorPositionExFn getCursorPositionEx_ = nullptr; TimeMapGetTimeSigAtTimeFn timeMapGetTimeSigAtTime_ = nullptr; // The one prefix guard both public writers route through, so the two cannot diverge // in how strictly they refuse a key. bool writeGuarded(const std::string& key, const std::string& value, const char* requiredPrefix); // 0 when the action is not registered (the extension is absent or older). REAPER's // documented "not found" return is 0 by convention only — the header does not state // it — so every caller treats 0 as unavailable and never as a valid command id. int lookupCommand(const std::string& commandName); }; } // namespace reasampler::vst