// instrument_drop_win.cpp — see instrument_drop_win.h. main.cpp owns the API // pointers; this TU gets them extern via the WANT list. #include "shell/actions/instrument_drop_win.h" #include #include #include #include #include #include #include #include "core/version/app_version.h" // vstPluginName() — the CHANNEL-correct FX name (stable/beta pairing) #include "core/wire/instrument_drop.h" // classifyReaperSurface — the PURE, unit-tested 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_GetCount #define REAPERAPI_WANT_TrackFX_SetPreset #define REAPERAPI_WANT_Undo_BeginBlock2 #define REAPERAPI_WANT_Undo_EndBlock2 #include "reaper_plugin_functions.h" namespace reasampler { // Real-namespace-home using-declarations (Q-W6: the namespaces.h shim is retired). using version::vstPluginName; using wire::decideDropOutcome; using wire::DropAttempt; using wire::DropOutcome; using wire::classifyReaperSurface; namespace { // Writes `bytes` to a fresh uniquely-named .vstpreset in the OS temp dir; empty path // on any failure. The .vstpreset extension is load-bearing — TrackFX_SetPreset's // full-path form is documented for .vstpreset files (VST3). Transient: the caller // deletes it right after the SetPreset call. // // Non-throwing: every std::filesystem call uses the error_code overload, and the // whole body is try/catch-wrapped so no exception crosses the REAPER callback // boundary. Returns the path object (not a narrow string) so the caller can pass // path.u8string() to TrackFX_SetPreset (UTF-8, not ACP-converted) and delete via the // same retained path — never a re-parsed narrow string. 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: two concurrent REAPER instances (stable + beta) cannot // collide in the shared temp dir. 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 DropProbe probeDropTarget(int screenX, int screenY) { DropProbe out; char info[256] = {0}; // GetThingFromPoint may return a null track together with a valid info string (its own // doc-comment says so), so the track and the surface are two independent facts and both // are reported. The pure classifier owns every token rule. out.track = GetThingFromPoint(screenX, screenY, info, sizeof(info)); out.surface = classifyReaperSurface(info, out.track != nullptr); 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; // Channel-correct FX name ("VST3:ReaSampler 9000[ beta]") sourced from the same // accessor the VST factory display name derives from, so the pairing invariant // (beta extension <-> beta VST) has no literal to drift. const std::string fxName = "VST3:" + vstPluginName(); // An EXPLICIT top-level insertion position (instantiate <= -1000 IS the position, -1000 // = first in chain), not the bare -1 — this form is documented in the SDK header. Both // always create a new instance; the bare form additionally leaves placement to REAPER's // ambient FX-chain insert point, which a drop onto an FX container/chain-window is // suspected (unconfirmed by experiment) to move — if so, the index handed to // TrackFX_SetPreset and the instance just created would stop denoting the same FX and the // capture would never land. The bare-form retry keeps the reference path alive if the // positional form is ever refused. // recFX = false: a normal track FX chain instance, not a record/monitoring FX. const int insertPos = TrackFX_GetCount(track); int fxIndex = TrackFX_AddByName(track, fxName.c_str(), /*recFX=*/false, /*instantiate=*/-1000 - insertPos); // Only retry with the bare form when the chain is PROVABLY unchanged (count still // insertPos): a negative return with the count grown means the positional add DID create // an instance and just reported -1 — retrying then would add a SECOND instance, leaving // the first orphaned (no preset applied, unreachable for rollback), which is exactly the // all-or-nothing violation this contract forbids. if (fxIndex < 0 && TrackFX_GetCount(track) == insertPos) fxIndex = TrackFX_AddByName(track, fxName.c_str(), /*recFX=*/false, /*instantiate=*/-1); // 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. DropAttempt attempt; attempt.addedFxIndex = fxIndex; if (fxIndex >= 0) attempt.presetApplied = TrackFX_SetPreset(track, fxIndex, presetPath.u8string().c_str()); std::error_code ec; std::filesystem::remove(presetPath, ec); // transient regardless of outcome const DropOutcome outcome = decideDropOutcome(attempt); if (outcome.rollbackFxIndex >= 0) TrackFX_Delete(track, outcome.rollbackFxIndex); return outcome.loaded; } bool performInstrumentDrop(MediaTrack* track, const std::vector& presetBytes) { if (!track || presetBytes.empty()) return false; Undo_BeginBlock2(nullptr); const bool ok = loadInstrumentOntoTrack(track, presetBytes); Undo_EndBlock2(nullptr, "ReaSampler: drop capture onto FX chain", -1); return ok; } } // namespace reasampler