diff --git a/CMakeLists.txt b/CMakeLists.txt index a0328a4..368cd3f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,9 +34,13 @@ set(REASAMPLER_CHANNEL "stable" CACHE STRING "Build channel: stable (default) or if(REASAMPLER_CHANNEL STREQUAL "beta") set(REASAMPLER_CHANNEL_IS_BETA 1) set(REASAMPLER_OUTPUT_NAME "reaper_reasampler_beta") + # The VST3 instrument's on-disk name forks the same way (S18) — must match + # app_version::vstOutputName() so the artifact name and the in-binary self-id agree. + set(REASAMPLER_VST_OUTPUT_NAME "reasampler_9000_beta") elseif(REASAMPLER_CHANNEL STREQUAL "stable") set(REASAMPLER_CHANNEL_IS_BETA 0) set(REASAMPLER_OUTPUT_NAME "reaper_reasampler") + set(REASAMPLER_VST_OUTPUT_NAME "reasampler_9000") else() message(FATAL_ERROR "REASAMPLER_CHANNEL must be 'stable' or 'beta' (got '${REASAMPLER_CHANNEL}')") @@ -908,9 +912,12 @@ if(WIN32 AND EXISTS "${VST3_SDK}/public.sdk/source/main/pluginfactory.cpp") # WDL_INC gives LICE for the editor. The VST3 SDK headers come from vst3_sdk PUBLIC. target_include_directories(reasampler_vst PRIVATE ${SDK_INC} ${WDL_INC}) # A .vst3 is a DLL with a .vst3 extension and no lib-prefix. OUTPUT_NAME is the on-disk - # product name (S-NAME-1, SETTLED 2026-07-26): reasampler_9000.vst3, matching the - # "ReaSampler 9000" display strings. The VST3 class UID (reasampler_vst.h) is unchanged - # — the compat anchor a saved instance rebinds by (save-rename-reopen is a DAW-verify). + # product name, channel-forked (S18): reasampler_9000.vst3 (stable, byte-identical to + # pre-S18) / reasampler_9000_beta.vst3 (beta) — driven by REASAMPLER_VST_OUTPUT_NAME set + # from the ONE channel decision above, mirroring the extension's REASAMPLER_OUTPUT_NAME + # and matching app_version::vstOutputName(). The two channels install side-by-side; the + # per-channel VST3 class UID (reasampler_vst.h) keeps a saved instance rebinding to its + # own channel (save-rename-reopen is a DAW-verify). set_target_properties(reasampler_vst PROPERTIES PREFIX "" SUFFIX ".vst3" - OUTPUT_NAME "reasampler_9000") + OUTPUT_NAME "${REASAMPLER_VST_OUTPUT_NAME}") endif() diff --git a/src/app_version.cpp b/src/app_version.cpp index b51e7cd..247c24a 100644 --- a/src/app_version.cpp +++ b/src/app_version.cpp @@ -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; } diff --git a/src/app_version.h b/src/app_version.h index 052c273..3bee96f 100644 --- a/src/app_version.h +++ b/src/app_version.h @@ -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) diff --git a/src/vst/reasampler_editor.cpp b/src/vst/reasampler_editor.cpp index 24a4912..2e23c32 100644 --- a/src/vst/reasampler_editor.cpp +++ b/src/vst/reasampler_editor.cpp @@ -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]"; diff --git a/src/vst/reasampler_embed.cpp b/src/vst/reasampler_embed.cpp index ca4b5b9..792206f 100644 --- a/src/vst/reasampler_embed.cpp +++ b/src/vst/reasampler_embed.cpp @@ -7,6 +7,7 @@ #include #include +#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 { diff --git a/src/vst/reasampler_vst.h b/src/vst/reasampler_vst.h index fc1b942..b6343f5 100644 --- a/src/vst/reasampler_vst.h +++ b/src/vst/reasampler_vst.h @@ -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 diff --git a/src/vst/vst_entry.cpp b/src/vst/vst_entry.cpp index d24ce1a..29fc9ac 100644 --- a/src/vst/vst_entry.cpp +++ b/src/vst/vst_entry.cpp @@ -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) diff --git a/tests/test_app_version.cpp b/tests/test_app_version.cpp index 4f05eab..1b5a312 100644 --- a/tests/test_app_version.cpp +++ b/tests/test_app_version.cpp @@ -74,6 +74,40 @@ static void testChannelDerivedIdentityStrings() { } } +static void testVstIdentityStringsForkByChannel() { + // S18: the VST3 instrument's on-disk name and display name fork from the SAME one channel + // bit as the extension's idents above. Stable values are BYTE-IDENTICAL to pre-S18 — any + // drift in the stable branch orphans a saved instance's on-disk reference / mislabels the + // FX browser. These are the accessors vst_entry.cpp's factory, the editor title, and the + // embed label all source; each branch is the assertion the OTHER config's build would fail. + if (isBeta()) { + CHECK(vstOutputName() == "reasampler_9000_beta"); + CHECK(vstPluginName() == "ReaSampler 9000 beta"); + } else { + CHECK(vstOutputName() == "reasampler_9000"); + CHECK(vstPluginName() == "ReaSampler 9000"); + } + // The on-disk name must match the CMake OUTPUT_NAME fork (REASAMPLER_VST_OUTPUT_NAME): a + // divergence between this accessor and the artifact name would ship a binary whose + // self-identification disagrees with its filename. (The CMake side is the authoritative + // artifact name; this pins the in-binary derivation to the same two literals.) + CHECK(vstOutputName() == (isBeta() ? "reasampler_9000_beta" : "reasampler_9000")); +} + +static void testVstIdentityAndDataNamespaceShareOneChannel() { + // The S18 pairing invariant at the SEAM: the VST's plugin identity (its display/output + // names) and the DATA namespace its bridge reads (extStateNamespace(), what ext_keys + // delegates to) must resolve to the SAME channel — a beta-named plugin reading the stable + // namespace, or vice versa, is precisely the split the invariant forbids. Both forks fan + // out from the one isBeta() bit, so this assertion fails if EITHER fork regressed + // independently (a beta output name paired with the stable namespace trips the beta arm). + const bool identityIsBeta = + (vstPluginName() == "ReaSampler 9000 beta") && (vstOutputName() == "reasampler_9000_beta"); + const bool dataIsBeta = (extStateNamespace() == "reasampler_beta"); + CHECK(identityIsBeta == dataIsBeta); // identity and data agree on the channel + CHECK(identityIsBeta == isBeta()); // and both agree with the compiled bit +} + static void testChannelQualifiedIdAndNameComposition() { // The two composition helpers the shells funnel through. A representative shipped id // (CAPTURE_TRACK) and phrase must compose to the exact channel-qualified strings — this @@ -205,6 +239,8 @@ int main() { testVersionConstantRendersExactString(); testChannelDerivedRendering(); testChannelDerivedIdentityStrings(); + testVstIdentityStringsForkByChannel(); + testVstIdentityAndDataNamespaceShareOneChannel(); testChannelQualifiedIdAndNameComposition(); testStampClassifiesAsStampedOnOwnChannel(); testParseWellFormed();