Q-W1 pt2: core/shell/app relocation + sub-namespaces; one concrete ui::Rect (LTRB fork retired); slot_map split from bank_book; BankIndex→BankModel; 59/59 green

This commit is contained in:
2026-07-28 20:48:56 -04:00
parent 67a41728f3
commit 847936f813
222 changed files with 2247 additions and 2079 deletions
+161
View File
@@ -0,0 +1,161 @@
#include "core/namespaces.h"
// reaper_bridge.cpp — see reaper_bridge.h. The DAW-facing edge; keep it thin.
#include "shell/instrument/reaper_bridge.h"
#include <vector>
#include "core/instrument/map/bridge_marshal.h"
#include "core/capture/capture_paths.h" // projectDirOfRpp (shared M4 project-dir derivation)
#include "ext_keys.h" // kProjExtNamespace (shared wire contract)
// The VST3 base types must be included before REAPER's VST3 interface header, which
// uses FUnknown / CStringA / uint32 / DECLARE_CLASS_IID / PLUGIN_API from
// pluginterfaces/base — all in namespace Steinberg.
#include "pluginterfaces/base/funknown.h"
#include "pluginterfaces/base/ftypes.h"
// REAPER's VST3-side bridge interface (vendored). IReaperHostApplication is what REAPER
// passes (as an IHostApplication) to IComponent::initialize; it exposes getReaperApi
// (resolve-by-name) and getReaperParent (host context). The header uses UNQUALIFIED
// Steinberg types (FUnknown, CStringA, uint32, FUID, DECLARE_CLASS_IID, PLUGIN_API), so
// it must be pulled into the Steinberg namespace — the same way REAPER's own VST3
// examples include it.
namespace Steinberg {
#include "reaper_vst3_interfaces.h"
} // namespace Steinberg
// DECLARE_CLASS_IID in the REAPER header only DECLARES IReaperHostApplication::iid; some
// TU must DEFINE it. We do it here — this is the only place that queries for the
// interface (FUnknownPtr uses the iid), so the definition lives with its sole use.
DEF_CLASS_IID(Steinberg::IReaperHostApplication)
// The ext-state namespace is the SHARED wire contract between the extension (writer)
// and this instrument (reader); it lives in ext_keys.h (pure, REAPER-free) —
// reasampler::kProjExtNamespace() — so the two artifacts read one symbol and cannot
// drift. Channel-derived (Phase V, V4): the accessor returns "reasampler" (stable) or
// "reasampler_beta" (beta), matching whatever the extension wrote. The S1 spike
// duplicated it locally; that duplication is retired.
namespace reasampler::vst {
bool ReaperBridge::connect(Steinberg::FUnknown* context) {
getProjExtState_ = nullptr;
enumProjExtState_ = nullptr;
enumProjects_ = nullptr;
setProjExtState_ = nullptr;
getTrackGuid_ = nullptr;
guidToString_ = nullptr;
hostApp_ = nullptr;
if (!context) return false;
// Query the host context for REAPER's bridge interface. In a non-REAPER host this
// query fails and we stay unconnected — the instrument still loads.
Steinberg::FUnknownPtr<Steinberg::IReaperHostApplication> reaper(context);
if (!reaper) return false;
hostApp_ = reaper.get();
// Resolve the ext-state functions by name. getReaperApi returns the same function
// pointers the extension resolves via rec->GetFunc; a null return means the symbol
// is unavailable (very old REAPER) — degrade gracefully.
getProjExtState_ = reinterpret_cast<GetProjExtStateFn>(
reaper->getReaperApi("GetProjExtState"));
enumProjExtState_ = reinterpret_cast<EnumProjExtStateFn>(
reaper->getReaperApi("EnumProjExtState"));
// EnumProjects(-1, ...) yields the active project AND its .rpp path — the same call
// persist.cpp uses, so the instrument derives the project directory identically.
enumProjects_ = reinterpret_cast<EnumProjectsFn>(
reaper->getReaperApi("EnumProjects"));
// pS-usage: the (prefix-guarded) usage publish write + the track-identity pair the
// usage record stamps. All degrade to null gracefully — an old REAPER just never
// publishes usage (the extension then protects by bank references only).
setProjExtState_ = reinterpret_cast<SetProjExtStateFn>(
reaper->getReaperApi("SetProjExtState"));
getTrackGuid_ = reinterpret_cast<GetTrackGuidFn>(
reaper->getReaperApi("GetTrackGUID"));
guidToString_ = reinterpret_cast<GuidToStringFn>(
reaper->getReaperApi("guidToString"));
return getProjExtState_ != nullptr;
}
std::optional<std::string> ReaperBridge::readReasamplerExtState(const std::string& key) {
if (!getProjExtState_ || !hostApp_) return std::nullopt;
// Fetch the host project (getReaperParent(3) — project). Reads that live "reasampler"
// ext-state against the ACTIVE project the instrument was instantiated in, so it
// follows project switches for free (D6).
auto* reaper = static_cast<Steinberg::IReaperHostApplication*>(hostApp_);
void* proj = reaper->getReaperParent(3);
// A null project is legitimate (e.g. instantiated before a project context exists);
// REAPER treats null as the current project for these calls, so we pass it through
// rather than bailing — but if the read yields nothing the caller sees nullopt.
// GetProjExtState writes into a caller buffer; the bank blob can be large (many
// samples), so grow the buffer until the value fits rather than risk a silent
// truncation — mirrors persist.cpp's getProjExtStateString growing strategy. The
// return value is the value length; if it fits strictly inside the buffer it is
// complete, else grow and retry up to a 16 MB ceiling.
for (int cap = 1 << 16; cap <= (1 << 24); cap <<= 2) {
std::vector<char> buf(static_cast<std::size_t>(cap), '\0');
const int rv = getProjExtState_(proj, kProjExtNamespace(), key.c_str(),
buf.data(), cap);
if (rv <= 0) return std::nullopt; // absent / empty key
std::string s(buf.data());
if (static_cast<int>(s.size()) + 1 < cap) {
return decodeGetProjExtState(rv, s);
}
// else: possibly truncated -> grow and retry.
}
return std::nullopt; // pathologically large (>16 MB) — give up rather than loop
}
bool ReaperBridge::writeUsageExtState(const std::string& usageKey,
const std::string& value) {
if (!setProjExtState_ || !hostApp_) return false;
// STRUCTURAL read-only-bank guard: this module writes usage keys and nothing else.
// A non-"rsusage_" key is a programming error upstream — refuse rather than widen
// the instrument's write surface (banks/view/tail/assign stay extension-owned).
const std::string prefix = kProjExtUsageKeyPrefix;
if (usageKey.compare(0, prefix.size(), prefix) != 0) return false;
auto* reaper = static_cast<Steinberg::IReaperHostApplication*>(hostApp_);
void* proj = reaper->getReaperParent(3); // null = current project (same as reads)
// SetProjExtState returns "the size of the state for this extname" (SDK ~6288) —
// after storing our non-empty value the namespace state is necessarily > 0, so a
// <= 0 return means the write did not land. Reported to the caller (the publish
// path retries on the next reload tick); a silently-dropped record would leave the
// instance's holds unprotected.
const int rv =
setProjExtState_(proj, kProjExtNamespace(), usageKey.c_str(), value.c_str());
// Deliberately NO MarkProjectDirty: a usage change always accompanies a component-
// state change that already dirties the project; an idempotent load-time republish
// must not flag an untouched project as modified.
return rv > 0;
}
std::string ReaperBridge::currentTrackGuid() {
if (!hostApp_ || !getTrackGuid_ || !guidToString_) return {};
auto* reaper = static_cast<Steinberg::IReaperHostApplication*>(hostApp_);
void* track = reaper->getReaperParent(1); // the hosting MediaTrack*
if (!track) return {}; // no track context (unusual host state)
void* guid = getTrackGuid_(track);
if (!guid) return {};
char buf[64] = {0}; // guidToString's documented destNeed64 contract
guidToString_(guid, buf);
return std::string(buf);
}
std::string ReaperBridge::activeProjectDir() {
if (!enumProjects_) return {};
// idx=-1 is the current project tab; the out-buffer receives the full .rpp path,
// EMPTY for a never-saved project. Same call + convention as persist.cpp; the pure
// projectDirOfRpp turns the .rpp path into the project directory (parent, forward-
// slashed) and keeps an unsaved project's empty path empty (no default-location
// fallback — the tool's invariant).
std::vector<char> buf(4096, '\0');
enumProjects_(-1, buf.data(), static_cast<int>(buf.size()));
return projectDirOfRpp(std::string(buf.data()));
}
} // namespace reasampler::vst
+111
View File
@@ -0,0 +1,111 @@
#include "core/namespaces.h"
// reaper_bridge.h — the REAPER VST-host bridge (Phase S1 read spike). THIN shell:
// resolves REAPER API functions by name over the host context and reads the live
// "reasampler" project ext-state. The fiddly decode lives in bridge_marshal (pure).
//
// VERIFIED BRIDGE MECHANISM (corrects §1a's estimate). §1a described the VST2-style
// hostcb opcode pattern (hostcb(&effect, 0xdeadbeef, 0xdeadf00d, ...)). That is the
// VST2 path (video_processor.h documents it for a VST2 aEffect). For a VST3 plugin the
// bridge is exposed differently and more cleanly: REAPER passes an IHostApplication as
// the `context` to IComponent::initialize(FUnknown* context); querying it for
// IReaperHostApplication (vendor/reaper-sdk/sdk/reaper_vst3_interfaces.h) yields:
// * getReaperApi(funcname) -> resolve a REAPER API function pointer by name
// (the VST3 equivalent of opcode 0xdeadf00d), and
// * getReaperParent(3) -> the host ReaProject* (the VST3 equivalent of the
// 0xdeadf00e host-context fetch; 1=track, 2=take, 3=project, 4=fxdsp, 5=trackchan).
// So a VST3 uses IReaperHostApplication, not the raw hostcb opcodes. Verified against
// reaper_vst3_interfaces.h + reaper_plugin_functions.h at the spike.
#pragma once
#include <optional>
#include <string>
#include "pluginterfaces/base/funknown.h"
namespace reasampler::vst {
// Wraps the REAPER host bridge for a single plugin instance. Constructed cheaply;
// connect() must be called with the initialize() context before any read. All reads
// degrade to nullopt (never crash) when the host is not REAPER or a symbol is absent —
// the instrument must load in non-REAPER hosts too, just without live state.
class ReaperBridge {
public:
ReaperBridge() = default;
// Bind to the host. `context` is the FUnknown* REAPER hands IComponent::initialize.
// Returns true when the REAPER bridge is available (host is REAPER and the ext-state
// API resolved). Safe to call with a null or non-REAPER context — returns false.
bool connect(Steinberg::FUnknown* context);
// True once connect() found the REAPER host application AND resolved the ext-state
// functions.
bool isConnected() const { return getProjExtState_ != nullptr; }
// Read a "reasampler" ext-state value by key from the host's active project.
// Returns nullopt when unconnected, when the project can't be resolved, or when the
// key is absent. This is the S1 read-spike entry point.
//
// NOT REAL-TIME SAFE (it allocates a read buffer and calls into REAPER): callers on
// the audio thread MUST NOT invoke it. The S4 instrument reads on the main/UI thread
// and hands a snapshot to the process path (see reasampler_processor.cpp).
std::optional<std::string> readReasamplerExtState(const std::string& key);
// The active project's directory (the folder holding its .rpp), forward-slashed,
// no trailing slash — the M4 convention persist uses to place the bank alongside
// the .rpp. Empty for an unsaved project or when unconnected. The instrument
// resolves relative sample paths against this the SAME way persist does
// (capture_paths::projectDirOfRpp over EnumProjects(-1)'s .rpp path). Not RT-safe.
std::string activeProjectDir();
// Write THIS INSTANCE's usage record (pS-usage): the ONE sanctioned instrument-side
// ext-state write. `usageKey` MUST carry the "rsusage_" prefix (ext_keys.h's
// usageKeyFor) — any other key is REFUSED here, so the read-only-BANK invariant is
// enforced structurally: this module can publish the instance's own usage and
// nothing else (banks/view/tail/assign remain unwritable from the instrument).
// Returns true iff written (the SetProjExtState return is checked — a dropped
// write must not silently claim protection). NOT RT-safe (calls into REAPER) —
// publish sites are the off-audio-thread reload path only. Deliberately does NOT
// mark the project dirty: a usage change always rides a component-state change
// that already does.
bool writeUsageExtState(const std::string& usageKey, const std::string& value);
// The canonical "{XXXXXXXX-...}" GUID string of the track hosting this FX instance
// (getReaperParent(1) -> GetTrackGUID -> guidToString — the same rendering as the
// extension's track_guid::guidString, so usage records and the extension's live-FX
// enumeration compare byte-equal). Empty when unconnected or no track context (the
// usage reader then falls back to any-instance liveness — fail-safe). Not RT-safe.
std::string currentTrackGuid();
private:
// Resolved REAPER API function pointers (by name via getReaperApi). Signatures
// verified against reaper_plugin_functions.h.
using GetProjExtStateFn = int (*)(void* proj, const char* extname, const char* key,
char* valOutNeedBig, int valOutNeedBig_sz);
using EnumProjExtStateFn = bool (*)(void* proj, const char* extname, int idx,
char* keyOut, int keyOut_sz, char* valOut,
int valOut_sz);
// EnumProjects(-1, projfnOut, sz) -> active project + its .rpp path (SDK line
// ~1264). The instrument uses idx=-1 (current tab) so it follows the active project,
// and reads the .rpp path from the out-buffer exactly as persist.cpp does.
using EnumProjectsFn = void* (*)(int idx, char* projfnOut, int projfnOut_sz);
// SetProjExtState(proj, extname, key, value) -> int (SDK line ~6290). Used ONLY by
// writeUsageExtState (prefix-guarded) — see the read-only-bank note there.
using SetProjExtStateFn = int (*)(void* proj, const char* extname, const char* key,
const char* value);
// GetTrackGUID(MediaTrack*) -> GUID* (SDK ~3562) + guidToString(const GUID*, char*
// destNeed64) (SDK ~3848). Both held as opaque-pointer signatures so the header
// stays SDK-type-free; the GUID* is passed straight through, never dereferenced here.
using GetTrackGuidFn = void* (*)(void* tr);
using GuidToStringFn = void (*)(const void* g, char* destNeed64);
void* hostApp_ = nullptr; // IReaperHostApplication* (opaque here; used in .cpp)
GetProjExtStateFn getProjExtState_ = nullptr;
EnumProjExtStateFn enumProjExtState_ = nullptr;
EnumProjectsFn enumProjects_ = nullptr;
SetProjExtStateFn setProjExtState_ = nullptr;
GetTrackGuidFn getTrackGuid_ = nullptr;
GuidToStringFn guidToString_ = nullptr;
};
} // namespace reasampler::vst
+274
View File
@@ -0,0 +1,274 @@
#include "core/namespaces.h"
// reasampler_embed.cpp — see reasampler_embed.h. The IReaperUIEmbedInterface shell.
// Windows-only (D5); guarded so a non-Windows build degrades to a stub that reports
// "not supported" and draws nothing.
#include "shell/instrument/reasampler_embed.h"
#include <string>
#include <vector>
#include "core/version/app_version.h" // vstPluginName (channel-derived embed label, S18)
#include "core/instrument/map/bank_sync.h" // parseBankGeneration (S9 dirty-guard over the per-paint refresh)
#include "core/ui/component_geometry.h" // KitBox — the kit text/fill draw box (Phase L, L3)
#include "shell/panel/draw_kit.h" // the L1 draw kit: fillSurface/text (L3)
#include "core/instrument/ui/editor_geometry.h" // Rect (shared with embed_strip)
#include "core/instrument/ui/embed_strip.h" // the pure strip layout + hit-test
#include "ext_keys.h" // kProjExtBanksKey / kProjExtBankGenKey
#include "shell/instrument/reaper_bridge.h"
#include "reasampler_processor.h"
#include "core/ui/theme.h" // Role / InteractionState / spectralColor (L3)
// wdltypes.h first: it defines INT_PTR portably (and pulls <windows.h> on Windows), which
// reaper_plugin_fx_embed.h's REAPER_FXEMBED_IBitmap::Extended needs as its return type.
#include "wdltypes.h"
// REAPER's embed message/bitmap contract (vendored). REAPER_FXEMBED_IBitmap is an alias of
// LICE_IBitmap, and the WM_* / DrawInfo / SizeHints definitions live here.
#include "reaper_plugin_fx_embed.h"
#ifdef _WIN32
// LICE — the same drawing stack the IPlugView editor and bank_panel use. REAPER hands us a
// LICE bitmap; we draw into it with the same calls, then return (REAPER blits it).
#include "lice/lice.h"
#endif
using namespace Steinberg;
// DECLARE_CLASS_IID in the REAPER header only DECLARES IReaperUIEmbedInterface::iid; some
// TU must DEFINE it. This is the only place that answers queryInterface for it, so the
// definition lives with its sole use (mirrors reaper_bridge.cpp doing this for
// IReaperHostApplication).
DEF_CLASS_IID(Steinberg::IReaperUIEmbedInterface)
namespace reasampler::vst {
namespace {
#ifdef _WIN32
// Kit adapter (Phase L, L3): the embed shell's Rect (editor_geometry) -> the kit's KitBox
// (component_geometry). Every embed surface now draws by palette ROLE via the L1 kit, retiring
// the local pre-L1 forest-green palette + raw GDI DrawTextA.
KitBox toKitBox(const Rect& r) {
return KitBox{r.x, r.y, r.width, r.height};
}
// A short display name for a bank sample id, from the snapshotted list (the editor's helper,
// duplicated small rather than shared across the shell/pure boundary).
std::string sampleLabel(const std::vector<SampleChoice>& samples, const std::string& id) {
for (const SampleChoice& c : samples) {
if (c.id == id) return c.displayName.empty() ? c.id : c.displayName;
}
return "?";
}
#endif
// Project the instrument's performance map into the strip's minimal zone shape (key ranges
// only). Pure projection — kept here (shell side) because it reads PerformanceMap, a shell
// type; embed_strip stays free of it.
std::vector<EmbedZone> toEmbedZones(const PerformanceMap& map) {
std::vector<EmbedZone> out;
out.reserve(map.zones.size());
for (const PerformanceZone& z : map.zones) out.push_back(EmbedZone{z.lowNote, z.highNote});
return out;
}
} // namespace
tresult PLUGIN_API ReaSamplerEmbed::queryInterface(const TUID iid, void** obj) {
QUERY_INTERFACE(iid, obj, FUnknown::iid, IReaperUIEmbedInterface)
QUERY_INTERFACE(iid, obj, IReaperUIEmbedInterface::iid, IReaperUIEmbedInterface)
*obj = nullptr;
return kNoInterface;
}
void ReaSamplerEmbed::refresh() {
if (!processor_) {
samples_.clear();
map_.zones.clear();
selectedZone_ = -1;
return;
}
auto banks = processor_->bridge().readReasamplerExtState(reasampler::kProjExtBanksKey);
samples_ = banks ? listSamples(*banks) : std::vector<SampleChoice>{};
map_ = processor_->performanceMap();
if (selectedZone_ >= static_cast<int>(map_.zones.size())) selectedZone_ = -1;
}
void ReaSamplerEmbed::maybeRefresh() {
if (!processor_) { refresh(); return; } // clears state; cheap
// The performance map is a cheap in-process accessor (mutex + copy), and the editor may
// have edited zones with NO bank-content change — always re-snapshot it so a zone edit
// reflects immediately.
map_ = processor_->performanceMap();
if (selectedZone_ >= static_cast<int>(map_.zones.size())) selectedZone_ = -1;
// The EXPENSIVE part is the bank-blob bridge read (samples_). Gate it on the S9 bank-
// generation stamp (a small ext-state read): only re-read the bank when the generation
// changed since the last paint (a recapture / ingest / remove), or on the first paint
// (lastSeenBankGeneration_ == -1). A pre-S9 project reads generation 0; the first paint
// folds it and subsequent idle paints skip the bank read entirely.
std::int64_t currentGen = lastSeenBankGeneration_;
if (auto rawGen =
processor_->bridge().readReasamplerExtState(reasampler::kProjExtBankGenKey)) {
currentGen = parseBankGeneration(*rawGen);
} else if (lastSeenBankGeneration_ < 0) {
currentGen = 0; // unprimed + no stamp (pre-S9): treat as generation 0 for the first read
}
// Intentional asymmetry: a TRANSIENT bridge failure (readReasamplerExtState returned
// nullopt after we were already primed) leaves currentGen == lastSeenBankGeneration_,
// so the bank-blob read is skipped and the editor keeps its last-known sample list.
// A stale-but-intact list is better than clearing samples_ on every transient hiccup.
if (lastSeenBankGeneration_ < 0 || currentGen != lastSeenBankGeneration_) {
auto banks =
processor_->bridge().readReasamplerExtState(reasampler::kProjExtBanksKey);
samples_ = banks ? listSamples(*banks) : std::vector<SampleChoice>{};
lastSeenBankGeneration_ = currentGen;
}
}
TPtrInt ReaSamplerEmbed::embed_message(int msg, TPtrInt parm2, TPtrInt parm3) {
switch (msg) {
case REAPER_FXEMBED_WM_IS_SUPPORTED:
#ifdef _WIN32
return 1; // supported and available
#else
return 0; // not a build target off Windows
#endif
case REAPER_FXEMBED_WM_CREATE:
#ifdef _WIN32
// Create the kit's cached AA fonts before the first paint (Phase L, L3).
// Idempotent + process-global (shared with the editor in this binary); NOT torn
// down per-view — the OS reclaims the tiny static HFONT set at module unload.
kitFontsInit();
#endif
refresh(); // prime the first paint's snapshot
return 0;
case REAPER_FXEMBED_WM_DESTROY:
return 0;
case REAPER_FXEMBED_WM_GETMINMAXINFO: {
auto* hints = reinterpret_cast<REAPER_FXEMBED_SizeHints*>(parm3);
if (!hints) return 0;
// Minimum usable strip height: the keymap must not collapse below its floor
// (kEmbedKeymapMinHeight) plus the level band.
hints->min_width = 64;
hints->max_width = 0; // 0 = unconstrained
hints->min_height = kEmbedKeymapMinHeight + kEmbedLevelBandHeight;
hints->max_height = 0; // 0 = unconstrained
// Preferred aspect: wide strip, roughly 8:1 (w:h). 16.16 fixed point.
hints->preferred_aspect = (8 << 16) / 1;
hints->minimum_aspect = (4 << 16) / 1;
return 1;
}
#ifdef _WIN32
case REAPER_FXEMBED_WM_PAINT:
return paint(parm2, parm3) ? 1 : 0;
case REAPER_FXEMBED_WM_LBUTTONDOWN:
// Selection at most (S6): map the click to a zone; force a redraw if it changed.
return onMouseDown(parm3) ? REAPER_FXEMBED_RETNOTIFY_INVALIDATE : 0;
#endif
default:
return 0; // unhandled messages (cursor, wheel, hittest) fall through
}
}
#ifdef _WIN32
bool ReaSamplerEmbed::paint(TPtrInt bitmap, TPtrInt drawInfo) {
auto* bmp = reinterpret_cast<LICE_IBitmap*>(bitmap);
auto* di = reinterpret_cast<const REAPER_FXEMBED_DrawInfo*>(drawInfo);
if (!bmp || !di) return false;
const int w = di->width;
const int h = di->height;
if (w <= 0 || h <= 0) return false;
// Re-read live state each paint (UI thread) so the strip reflects keymap edits + bank
// changes without its own timer — REAPER repaints the embed surface on its cadence. S9
// dirty-guard: maybeRefresh does the EXPENSIVE bank-blob read only when the bank generation
// changed (the flagged S6 follow-up), always refreshing the cheap performance map.
maybeRefresh();
// REAPER hands us its own bitmap sized to the embed area; draw directly into it (unlike
// the editor, which owns a LICE_SysBitmap and BitBlt's). Origin is the bitmap's (0,0).
// Base canvas through the kit (bg/base + micro-gradient), Phase L L3.
fillSurface(bmp, KitBox{0, 0, w, h}, Role::BgBase, InteractionState::Rest);
const EmbedLayout layout = layoutEmbed(w, h);
if (map_.zones.empty()) {
// No opt-in zones authored: a faint bg/cell band spanning the keymap area so the strip
// reads as "present, no zones" — the default single-capture face lives in the editor.
LICE_FillRect(bmp, layout.keymap.x, layout.keymap.y, layout.keymap.width,
layout.keymap.height, toLice(roleColor(Role::BgCell)), 0.5f, 0);
const std::string label = reasampler::vstPluginName() + // channel-derived (S18)
(samples_.empty() ? " (bank empty)" : " (no zones)");
const Rect labelR = Rect::ltrb(layout.keymap.x + 4, layout.keymap.y, layout.keymap.right(),
layout.keymap.bottom());
text(bmp, toKitBox(labelR), label.c_str(), Font::Label, Role::TextPrimary, Align::Left);
} else {
// Draw each zone as a segment across the keymap span, first-match order (so the painted
// order matches selection + playback). Each segment takes its PASTEL SPECTRAL hue from
// the center of its key span (spectralColor — §4), so the strip reads as the same
// spectrum as the editor's keyboard strip. The SELECTED zone lifts to accent-primary
// + a static glow ("which zone is live", never a pulse — §3.5).
for (int i = 0; i < static_cast<int>(map_.zones.size()); ++i) {
const PerformanceZone& z = map_.zones[i];
const Rect r = zoneSegmentRect(layout, z.lowNote, z.highNote);
if (r.width <= 0) continue;
const bool sel = (i == selectedZone_);
if (sel) {
// Static glow halo, then the crisp accent-primary fill.
LICE_FillRect(bmp, r.x - 2, r.y, r.width + 4, r.height,
toLice(roleColor(Role::AccentHot)), 0.30f, 0);
LICE_FillRect(bmp, r.x, r.y, r.width, r.height,
toLice(roleColor(Role::AccentPrimary)), 1.0f, 0);
} else {
const double t = ((z.lowNote + z.highNote) * 0.5) / 127.0;
LICE_FillRect(bmp, r.x, r.y, r.width, r.height,
toLice(spectralColor(t)), 0.65f, 0);
}
LICE_DrawRect(bmp, r.x, r.y, r.width - 1, r.height - 1,
toLice(roleColor(Role::LineHairline)), 1.0f, 0);
// Label the segment with the sample name when it is wide enough to read. The
// selected (accent-fill) segment draws its label in bg/base for contrast (the
// tight text-on-pastel pair, §4); the rest in text/primary.
if (r.width >= 24) {
const Rect lr = Rect::ltrb(r.x + 3, r.y, r.right() - 2, r.bottom());
text(bmp, toKitBox(lr), sampleLabel(samples_, z.sampleId).c_str(),
Font::Label, sel ? Role::BgBase : Role::TextPrimary, Align::Left);
}
}
}
// The level band: a recessed bg/cell channel with an accent-primary fill following the
// live activity level (a direct level follow — the one permitted "motion", §3.5).
if (layout.levelBand.height > 0) {
fillSurface(bmp, toKitBox(layout.levelBand), Role::BgCell, InteractionState::Pressed);
const double level = processor_ ? processor_->embedActivityLevel() : 0.0;
const Rect fill = levelFillRect(layout, level);
if (fill.width > 0) {
LICE_FillRect(bmp, fill.x, fill.y, fill.width, fill.height,
toLice(roleColor(Role::AccentPrimary)), 1.0f, 0);
}
}
return true;
}
bool ReaSamplerEmbed::onMouseDown(TPtrInt drawInfo) {
auto* di = reinterpret_cast<const REAPER_FXEMBED_DrawInfo*>(drawInfo);
if (!di || di->width <= 0 || di->height <= 0) return false;
refresh();
const EmbedLayout layout = layoutEmbed(di->width, di->height);
const std::vector<EmbedZone> zones = toEmbedZones(map_);
const int hit = zoneAtPoint(layout, zones.data(), static_cast<int>(zones.size()),
di->mouse_x, di->mouse_y);
if (hit == selectedZone_) return false; // no change -> no redraw
selectedZone_ = hit;
return true;
}
#endif // _WIN32
} // namespace reasampler::vst
+113
View File
@@ -0,0 +1,113 @@
#include "core/namespaces.h"
// reasampler_embed.h — the S6 embedded TCP/MCP UI shell. Implements REAPER's
// IReaperUIEmbedInterface (vendor/reaper-sdk/sdk/reaper_plugin_fx_embed.h +
// reaper_vst3_interfaces.h) so the instrument draws a compact keymap/level strip INLINE in
// the track/mixer control panel — the same Cockos surface REAPER's own embedded FX use.
//
// VERIFIED CONTRACT (against reaper_plugin_fx_embed.h + reaper_vst3_interfaces.h):
// * VST3 exposes this by having the IEditController answer queryInterface for
// IReaperUIEmbedInterface (iid {0x049bf9e7,0xbc74ead0,0xc4101e86,0x7f725981}). Our
// SingleComponentEffect IS the edit controller, so the processor's queryInterface hands
// REAPER a reference to this object.
// * The single method is embed_message(int msg, TPtrInt parm2, TPtrInt parm3). msg is a
// REAPER_FXEMBED_WM_* value (aliased to Win32 WM_*):
// - WM_IS_SUPPORTED (0x0000): return 1 (supported+available), -1, or 0.
// - WM_CREATE (0x0001) / WM_DESTROY (0x0002): embed begin/end; return ignored.
// - WM_PAINT (0x000F): parm2 = REAPER_FXEMBED_IBitmap* (alias LICE_IBitmap) to draw
// into; parm3 = REAPER_FXEMBED_DrawInfo* (context TCP=1/MCP=2, width/height, mouse,
// flags). Return 1 if drawing occurred, 0 otherwise.
// - WM_GETMINMAXINFO (0x0024): parm3 = SizeHints*; return 1 if filled.
// - mouse WM_* (0x0200..0x020A): parm3 = DrawInfo*; return RETNOTIFY_INVALIDATE
// (0x1000000) to force a redraw. Capture is auto-managed by the host.
// * There is NO plugin-owned window/HWND here (unlike the IPlugView editor): REAPER hands
// a LICE bitmap per paint; we only draw into it and read mouse coords from DrawInfo.
//
// RT DISCIPLINE (S6 constraint): all embed messages arrive on REAPER's UI thread; nothing
// here runs in process(). It reads the same live state the editor reads (bank over the
// bridge + the processor's performance map) with the same off-audio-thread accessors — no
// new locks visible to process, read-only over the bank. Windows-only (D5), guarded so a
// non-Windows build stays compilable.
//
// The strip's LAYOUT + HIT-TEST is pure (embed_strip.h, unit-tested); this shell marshals
// REAPER's messages to/from it and draws with the same LICE idiom as reasampler_editor.
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include "pluginterfaces/base/funknown.h"
#include "core/instrument/map/sample_map.h" // SampleChoice, PerformanceMap (the state the strip reflects)
// REAPER's VST3-side embed interface (vendored). Uses UNQUALIFIED Steinberg types, so it is
// pulled into the Steinberg namespace the same way reaper_bridge.cpp includes the host
// interface header. Its iid is DEFINEd (DEF_CLASS_IID) in reasampler_embed.cpp.
namespace Steinberg {
#include "reaper_vst3_interfaces.h"
} // namespace Steinberg
namespace reasampler::vst {
class ReaSamplerProcessor;
// Implements IReaperUIEmbedInterface. Lifetime is OWNED by the processor (the processor
// holds the sole unique_ptr and hands out AddRef'd references from queryInterface); the
// back-pointer to the processor is therefore always valid while this lives.
class ReaSamplerEmbed : public Steinberg::IReaperUIEmbedInterface {
public:
explicit ReaSamplerEmbed(ReaSamplerProcessor* processor) : processor_(processor) {}
// The one embed entry point. Routes each REAPER_FXEMBED_WM_* message; see the header
// note above for the per-message contract. UI thread only.
Steinberg::TPtrInt embed_message(int msg, Steinberg::TPtrInt parm2,
Steinberg::TPtrInt parm3) override;
// FUnknown: this object's lifetime is owned by the processor, not the host refcount, so
// AddRef/release are no-ops (the processor's unique_ptr governs destruction) and
// queryInterface answers only FUnknown + IReaperUIEmbedInterface. This mirrors how the
// SDK's OBJ refcount would otherwise churn; here the owning processor guarantees the
// object outlives every borrowed reference REAPER holds during embedding.
Steinberg::tresult PLUGIN_API queryInterface(const Steinberg::TUID iid,
void** obj) override;
Steinberg::uint32 PLUGIN_API addRef() override { return 1000; }
Steinberg::uint32 PLUGIN_API release() override { return 1000; }
private:
#ifdef _WIN32
// Draw the current strip into REAPER's supplied LICE bitmap. Returns true if it drew.
bool paint(Steinberg::TPtrInt bitmap, Steinberg::TPtrInt drawInfo);
// Handle a mouse-down inside the strip: map to a zone and select it (S6: selection at
// most — no new editing semantics). Returns true if the selection changed (the caller
// then asks REAPER to invalidate).
bool onMouseDown(Steinberg::TPtrInt drawInfo);
#endif
// Snapshot the live bank + the instrument's performance map for the next paint, exactly
// as the editor's refreshSampleList does (bridge read + processor accessors, UI thread).
void refresh();
// The S9 dirty-guard over refresh() (the S6 flagged follow-up): read the cheap bank-
// generation stamp; do the EXPENSIVE bank-blob bridge read (refresh()) only when the
// generation changed since the last paint (or on the first paint) — the strip re-read
// per paint was wasteful now that a generation counter exists. The performance map (a
// cheap in-process accessor, edited by the editor independently of bank content) is
// ALWAYS refreshed so a zone edit still reflects immediately. UI thread only.
void maybeRefresh();
ReaSamplerProcessor* processor_ = nullptr;
// The bank generation last folded into samples_ (S9 dirty-guard). -1 forces the first
// maybeRefresh() to do a full read (no generation can be negative — parseBankGeneration
// yields >= 0 — so -1 is an "unprimed" sentinel distinct from a real generation 0).
std::int64_t lastSeenBankGeneration_ = -1;
// Snapshotted for the current paint (refreshed each paint off the audio thread).
std::vector<SampleChoice> samples_;
PerformanceMap map_;
// The zone the last click selected (local/visual only — S6 selection constraint; the
// processor's editor-shared selection is NOT updated from here); -1 = none.
// Drives the strip's highlight.
int selectedZone_ = -1;
};
} // namespace reasampler::vst
+46
View File
@@ -0,0 +1,46 @@
#pragma once
// reasampler_uid.h — the FOREVER-FROZEN VST3 class-UID constants, SDK-FREE.
//
// Split out of reasampler_vst.h (S-GA-DropFX) so the PURE extension side can derive the
// class-ID string a .vstpreset file carries (instrument_drop::vstClassIdHex) WITHOUT
// including the VST3 SDK: reasampler_vst.h needs Steinberg::FUID (SDK), but the UID VALUES
// are plain integer macros. This header owns the values + the channel selection; nothing
// else. reasampler_vst.h includes it to build the runtime FUID; instrument_drop includes it
// to render the 32-char hex string. ONE source of truth — the frozen constants are written
// exactly once, here.
//
// A class UID is FOREVER-STABLE once shipped: a REAPER project that instantiates the
// instrument records the UID, so changing it orphans every saved instance. Minted once;
// do not regenerate. See reasampler_vst.h for the full channel-isolation story (S18).
#include "version_generated.h" // REASAMPLER_CHANNEL_IS_BETA — the one channel bit
// 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
// 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 — exactly one class UID per binary. The factory's
// INLINE_UID (compile-time brace init) and the runtime FUID in reasampler_vst.h both source
// these, as does the extension's vstClassIdHex (the .vstpreset class-ID string), so the
// binary identity and the preset-file identity cannot diverge.
#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
+53
View File
@@ -0,0 +1,53 @@
#include "core/namespaces.h"
// reasampler_vst.h — shared identity constants for the ReaSampler VST3 instrument
// (Phase S). One place for the plugin's class UID, name, vendor, and version so the
// processor, factory, and editor agree.
//
// 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 "shell/instrument/reasampler_uid.h" // the FROZEN UID macros + channel selection (SDK-free values)
namespace reasampler::vst {
// 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 two FOREVER-FROZEN VST3 class UIDs — one per channel — live in reasampler_uid.h
// (SDK-free, so the extension's pure instrument_drop can render the .vstpreset class-ID
// string from the SAME constants without pulling the VST3 SDK). A saved REAPER project
// records the UID of the instance it instantiated and rebinds by it on reopen, so each is
// a permanent commitment. The channel bit selects which one this binary's factory registers
// — one class per binary, never both. The UID selection 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 — reasampler_uid.h owns the binary UID fork,
// app_version owns the string fork).
// The runtime FUID for the class this binary registers — the channel-selected UID.
static const Steinberg::FUID kReaSamplerProcessorUID(REASAMPLER_ACTIVE_UID_1,
REASAMPLER_ACTIVE_UID_2,
REASAMPLER_ACTIVE_UID_3,
REASAMPLER_ACTIVE_UID_4);
} // namespace reasampler::vst
+84
View File
@@ -0,0 +1,84 @@
#include "core/namespaces.h"
// vst_entry.cpp — the VST3 module class factory (Phase S1). Enumerates the one class
// this module offers (the ReaSampler instrument) via the SDK's factory macros. The
// Windows module exports — GetPluginFactory (here, via BEGIN_FACTORY) and
// InitDll/ExitDll (from the SDK's dllmain.cpp) — are how REAPER discovers and loads a
// VST3.
//
// VERIFIED (corrects §1a's "experienced estimate" flags on export names + macros,
// against vendor/vst3sdk/public.sdk/source/main/):
// * Windows exports: InitDll / ExitDll (SMTG_EXPORT_SYMBOL, in dllmain.cpp) +
// GetPluginFactory (SMTG_EXPORT_SYMBOL IPluginFactory* PLUGIN_API, emitted by the
// BEGIN_FACTORY macro). The plug-in must provide InitModule/DeinitModule — supplied
// here by linking moduleinit.cpp (the SDK's default one-time init/term).
// * Factory macros: BEGIN_FACTORY(vendor,url,email,flags) / DEF_CLASS2(...) /
// END_FACTORY — exact spellings from pluginfactory.h.
// * Instrument subcategory string: "Instrument|Synth|Sampler"
// (PlugType::kInstrumentSynthSampler, ivstaudioprocessor.h).
// * classFlags = 0 for a SingleComponentEffect (non-distributable), matching the
// AGain example.
#include "public.sdk/source/main/pluginfactory.h"
#include "pluginterfaces/vst/ivstaudioprocessor.h" // kVstAudioEffectClass, PlugType
#include "core/version/app_version.h" // vstPluginName / appVersion — the channel-derived identity
#include "ext_keys.h" // kProjExtNamespace — the pairing-surface assertion target
#include "reasampler_processor.h"
#include "shell/instrument/reasampler_vst.h" // channel-selected class UID (REASAMPLER_ACTIVE_UID_*)
// 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 &&
REASAMPLER_ACTIVE_UID_2 == REASAMPLER_PROC_UID_BETA_2 &&
REASAMPLER_ACTIVE_UID_3 == REASAMPLER_PROC_UID_BETA_3 &&
REASAMPLER_ACTIVE_UID_4 == REASAMPLER_PROC_UID_BETA_4,
"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 &&
REASAMPLER_ACTIVE_UID_2 == REASAMPLER_PROC_UID_2 &&
REASAMPLER_ACTIVE_UID_3 == REASAMPLER_PROC_UID_3 &&
REASAMPLER_ACTIVE_UID_4 == REASAMPLER_PROC_UID_4,
"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)
// 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" (live literals in
// app_version.cpp). appVersion(): the configured version string / that string plus
// "-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::vstPluginName().c_str(), // plug-in display name (channel-derived)
0, // single-component => 0
Steinberg::Vst::PlugType::kInstrumentSynthSampler, // subcategory
reasampler::appVersion().c_str(), // plug-in version (channel: -beta render)
kVstVersionString, // VST3 SDK version (fixed)
reasampler::vst::ReaSamplerProcessor::createInstance)
END_FACTORY