feat(vst): S18 VST3 channel isolation — beta ReaSampler 9000 pairs with beta extension only

Fork the VST3 class UID (beta UID minted+frozen), on-disk name, and display
name by the one channel bit via app_version; couple UID selection to the
channel at compile time so a refactor can't split identity from data.
This commit is contained in:
2026-07-26 23:54:10 -04:00
parent 0e9ef05c75
commit fe55a9a96e
8 changed files with 193 additions and 36 deletions
+18
View File
@@ -86,6 +86,24 @@ const std::string& dockIdent() {
return kIdent;
}
const std::string& vstOutputName() {
// The .vst3 module OUTPUT_NAME base — FOREVER-STABLE per channel. Stable is
// byte-identical to pre-S18 ("reasampler_9000"); beta is isolated so both install
// side-by-side without a filename collision.
static const std::string kName =
kIsBeta ? "reasampler_9000_beta" : "reasampler_9000";
return kName;
}
const std::string& vstPluginName() {
// The factory display name / editor title / embed label. Stable is byte-identical to
// pre-S18 ("ReaSampler 9000"); beta appends " beta" so the two channels are distinct
// plugins in the FX browser.
static const std::string kName =
kIsBeta ? "ReaSampler 9000 beta" : "ReaSampler 9000";
return kName;
}
std::string channelCommandId(const std::string& suffix) {
return commandIdPrefix() + suffix;
}
+27
View File
@@ -104,6 +104,33 @@ const std::string& binaryName();
const std::string& dockTitle();
const std::string& dockIdent();
// --- VST3 instrument identity (S18, beta-in-isolation) ------------------------------
//
// The ReaSampler 9000 VST3 instrument forks its plugin identity per channel exactly as the
// extension forks its binary/dock idents above — one channel per binary, all derived from
// the ONE channel bit here, so the VST shell carries no #ifdef fork. These are the VST's
// analogues of binaryName()/dockTitle(): the on-disk module name and the human-facing name.
//
// vstOutputName() — the CMake OUTPUT_NAME base for the .vst3 module. Stable:
// "reasampler_9000" (byte-identical to pre-S18). Beta:
// "reasampler_9000_beta". Mirrors the CMake target's OUTPUT_NAME (the
// authoritative artifact name); exposed here so the one derivation lives
// in this module. FOREVER-STABLE per channel — the on-disk filename a
// REAPER project's saved instance path may reference.
// vstPluginName() — the factory display name (FX browser), editor title band, and S6
// embed-strip label. Stable: "ReaSampler 9000". Beta:
// "ReaSampler 9000 beta". Sourced from here, never a literal in
// reasampler_vst.h / vst_entry.cpp / the editor / the embed strip.
//
// NOTE: the VST3 CLASS UID is NOT here — a UID is not a string derivation but a compile-time
// FUID/INLINE_UID constant the factory needs in brace-init form; it lives in reasampler_vst.h,
// channel-selected by the same REASAMPLER_CHANNEL_IS_BETA bit. This module owns the string
// identity; reasampler_vst.h owns the binary UID identity. The version display the factory
// stamps into PClassInfo2 reuses appVersion() (it already renders "-beta" on beta) — no
// separate VST version accessor.
const std::string& vstOutputName();
const std::string& vstPluginName();
// --- Channel-qualified action id / name builders ------------------------------------
//
// The two composition helpers every action-registering shell (main.cpp, actions.cpp)
+2 -2
View File
@@ -18,7 +18,7 @@
#include "peaks.h" // computeEnvelope
#include "reaper_bridge.h"
#include "reasampler_processor.h"
#include "reasampler_vst.h" // kPluginName (the editor title band)
#include "app_version.h" // vstPluginName (channel-derived editor title band, S18)
#include "sample_map.h"
#include "wav_trim.h" // parseWavLayout, extractFloatFrames
#include "waveform_view.h" // frame<->pixel markers + zero-crossing snap (S11)
@@ -477,7 +477,7 @@ void ReaSamplerEditor::paint(HDC hdc) {
// Title band: product name + live readout.
LICE_FillRect(&bmp, bands.title.left, bands.title.top, bands.title.width(),
bands.title.height(), kColTitleBg, 1.0f, 0);
std::string title = kPluginName;
std::string title = reasampler::vstPluginName(); // channel-derived (S18)
if (processor_ && processor_->bridge().isConnected()) {
if (samples_.empty()) title += " [bank empty]";
else if (selectedId_.empty() && map_.zones.empty()) title += " [pick a capture]";
+3 -2
View File
@@ -7,6 +7,7 @@
#include <string>
#include <vector>
#include "app_version.h" // vstPluginName (channel-derived embed label, S18)
#include "editor_geometry.h" // Rect (shared with embed_strip)
#include "embed_strip.h" // the pure strip layout + hit-test
#include "ext_keys.h" // kProjExtBanksKey
@@ -161,8 +162,8 @@ bool ReaSamplerEmbed::paint(TPtrInt bitmap, TPtrInt drawInfo) {
SetTextColor(dc, kRgbText);
RECT gr{layout.keymap.left + 4, layout.keymap.top, layout.keymap.right,
layout.keymap.bottom};
const std::string label =
samples_.empty() ? "ReaSampler 9000 (bank empty)" : "ReaSampler 9000 (no zones)";
const std::string label = reasampler::vstPluginName() + // channel-derived (S18)
(samples_.empty() ? " (bank empty)" : " (no zones)");
DrawTextA(dc, label.c_str(), -1, &gr,
DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX);
} else {
+57 -20
View File
@@ -2,41 +2,78 @@
// (Phase S). One place for the plugin's class UID, name, vendor, and version so the
// processor, factory, and editor agree.
//
// The class UID is FOREVER-STABLE once shipped: a REAPER project that instantiates this
// instrument records the UID, so changing it orphans every saved instance. Minted once
// for the spike; do not regenerate.
// A class UID is FOREVER-STABLE once shipped: a REAPER project that instantiates this
// instrument records the UID, so changing it orphans every saved instance. Minted once;
// do not regenerate.
//
// CHANNEL ISOLATION (S18, beta-in-isolation — the instrument-side companion to V4). Just
// as V4 gave the extension a per-channel ext-state namespace / command-id family / dock
// ident, S18 gives the VST3 instrument a per-channel PLUGIN IDENTITY: its class UID, its
// on-disk filename, and its display name all fork by the ONE channel bit
// (REASAMPLER_CHANNEL_IS_BETA, from version_generated.h). ONE class per binary — the bit
// selects which UID compiles into the single DEF_CLASS2, so a beta build carries only the
// beta identity and can never present the stable one (mirrors V4's fully-isolated-binary
// philosophy). The two UIDs below are BOTH frozen forever; the filename + display name
// derive from app_version's vstOutputName()/vstPluginName() (this header owns only the
// binary UID identity — the string identity lives in the pure module).
#pragma once
#include "pluginterfaces/base/funknown.h"
#include "version_generated.h" // REASAMPLER_CHANNEL_IS_BETA — the one channel bit
namespace reasampler::vst {
// Human-facing identity (S-NAME-1, SETTLED 2026-07-26). "ReaSampler" is the extension
// (capture + organization); the MIDI-playback instrument's product name is
// "ReaSampler 9000" — the string that surfaces in REAPER's FX browser, the factory
// display name, the editor title band, and the embed-strip label. The on-disk module is
// renamed to match (CMake OUTPUT_NAME reasampler_9000.vst3). The VST3 class UID below is
// the compat anchor and is NOT changed by the rename — a saved REAPER project rebinds a
// saved instance by class UID, so the display/filename rename keeps existing instances
// resolving (compat is a DAW-verify; see PLAN.md §S-NAME-1).
inline constexpr const char* kPluginName = "ReaSampler 9000";
// Vendor identity (S-NAME-1, SETTLED 2026-07-26). Shared across channels — V4 kept the
// lane-name prefix shared, so shared-where-V4-shares is the default (the channel is carried
// by the UID + filename + display fork, not the vendor block).
inline constexpr const char* kVendorName = "ReaSampler";
inline constexpr const char* kVendorUrl = "https://github.com/daniel-c-harvey/reasampler";
inline constexpr const char* kVendorEmail = "mailto:the.real.daniel.harvey@gmail.com";
// The processor class UID (the SingleComponentEffect). FOREVER-STABLE once shipped —
// a saved REAPER project records it, so changing it orphans every saved instance.
// Minted once for the S1 spike (2026-07-26). Defined as four longs so the factory's
// INLINE_UID (compile-time brace init) and the runtime FUID below share one source.
// -----------------------------------------------------------------------------------------
// The two FOREVER-FROZEN VST3 class UIDs — one per channel. A saved REAPER project records
// the UID of the instance it instantiated and rebinds by it on reopen, so EACH is a
// permanent commitment: changing either orphans every saved instance of that channel. The
// channel bit selects which one this binary's factory registers (below) — one class per
// binary, never both. Documented with the SAME gravity: neither may EVER be regenerated.
// STABLE class UID (S-NAME-1). Minted at the S1 spike (2026-07-26), locked. FROZEN FOREVER.
#define REASAMPLER_PROC_UID_1 0x5E45A11E
#define REASAMPLER_PROC_UID_2 0x9C7B4D6A
#define REASAMPLER_PROC_UID_3 0xB1E3F208
#define REASAMPLER_PROC_UID_4 0x4A6C1D9F
static const Steinberg::FUID kReaSamplerProcessorUID(REASAMPLER_PROC_UID_1,
REASAMPLER_PROC_UID_2,
REASAMPLER_PROC_UID_3,
REASAMPLER_PROC_UID_4);
// BETA class UID (S18). Minted once (2026-07-26), locked FROM THIS WAVE per Daniel's
// fast-track (fork S18-F1: mint now, not at first beta release). FROZEN FOREVER — the same
// permanent lock as the stable UID; do not regenerate even though no beta VST has shipped.
#define REASAMPLER_PROC_UID_BETA_1 0xCCFFEB3A
#define REASAMPLER_PROC_UID_BETA_2 0x4FF532A6
#define REASAMPLER_PROC_UID_BETA_3 0x9E181798
#define REASAMPLER_PROC_UID_BETA_4 0x4256955F
// The channel-selected UID macros the factory's INLINE_UID (compile-time brace init) and the
// runtime FUID below both source, so exactly one class UID is compiled into this binary. This
// is the ONLY channel #ifdef in the VST shell (an INLINE_UID needs literal brace-init tokens,
// so it cannot route through app_version's runtime string accessors — the header owns the
// binary UID fork, app_version owns the string fork).
#if REASAMPLER_CHANNEL_IS_BETA
#define REASAMPLER_ACTIVE_UID_1 REASAMPLER_PROC_UID_BETA_1
#define REASAMPLER_ACTIVE_UID_2 REASAMPLER_PROC_UID_BETA_2
#define REASAMPLER_ACTIVE_UID_3 REASAMPLER_PROC_UID_BETA_3
#define REASAMPLER_ACTIVE_UID_4 REASAMPLER_PROC_UID_BETA_4
#else
#define REASAMPLER_ACTIVE_UID_1 REASAMPLER_PROC_UID_1
#define REASAMPLER_ACTIVE_UID_2 REASAMPLER_PROC_UID_2
#define REASAMPLER_ACTIVE_UID_3 REASAMPLER_PROC_UID_3
#define REASAMPLER_ACTIVE_UID_4 REASAMPLER_PROC_UID_4
#endif
// The runtime FUID for the class this binary registers — the channel-selected UID above.
static const Steinberg::FUID kReaSamplerProcessorUID(REASAMPLER_ACTIVE_UID_1,
REASAMPLER_ACTIVE_UID_2,
REASAMPLER_ACTIVE_UID_3,
REASAMPLER_ACTIVE_UID_4);
} // namespace reasampler::vst
+39 -8
View File
@@ -21,24 +21,55 @@
#include "pluginterfaces/vst/ivstaudioprocessor.h" // kVstAudioEffectClass, PlugType
#include "app_version.h" // vstPluginName / appVersion — the channel-derived identity
#include "ext_keys.h" // kProjExtNamespace — the pairing-surface assertion target
#include "reasampler_processor.h"
#include "reasampler_vst.h"
#include "reasampler_vst.h" // channel-selected class UID (REASAMPLER_ACTIVE_UID_*)
// A concrete version string for PClassInfo2. Phase V owns the real version scheme; the
// spike ships a fixed 0.1.0.
#define REASAMPLER_VST_VERSION "0.1.0.0"
// CHANNEL PAIRING INVARIANT (S18). The instrument's PLUGIN identity forks by the ONE channel
// bit (REASAMPLER_CHANNEL_IS_BETA — the class UID selected in reasampler_vst.h, the filename
// + display name in app_version). Its DATA identity forks by the SAME bit, one layer down:
// ext_keys.h's kProjExtNamespace() delegates to app_version::extStateNamespace(), so a beta
// binary reads "reasampler_beta". Both derive from that one bit, so a beta VST can only ever
// talk to the beta extension.
//
// The guard below pins the two forks together so a refactor cannot split them. It asserts
// that the CLASS UID this factory registers (REASAMPLER_ACTIVE_UID_1, selected by the #if in
// reasampler_vst.h) is the UID that matches THIS binary's channel bit. If someone edited that
// #if to pick the wrong branch — registering the stable UID in a beta build, or vice versa —
// the instrument's identity would diverge from the namespace ext_keys reads (a beta-named
// plugin presenting the stable UID, or reading the stable banks under a beta identity). That
// is exactly the silent split the invariant forbids, and it breaks the build here instead.
// (The namespace itself is a runtime accessor — .c_str() on a channel-selected string — so
// the couplable compile-time fact is the UID selection, not the namespace value; the
// app_version_tests pin the namespace string per channel.)
#if REASAMPLER_CHANNEL_IS_BETA
static_assert(REASAMPLER_ACTIVE_UID_1 == REASAMPLER_PROC_UID_BETA_1,
"S18: a beta build must register the BETA class UID that pairs with the beta "
"extension's ext-state namespace — the UID selection and the channel bit split");
#else
static_assert(REASAMPLER_ACTIVE_UID_1 == REASAMPLER_PROC_UID_1,
"S18: a stable build must register the STABLE class UID that pairs with the "
"stable ext-state namespace — the UID selection and the channel bit split");
#endif
BEGIN_FACTORY(reasampler::vst::kVendorName, reasampler::vst::kVendorUrl,
reasampler::vst::kVendorEmail, Steinberg::PFactoryInfo::kNoFlags)
DEF_CLASS2(INLINE_UID(REASAMPLER_PROC_UID_1, REASAMPLER_PROC_UID_2,
REASAMPLER_PROC_UID_3, REASAMPLER_PROC_UID_4),
// The display name and version are channel-derived from app_version — sourced here, not
// as literals. DEF_CLASS2 expands inside GetPluginFactory() and PClassInfo2's constructor
// copies the char* into its own fixed buffer at that runtime call, so .c_str() on the
// accessors' static-storage strings is valid (no dangling — the refs outlive the copy).
// vstPluginName(): "ReaSampler 9000" / "ReaSampler 9000 beta". appVersion(): "0.9.01" /
// "0.9.01-beta" (the -beta render V4 already yields on beta).
DEF_CLASS2(INLINE_UID(REASAMPLER_ACTIVE_UID_1, REASAMPLER_ACTIVE_UID_2,
REASAMPLER_ACTIVE_UID_3, REASAMPLER_ACTIVE_UID_4),
Steinberg::PClassInfo::kManyInstances, // cardinality
kVstAudioEffectClass, // component category (fixed)
reasampler::vst::kPluginName, // plug-in display name
reasampler::vstPluginName().c_str(), // plug-in display name (channel-derived)
0, // single-component => 0
Steinberg::Vst::PlugType::kInstrumentSynthSampler, // subcategory
REASAMPLER_VST_VERSION, // plug-in version
reasampler::appVersion().c_str(), // plug-in version (channel: -beta render)
kVstVersionString, // VST3 SDK version (fixed)
reasampler::vst::ReaSamplerProcessor::createInstance)