fix(drop-fx): inject via .vstpreset + TrackFX_SetPreset (vst_chunk is REAPER-framed, raw bytes silently no-op); FX hotspot now prefix tcp.fx*/mcp.fx*/fx_*

This commit is contained in:
2026-07-28 06:19:29 -04:00
parent 104a25f390
commit b4dd6dc9f1
11 changed files with 494 additions and 344 deletions
+65 -19
View File
@@ -5,7 +5,13 @@
#include "instrument_drop_win.h"
#include <atomic>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <string>
#include <system_error>
#include <vector>
#include "app_version.h" // vstPluginName() — the CHANNEL-correct FX name (stable/beta pairing)
#include "instrument_drop.h" // infoNamesFxHotspot — the PURE, unit-tested hotspot classifier
@@ -16,13 +22,40 @@
#define REAPERAPI_WANT_GetThingFromPoint
#define REAPERAPI_WANT_TrackFX_AddByName
#define REAPERAPI_WANT_TrackFX_Delete
#define REAPERAPI_WANT_TrackFX_SetNamedConfigParm
#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
// absolute path; empty string 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.
std::string writeTempPreset(const std::vector<std::uint8_t>& bytes) {
static std::atomic<unsigned> counter{0};
std::error_code ec;
const std::filesystem::path dir = std::filesystem::temp_directory_path(ec);
if (ec) return {};
const std::filesystem::path path =
dir / ("reasampler_drop_" + std::to_string(counter.fetch_add(1)) + ".vstpreset");
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.string();
}
} // namespace
FxDropTarget resolveFxDropTarget(int screenX, int screenY) {
FxDropTarget out;
char info[256] = {0};
@@ -33,35 +66,48 @@ FxDropTarget resolveFxDropTarget(int screenX, int screenY) {
MediaTrack* track = GetThingFromPoint(screenX, screenY, info, sizeof(info));
out.track = track;
out.overReaperUi = (track != nullptr) || (info[0] != '\0');
// S-VIEW-BUG-1: the hotspot is either the FX chain/floating window ("fx_*") OR the track/
// mixer panel that hosts the FX button ("tcp*"/"mcp*"). The pure classifier owns the rule.
// 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::string& chunkBase64) {
if (!track || chunkBase64.empty()) return false;
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::string 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.
// 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);
if (fxIndex < 0) return false;
bool ok = fxIndex >= 0;
// Inject the instrument's OWN component-state blob (the dragged capture pre-selected)
// via the documented vst_chunk write-parm. The blob was built by the shared writer
// (instrument_drop::buildInstrumentDropChunk -> sample_map::serializeComponentState),
// so these bytes are exactly what ReaSampler 9000's setState accepts.
const bool ok =
TrackFX_SetNamedConfigParm(track, fxIndex, "vst_chunk", chunkBase64.c_str());
if (!ok) {
// All-or-nothing: if the chunk write fails, remove the empty FX instance we just
// 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.
if (ok) ok = TrackFX_SetPreset(track, fxIndex, presetPath.c_str());
// The preset file is transient regardless of outcome; best-effort cleanup (temp dir).
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);
@@ -69,13 +115,13 @@ bool loadInstrumentOntoTrack(MediaTrack* track, const std::string& chunkBase64)
return ok;
}
bool performInstrumentDrop(MediaTrack* track, const std::string& chunkBase64) {
if (!track || chunkBase64.empty()) return false;
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 write are REAPER-undoable, so Ctrl-Z removes the instance cleanly.
// FX add and the state apply are REAPER-undoable, so Ctrl-Z removes the instance cleanly.
Undo_BeginBlock2(nullptr);
const bool ok = loadInstrumentOntoTrack(track, chunkBase64);
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;