f3be4d8cce
capture.h realtime seam split to capture_realtime_shell.h; GetProjExtState grow-loop rehomed to core/wire/ext_state_read; stale persist.cpp/bank_panel.cpp comment refs fixed; CLAUDE.md persist/bank_book/actions bullets updated. Command-id suffixes, display phrases, and undo labels byte-identical.
162 lines
7.9 KiB
C++
162 lines
7.9 KiB
C++
// 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 "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" // 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 {
|
|
|
|
// Real-namespace-home using-declarations (Q-W6: the namespaces.h shim is retired).
|
|
using version::vstPluginName;
|
|
using wire::infoNamesFxHotspot;
|
|
|
|
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 the prune shell uses — see prune_fs.cpp's non-throwing scan 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<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 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<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
|
|
|
|
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<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;
|
|
|
|
// 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<std::uint8_t>& 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
|