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
+70 -88
View File
@@ -1,29 +1,79 @@
// instrument_drop — pure implementation. See instrument_drop.h.
// NO REAPER / SWELL / VST3 SDK / vendor. Reuses sample_map's ComponentState serializer.
// NO REAPER / SWELL / VST3 SDK / vendor. Reuses sample_map's ComponentState serializer and
// the SDK-free UID macros (vst/reasampler_uid.h).
#include "instrument_drop.h"
#include "vst/sample_map.h" // ComponentState + serializeComponentState (the SHARED writer)
#include <cstdio>
#include "vst/reasampler_uid.h" // REASAMPLER_ACTIVE_UID_* — the FROZEN, channel-selected class UID
#include "vst/sample_map.h" // ComponentState + serializeComponentState (the SHARED writer)
namespace reasampler {
namespace {
constexpr char kB64Alphabet[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
// Little-endian appenders — the .vstpreset container stores its integers little-endian on
// disk (public.sdk vstpresetfile.cpp swaps only on big-endian hosts).
void appendU32LE(std::vector<std::uint8_t>& out, std::uint32_t v) {
out.push_back(static_cast<std::uint8_t>(v & 0xFF));
out.push_back(static_cast<std::uint8_t>((v >> 8) & 0xFF));
out.push_back(static_cast<std::uint8_t>((v >> 16) & 0xFF));
out.push_back(static_cast<std::uint8_t>((v >> 24) & 0xFF));
}
// -1 = not a base64 char; index by unsigned byte. Built once.
int b64Value(unsigned char c) {
if (c >= 'A' && c <= 'Z') return c - 'A';
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
if (c >= '0' && c <= '9') return c - '0' + 52;
if (c == '+') return 62;
if (c == '/') return 63;
return -1;
void appendU64LE(std::vector<std::uint8_t>& out, std::uint64_t v) {
for (int i = 0; i < 8; ++i)
out.push_back(static_cast<std::uint8_t>((v >> (8 * i)) & 0xFF));
}
void appendFourCC(std::vector<std::uint8_t>& out, const char id[4]) {
out.insert(out.end(), id, id + 4);
}
} // namespace
std::string vstClassIdHex() {
// FUID::toString reduces to the four INLINE_UID words as "%08X" in order on BOTH byte
// layouts (see header contract), so rendering the macros directly is the platform-stable
// derivation of the string the .vstpreset header must carry.
char buf[33];
std::snprintf(buf, sizeof(buf), "%08X%08X%08X%08X",
static_cast<unsigned>(REASAMPLER_ACTIVE_UID_1),
static_cast<unsigned>(REASAMPLER_ACTIVE_UID_2),
static_cast<unsigned>(REASAMPLER_ACTIVE_UID_3),
static_cast<unsigned>(REASAMPLER_ACTIVE_UID_4));
return std::string(buf, 32);
}
std::vector<std::uint8_t> buildVstPresetBytes(
const std::string& classIdHex32, const std::vector<std::uint8_t>& componentState) {
std::vector<std::uint8_t> out;
if (classIdHex32.size() != 32) return out; // contract violation -> empty, never throws
// Header (48 bytes): 'VST3' + int32 version + 32-char class ID + int64 list offset.
constexpr std::uint64_t kHeaderSize = 4 + 4 + 32 + 8;
const std::uint64_t compSize = componentState.size();
const std::uint64_t listOffset = kHeaderSize + compSize;
out.reserve(static_cast<std::size_t>(listOffset) + 4 + 4 + (4 + 8 + 8));
appendFourCC(out, "VST3");
appendU32LE(out, 1); // kFormatVersion
out.insert(out.end(), classIdHex32.begin(), classIdHex32.end());
appendU64LE(out, listOffset);
// Data area: the one 'Comp' chunk's bytes, at offset kHeaderSize.
out.insert(out.end(), componentState.begin(), componentState.end());
// Chunk list: 'List' + entry count + one entry {'Comp', offset, size}.
appendFourCC(out, "List");
appendU32LE(out, 1);
appendFourCC(out, "Comp");
appendU64LE(out, kHeaderSize);
appendU64LE(out, compSize);
return out;
}
std::vector<std::uint8_t> instrumentDropStateBytes(const std::string& sampleId) {
// The ONE fact the drop carries: this capture is the instance's selection. Everything
// else stays at the fresh-instance defaults (no zones, mono, generation 0) — the same
@@ -35,86 +85,18 @@ std::vector<std::uint8_t> instrumentDropStateBytes(const std::string& sampleId)
return serializeComponentState(cs);
}
std::string buildInstrumentDropChunk(const std::string& sampleId) {
return encodeBase64(instrumentDropStateBytes(sampleId));
std::vector<std::uint8_t> buildInstrumentDropPreset(const std::string& sampleId) {
return buildVstPresetBytes(vstClassIdHex(), instrumentDropStateBytes(sampleId));
}
bool infoNamesFxHotspot(const std::string& info) {
// See the header contract. Two documented FX-bearing surfaces (SDK §GetThingFromPoint):
// "fx_chain" / "fx_N" — the FX-chain and floating-FX windows.
// "tcp.fx" / "mcp.fx" — the TCP / MCP FX button specifically.
// Bare "tcp" / "mcp" and any other "tcp.*" / "mcp.*" sub-element (e.g. "tcp.mute",
// "tcp.vol") are track-panel hits that are NOT on the FX button — those must not trigger
// an instrument drop. Exact-string match for the two .fx tokens; prefix-match for fx_.
if (info == "tcp.fx" || info == "mcp.fx") return true;
// See the header contract. Prefix rule (S-GA-DropFX): "fx_" names the FX-chain /
// floating-FX windows; "tcp.fx" / "mcp.fx" prefixes name the TCP/MCP FX button and its
// sibling FX sub-elements (fxbyp/fxparm/fxembed/fxlist...), tolerant of the
// SDK-documented "may append additional information". Bare "tcp"/"mcp" and non-FX
// sub-elements ("tcp.mute", "tcp.vol") must NOT trigger an instrument drop.
auto startsWith = [&info](const char* p) { return info.rfind(p, 0) == 0; };
return startsWith("fx_");
}
std::string encodeBase64(const std::vector<std::uint8_t>& bytes) {
std::string out;
out.reserve(((bytes.size() + 2) / 3) * 4);
std::size_t i = 0;
const std::size_t n = bytes.size();
while (i + 3 <= n) {
const std::uint32_t triple = (static_cast<std::uint32_t>(bytes[i]) << 16) |
(static_cast<std::uint32_t>(bytes[i + 1]) << 8) |
static_cast<std::uint32_t>(bytes[i + 2]);
out.push_back(kB64Alphabet[(triple >> 18) & 0x3F]);
out.push_back(kB64Alphabet[(triple >> 12) & 0x3F]);
out.push_back(kB64Alphabet[(triple >> 6) & 0x3F]);
out.push_back(kB64Alphabet[triple & 0x3F]);
i += 3;
}
const std::size_t rem = n - i;
if (rem == 1) {
const std::uint32_t triple = static_cast<std::uint32_t>(bytes[i]) << 16;
out.push_back(kB64Alphabet[(triple >> 18) & 0x3F]);
out.push_back(kB64Alphabet[(triple >> 12) & 0x3F]);
out.push_back('=');
out.push_back('=');
} else if (rem == 2) {
const std::uint32_t triple = (static_cast<std::uint32_t>(bytes[i]) << 16) |
(static_cast<std::uint32_t>(bytes[i + 1]) << 8);
out.push_back(kB64Alphabet[(triple >> 18) & 0x3F]);
out.push_back(kB64Alphabet[(triple >> 12) & 0x3F]);
out.push_back(kB64Alphabet[(triple >> 6) & 0x3F]);
out.push_back('=');
}
return out;
}
std::vector<std::uint8_t> decodeBase64(const std::string& b64) {
std::vector<std::uint8_t> out;
if (b64.size() % 4 != 0) return out; // malformed length -> empty (never throws)
out.reserve((b64.size() / 4) * 3);
for (std::size_t i = 0; i < b64.size(); i += 4) {
const char c0 = b64[i], c1 = b64[i + 1], c2 = b64[i + 2], c3 = b64[i + 3];
const int v0 = b64Value(static_cast<unsigned char>(c0));
const int v1 = b64Value(static_cast<unsigned char>(c1));
if (v0 < 0 || v1 < 0) return {}; // illegal char in a non-pad position -> empty
// Padding is only legal in the last two positions of the last quad.
const bool pad2 = (c2 == '=');
const bool pad3 = (c3 == '=');
if ((pad2 || pad3) && i + 4 != b64.size()) return {}; // pad before the final quad
if (pad2 && !pad3) return {}; // "=X" is malformed
std::uint32_t triple = (static_cast<std::uint32_t>(v0) << 18) |
(static_cast<std::uint32_t>(v1) << 12);
out.push_back(static_cast<std::uint8_t>((triple >> 16) & 0xFF));
if (!pad2) {
const int v2 = b64Value(static_cast<unsigned char>(c2));
if (v2 < 0) return {};
triple |= static_cast<std::uint32_t>(v2) << 6;
out.push_back(static_cast<std::uint8_t>((triple >> 8) & 0xFF));
if (!pad3) {
const int v3 = b64Value(static_cast<unsigned char>(c3));
if (v3 < 0) return {};
triple |= static_cast<std::uint32_t>(v3);
out.push_back(static_cast<std::uint8_t>(triple & 0xFF));
}
}
}
return out;
return startsWith("fx_") || startsWith("tcp.fx") || startsWith("mcp.fx");
}
} // namespace reasampler