// instrument_drop_win — the REAPER shell for S17 drop-and-load. See instrument_drop_win.h. // // Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h WITHOUT // REAPERAPI_IMPLEMENT (main.cpp owns the pointers; here they are extern via the WANT list). #include "instrument_drop_win.h" #include #include #include #include #include #include #include #include "app_version.h" // vstPluginName() — the CHANNEL-correct FX name (stable/beta pairing) #include "instrument_drop.h" // infoNamesFxHotspot — the PURE, unit-tested hotspot classifier #include "reaper_plugin.h" #define REAPERAPI_MINIMAL #define REAPERAPI_WANT_GetThingFromPoint #define REAPERAPI_WANT_TrackFX_AddByName #define REAPERAPI_WANT_TrackFX_Delete #define REAPERAPI_WANT_TrackFX_SetPreset #define REAPERAPI_WANT_Undo_BeginBlock2 #define REAPERAPI_WANT_Undo_EndBlock2 #include "reaper_plugin_functions.h" namespace reasampler { namespace { // Write `bytes` to a fresh uniquely-named .vstpreset in the OS temp dir and return its path; // returns an empty path on any failure. The .vstpreset extension is load-bearing — // TrackFX_SetPreset's full-path form is documented for .vstpreset files (VST3). The file is // transient: the caller deletes it right after the SetPreset call. // // The temp filename embeds the process ID so two concurrent REAPER instances (e.g. stable + // beta) cannot collide in the shared OS temp dir, and one instance's cleanup cannot // accidentally delete another's in-flight file. // // Non-throwing: every std::filesystem call uses the error_code overload. The whole body is // wrapped in try/catch to guarantee no exception crosses the REAPER C callback boundary // (the same discipline persist.cpp uses — see its non-throwing scanPruneOrphans comment). // // Returns the path object (not a narrow string) so the caller can: // (a) pass path.u8string() to TrackFX_SetPreset — UTF-8 on MSVC, not ACP-converted, // so a temp dir with accented or CJK user-name bytes is handled correctly; // (b) delete via the retained path object — not via re-parsing the narrow string — // so the cleanup cannot leak if the conversion above were to round-trip incorrectly. std::filesystem::path writeTempPreset(const std::vector& bytes) { try { static std::atomic counter{0}; std::error_code ec; const std::filesystem::path dir = std::filesystem::temp_directory_path(ec); if (ec) return {}; // PID in the name keeps files from distinct REAPER instances distinct in the shared // temp dir — prevents cross-instance collisions and spurious post-apply deletions. const std::string name = "reasampler_drop_" + std::to_string(GetCurrentProcessId()) + "_" + std::to_string(counter.fetch_add(1)) + ".vstpreset"; const std::filesystem::path path = dir / name; std::ofstream out(path, std::ios::binary | std::ios::trunc); if (!out) return {}; out.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); out.close(); if (!out) { // short write / flush failure -> don't hand REAPER a truncated preset std::filesystem::remove(path, ec); return {}; } return path; } catch (...) { return {}; } } } // namespace FxDropTarget resolveFxDropTarget(int screenX, int screenY) { FxDropTarget out; char info[256] = {0}; // GetThingFromPoint returns the track under the point (may be null for a non-track thing) // and fills `info` with what was hit. A non-empty info OR a non-null track means the point // is over REAPER's own UI; a null track with an empty info means the pointer has left // REAPER entirely (over another app / the desktop) — the OsDrag boundary. MediaTrack* track = GetThingFromPoint(screenX, screenY, info, sizeof(info)); out.track = track; out.overReaperUi = (track != nullptr) || (info[0] != '\0'); // The hotspot is either the FX chain/floating window ("fx_*") OR the FX-button family of // the track/mixer panel ("tcp.fx*"/"mcp.fx*"). The pure classifier owns the rule. out.overFxHotspot = (track != nullptr) && infoNamesFxHotspot(info); return out; } bool loadInstrumentOntoTrack(MediaTrack* track, const std::vector& presetBytes) { if (!track || presetBytes.empty()) return false; // Materialize the .vstpreset FIRST so an I/O failure leaves the track untouched (no FX // added yet — nothing to roll back). const std::filesystem::path presetPath = writeTempPreset(presetBytes); if (presetPath.empty()) return false; // The CHANNEL-correct FX name: "VST3:ReaSampler 9000" on stable, "VST3:ReaSampler 9000 // beta" on beta. Sourcing it from app_version::vstPluginName() (the same accessor the VST // factory display name derives from) keeps the pairing invariant intact — a beta extension // drops the beta VST, a stable extension the stable VST — with no literal to drift. (The // preset's class ID forks by the same channel bit inside buildInstrumentDropPreset.) const std::string fxName = "VST3:" + vstPluginName(); // Negative `instantiate` => always create a NEW instance (verified in the header). recFX // = false: a normal track FX chain instance, not a record/monitoring FX. const int fxIndex = TrackFX_AddByName(track, fxName.c_str(), /*recFX=*/false, /*instantiate=*/-1); bool ok = fxIndex >= 0; // Apply the dragged capture's component state through the DOCUMENTED channel: a full // .vstpreset path handed to TrackFX_SetPreset (SDK: "Full paths to .vstpreset files are // also supported for VST3 plug-ins"). REAPER parses the Steinberg container and feeds the // 'Comp' chunk to the instance's setState — the same bytes the instrument's own // serializer produced (instrument_drop::buildInstrumentDropPreset -> // sample_map::serializeComponentState). Unlike the former "vst_chunk" named-config-parm // write, a failure here is REPORTED (false), not silently ignored. // // u8string() gives UTF-8 bytes on MSVC (not ACP-converted), so a temp dir under an // accented or CJK user-name is handled correctly by REAPER's path APIs. if (ok) ok = TrackFX_SetPreset(track, fxIndex, presetPath.u8string().c_str()); // The preset file is transient regardless of outcome; delete via the retained path object // (not a re-parsed narrow string) so cleanup cannot leak even if the UTF-8 conversion // round-trip were incorrect. std::error_code ec; std::filesystem::remove(presetPath, ec); if (!ok && fxIndex >= 0) { // All-or-nothing: if the preset apply fails, remove the empty FX instance we just // added so the track is left exactly as it was. TrackFX_Delete signature (verified // in reaper_plugin_functions.h:7236): bool TrackFX_Delete(MediaTrack*, int fx). TrackFX_Delete(track, fxIndex); } return ok; } bool performInstrumentDrop(MediaTrack* track, const std::vector& presetBytes) { if (!track || presetBytes.empty()) return false; // One undo point for the whole gesture (mirrors the bank-verb undo discipline). Both the // FX add and the state apply are REAPER-undoable, so Ctrl-Z removes the instance cleanly. Undo_BeginBlock2(nullptr); const bool ok = loadInstrumentOntoTrack(track, presetBytes); // The undo label reflects the placement-of-the-player framing (not a capture, not an insert). Undo_EndBlock2(nullptr, "ReaSampler: drop capture onto FX chain", -1); return ok; } } // namespace reasampler