Merge ps-w1-t1-vst3-spike: S1 VST3 skeleton, IPlugView-LICE editor, REAPER bridge read
This commit is contained in:
@@ -4,3 +4,6 @@
|
|||||||
[submodule "vendor/WDL"]
|
[submodule "vendor/WDL"]
|
||||||
path = vendor/WDL
|
path = vendor/WDL
|
||||||
url = https://github.com/justinfrankel/WDL
|
url = https://github.com/justinfrankel/WDL
|
||||||
|
[submodule "vendor/vst3sdk"]
|
||||||
|
path = vendor/vst3sdk
|
||||||
|
url = https://github.com/steinbergmedia/vst3sdk
|
||||||
|
|||||||
+102
@@ -584,6 +584,28 @@ add_executable(card_drag_tests tests/test_card_drag.cpp)
|
|||||||
target_link_libraries(card_drag_tests PRIVATE card_drag)
|
target_link_libraries(card_drag_tests PRIVATE card_drag)
|
||||||
add_test(NAME card_drag_tests COMMAND card_drag_tests)
|
add_test(NAME card_drag_tests COMMAND card_drag_tests)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 2i) Pure VST3-instrument helpers (Phase S1) — NO VST3, NO REAPER, NO SWELL/LICE.
|
||||||
|
# editor_geometry: the IPlugView LICE editor's rectangle layout + hit-test math
|
||||||
|
# (mirror of mode_switch/bank_grid). bridge_marshal: the REAPER VST-host bridge
|
||||||
|
# read marshalling — GetProjExtState result decode + a small JSON string-field
|
||||||
|
# reader (mirror of capture_paths/wav_trim). Both are unit-tested outside the DAW;
|
||||||
|
# the VST3 shell (src/vst/*) that draws/routes/invokes is DAW-verified.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
add_library(editor_geometry STATIC src/vst/editor_geometry.cpp)
|
||||||
|
target_include_directories(editor_geometry PUBLIC src/vst)
|
||||||
|
|
||||||
|
add_library(bridge_marshal STATIC src/vst/bridge_marshal.cpp)
|
||||||
|
target_include_directories(bridge_marshal PUBLIC src/vst)
|
||||||
|
|
||||||
|
add_executable(editor_geometry_tests tests/test_editor_geometry.cpp)
|
||||||
|
target_link_libraries(editor_geometry_tests PRIVATE editor_geometry)
|
||||||
|
add_test(NAME editor_geometry_tests COMMAND editor_geometry_tests)
|
||||||
|
|
||||||
|
add_executable(bridge_marshal_tests tests/test_bridge_marshal.cpp)
|
||||||
|
target_link_libraries(bridge_marshal_tests PRIVATE bridge_marshal)
|
||||||
|
add_test(NAME bridge_marshal_tests COMMAND bridge_marshal_tests)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 4) The REAPER extension — a loadable module (dlopen'd by REAPER, not linked).
|
# 4) The REAPER extension — a loadable module (dlopen'd by REAPER, not linked).
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -671,3 +693,83 @@ else()
|
|||||||
# php ${WDL_INC}/swell/mac_resgen.php src/resource.rc
|
# php ${WDL_INC}/swell/mac_resgen.php src/resource.rc
|
||||||
# target_sources(reaper_reasampler PRIVATE src/resource.rc_mac_dlg.h) # generated
|
# target_sources(reaper_reasampler PRIVATE src/resource.rc_mac_dlg.h) # generated
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# 5) The ReaSampler VST3 instrument — the SECOND build artifact (Phase S1).
|
||||||
|
#
|
||||||
|
# Windows-only, VST3-only, REAPER-only (D5). A separate native VST3 plugin the user
|
||||||
|
# instantiates on an instrument track. Additive: the reaper_reasampler target above
|
||||||
|
# builds unchanged. This is the S1 opening spike — a silent-but-loading
|
||||||
|
# SingleComponentEffect skeleton, an IPlugView<->LICE editor, and the REAPER VST-host
|
||||||
|
# bridge read — not yet a sampler.
|
||||||
|
#
|
||||||
|
# ONE-TIME SDK SUBMODULE SETUP (see README / .gitmodules): the vst3sdk superproject is
|
||||||
|
# vendored pinned to tag v3.7.9_build_61; only three of its sub-submodules are needed
|
||||||
|
# (VSTGUI/examples/tests are NOT). After `git submodule update --init vendor/vst3sdk`:
|
||||||
|
# cd vendor/vst3sdk && git submodule update --init pluginterfaces base public.sdk
|
||||||
|
# ===========================================================================
|
||||||
|
if(WIN32)
|
||||||
|
set(VST3_SDK ${CMAKE_CURRENT_SOURCE_DIR}/vendor/vst3sdk)
|
||||||
|
|
||||||
|
# --- 5a) The bounded slice of the Steinberg VST3 SDK this spike needs. --------
|
||||||
|
# Enumerated (not add_subdirectory of the whole SDK) to keep the build hermetic and
|
||||||
|
# lean, matching the project's two-submodule discipline: no VSTGUI, no examples, no
|
||||||
|
# SDK-global CMake helpers/install machinery. Pinned to tag v3.7.9_build_61, so the
|
||||||
|
# list is fixed. If the SDK tag is bumped, re-verify this set.
|
||||||
|
add_library(vst3_sdk STATIC
|
||||||
|
# pluginterfaces/base — FUnknown, IIDs, string table, ustring.
|
||||||
|
${VST3_SDK}/pluginterfaces/base/funknown.cpp
|
||||||
|
${VST3_SDK}/pluginterfaces/base/coreiids.cpp
|
||||||
|
${VST3_SDK}/pluginterfaces/base/conststringtable.cpp
|
||||||
|
${VST3_SDK}/pluginterfaces/base/ustring.cpp
|
||||||
|
# base/source — FObject, strings, buffers, streamer, debug, IIDs, update handler.
|
||||||
|
${VST3_SDK}/base/source/fobject.cpp
|
||||||
|
${VST3_SDK}/base/source/fstring.cpp
|
||||||
|
${VST3_SDK}/base/source/fbuffer.cpp
|
||||||
|
${VST3_SDK}/base/source/fstreamer.cpp
|
||||||
|
${VST3_SDK}/base/source/fdebug.cpp
|
||||||
|
${VST3_SDK}/base/source/baseiids.cpp
|
||||||
|
${VST3_SDK}/base/source/updatehandler.cpp
|
||||||
|
${VST3_SDK}/base/thread/source/flock.cpp
|
||||||
|
# public.sdk/source/vst — the SingleComponentEffect base + its deps. NOTE:
|
||||||
|
# vstsinglecomponenteffect.cpp #includes vsteditcontroller.cpp (unity-style), so
|
||||||
|
# vsteditcontroller.cpp must NOT be listed separately (double definition).
|
||||||
|
${VST3_SDK}/public.sdk/source/vst/vstsinglecomponenteffect.cpp
|
||||||
|
${VST3_SDK}/public.sdk/source/vst/vstcomponentbase.cpp
|
||||||
|
${VST3_SDK}/public.sdk/source/vst/vstbus.cpp
|
||||||
|
${VST3_SDK}/public.sdk/source/vst/vstparameters.cpp
|
||||||
|
${VST3_SDK}/public.sdk/source/vst/vstinitiids.cpp
|
||||||
|
# public.sdk/source/common — CPluginView (IPlugView base) + IIDs.
|
||||||
|
${VST3_SDK}/public.sdk/source/common/pluginview.cpp
|
||||||
|
${VST3_SDK}/public.sdk/source/common/commoniids.cpp
|
||||||
|
# public.sdk/source/main — the class-factory (GetPluginFactory) support. NOTE:
|
||||||
|
# dllmain.cpp + moduleinit.cpp (which carry the InitDll/ExitDll dll exports) are
|
||||||
|
# compiled into the MODULE target directly, NOT here: their SMTG_EXPORT_SYMBOL
|
||||||
|
# functions have no internal referrer, so the linker strips them from a static
|
||||||
|
# lib. Compiling them into the module keeps the exports.
|
||||||
|
${VST3_SDK}/public.sdk/source/main/pluginfactory.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(vst3_sdk PUBLIC ${VST3_SDK})
|
||||||
|
# The SDK requires exactly one of RELEASE / DEVELOPMENT (fdebug.cpp keys off it).
|
||||||
|
target_compile_definitions(vst3_sdk PUBLIC $<IF:$<CONFIG:Debug>,DEVELOPMENT=1,RELEASE=1>)
|
||||||
|
|
||||||
|
# --- 5b) The VST3 module (loadable .vst3 DLL). -------------------------------
|
||||||
|
add_library(reasampler_vst MODULE
|
||||||
|
src/vst/vst_entry.cpp
|
||||||
|
src/vst/reasampler_processor.cpp
|
||||||
|
src/vst/reasampler_editor.cpp
|
||||||
|
src/vst/reaper_bridge.cpp
|
||||||
|
# SDK module entry — compiled into the module (not the static lib) so the
|
||||||
|
# InitDll/ExitDll dll exports survive the link (see vst3_sdk note above).
|
||||||
|
${VST3_SDK}/public.sdk/source/main/dllmain.cpp
|
||||||
|
${VST3_SDK}/public.sdk/source/main/moduleinit.cpp
|
||||||
|
${LICE_SRC}
|
||||||
|
)
|
||||||
|
target_link_libraries(reasampler_vst PRIVATE vst3_sdk editor_geometry bridge_marshal)
|
||||||
|
# SDK_INC gives reaper_vst3_interfaces.h + reaper_plugin_functions.h for the bridge;
|
||||||
|
# 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.
|
||||||
|
set_target_properties(reasampler_vst PROPERTIES PREFIX "" SUFFIX ".vst3"
|
||||||
|
OUTPUT_NAME "reasampler_vst")
|
||||||
|
endif()
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
// bridge_marshal.cpp — see bridge_marshal.h. Pure; no host types.
|
||||||
|
|
||||||
|
#include "bridge_marshal.h"
|
||||||
|
|
||||||
|
namespace reasampler::vst {
|
||||||
|
|
||||||
|
std::optional<std::string> decodeGetProjExtState(int apiReturn,
|
||||||
|
const std::string& buffer) {
|
||||||
|
// REAPER returns the length of the stored value; 0 means the key is absent. Guard
|
||||||
|
// both the return AND the buffer: a caller that reused a dirty buffer must not
|
||||||
|
// surface stale bytes as a value when the API reported nothing.
|
||||||
|
if (apiReturn <= 0 || buffer.empty()) return std::nullopt;
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> extractJsonStringField(const std::string& json,
|
||||||
|
const std::string& key) {
|
||||||
|
// Find the member token: "key" followed (after optional whitespace) by ':' then a
|
||||||
|
// quoted string. Scan for each candidate occurrence of the quoted key so a value
|
||||||
|
// that happens to contain the key text can't produce a false match.
|
||||||
|
const std::string needle = "\"" + key + "\"";
|
||||||
|
size_t searchFrom = 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const size_t keyPos = json.find(needle, searchFrom);
|
||||||
|
if (keyPos == std::string::npos) return std::nullopt;
|
||||||
|
|
||||||
|
size_t i = keyPos + needle.size();
|
||||||
|
searchFrom = i; // next candidate starts after this key token
|
||||||
|
|
||||||
|
// Skip whitespace to the ':'.
|
||||||
|
while (i < json.size() &&
|
||||||
|
(json[i] == ' ' || json[i] == '\t' || json[i] == '\n' ||
|
||||||
|
json[i] == '\r')) {
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
if (i >= json.size() || json[i] != ':') continue; // not a member — keep looking
|
||||||
|
++i;
|
||||||
|
|
||||||
|
// Skip whitespace to the value.
|
||||||
|
while (i < json.size() &&
|
||||||
|
(json[i] == ' ' || json[i] == '\t' || json[i] == '\n' ||
|
||||||
|
json[i] == '\r')) {
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
if (i >= json.size() || json[i] != '"') return std::nullopt; // value not a string
|
||||||
|
++i;
|
||||||
|
|
||||||
|
// Read the string body, honoring the common JSON escapes.
|
||||||
|
std::string out;
|
||||||
|
while (i < json.size()) {
|
||||||
|
const char c = json[i];
|
||||||
|
if (c == '\\') {
|
||||||
|
if (i + 1 >= json.size()) return std::nullopt; // dangling escape
|
||||||
|
const char e = json[i + 1];
|
||||||
|
switch (e) {
|
||||||
|
case '"': out.push_back('"'); break;
|
||||||
|
case '\\': out.push_back('\\'); break;
|
||||||
|
case '/': out.push_back('/'); break;
|
||||||
|
case 'n': out.push_back('\n'); break;
|
||||||
|
case 't': out.push_back('\t'); break;
|
||||||
|
case 'r': out.push_back('\r'); break;
|
||||||
|
case 'b': out.push_back('\b'); break;
|
||||||
|
case 'f': out.push_back('\f'); break;
|
||||||
|
default: out.push_back(e); break; // pass through unknown escapes
|
||||||
|
}
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c == '"') return out; // closing quote — done
|
||||||
|
out.push_back(c);
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
return std::nullopt; // unterminated string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace reasampler::vst
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// bridge_marshal.h — PURE marshalling helpers for the REAPER VST-host bridge read
|
||||||
|
// (Phase S1). NO VST3, NO REAPER types at the boundary.
|
||||||
|
//
|
||||||
|
// The bridge shell (reaper_bridge.cpp) resolves REAPER API functions by name over the
|
||||||
|
// host callback and invokes them; the fiddly-and-easy-to-get-wrong parts around those
|
||||||
|
// calls — interpreting GetProjExtState's int return, walking EnumProjExtState's
|
||||||
|
// index-until-false contract into a key set, and extracting a single value out of the
|
||||||
|
// "reasampler" bank JSON — are pure and unit-tested here. Mirror of capture_paths /
|
||||||
|
// wav_trim splitting the arithmetic out of a REAPER-facing shell.
|
||||||
|
//
|
||||||
|
// Verified against vendor/reaper-sdk/sdk/reaper_plugin_functions.h:
|
||||||
|
// int GetProjExtState (ReaProject*, extname, key, valOutNeedBig, valOutNeedBig_sz);
|
||||||
|
// -- returns the length written (0 when the key is absent).
|
||||||
|
// bool EnumProjExtState(ReaProject*, extname, idx, keyOut, keyOut_sz, valOut, valOut_sz);
|
||||||
|
// -- returns false when idx is past the last entry.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace reasampler::vst {
|
||||||
|
|
||||||
|
// Interpret a GetProjExtState result: the int return value (bytes the API reports for
|
||||||
|
// the key) and the buffer it filled. Returns the value only when the API reported a
|
||||||
|
// non-empty result AND the buffer is non-empty — REAPER writes 0 and leaves the buffer
|
||||||
|
// untouched for an absent key, and we must not treat stale buffer contents as a hit.
|
||||||
|
//
|
||||||
|
// `apiReturn` is GetProjExtState's return; `buffer` is the NUL-terminated string it
|
||||||
|
// wrote (already truncated to the C string by the caller).
|
||||||
|
std::optional<std::string> decodeGetProjExtState(int apiReturn,
|
||||||
|
const std::string& buffer);
|
||||||
|
|
||||||
|
// Extract the string value for `key` out of a flat one-level JSON object — the shape
|
||||||
|
// persist.cpp writes under the "reasampler" ext-state (e.g. the bank blob's top-level
|
||||||
|
// fields). This is a deliberately small, dependency-free reader for the SPIKE's
|
||||||
|
// "read a known value" proof, NOT a general JSON parser: it finds "key" as an object
|
||||||
|
// member and returns its string value, handling the common escapes (\" \\ \n \t \/).
|
||||||
|
// Returns nullopt if the key is absent or its value is not a string.
|
||||||
|
//
|
||||||
|
// The real instrument (S4) will read the bank index through the shared bank_model JSON
|
||||||
|
// path, not this helper; this exists only to give the S1 bridge read a testable,
|
||||||
|
// REAPER-free decode step.
|
||||||
|
std::optional<std::string> extractJsonStringField(const std::string& json,
|
||||||
|
const std::string& key);
|
||||||
|
|
||||||
|
} // namespace reasampler::vst
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
// editor_geometry.cpp — see editor_geometry.h. Pure math; no host types.
|
||||||
|
|
||||||
|
#include "editor_geometry.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace reasampler::vst {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Spike editor layout constants. These are the editor's fixed metrics; the real
|
||||||
|
// editor (S4/S5) will parameterize as its content demands.
|
||||||
|
constexpr int kTitleBarHeight = 28;
|
||||||
|
constexpr int kButtonMargin = 10;
|
||||||
|
constexpr int kButtonWidth = 120;
|
||||||
|
constexpr int kButtonHeight = 24;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool contains(const Rect& r, int x, int y) {
|
||||||
|
if (r.width() <= 0 || r.height() <= 0) return false;
|
||||||
|
return x >= r.left && x < r.right && y >= r.top && y < r.bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorLayout layoutEditor(int w, int h) {
|
||||||
|
// Clamp the surface to non-negative extents so a degenerate view can't produce
|
||||||
|
// inverted rects.
|
||||||
|
const int cw = std::max(0, w);
|
||||||
|
const int ch = std::max(0, h);
|
||||||
|
|
||||||
|
EditorLayout out;
|
||||||
|
|
||||||
|
// Title bar spans the top, clamped so it never exceeds the client height.
|
||||||
|
const int titleH = std::min(kTitleBarHeight, ch);
|
||||||
|
out.titleBar = Rect{0, 0, cw, titleH};
|
||||||
|
|
||||||
|
// Canvas is everything below the title bar.
|
||||||
|
out.canvas = Rect{0, titleH, cw, ch};
|
||||||
|
|
||||||
|
// Button sits at the top-left of the canvas, inset by a margin, and is clamped to
|
||||||
|
// fit inside the canvas so it never overhangs on a small view.
|
||||||
|
const int bx = out.canvas.left + kButtonMargin;
|
||||||
|
const int by = out.canvas.top + kButtonMargin;
|
||||||
|
const int bRight = std::min(bx + kButtonWidth, out.canvas.right);
|
||||||
|
const int bBottom = std::min(by + kButtonHeight, out.canvas.bottom);
|
||||||
|
out.button = Rect{bx, by, std::max(bx, bRight), std::max(by, bBottom)};
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
HitTarget hitTest(const EditorLayout& layout, int x, int y) {
|
||||||
|
if (contains(layout.button, x, y)) return HitTarget::kButton;
|
||||||
|
return HitTarget::kNone;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace reasampler::vst
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
// editor_geometry.h — PURE view geometry + hit-test for the VST3 IPlugView LICE
|
||||||
|
// editor (Phase S1). NO VST3, NO REAPER, NO SWELL/LICE types at the boundary.
|
||||||
|
//
|
||||||
|
// The IPlugView shell (reasampler_editor.cpp) owns the window/bitmap/SWELL plumbing
|
||||||
|
// and is DAW-verified; this module holds the fiddly rectangle math and hit-testing so
|
||||||
|
// it can be unit-tested outside the DAW — the mirror of how bank_grid / mode_switch /
|
||||||
|
// tab_strip split their layout math out of the panel shell.
|
||||||
|
//
|
||||||
|
// The spike's editor is deliberately trivial (a title band + one clickable button),
|
||||||
|
// enough to PROVE the host->draw/hit-test event routing works. As the real editor
|
||||||
|
// (S4/S5) grows, its layout math accretes here, not in the shell.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace reasampler::vst {
|
||||||
|
|
||||||
|
// A plain integer rectangle. left/top inclusive, right/bottom exclusive — the same
|
||||||
|
// half-open convention LICE/SWELL RECTs use, kept REAPER-free here.
|
||||||
|
struct Rect {
|
||||||
|
int left = 0;
|
||||||
|
int top = 0;
|
||||||
|
int right = 0;
|
||||||
|
int bottom = 0;
|
||||||
|
|
||||||
|
int width() const { return right - left; }
|
||||||
|
int height() const { return bottom - top; }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns true if (x, y) falls inside r under the half-open convention
|
||||||
|
// (left <= x < right, top <= y < bottom). A zero-or-negative-area rect contains
|
||||||
|
// nothing.
|
||||||
|
bool contains(const Rect& r, int x, int y);
|
||||||
|
|
||||||
|
// The regions the spike editor draws, derived from the current view size. All are
|
||||||
|
// clamped to the client area so a degenerate (too-small) view never yields a region
|
||||||
|
// that spills outside the surface.
|
||||||
|
struct EditorLayout {
|
||||||
|
Rect titleBar; // top band: the plugin name + a live-state readout
|
||||||
|
Rect button; // a single clickable button (proves hit-test routing)
|
||||||
|
Rect canvas; // the remaining surface below the title bar
|
||||||
|
};
|
||||||
|
|
||||||
|
// Divide a (w x h) client area into the spike editor's regions. Pure: the same
|
||||||
|
// inputs always yield the same layout. Guards tiny sizes — every returned rect stays
|
||||||
|
// within [0,w] x [0,h], and the button never overhangs the canvas.
|
||||||
|
EditorLayout layoutEditor(int w, int h);
|
||||||
|
|
||||||
|
// The editor's hit-test targets. kNone means the point landed on inert surface.
|
||||||
|
enum class HitTarget {
|
||||||
|
kNone,
|
||||||
|
kButton,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Classify a click at (x, y) against a layout. The button wins only when the point is
|
||||||
|
// inside the button rect; everything else (including the title bar and empty canvas)
|
||||||
|
// is kNone in the spike.
|
||||||
|
HitTarget hitTest(const EditorLayout& layout, int x, int y);
|
||||||
|
|
||||||
|
} // namespace reasampler::vst
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
// reaper_bridge.cpp — see reaper_bridge.h. The DAW-facing edge; keep it thin.
|
||||||
|
|
||||||
|
#include "reaper_bridge.h"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "bridge_marshal.h"
|
||||||
|
|
||||||
|
// 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 "reasampler" ext-state namespace + bank key. Kept in sync with persist.h by
|
||||||
|
// value (the extension writes them); we only READ here, so we duplicate the two string
|
||||||
|
// constants rather than pull the whole REAPER-facing persist.h into the VST artifact.
|
||||||
|
// If persist.h's kProjExtNamespace / kProjExtBanksKey ever change, these must follow —
|
||||||
|
// they are the shared wire contract between the extension (writer) and instrument
|
||||||
|
// (reader). VERIFY against persist.h.
|
||||||
|
namespace {
|
||||||
|
constexpr const char* kReasamplerNamespace = "reasampler";
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace reasampler::vst {
|
||||||
|
|
||||||
|
bool ReaperBridge::connect(Steinberg::FUnknown* context) {
|
||||||
|
getProjExtState_ = nullptr;
|
||||||
|
enumProjExtState_ = 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"));
|
||||||
|
|
||||||
|
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; size it generously for a JSON blob
|
||||||
|
// and let the pure decoder interpret the result. The buffer is NUL-terminated by
|
||||||
|
// REAPER on success.
|
||||||
|
std::vector<char> buf(64 * 1024, '\0');
|
||||||
|
const int rv = getProjExtState_(proj, kReasamplerNamespace, key.c_str(), buf.data(),
|
||||||
|
static_cast<int>(buf.size()));
|
||||||
|
|
||||||
|
// Marshal the raw result through the pure decoder (handles the absent-key case).
|
||||||
|
return decodeGetProjExtState(rv, std::string(buf.data()));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace reasampler::vst
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
// 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.
|
||||||
|
std::optional<std::string> readReasamplerExtState(const std::string& key);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
void* hostApp_ = nullptr; // IReaperHostApplication* (opaque here; used in .cpp)
|
||||||
|
GetProjExtStateFn getProjExtState_ = nullptr;
|
||||||
|
EnumProjExtStateFn enumProjExtState_ = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace reasampler::vst
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
// reasampler_editor.cpp — see reasampler_editor.h. The IPlugView<->LICE bridge.
|
||||||
|
// Windows-only (D5); the whole file is guarded so a non-Windows build (not a target,
|
||||||
|
// but keeps the TU honest) degrades to the CPluginView defaults.
|
||||||
|
|
||||||
|
#include "reasampler_editor.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "editor_geometry.h"
|
||||||
|
#include "reaper_bridge.h"
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windowsx.h> // GET_X_LPARAM / GET_Y_LPARAM
|
||||||
|
|
||||||
|
// LICE — the same drawing stack bank_panel uses. On Windows LICE routes through native
|
||||||
|
// GDI; no SWELL needed for the child window itself.
|
||||||
|
#include "wdltypes.h"
|
||||||
|
#include "lice/lice.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
using namespace Steinberg;
|
||||||
|
|
||||||
|
namespace reasampler::vst {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
#ifdef _WIN32
|
||||||
|
constexpr const wchar_t* kChildClassName = L"ReaSamplerVstEditor";
|
||||||
|
|
||||||
|
// Palette — house style, mirrored from bank_panel's dark theme so the instrument reads
|
||||||
|
// as the same tool.
|
||||||
|
const LICE_pixel kColBackground = LICE_RGBA(28, 28, 30, 255);
|
||||||
|
const LICE_pixel kColTitleBg = LICE_RGBA(20, 20, 22, 255);
|
||||||
|
const LICE_pixel kColBtnBg = LICE_RGBA(44, 44, 48, 255);
|
||||||
|
const LICE_pixel kColBtnHitBg = LICE_RGBA(58, 96, 84, 255);
|
||||||
|
const LICE_pixel kColBtnBorder = LICE_RGBA(120, 200, 160, 255);
|
||||||
|
const COLORREF kRgbText = RGB(210, 230, 220);
|
||||||
|
|
||||||
|
void drawText(LICE_IBitmap* bmp, const Rect& r, const char* s, COLORREF col) {
|
||||||
|
// LICE has no built-in font handle here; use GDI text into the bitmap DC, matching
|
||||||
|
// bank_panel's drawCenteredText approach (SetTextColor + DrawText on getDC()).
|
||||||
|
HDC dc = bmp->getDC();
|
||||||
|
SetBkMode(dc, TRANSPARENT);
|
||||||
|
SetTextColor(dc, col);
|
||||||
|
RECT gr{r.left, r.top, r.right, r.bottom};
|
||||||
|
DrawTextA(dc, s, -1, &gr, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ReaSamplerEditor::ReaSamplerEditor(ReaperBridge* bridge)
|
||||||
|
: CPluginView(nullptr), bridge_(bridge) {
|
||||||
|
// Default view size; the host may resize (canResize() == true).
|
||||||
|
ViewRect r(0, 0, 420, 260);
|
||||||
|
setRect(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
ReaSamplerEditor::~ReaSamplerEditor() {
|
||||||
|
#ifdef _WIN32
|
||||||
|
// Defensive teardown: the host normally calls removed() (which destroys the child)
|
||||||
|
// before releasing us, but if we're destroyed while still attached, don't leak the
|
||||||
|
// window — mirror the create in attachedToParent().
|
||||||
|
if (childHwnd_) {
|
||||||
|
DestroyWindow(childHwnd_);
|
||||||
|
childHwnd_ = nullptr;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
tresult PLUGIN_API ReaSamplerEditor::isPlatformTypeSupported(FIDString type) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
if (type && std::string(type) == kPlatformTypeHWND) return kResultTrue;
|
||||||
|
#endif
|
||||||
|
return kResultFalse;
|
||||||
|
}
|
||||||
|
|
||||||
|
tresult PLUGIN_API ReaSamplerEditor::canResize() {
|
||||||
|
return kResultTrue;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
|
||||||
|
void ReaSamplerEditor::attachedToParent() {
|
||||||
|
// systemWindow is the parent HWND the host created (CPluginView::attached set it
|
||||||
|
// from the void* parent when the type is HWND).
|
||||||
|
HWND parent = static_cast<HWND>(systemWindow);
|
||||||
|
if (!parent) return;
|
||||||
|
|
||||||
|
HINSTANCE hInst = reinterpret_cast<HINSTANCE>(
|
||||||
|
GetWindowLongPtr(parent, GWLP_HINSTANCE));
|
||||||
|
if (!hInst) hInst = GetModuleHandle(nullptr);
|
||||||
|
|
||||||
|
// Register the child window class once per module.
|
||||||
|
static bool classRegistered = false;
|
||||||
|
if (!classRegistered) {
|
||||||
|
WNDCLASSW wc{};
|
||||||
|
wc.lpfnWndProc = &ReaSamplerEditor::wndProc;
|
||||||
|
wc.hInstance = hInst;
|
||||||
|
wc.lpszClassName = kChildClassName;
|
||||||
|
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
||||||
|
wc.style = CS_HREDRAW | CS_VREDRAW;
|
||||||
|
RegisterClassW(&wc);
|
||||||
|
classRegistered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ViewRect& r = getRect();
|
||||||
|
childHwnd_ = CreateWindowExW(
|
||||||
|
0, kChildClassName, L"", WS_CHILD | WS_VISIBLE, 0, 0, r.getWidth(),
|
||||||
|
r.getHeight(), parent, nullptr, hInst, nullptr);
|
||||||
|
if (childHwnd_) {
|
||||||
|
// Stash `this` so wndProc can route messages back to the instance.
|
||||||
|
SetWindowLongPtr(childHwnd_, GWLP_USERDATA,
|
||||||
|
reinterpret_cast<LONG_PTR>(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReaSamplerEditor::removedFromParent() {
|
||||||
|
if (childHwnd_) {
|
||||||
|
DestroyWindow(childHwnd_);
|
||||||
|
childHwnd_ = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tresult PLUGIN_API ReaSamplerEditor::onSize(ViewRect* newSize) {
|
||||||
|
// Let CPluginView latch the new rect, then resize the child window to match so the
|
||||||
|
// LICE surface fills the host-provided seat.
|
||||||
|
tresult res = CPluginView::onSize(newSize);
|
||||||
|
if (childHwnd_ && newSize) {
|
||||||
|
MoveWindow(childHwnd_, 0, 0, newSize->getWidth(), newSize->getHeight(), TRUE);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReaSamplerEditor::paint(HDC hdc) {
|
||||||
|
RECT cr{};
|
||||||
|
GetClientRect(childHwnd_, &cr);
|
||||||
|
const int w = cr.right - cr.left;
|
||||||
|
const int h = cr.bottom - cr.top;
|
||||||
|
if (w <= 0 || h <= 0) return;
|
||||||
|
|
||||||
|
// Same double-buffered LICE pattern as bank_panel::paintPanel: draw into a
|
||||||
|
// LICE_SysBitmap, then BitBlt to the window DC.
|
||||||
|
LICE_SysBitmap bmp(w, h);
|
||||||
|
LICE_Clear(&bmp, kColBackground);
|
||||||
|
|
||||||
|
const EditorLayout layout = layoutEditor(w, h);
|
||||||
|
|
||||||
|
// Title band.
|
||||||
|
LICE_FillRect(&bmp, layout.titleBar.left, layout.titleBar.top,
|
||||||
|
layout.titleBar.width(), layout.titleBar.height(), kColTitleBg, 1.0f,
|
||||||
|
0);
|
||||||
|
// Read a known live-state value over the bridge to prove the read spike. Show the
|
||||||
|
// raw ext-state presence (never the full blob) so the title reflects live project
|
||||||
|
// state without dumping JSON into the UI.
|
||||||
|
std::string title = "ReaSampler Instrument";
|
||||||
|
if (bridge_ && bridge_->isConnected()) {
|
||||||
|
auto banks = bridge_->readReasamplerExtState("banks");
|
||||||
|
title += banks ? " [bank: linked]" : " [bank: none]";
|
||||||
|
} else {
|
||||||
|
title += " [host: no bridge]";
|
||||||
|
}
|
||||||
|
Rect titleText{layout.titleBar.left + 8, layout.titleBar.top,
|
||||||
|
layout.titleBar.right - 8, layout.titleBar.bottom};
|
||||||
|
drawText(&bmp, titleText, title.c_str(), kRgbText);
|
||||||
|
|
||||||
|
// The clickable button — fill reflects the last hit-test (the routing proof).
|
||||||
|
LICE_FillRect(&bmp, layout.button.left, layout.button.top, layout.button.width(),
|
||||||
|
layout.button.height(), buttonHit_ ? kColBtnHitBg : kColBtnBg, 1.0f,
|
||||||
|
0);
|
||||||
|
LICE_DrawRect(&bmp, layout.button.left, layout.button.top, layout.button.width(),
|
||||||
|
layout.button.height(), kColBtnBorder, 1.0f, 0);
|
||||||
|
Rect btnText{layout.button.left + 8, layout.button.top, layout.button.right,
|
||||||
|
layout.button.bottom};
|
||||||
|
drawText(&bmp, btnText, buttonHit_ ? "clicked" : "click me", kRgbText);
|
||||||
|
|
||||||
|
BitBlt(hdc, 0, 0, w, h, bmp.getDC(), 0, 0, SRCCOPY);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReaSamplerEditor::onClick(int x, int y) {
|
||||||
|
RECT cr{};
|
||||||
|
GetClientRect(childHwnd_, &cr);
|
||||||
|
const EditorLayout layout = layoutEditor(cr.right - cr.left, cr.bottom - cr.top);
|
||||||
|
if (hitTest(layout, x, y) == HitTarget::kButton) {
|
||||||
|
buttonHit_ = !buttonHit_;
|
||||||
|
InvalidateRect(childHwnd_, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LRESULT CALLBACK ReaSamplerEditor::wndProc(HWND hwnd, UINT msg, WPARAM wParam,
|
||||||
|
LPARAM lParam) {
|
||||||
|
auto* self = reinterpret_cast<ReaSamplerEditor*>(
|
||||||
|
GetWindowLongPtr(hwnd, GWLP_USERDATA));
|
||||||
|
switch (msg) {
|
||||||
|
case WM_PAINT: {
|
||||||
|
PAINTSTRUCT ps{};
|
||||||
|
HDC hdc = BeginPaint(hwnd, &ps);
|
||||||
|
if (self) self->paint(hdc);
|
||||||
|
EndPaint(hwnd, &ps);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
case WM_LBUTTONDOWN:
|
||||||
|
if (self) self->onClick(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
|
||||||
|
return 0;
|
||||||
|
case WM_ERASEBKGND:
|
||||||
|
return 1; // we fully repaint in WM_PAINT; skip the flicker-inducing erase
|
||||||
|
default:
|
||||||
|
return DefWindowProcW(hwnd, msg, wParam, lParam);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#else // non-Windows: not a build target (D5), but keep the TU compilable.
|
||||||
|
|
||||||
|
void ReaSamplerEditor::attachedToParent() {}
|
||||||
|
void ReaSamplerEditor::removedFromParent() {}
|
||||||
|
tresult PLUGIN_API ReaSamplerEditor::onSize(ViewRect* newSize) {
|
||||||
|
return CPluginView::onSize(newSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // _WIN32
|
||||||
|
|
||||||
|
} // namespace reasampler::vst
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
// reasampler_editor.h — the VST3 IPlugView LICE editor (Phase S1 bridge spike). THIN
|
||||||
|
// shell: hosts a LICE-drawn child window inside the host's IPlugView seat and routes
|
||||||
|
// host paint/click into the pure editor_geometry hit-test. Windows-only (D5).
|
||||||
|
//
|
||||||
|
// This is the phase's ONE genuine unknown (per CONTEXT.md §Phase S / S1): wiring LICE
|
||||||
|
// into an IPlugView. It reuses the bank_panel LICE/SWELL competence — a LICE_SysBitmap
|
||||||
|
// blitted in WM_PAINT, GET_X/Y_LPARAM hit-testing in WM_LBUTTONDOWN — but hangs it off a
|
||||||
|
// child HWND created in IPlugView::attached() rather than a docked SWELL dialog.
|
||||||
|
//
|
||||||
|
// Subclasses CPluginView (public.sdk/source/common/pluginview.h) for the IPlugView
|
||||||
|
// refcount + attached/removed/getSize/onSize boilerplate; we override the attach/remove
|
||||||
|
// hooks to create/destroy the child window and onSize to resize it.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "public.sdk/source/common/pluginview.h"
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace reasampler::vst {
|
||||||
|
|
||||||
|
class ReaperBridge;
|
||||||
|
|
||||||
|
class ReaSamplerEditor : public Steinberg::CPluginView {
|
||||||
|
public:
|
||||||
|
// `bridge` is owned by the processor and outlives the editor; the editor reads
|
||||||
|
// (never mutates) it to show a live-state readout. May be null (non-REAPER host).
|
||||||
|
explicit ReaSamplerEditor(ReaperBridge* bridge);
|
||||||
|
~ReaSamplerEditor() override;
|
||||||
|
|
||||||
|
// Accept only the Windows HWND platform type (D5: Windows-only).
|
||||||
|
Steinberg::tresult PLUGIN_API isPlatformTypeSupported(
|
||||||
|
Steinberg::FIDString type) override;
|
||||||
|
|
||||||
|
// The view is user-resizable in the spike so we exercise the onSize path.
|
||||||
|
Steinberg::tresult PLUGIN_API canResize() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// CPluginView hooks: systemWindow is set by the time attachedToParent() fires.
|
||||||
|
void attachedToParent() override;
|
||||||
|
void removedFromParent() override;
|
||||||
|
|
||||||
|
Steinberg::tresult PLUGIN_API onSize(Steinberg::ViewRect* newSize) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
#ifdef _WIN32
|
||||||
|
// Draw the current surface into the child window's DC via a LICE bitmap.
|
||||||
|
void paint(HDC hdc);
|
||||||
|
// Route a client-space click through editor_geometry::hitTest.
|
||||||
|
void onClick(int x, int y);
|
||||||
|
|
||||||
|
static LRESULT CALLBACK wndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
|
||||||
|
|
||||||
|
HWND childHwnd_ = nullptr;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
ReaperBridge* bridge_ = nullptr;
|
||||||
|
// Latched on click so the paint reflects the last hit-test result — the spike's
|
||||||
|
// proof that host->click->draw routing round-trips.
|
||||||
|
bool buttonHit_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace reasampler::vst
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
// reasampler_processor.cpp — see reasampler_processor.h.
|
||||||
|
|
||||||
|
#include "reasampler_processor.h"
|
||||||
|
|
||||||
|
#include "pluginterfaces/vst/ivstaudioprocessor.h"
|
||||||
|
#include "pluginterfaces/vst/vstspeaker.h"
|
||||||
|
|
||||||
|
#include "reasampler_editor.h"
|
||||||
|
|
||||||
|
using namespace Steinberg;
|
||||||
|
using namespace Steinberg::Vst;
|
||||||
|
|
||||||
|
namespace reasampler::vst {
|
||||||
|
|
||||||
|
FUnknown* ReaSamplerProcessor::createInstance(void* /*context*/) {
|
||||||
|
// The host owns the returned reference. Cast up to the combined interface the SDK
|
||||||
|
// exposes (IAudioProcessor) so the FUnknown refcount is correctly rooted.
|
||||||
|
return static_cast<IAudioProcessor*>(new ReaSamplerProcessor());
|
||||||
|
}
|
||||||
|
|
||||||
|
tresult PLUGIN_API ReaSamplerProcessor::initialize(FUnknown* context) {
|
||||||
|
tresult result = SingleComponentEffect::initialize(context);
|
||||||
|
if (result != kResultOk) return result;
|
||||||
|
|
||||||
|
// Connect the REAPER bridge. Non-fatal if it fails (non-REAPER host): the
|
||||||
|
// instrument still loads, the editor just shows "no bridge".
|
||||||
|
bridge_.connect(context);
|
||||||
|
|
||||||
|
// Instrument bus topology: one event input (MIDI in, 16 channels), one stereo audio
|
||||||
|
// output, no audio input. This is the standard VSTi arrangement.
|
||||||
|
addEventInput(STR16("MIDI In"), 16);
|
||||||
|
addAudioOutput(STR16("Stereo Out"), SpeakerArr::kStereo);
|
||||||
|
|
||||||
|
return kResultOk;
|
||||||
|
}
|
||||||
|
|
||||||
|
tresult PLUGIN_API ReaSamplerProcessor::terminate() {
|
||||||
|
return SingleComponentEffect::terminate();
|
||||||
|
}
|
||||||
|
|
||||||
|
tresult PLUGIN_API ReaSamplerProcessor::setActive(TBool /*state*/) {
|
||||||
|
// Nothing to allocate/free in the silent skeleton; S4 will size voice buffers here
|
||||||
|
// against the setupProcessing block size.
|
||||||
|
return kResultOk;
|
||||||
|
}
|
||||||
|
|
||||||
|
tresult PLUGIN_API ReaSamplerProcessor::setupProcessing(ProcessSetup& setup) {
|
||||||
|
return SingleComponentEffect::setupProcessing(setup);
|
||||||
|
}
|
||||||
|
|
||||||
|
tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) {
|
||||||
|
// Silent skeleton: emit silence on the output bus so the instrument runs cleanly in
|
||||||
|
// REAPER's render/record path without a null buffer. S4 marshals MIDI->core->audio.
|
||||||
|
if (data.numOutputs > 0 && data.outputs && data.numSamples > 0) {
|
||||||
|
AudioBusBuffers& out = data.outputs[0];
|
||||||
|
for (int32 ch = 0; ch < out.numChannels; ++ch) {
|
||||||
|
if (data.symbolicSampleSize == kSample32) {
|
||||||
|
if (float* buf = out.channelBuffers32[ch]) {
|
||||||
|
for (int32 i = 0; i < data.numSamples; ++i) buf[i] = 0.f;
|
||||||
|
}
|
||||||
|
} else if (data.symbolicSampleSize == kSample64) {
|
||||||
|
if (double* buf = out.channelBuffers64[ch]) {
|
||||||
|
for (int32 i = 0; i < data.numSamples; ++i) buf[i] = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Flag output silence so the host can optimize (nothing plays yet).
|
||||||
|
out.silenceFlags = (out.numChannels >= 64)
|
||||||
|
? ~0ULL
|
||||||
|
: ((1ULL << out.numChannels) - 1);
|
||||||
|
}
|
||||||
|
return kResultOk;
|
||||||
|
}
|
||||||
|
|
||||||
|
IPlugView* PLUGIN_API ReaSamplerProcessor::createView(FIDString name) {
|
||||||
|
if (name && FIDStringsEqual(name, ViewType::kEditor)) {
|
||||||
|
return new ReaSamplerEditor(&bridge_);
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace reasampler::vst
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
// reasampler_processor.h — the VST3 SingleComponentEffect skeleton (Phase S1). THIN
|
||||||
|
// shell: an instrument that declares an event-input bus + a stereo audio-output bus,
|
||||||
|
// sets up processing, and runs an empty (silent) process. Nothing plays yet — S4 wires
|
||||||
|
// the pure sampler core into process(); S1 only proves REAPER hosts it.
|
||||||
|
//
|
||||||
|
// SingleComponentEffect is the SDK's combined processor+controller base — sanctioned
|
||||||
|
// for a non-distributable, REAPER-only plugin under D5/D6 (verified: SDK class
|
||||||
|
// reference). It gives us addAudioOutput/addEventInput and the IEditController seat, so
|
||||||
|
// createView() can hand the host our IPlugView LICE editor.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "public.sdk/source/vst/vstsinglecomponenteffect.h"
|
||||||
|
|
||||||
|
#include "reaper_bridge.h"
|
||||||
|
|
||||||
|
namespace reasampler::vst {
|
||||||
|
|
||||||
|
class ReaSamplerProcessor : public Steinberg::Vst::SingleComponentEffect {
|
||||||
|
public:
|
||||||
|
ReaSamplerProcessor() = default;
|
||||||
|
|
||||||
|
// The factory create function (registered in vst_entry.cpp).
|
||||||
|
static Steinberg::FUnknown* createInstance(void* /*context*/);
|
||||||
|
|
||||||
|
//--- from IComponent / IPluginBase -------------------------------------
|
||||||
|
// Connects the REAPER bridge (context is REAPER's IHostApplication) and declares
|
||||||
|
// the instrument bus topology.
|
||||||
|
Steinberg::tresult PLUGIN_API initialize(Steinberg::FUnknown* context) override;
|
||||||
|
Steinberg::tresult PLUGIN_API terminate() override;
|
||||||
|
Steinberg::tresult PLUGIN_API setActive(Steinberg::TBool state) override;
|
||||||
|
|
||||||
|
//--- from IAudioProcessor ----------------------------------------------
|
||||||
|
Steinberg::tresult PLUGIN_API setupProcessing(
|
||||||
|
Steinberg::Vst::ProcessSetup& setup) override;
|
||||||
|
// Empty in the spike: emits silence (S4 fills it).
|
||||||
|
Steinberg::tresult PLUGIN_API process(
|
||||||
|
Steinberg::Vst::ProcessData& data) override;
|
||||||
|
|
||||||
|
//--- from IEditController -----------------------------------------------
|
||||||
|
// Hands the host our LICE IPlugView editor.
|
||||||
|
Steinberg::IPlugView* PLUGIN_API createView(Steinberg::FIDString name) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
ReaperBridge bridge_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace reasampler::vst
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "pluginterfaces/base/funknown.h"
|
||||||
|
|
||||||
|
namespace reasampler::vst {
|
||||||
|
|
||||||
|
// Human-facing identity. "ReaSampler" is the tool; the instrument surfaces as
|
||||||
|
// "ReaSampler Instrument" in REAPER's FX browser to distinguish it from the extension.
|
||||||
|
inline constexpr const char* kPluginName = "ReaSampler Instrument";
|
||||||
|
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.
|
||||||
|
#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);
|
||||||
|
|
||||||
|
} // namespace reasampler::vst
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
// 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 "reasampler_processor.h"
|
||||||
|
#include "reasampler_vst.h"
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
|
||||||
|
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),
|
||||||
|
Steinberg::PClassInfo::kManyInstances, // cardinality
|
||||||
|
kVstAudioEffectClass, // component category (fixed)
|
||||||
|
reasampler::vst::kPluginName, // plug-in display name
|
||||||
|
0, // single-component => 0
|
||||||
|
Steinberg::Vst::PlugType::kInstrumentSynthSampler, // subcategory
|
||||||
|
REASAMPLER_VST_VERSION, // plug-in version
|
||||||
|
kVstVersionString, // VST3 SDK version (fixed)
|
||||||
|
reasampler::vst::ReaSamplerProcessor::createInstance)
|
||||||
|
|
||||||
|
END_FACTORY
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
// Standalone tests for reasampler::vst::bridge_marshal — no VST3, no REAPER, no test
|
||||||
|
// framework. Same fast assert loop as the sibling pure tests: assert the REAPER
|
||||||
|
// bridge-read marshalling (GetProjExtState result decode + a small JSON string-field
|
||||||
|
// reader) directly, so the DAW-facing shell only has to invoke the API.
|
||||||
|
//
|
||||||
|
// Covers: decodeGetProjExtState hit/absent/zero-return/empty-buffer (the stale-buffer
|
||||||
|
// guard); extractJsonStringField present/absent/escapes/whitespace/value-vs-key
|
||||||
|
// disambiguation/non-string-value/malformed.
|
||||||
|
|
||||||
|
#include "../src/vst/bridge_marshal.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
using namespace reasampler::vst;
|
||||||
|
|
||||||
|
static int g_fail = 0;
|
||||||
|
#define CHECK(cond) do { if(!(cond)) { \
|
||||||
|
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||||
|
|
||||||
|
// --- decodeGetProjExtState ----------------------------------------------------
|
||||||
|
|
||||||
|
static void testDecodeHit() {
|
||||||
|
// REAPER reports a non-zero length and filled the buffer: that IS the value.
|
||||||
|
auto v = decodeGetProjExtState(5, "hello");
|
||||||
|
CHECK(v.has_value());
|
||||||
|
CHECK(v && *v == "hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testDecodeAbsentKey() {
|
||||||
|
// REAPER returns 0 for an absent key. Even if a caller passed a dirty buffer, the
|
||||||
|
// decoder must NOT surface it — the zero return means "no value".
|
||||||
|
auto v = decodeGetProjExtState(0, "stale-bytes-from-a-prior-read");
|
||||||
|
CHECK(!v.has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testDecodeNegativeReturn() {
|
||||||
|
auto v = decodeGetProjExtState(-1, "whatever");
|
||||||
|
CHECK(!v.has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testDecodeEmptyBuffer() {
|
||||||
|
// Positive return but empty buffer — treat as no value (defensive).
|
||||||
|
auto v = decodeGetProjExtState(3, "");
|
||||||
|
CHECK(!v.has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- extractJsonStringField ---------------------------------------------------
|
||||||
|
|
||||||
|
static void testExtractPresent() {
|
||||||
|
const std::string json = R"({"guid":"ABC-123","name":"kick"})";
|
||||||
|
auto g = extractJsonStringField(json, "guid");
|
||||||
|
CHECK(g && *g == "ABC-123");
|
||||||
|
auto n = extractJsonStringField(json, "name");
|
||||||
|
CHECK(n && *n == "kick");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testExtractAbsent() {
|
||||||
|
const std::string json = R"({"guid":"ABC-123"})";
|
||||||
|
CHECK(!extractJsonStringField(json, "missing").has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testExtractWhitespaceTolerant() {
|
||||||
|
const std::string json = "{ \"guid\" : \"X\" , \"n\":\"y\" }";
|
||||||
|
auto g = extractJsonStringField(json, "guid");
|
||||||
|
CHECK(g && *g == "X");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testExtractEscapes() {
|
||||||
|
// \" \\ \/ \n \t all decode.
|
||||||
|
const std::string json = R"({"path":"a\\b\/c\"d\ne"})";
|
||||||
|
auto p = extractJsonStringField(json, "path");
|
||||||
|
CHECK(p && *p == "a\\b/c\"d\ne");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testExtractValueContainingKeyText() {
|
||||||
|
// A VALUE that contains the key text must not be mistaken for the member. Here the
|
||||||
|
// first "guid" occurrence is inside another value; the real member comes later.
|
||||||
|
const std::string json = R"({"note":"the guid is here","guid":"REAL"})";
|
||||||
|
auto g = extractJsonStringField(json, "guid");
|
||||||
|
CHECK(g && *g == "REAL");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testExtractNonStringValue() {
|
||||||
|
// A numeric/object value is not a string — return nullopt rather than garbage.
|
||||||
|
const std::string json = R"({"count":42,"name":"ok"})";
|
||||||
|
CHECK(!extractJsonStringField(json, "count").has_value());
|
||||||
|
// The sibling string field still reads.
|
||||||
|
auto n = extractJsonStringField(json, "name");
|
||||||
|
CHECK(n && *n == "ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testExtractMalformed() {
|
||||||
|
CHECK(!extractJsonStringField(R"({"guid":"unterminated)", "guid").has_value());
|
||||||
|
CHECK(!extractJsonStringField(R"({"guid":)", "guid").has_value());
|
||||||
|
CHECK(!extractJsonStringField(R"({"guid")", "guid").has_value());
|
||||||
|
CHECK(!extractJsonStringField("", "guid").has_value());
|
||||||
|
// Dangling escape at end of string.
|
||||||
|
CHECK(!extractJsonStringField(R"({"guid":"abc\)", "guid").has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
testDecodeHit();
|
||||||
|
testDecodeAbsentKey();
|
||||||
|
testDecodeNegativeReturn();
|
||||||
|
testDecodeEmptyBuffer();
|
||||||
|
testExtractPresent();
|
||||||
|
testExtractAbsent();
|
||||||
|
testExtractWhitespaceTolerant();
|
||||||
|
testExtractEscapes();
|
||||||
|
testExtractValueContainingKeyText();
|
||||||
|
testExtractNonStringValue();
|
||||||
|
testExtractMalformed();
|
||||||
|
|
||||||
|
if (g_fail == 0) std::printf("bridge_marshal: all tests passed\n");
|
||||||
|
return g_fail != 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
// Standalone tests for reasampler::vst::editor_geometry — no VST3, no REAPER, no test
|
||||||
|
// framework. Same fast assert loop as the sibling pure tests (mode_switch et al.):
|
||||||
|
// assert the IPlugView LICE editor's layout math + hit-testing directly.
|
||||||
|
//
|
||||||
|
// Covers: contains() half-open convention + degenerate rects; layoutEditor regions on a
|
||||||
|
// normal view (title band + button + canvas), a tiny view (button clamped to canvas,
|
||||||
|
// never overhanging), and a zero view (all rects empty, no inversion); hitTest hitting
|
||||||
|
// the button, missing on the title/canvas, missing outside the surface, and boundary
|
||||||
|
// pixels; layout<->hit-test agreement (a click on the drawn button rect hits it).
|
||||||
|
|
||||||
|
#include "../src/vst/editor_geometry.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
using namespace reasampler::vst;
|
||||||
|
|
||||||
|
static int g_fail = 0;
|
||||||
|
#define CHECK(cond) do { if(!(cond)) { \
|
||||||
|
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||||
|
|
||||||
|
// --- contains() ---------------------------------------------------------------
|
||||||
|
|
||||||
|
static void testContainsHalfOpen() {
|
||||||
|
Rect r{10, 20, 50, 40}; // [10,50) x [20,40)
|
||||||
|
CHECK(contains(r, 10, 20)); // top-left inclusive
|
||||||
|
CHECK(contains(r, 49, 39)); // bottom-right exclusive edge, inside
|
||||||
|
CHECK(!contains(r, 50, 30)); // right edge excluded
|
||||||
|
CHECK(!contains(r, 30, 40)); // bottom edge excluded
|
||||||
|
CHECK(!contains(r, 9, 30)); // left of rect
|
||||||
|
CHECK(!contains(r, 30, 19)); // above rect
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testContainsDegenerate() {
|
||||||
|
CHECK(!contains(Rect{10, 10, 10, 20}, 10, 15)); // zero width
|
||||||
|
CHECK(!contains(Rect{10, 10, 20, 10}, 15, 10)); // zero height
|
||||||
|
CHECK(!contains(Rect{20, 10, 10, 20}, 15, 15)); // inverted (right < left)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- layoutEditor: normal view ------------------------------------------------
|
||||||
|
|
||||||
|
static void testLayoutNormalView() {
|
||||||
|
// A comfortable 400x260 view: title band spans the top full width; canvas is the
|
||||||
|
// rest; button sits inside the canvas, inset by the margin.
|
||||||
|
const EditorLayout L = layoutEditor(400, 260);
|
||||||
|
|
||||||
|
CHECK(L.titleBar.left == 0 && L.titleBar.top == 0);
|
||||||
|
CHECK(L.titleBar.right == 400);
|
||||||
|
CHECK(L.titleBar.height() > 0 && L.titleBar.height() <= 260);
|
||||||
|
|
||||||
|
// Canvas begins right below the title bar and reaches the bottom-right.
|
||||||
|
CHECK(L.canvas.top == L.titleBar.bottom);
|
||||||
|
CHECK(L.canvas.right == 400 && L.canvas.bottom == 260);
|
||||||
|
|
||||||
|
// Button is inside the canvas (does not overhang any edge).
|
||||||
|
CHECK(L.button.left >= L.canvas.left);
|
||||||
|
CHECK(L.button.top >= L.canvas.top);
|
||||||
|
CHECK(L.button.right <= L.canvas.right);
|
||||||
|
CHECK(L.button.bottom <= L.canvas.bottom);
|
||||||
|
CHECK(L.button.width() > 0 && L.button.height() > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- layoutEditor: tiny view (clamping) ---------------------------------------
|
||||||
|
|
||||||
|
static void testLayoutTinyViewClampsButton() {
|
||||||
|
// A view narrower/shorter than the button's natural size: the button must clamp to
|
||||||
|
// the canvas and never produce an inverted or overhanging rect.
|
||||||
|
const EditorLayout L = layoutEditor(40, 40);
|
||||||
|
CHECK(L.button.right <= L.canvas.right);
|
||||||
|
CHECK(L.button.bottom <= L.canvas.bottom);
|
||||||
|
CHECK(L.button.right >= L.button.left); // never inverted
|
||||||
|
CHECK(L.button.bottom >= L.button.top);
|
||||||
|
// Title bar clamps to the client height when the view is shorter than its height.
|
||||||
|
CHECK(L.titleBar.bottom <= 40);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- layoutEditor: zero view (all empty, no inversion) ------------------------
|
||||||
|
|
||||||
|
static void testLayoutZeroView() {
|
||||||
|
const EditorLayout L = layoutEditor(0, 0);
|
||||||
|
CHECK(L.titleBar.width() <= 0 || L.titleBar.height() <= 0);
|
||||||
|
CHECK(L.canvas.width() <= 0 || L.canvas.height() <= 0);
|
||||||
|
// No rect is inverted.
|
||||||
|
CHECK(L.button.right >= L.button.left);
|
||||||
|
CHECK(L.button.bottom >= L.button.top);
|
||||||
|
CHECK(L.canvas.right >= L.canvas.left);
|
||||||
|
CHECK(L.canvas.bottom >= L.canvas.top);
|
||||||
|
// A click anywhere on an empty layout hits nothing.
|
||||||
|
CHECK(hitTest(L, 0, 0) == HitTarget::kNone);
|
||||||
|
CHECK(hitTest(L, 5, 5) == HitTarget::kNone);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- hitTest ------------------------------------------------------------------
|
||||||
|
|
||||||
|
static void testHitTestButton() {
|
||||||
|
const EditorLayout L = layoutEditor(400, 260);
|
||||||
|
// Center of the button hits it.
|
||||||
|
const int cx = (L.button.left + L.button.right) / 2;
|
||||||
|
const int cy = (L.button.top + L.button.bottom) / 2;
|
||||||
|
CHECK(hitTest(L, cx, cy) == HitTarget::kButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testHitTestMissesNonButton() {
|
||||||
|
const EditorLayout L = layoutEditor(400, 260);
|
||||||
|
// Title bar is inert in the spike.
|
||||||
|
CHECK(hitTest(L, 200, L.titleBar.top + 1) == HitTarget::kNone);
|
||||||
|
// Empty canvas away from the button.
|
||||||
|
CHECK(hitTest(L, 380, 240) == HitTarget::kNone);
|
||||||
|
// Outside the surface entirely.
|
||||||
|
CHECK(hitTest(L, -5, -5) == HitTarget::kNone);
|
||||||
|
CHECK(hitTest(L, 500, 500) == HitTarget::kNone);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testHitTestButtonBoundary() {
|
||||||
|
const EditorLayout L = layoutEditor(400, 260);
|
||||||
|
// Top-left corner of the button is inclusive; the right/bottom edges are excluded.
|
||||||
|
CHECK(hitTest(L, L.button.left, L.button.top) == HitTarget::kButton);
|
||||||
|
CHECK(hitTest(L, L.button.right, L.button.top) == HitTarget::kNone);
|
||||||
|
CHECK(hitTest(L, L.button.left, L.button.bottom) == HitTarget::kNone);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- layout<->hit-test agreement ----------------------------------------------
|
||||||
|
|
||||||
|
// Every pixel inside the drawn button rect must hit the button; this is the
|
||||||
|
// load-bearing consistency invariant between what the shell draws and what it routes.
|
||||||
|
static void testHitTestMatchesDrawnButton() {
|
||||||
|
const EditorLayout L = layoutEditor(320, 200);
|
||||||
|
for (int y = L.button.top; y < L.button.bottom; ++y) {
|
||||||
|
for (int x = L.button.left; x < L.button.right; ++x) {
|
||||||
|
CHECK(hitTest(L, x, y) == HitTarget::kButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
testContainsHalfOpen();
|
||||||
|
testContainsDegenerate();
|
||||||
|
testLayoutNormalView();
|
||||||
|
testLayoutTinyViewClampsButton();
|
||||||
|
testLayoutZeroView();
|
||||||
|
testHitTestButton();
|
||||||
|
testHitTestMissesNonButton();
|
||||||
|
testHitTestButtonBoundary();
|
||||||
|
testHitTestMatchesDrawnButton();
|
||||||
|
|
||||||
|
if (g_fail == 0) std::printf("editor_geometry: all tests passed\n");
|
||||||
|
return g_fail != 0;
|
||||||
|
}
|
||||||
+1
Submodule vendor/vst3sdk added at dfff2e399c
Reference in New Issue
Block a user