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:
@@ -0,0 +1,86 @@
|
||||
// embed_strip.cpp — see embed_strip.h. Pure math; no host types.
|
||||
|
||||
#include "embed_strip.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace reasampler::vst {
|
||||
|
||||
namespace {
|
||||
|
||||
// Clamp a MIDI note to [0, kEmbedKeyCount-1].
|
||||
int clampNote(int n) {
|
||||
if (n < 0) return 0;
|
||||
if (n > kEmbedKeyCount - 1) return kEmbedKeyCount - 1;
|
||||
return n;
|
||||
}
|
||||
|
||||
// Map a key boundary in [0, kEmbedKeyCount] to an x pixel inside a band of the given
|
||||
// left/width. keyEdge is a boundary (0..128), so keyEdge==128 maps to the band's right.
|
||||
// Integer math, floored — a zone's left uses floor(low) and its right uses floor(high+1),
|
||||
// which tiles adjacent zones without a seam.
|
||||
int keyEdgeToX(int bandLeft, int bandWidth, int keyEdge) {
|
||||
if (keyEdge <= 0) return bandLeft;
|
||||
if (keyEdge >= kEmbedKeyCount) return bandLeft + bandWidth;
|
||||
return bandLeft + (keyEdge * bandWidth) / kEmbedKeyCount;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
EmbedLayout layoutEmbed(int w, int h) {
|
||||
const int cw = std::max(0, w);
|
||||
const int ch = std::max(0, h);
|
||||
|
||||
EmbedLayout out;
|
||||
|
||||
// The level band takes a fixed height at the bottom, but never so much that the keymap
|
||||
// above it falls below its minimum (or that the band exceeds the area). On a very short
|
||||
// area the band yields to the keymap entirely.
|
||||
int bandH = std::min(kEmbedLevelBandHeight, ch);
|
||||
if (ch - bandH < kEmbedKeymapMinHeight) {
|
||||
bandH = std::max(0, ch - kEmbedKeymapMinHeight);
|
||||
}
|
||||
const int keymapBottom = ch - bandH;
|
||||
|
||||
out.keymap = Rect{0, 0, cw, keymapBottom};
|
||||
out.levelBand = Rect{0, keymapBottom, cw, ch};
|
||||
return out;
|
||||
}
|
||||
|
||||
Rect zoneSegmentRect(const EmbedLayout& layout, int lowNote, int highNote) {
|
||||
const Rect& band = layout.keymap;
|
||||
const int bandWidth = std::max(0, band.width());
|
||||
|
||||
int lo = clampNote(lowNote);
|
||||
int hi = clampNote(highNote);
|
||||
if (lo > hi) lo = hi; // defensive: a malformed zone collapses rather than inverts
|
||||
|
||||
const int leftX = keyEdgeToX(band.left, bandWidth, lo);
|
||||
const int rightX = keyEdgeToX(band.left, bandWidth, hi + 1);
|
||||
return Rect{leftX, band.top, std::max(leftX, rightX), band.bottom};
|
||||
}
|
||||
|
||||
int zoneAtPoint(const EmbedLayout& layout, const EmbedZone* zones, int zoneCount, int x,
|
||||
int y) {
|
||||
if (zoneCount <= 0 || zones == nullptr) return -1;
|
||||
if (!contains(layout.keymap, x, y)) return -1;
|
||||
// First covering zone in draw order wins (first-match, mirroring the core's resolve).
|
||||
for (int i = 0; i < zoneCount; ++i) {
|
||||
const Rect r = zoneSegmentRect(layout, zones[i].lowNote, zones[i].highNote);
|
||||
if (contains(r, x, y)) return i;
|
||||
}
|
||||
return -1; // on the band but on an uncovered key
|
||||
}
|
||||
|
||||
Rect levelFillRect(const EmbedLayout& layout, double level) {
|
||||
const Rect& band = layout.levelBand;
|
||||
if (band.width() <= 0 || band.height() <= 0) return Rect{};
|
||||
double l = level;
|
||||
if (l < 0.0) l = 0.0;
|
||||
if (l > 1.0) l = 1.0;
|
||||
const int fillW = static_cast<int>(l * band.width());
|
||||
if (fillW <= 0) return Rect{};
|
||||
return Rect{band.left, band.top, band.left + fillW, band.bottom};
|
||||
}
|
||||
|
||||
} // namespace reasampler::vst
|
||||
@@ -0,0 +1,76 @@
|
||||
// embed_strip.h — PURE layout + hit-test for the S6 embedded TCP/MCP strip. NO VST3,
|
||||
// NO REAPER, NO SWELL/LICE types at the boundary. The mirror of editor_geometry /
|
||||
// mode_switch: the fiddly rectangle math for the compact inline keymap/level strip lives
|
||||
// here so it is unit-tested outside the DAW, while the embed shell (reasampler_embed.cpp)
|
||||
// marshals REAPER's embed messages (paint bitmap + mouse coords) into these functions.
|
||||
//
|
||||
// The strip is a single compact band REAPER draws inline in the track/mixer control panel
|
||||
// (context TCP or MCP) via the Cockos embedded-UI surface. It shows:
|
||||
// * the zone layout — each performance zone as a horizontal segment across the keyboard
|
||||
// span (MIDI 0..127 mapped to the strip width), so the keymap reads at a glance; and
|
||||
// * a thin level band at the bottom — a 0..1 activity indicator the shell fills.
|
||||
// Interaction is zone SELECTION at most (S6 constraint: no new editing semantics) — a
|
||||
// click maps to the zone whose key range covers that point, or -1.
|
||||
//
|
||||
// It reuses the same Rect + contains() as editor_geometry (the strip and the editor share
|
||||
// one geometry idiom), so this header depends on editor_geometry.h rather than redefining
|
||||
// a second rectangle type.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "editor_geometry.h" // Rect, contains — one shared geometry idiom
|
||||
|
||||
namespace reasampler::vst {
|
||||
|
||||
// The full MIDI key span the strip maps across its width. 128 keys (0..127); the strip's
|
||||
// horizontal axis is this range, so a zone [lowNote, highNote] becomes a sub-rectangle.
|
||||
inline constexpr int kEmbedKeyCount = 128;
|
||||
|
||||
// Fixed metrics for the strip, exposed so the shell and tests agree.
|
||||
inline constexpr int kEmbedLevelBandHeight = 4; // the bottom activity band (px)
|
||||
inline constexpr int kEmbedKeymapMinHeight = 6; // keymap area collapses no smaller
|
||||
|
||||
// One zone rendered on the strip: its inclusive MIDI key range. This is the minimal
|
||||
// projection of a PerformanceZone the strip needs (it does not carry sample ids or PCM —
|
||||
// the shell resolves labels; the strip only lays out ranges). lowNote/highNote are
|
||||
// expected in [0,127] with low <= high, but the layout clamps defensively so a malformed
|
||||
// zone never yields an out-of-strip rect.
|
||||
struct EmbedZone {
|
||||
int lowNote = 0;
|
||||
int highNote = 127;
|
||||
};
|
||||
|
||||
// The strip's regions, derived from the (w x h) embed area REAPER reports. Both clamp to
|
||||
// the area so a degenerate (tiny) size never yields a region spilling outside the surface.
|
||||
struct EmbedLayout {
|
||||
Rect keymap; // top: the zone-segment band (the compact keymap)
|
||||
Rect levelBand; // bottom: the thin level/activity indicator
|
||||
};
|
||||
|
||||
// Divide a (w x h) embed area into the strip's regions. Pure: same inputs -> same layout.
|
||||
// The level band takes a fixed height at the bottom (clamped so it never exceeds the area
|
||||
// or starves the keymap below kEmbedKeymapMinHeight); the keymap takes the rest. A zero or
|
||||
// negative size yields empty rects (no inversion).
|
||||
EmbedLayout layoutEmbed(int w, int h);
|
||||
|
||||
// The horizontal sub-rectangle of the keymap band for a zone spanning [lowNote, highNote]
|
||||
// (inclusive). The 128-key span maps linearly across keymap.width(); the returned rect
|
||||
// spans the half-open pixel range [x(lowNote), x(highNote+1)) so adjacent zones (e.g.
|
||||
// 0..59 and 60..127) tile without a gap or overlap. Notes are clamped to [0,127] and low
|
||||
// is clamped to <= high, so a malformed zone yields an in-band (possibly zero-width) rect,
|
||||
// never an inverted one. Pure.
|
||||
Rect zoneSegmentRect(const EmbedLayout& layout, int lowNote, int highNote);
|
||||
|
||||
// The zone a click at (x, y) lands on, given the zones in draw order, or -1 for a click
|
||||
// outside the keymap band or on a key not covered by any zone. When zones overlap on a
|
||||
// key, the FIRST covering zone in order wins — mirroring the sampler core's first-match
|
||||
// Keymap::resolve and the editor's zone order, so selection agrees with playback. Pure.
|
||||
int zoneAtPoint(const EmbedLayout& layout, const EmbedZone* zones, int zoneCount, int x,
|
||||
int y);
|
||||
|
||||
// The filled portion of the level band for a 0..1 level. Clamps level to [0,1]; the
|
||||
// returned rect is the left sub-rectangle of levelBand whose width is level * band width
|
||||
// (rounded down). level <= 0 -> empty rect; level >= 1 -> the whole band. Pure.
|
||||
Rect levelFillRect(const EmbedLayout& layout, double level);
|
||||
|
||||
} // namespace reasampler::vst
|
||||
@@ -0,0 +1,213 @@
|
||||
// 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 "reasampler_embed.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "editor_geometry.h" // Rect (shared with embed_strip)
|
||||
#include "embed_strip.h" // the pure strip layout + hit-test
|
||||
#include "ext_keys.h" // kProjExtBanksKey
|
||||
#include "reaper_bridge.h"
|
||||
#include "reasampler_processor.h"
|
||||
|
||||
// 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
|
||||
// Palette — mirrored from reasampler_editor.cpp so the inline strip reads as the same tool.
|
||||
const LICE_pixel kColBackground = LICE_RGBA(28, 28, 30, 255);
|
||||
const LICE_pixel kColZone = LICE_RGBA(44, 44, 48, 255);
|
||||
const LICE_pixel kColZoneSel = LICE_RGBA(58, 96, 84, 255);
|
||||
const LICE_pixel kColZoneBorder = LICE_RGBA(20, 20, 22, 255);
|
||||
const LICE_pixel kColLevelBg = LICE_RGBA(20, 20, 22, 255);
|
||||
const LICE_pixel kColLevelFill = LICE_RGBA(120, 200, 160, 255);
|
||||
const LICE_pixel kColEmpty = LICE_RGBA(70, 70, 74, 255);
|
||||
const COLORREF kRgbText = RGB(210, 230, 220);
|
||||
|
||||
// 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();
|
||||
selectedId_.clear();
|
||||
selectedZone_ = -1;
|
||||
return;
|
||||
}
|
||||
auto banks = processor_->bridge().readReasamplerExtState(reasampler::kProjExtBanksKey);
|
||||
samples_ = banks ? listSamples(*banks) : std::vector<SampleChoice>{};
|
||||
map_ = processor_->performanceMap();
|
||||
selectedId_ = processor_->selectedSampleId();
|
||||
if (selectedZone_ >= static_cast<int>(map_.zones.size())) selectedZone_ = -1;
|
||||
}
|
||||
|
||||
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:
|
||||
refresh(); // prime the first paint's snapshot
|
||||
return 0;
|
||||
case REAPER_FXEMBED_WM_DESTROY:
|
||||
return 0;
|
||||
#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, minmax) 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.
|
||||
refresh();
|
||||
|
||||
// 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).
|
||||
LICE_FillRect(bmp, 0, 0, w, h, kColBackground, 1.0f, 0);
|
||||
|
||||
const EmbedLayout layout = layoutEmbed(w, h);
|
||||
|
||||
if (map_.zones.empty()) {
|
||||
// No keymap authored yet: show a single faint band spanning the keymap area so the
|
||||
// strip still reads as "present but empty" (Tier-0 fallback plays chromatically).
|
||||
LICE_FillRect(bmp, layout.keymap.left, layout.keymap.top, layout.keymap.width(),
|
||||
layout.keymap.height(), kColEmpty, 0.5f, 0);
|
||||
HDC dc = bmp->getDC();
|
||||
SetBkMode(dc, TRANSPARENT);
|
||||
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 (bank empty)" : "ReaSampler (no zones)";
|
||||
DrawTextA(dc, label.c_str(), -1, &gr,
|
||||
DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX);
|
||||
} else {
|
||||
// Draw each zone as a segment across the keymap span, first-match order (so the
|
||||
// painted order matches selection + playback). The selected zone is highlighted.
|
||||
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_);
|
||||
LICE_FillRect(bmp, r.left, r.top, r.width(), r.height(),
|
||||
sel ? kColZoneSel : kColZone, 1.0f, 0);
|
||||
LICE_DrawRect(bmp, r.left, r.top, r.width() - 1, r.height() - 1, kColZoneBorder,
|
||||
1.0f, 0);
|
||||
// Label the segment with the sample name when it is wide enough to read.
|
||||
if (r.width() >= 24) {
|
||||
HDC dc = bmp->getDC();
|
||||
SetBkMode(dc, TRANSPARENT);
|
||||
SetTextColor(dc, kRgbText);
|
||||
RECT gr{r.left + 3, r.top, r.right - 2, r.bottom};
|
||||
const std::string label = sampleLabel(samples_, z.sampleId);
|
||||
DrawTextA(dc, label.c_str(), -1, &gr,
|
||||
DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX | DT_END_ELLIPSIS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The level band: a static background here; a live activity level is a later refinement
|
||||
// (the processor would publish a peak the UI thread reads). Draw the empty band so the
|
||||
// strip's geometry is complete and the DAW-verify sees the band lifecycle now.
|
||||
if (layout.levelBand.height() > 0) {
|
||||
LICE_FillRect(bmp, layout.levelBand.left, layout.levelBand.top,
|
||||
layout.levelBand.width(), layout.levelBand.height(), kColLevelBg, 1.0f,
|
||||
0);
|
||||
const double level = processor_ ? processor_->embedActivityLevel() : 0.0;
|
||||
const Rect fill = levelFillRect(layout, level);
|
||||
if (fill.width() > 0) {
|
||||
LICE_FillRect(bmp, fill.left, fill.top, fill.width(), fill.height(),
|
||||
kColLevelFill, 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
|
||||
@@ -0,0 +1,99 @@
|
||||
// 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 <string>
|
||||
#include <vector>
|
||||
|
||||
#include "pluginterfaces/base/funknown.h"
|
||||
|
||||
#include "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();
|
||||
|
||||
ReaSamplerProcessor* processor_ = nullptr;
|
||||
// Snapshotted for the current paint (refreshed each paint off the audio thread).
|
||||
std::vector<SampleChoice> samples_;
|
||||
PerformanceMap map_;
|
||||
std::string selectedId_;
|
||||
// The zone the last click selected (mirrored to the processor's editor-shared selection
|
||||
// where meaningful); -1 = none. Drives the strip's highlight.
|
||||
int selectedZone_ = -1;
|
||||
};
|
||||
|
||||
} // namespace reasampler::vst
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "capture_paths.h" // resolveBankFile (shared M4 path resolution)
|
||||
#include "ext_keys.h" // kProjExtBanksKey (shared wire contract)
|
||||
#include "reasampler_editor.h"
|
||||
#include "reasampler_embed.h" // S6 embed shell + IReaperUIEmbedInterface (its iid DEF'd there)
|
||||
#include "sample_map.h" // selectSample, resolvePerformance, buildZonedKeymap, state (de)ser
|
||||
#include "wav_trim.h" // parseWavLayout, extractFloatFrames (shared WAV parse)
|
||||
|
||||
@@ -91,6 +92,23 @@ FUnknown* ReaSamplerProcessor::createInstance(void* /*context*/) {
|
||||
return static_cast<IAudioProcessor*>(new ReaSamplerProcessor());
|
||||
}
|
||||
|
||||
// Out-of-line so unique_ptr<ReaSamplerEmbed> sees the complete type here.
|
||||
ReaSamplerProcessor::~ReaSamplerProcessor() = default;
|
||||
|
||||
tresult PLUGIN_API ReaSamplerProcessor::queryInterface(const TUID iid, void** obj) {
|
||||
// S6: expose REAPER's inline-embed interface. REAPER queries the IEditController for
|
||||
// IReaperUIEmbedInterface (reaper_vst3_interfaces.h); hand it our lazily-created embed
|
||||
// shell. We own the shell (unique_ptr); the borrowed reference is valid because the
|
||||
// processor outlives it. All other iids fall through to the SDK's queryInterface.
|
||||
if (FUnknownPrivate::iidEqual(iid, IReaperUIEmbedInterface::iid)) {
|
||||
if (!embed_) embed_ = std::make_unique<ReaSamplerEmbed>(this);
|
||||
embed_->addRef();
|
||||
*obj = static_cast<IReaperUIEmbedInterface*>(embed_.get());
|
||||
return kResultOk;
|
||||
}
|
||||
return SingleComponentEffect::queryInterface(iid, obj);
|
||||
}
|
||||
|
||||
tresult PLUGIN_API ReaSamplerProcessor::initialize(FUnknown* context) {
|
||||
tresult result = SingleComponentEffect::initialize(context);
|
||||
if (result != kResultOk) return result;
|
||||
@@ -357,6 +375,16 @@ tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) {
|
||||
if (inst) {
|
||||
inst->engine.render(ch0, static_cast<std::size_t>(frames));
|
||||
}
|
||||
// Block peak (mono, pre-replicate) for the embedded strip's level indicator. A
|
||||
// single scan of ch0 + one relaxed atomic store — RT-safe (no alloc/IO/lock). The
|
||||
// UI thread reads it via embedActivityLevel(); a plain store is sufficient because
|
||||
// the readout is advisory (no ordering dependency on other state).
|
||||
float peak = 0.f;
|
||||
for (int32 i = 0; i < frames; ++i) {
|
||||
const float a = ch0[i] < 0.f ? -ch0[i] : ch0[i];
|
||||
if (a > peak) peak = a;
|
||||
}
|
||||
embedPeak_.store(peak, std::memory_order_relaxed);
|
||||
// Duplicate the mono render across the remaining output channels.
|
||||
for (int32 ch = 1; ch < out.numChannels; ++ch) {
|
||||
if (float* buf = out.channelBuffers32[ch]) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user