// reasampler_embed.cpp — see reasampler_embed.h. The IReaperUIEmbedInterface shell. // Windows-only; guarded so a non-Windows build degrades to a stub that reports "not // supported" and draws nothing. #include "shell/instrument/reasampler_embed.h" #include #include #include #include "core/version/app_version.h" // vstPluginName (channel-derived embed label) #include "core/instrument/map/bank_sync.h" // parseBankGeneration (dirty-guard over the per-paint refresh) #include "core/ui/component_geometry.h" // KitBox — the kit text/fill draw box #include "shell/panel/draw_kit.h" // the shared draw kit: fillSurface/text #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 "shell/instrument/reasampler_processor.h" #include "core/ui/theme.h" // Role / InteractionState / spectralColor // wdltypes.h first: it defines INT_PTR portably (needed by REAPER_FXEMBED_IBitmap::Extended's // return type in the header below). #include "wdltypes.h" // REAPER's embed message/bitmap contract (vendored): REAPER_FXEMBED_IBitmap aliases // LICE_IBitmap; WM_* / DrawInfo / SizeHints 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; // This is the only TU that answers queryInterface for IReaperUIEmbedInterface, so the // DEFINE lives here (mirrors reaper_bridge.cpp's IReaperHostApplication). DEF_CLASS_IID(Steinberg::IReaperUIEmbedInterface) namespace reasampler::vst { using namespace reasampler::instrument::map; using namespace reasampler::instrument::ui; using reasampler::ui::spectralColor; using version::vstPluginName; namespace { #ifdef _WIN32 // Kit adapter: the embed shell's Rect -> the kit's KitBox. KitBox toKitBox(const Rect& r) { return KitBox{r.x, r.y, r.width, r.height}; } // A short display name for a bank sample id (small duplicate of the editor's helper // rather than shared across the shell/pure boundary). std::string sampleLabel(const std::vector& samples, const std::string& id) { for (const SampleChoice& c : samples) { if (c.id == id) return c.displayName.empty() ? c.id : c.displayName; } return "?"; } #endif } // 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; } // The loaded capture id + effective root, both cheap in-process accessors. void ReaSamplerEmbed::refreshLoaded() { loadedId_ = processor_->selectedSampleId(); const InstrumentParams params = processor_->instrumentParams(); if (params.rootOverride) { rootNote_ = *params.rootOverride; } else { const SampleRefs refs = processor_->sampleRefs(); if (const SelectedSample* ref = findRef(refs, loadedId_)) { rootNote_ = ref->rootNote; } else { rootNote_ = 60; } } } void ReaSamplerEmbed::refresh() { if (!processor_) { samples_.clear(); loadedId_.clear(); rootNote_ = 60; return; } auto banks = processor_->bridge().readReasamplerExtState(reasampler::kProjExtBanksKey); samples_ = banks ? listSamples(*banks) : std::vector{}; refreshLoaded(); } void ReaSamplerEmbed::maybeRefresh() { if (!processor_) { refresh(); return; } // clears state; cheap // The loaded capture + root are cheap in-process accessors, and the editor may change // either with no bank-content change — always re-snapshot so an edit reflects at once. refreshLoaded(); // The expensive part is the bank-blob bridge read: gate it on the bank-generation // stamp, re-reading only when it changed (or on the first paint). A project with no // stamp reads generation 0; the first paint folds it and idle paints skip the read. 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: treat as generation 0 for the first read } // Intentional asymmetry: a transient bridge failure after priming leaves currentGen // unchanged, skipping the read — a stale-but-intact list beats clearing samples_ on // every hiccup. if (lastSeenBankGeneration_ < 0 || currentGen != lastSeenBankGeneration_) { auto banks = processor_->bridge().readReasamplerExtState(reasampler::kProjExtBanksKey); samples_ = banks ? listSamples(*banks) : std::vector{}; 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 // Idempotent + process-global (shared with the editor); 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(parm3); if (!hints) return 0; // The keymap must not collapse below its floor 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; #endif default: return 0; // unhandled messages (cursor, wheel, hittest) fall through } } #ifdef _WIN32 bool ReaSamplerEmbed::paint(TPtrInt bitmap, TPtrInt drawInfo) { auto* bmp = reinterpret_cast(bitmap); auto* di = reinterpret_cast(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 (no own timer) — REAPER repaints the embed surface on // its own cadence. 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). fillSurface(bmp, KitBox{0, 0, w, h}, Role::BgBase, InteractionState::Rest); const EmbedLayout layout = layoutEmbed(w, h); if (loadedId_.empty()) { // Nothing loaded: a faint band so the strip reads as "present, silent" — the pick // affordance 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 = version::vstPluginName() + // channel-derived (samples_.empty() ? " (bank empty)" : " (pick a capture)"); 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 { // The loaded capture spans the whole keyboard, drawn in its root's spectral hue so // the strip reads as the same spectrum as the editor's keyboard strip; the root key // lifts to accent-primary with a static glow ("this is where it plays at unity"). const Rect span = keySpanRect(layout, 0, 127); LICE_FillRect(bmp, span.x, span.y, span.width, span.height, toLice(spectralColor(rootNote_ / 127.0)), 0.65f, 0); LICE_DrawRect(bmp, span.x, span.y, span.width - 1, span.height - 1, toLice(roleColor(Role::LineHairline)), 1.0f, 0); const Rect root = keySpanRect(layout, rootNote_, rootNote_); const int rw = (std::max)(2, root.width); LICE_FillRect(bmp, root.x - 2, root.y, rw + 4, root.height, toLice(roleColor(Role::AccentHot)), 0.30f, 0); LICE_FillRect(bmp, root.x, root.y, rw, root.height, toLice(roleColor(Role::AccentPrimary)), 1.0f, 0); if (span.width >= 24) { const Rect lr = Rect::ltrb(span.x + 3, span.y, span.right() - 2, span.bottom()); text(bmp, toKitBox(lr), sampleLabel(samples_, loadedId_).c_str(), Font::Label, Role::TextPrimary, Align::Left); } } // The level band: a recessed channel with an accent-primary fill tracking the live // activity level (the one permitted "motion"). 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; } #endif // _WIN32 } // namespace reasampler::vst