S6: embedded TCP/MCP UI via IReaperUIEmbedInterface

Inline keymap/level strip drawn into REAPER's embed bitmap, reusing the LICE
idiom. Pure embed_strip layout/hit-test (tested); processor exposes the embed
interface off queryInterface and publishes an RT-safe block peak for the level.
This commit is contained in:
2026-07-26 18:12:27 -04:00
parent b653a4f6a7
commit 03e760631c
8 changed files with 706 additions and 1 deletions
+30
View File
@@ -33,6 +33,8 @@
namespace reasampler::vst {
class ReaSamplerEmbed; // S6 embedded TCP/MCP UI shell (owned below; see queryInterface)
// One fully-built, ready-to-play instrument snapshot: the decoded keymap and the voice
// engine that plays it. The engine holds a reference into the keymap, so the two MUST
// live and die together at a STABLE address — hence this is heap-allocated and neither
@@ -59,6 +61,9 @@ struct LoadedInstrument {
class ReaSamplerProcessor : public Steinberg::Vst::SingleComponentEffect {
public:
ReaSamplerProcessor() = default;
// Out-of-line so the owned ReaSamplerEmbed (held by unique_ptr, forward-declared here)
// is a complete type at the destruction point (defined in the .cpp).
~ReaSamplerProcessor() override;
// The factory create function (registered in vst_entry.cpp).
static Steinberg::FUnknown* createInstance(void* /*context*/);
@@ -87,6 +92,19 @@ public:
// Hands the host our LICE IPlugView editor.
Steinberg::IPlugView* PLUGIN_API createView(Steinberg::FIDString name) override;
// Override queryInterface to additionally expose REAPER's IReaperUIEmbedInterface (S6):
// REAPER queries the IEditController for it to drive the inline TCP/MCP embed surface.
// All other iids delegate to SingleComponentEffect's implementation unchanged.
Steinberg::tresult PLUGIN_API queryInterface(const Steinberg::TUID iid,
void** obj) override;
// The embedded-strip activity level (0..1), read by the S6 embed shell on the UI thread.
// Backed by embedPeak_, the per-block mono peak the audio thread stores relaxed — a
// lock-free advisory readout, never touched with a lock the audio thread could contend.
double embedActivityLevel() const {
return static_cast<double>(embedPeak_.load(std::memory_order_relaxed));
}
// Called by the editor (main/UI thread) when the user picks a sample, and internally
// on load. Reads the live bank over the bridge, resolves+decodes the selected WAV
// OFF the audio thread, and publishes the built instrument to process() via an
@@ -163,6 +181,18 @@ private:
// off-thread only.
double sampleRate_ = 44100.0;
Steinberg::int32 maxBlockSize_ = 4096;
// --- S6 embedded TCP/MCP UI ---------------------------------------------
// The embed shell (IReaperUIEmbedInterface), created lazily on the first queryInterface
// and owned here for the processor's lifetime. REAPER borrows AddRef'd references from
// queryInterface; the shell's refcount is a no-op because THIS unique_ptr governs its
// destruction (the processor always outlives the borrowed references).
std::unique_ptr<ReaSamplerEmbed> embed_;
// The per-block mono peak (0..1+) the audio thread stores relaxed; the embed strip's
// level indicator reads it via embedActivityLevel(). Advisory only — a plain atomic,
// no ordering coupling, never guarded by a lock the audio thread touches.
std::atomic<float> embedPeak_{0.f};
};
} // namespace reasampler::vst