148 lines
6.6 KiB
C++
148 lines
6.6 KiB
C++
// 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 <atomic>
|
|
#include <cstdint>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <string>
|
|
#include <system_error>
|
|
#include <vector>
|
|
|
|
#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<std::uint8_t>& bytes) {
|
|
try {
|
|
static std::atomic<unsigned> 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<const char*>(bytes.data()),
|
|
static_cast<std::streamsize>(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<std::uint8_t>& 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<std::uint8_t>& 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
|