From 5f869541a3cd70b9409d51d5f8ae21a52a486386 Mon Sep 17 00:00:00 2001 From: Edu Meneses Date: Wed, 1 Jul 2026 17:34:59 -0400 Subject: [PATCH 01/24] vst3: optional self-contained VSTGUI editor Adds an embedded VST3 editor built on VSTGUI (bundled with the VST3 SDK), so avnd VST3 plug-ins get a real GUI in any host without Qt. - vstgui_editor.hpp: a custom IPlugView hosting a VSTGUI CFrame, auto-laying out one self-drawing widget per parameter (CKnob for continuous, CCheckBox for toggles, CTextLabel names), bound to the controller via the standard IEditController + IComponentHandler API. No bitmaps, no uidesc. - Controller::createView returns it when built with AVND_VST3_VSTGUI. - avendish.vst3.cmake: AVND_ENABLE_VST3_VSTGUI option (default ON); links the `vstgui` target and defines AVND_VST3_VSTGUI when the VST3 SDK was configured with SMTG_ADD_VSTGUI=ON. First cut: generic auto-layout. Honoring the halp `struct ui` layout and theming are follow-ups. --- cmake/avendish.vst3.cmake | 11 + include/avnd/binding/vst3/controller.hpp | 10 + include/avnd/binding/vst3/vstgui_editor.hpp | 228 ++++++++++++++++++++ 3 files changed, 249 insertions(+) create mode 100644 include/avnd/binding/vst3/vstgui_editor.hpp diff --git a/cmake/avendish.vst3.cmake b/cmake/avendish.vst3.cmake index 3a1ad192..a87c5b9a 100644 --- a/cmake/avendish.vst3.cmake +++ b/cmake/avendish.vst3.cmake @@ -6,6 +6,10 @@ if(DEFINED AVND_ENABLE_VST3 AND NOT AVND_ENABLE_VST3) return() endif() +# Build a self-contained VSTGUI editor into VST3 plug-ins (no Qt). Needs the +# VST3 SDK configured with SMTG_ADD_VSTGUI=ON so the `vstgui` target exists. +option(AVND_ENABLE_VST3_VSTGUI "Embed a VSTGUI editor in VST3 plug-ins" ON) + set(VST3_SDK_ROOT "" CACHE PATH "VST3 SDK path") if(NOT VST3_SDK_ROOT) function(avnd_make_vst3) @@ -138,6 +142,13 @@ function(avnd_make_vst3) ) endif() + # Optional self-contained VSTGUI editor. Requires the VST3 SDK to have been + # configured with SMTG_ADD_VSTGUI=ON (which provides the `vstgui` target). + if(AVND_ENABLE_VST3_VSTGUI AND TARGET vstgui) + target_link_libraries(${AVND_FX_TARGET} PRIVATE vstgui) + target_compile_definitions(${AVND_FX_TARGET} PRIVATE AVND_VST3_VSTGUI=1) + endif() + avnd_common_setup("${AVND_TARGET}" "${AVND_FX_TARGET}") endfunction() diff --git a/include/avnd/binding/vst3/controller.hpp b/include/avnd/binding/vst3/controller.hpp index 4aed48ea..0c31914b 100644 --- a/include/avnd/binding/vst3/controller.hpp +++ b/include/avnd/binding/vst3/controller.hpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -69,6 +70,15 @@ class Controller final virtual ~Controller(); +#if defined(AVND_VST3_VSTGUI) + Steinberg::IPlugView* createView(const char* name) override + { + if(name && std::strcmp(name, Steinberg::Vst::ViewType::kEditor) == 0) + return new stv3::VstGuiEditor{this}; + return nullptr; + } +#endif + int32 getParameterCount() override { return inputs_info_t::size; } Steinberg::tresult getParameterInfo(int32 paramIndex, ParameterInfo& info) override diff --git a/include/avnd/binding/vst3/vstgui_editor.hpp b/include/avnd/binding/vst3/vstgui_editor.hpp new file mode 100644 index 00000000..4eb228e1 --- /dev/null +++ b/include/avnd/binding/vst3/vstgui_editor.hpp @@ -0,0 +1,228 @@ +#pragma once + +/* SPDX-License-Identifier: GPL-3.0-or-later */ + +// A self-contained VST3 editor built on VSTGUI. Enabled when the addon is +// compiled with AVND_VST3_VSTGUI (see avendish.vst3.cmake). It hosts a VSTGUI +// CFrame inside a host-provided window and auto-lays-out one self-drawing +// widget per parameter (knob for continuous, checkbox for toggles), bound to +// the controller through the standard IEditController + IComponentHandler API. +// No Qt, no bitmaps, no uidesc: it works for any avnd plugin out of the box. + +#if defined(AVND_VST3_VSTGUI) + +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +namespace stv3 +{ + +class VstGuiEditor final + : public Steinberg::IPlugView + , public VSTGUI::IControlListener +{ +public: + explicit VstGuiEditor(ControllerCommon* c) + : controller{c} + { + } + + // ---------------- IUnknown ---------------- + Steinberg::tresult PLUGIN_API queryInterface(const Steinberg::TUID iid, void** obj) override + { + using namespace Steinberg; + if(FUnknownPrivate::iidEqual(iid, FUnknown::iid) + || FUnknownPrivate::iidEqual(iid, IPlugView::iid)) + { + *obj = static_cast(this); + addRef(); + return kResultTrue; + } + *obj = nullptr; + return kNoInterface; + } + Steinberg::uint32 PLUGIN_API addRef() override { return ++m_ref; } + Steinberg::uint32 PLUGIN_API release() override + { + if(--m_ref == 0) { delete this; return 0; } + return m_ref; + } + + // ---------------- IPlugView ---------------- + Steinberg::tresult PLUGIN_API isPlatformTypeSupported(Steinberg::FIDString type) override + { + return std::strcmp(type, currentPlatformType()) == 0 ? Steinberg::kResultTrue + : Steinberg::kResultFalse; + } + + Steinberg::tresult PLUGIN_API attached(void* parent, Steinberg::FIDString /*type*/) override + { + computeSize(); + frame = new VSTGUI::CFrame(VSTGUI::CRect(0, 0, m_width, m_height), this); + frame->setBackgroundColor(VSTGUI::CColor{32, 32, 34, 255}); + + VSTGUI::PlatformType pt = +#if defined(_WIN32) + VSTGUI::PlatformType::kHWND; +#elif defined(__APPLE__) + VSTGUI::PlatformType::kNSView; +#else + VSTGUI::PlatformType::kX11EmbedWindowID; +#endif + frame->open(parent, pt); + buildControls(); + return Steinberg::kResultTrue; + } + + Steinberg::tresult PLUGIN_API removed() override + { + if(frame) + { + frame->close(); // forgets itself + frame = nullptr; + } + return Steinberg::kResultTrue; + } + + Steinberg::tresult PLUGIN_API onWheel(float) override { return Steinberg::kResultFalse; } + Steinberg::tresult PLUGIN_API onKeyDown(Steinberg::char16, Steinberg::int16, Steinberg::int16) override + { return Steinberg::kResultFalse; } + Steinberg::tresult PLUGIN_API onKeyUp(Steinberg::char16, Steinberg::int16, Steinberg::int16) override + { return Steinberg::kResultFalse; } + + Steinberg::tresult PLUGIN_API getSize(Steinberg::ViewRect* size) override + { + size->left = 0; size->top = 0; size->right = m_width; size->bottom = m_height; + return Steinberg::kResultTrue; + } + Steinberg::tresult PLUGIN_API onSize(Steinberg::ViewRect*) override { return Steinberg::kResultTrue; } + Steinberg::tresult PLUGIN_API onFocus(Steinberg::TBool) override { return Steinberg::kResultTrue; } + Steinberg::tresult PLUGIN_API setFrame(Steinberg::IPlugFrame* f) override + { + plugFrame = f; + return Steinberg::kResultTrue; + } + Steinberg::tresult PLUGIN_API canResize() override { return Steinberg::kResultFalse; } + Steinberg::tresult PLUGIN_API checkSizeConstraint(Steinberg::ViewRect*) override + { return Steinberg::kResultTrue; } + + // ---------------- IControlListener ---------------- + void valueChanged(VSTGUI::CControl* pControl) override + { + auto tag = static_cast(pControl->getTag()); + auto v = static_cast(pControl->getValueNormalized()); + if(controller->componentHandler) + { + controller->componentHandler->beginEdit(tag); + controller->componentHandler->performEdit(tag, v); + controller->componentHandler->endEdit(tag); + } + edit_controller()->setParamNormalized(tag, v); + } + +private: + static Steinberg::FIDString currentPlatformType() + { +#if defined(_WIN32) + return Steinberg::kPlatformTypeHWND; +#elif defined(__APPLE__) + return Steinberg::kPlatformTypeNSView; +#else + return Steinberg::kPlatformTypeX11EmbedWindowID; +#endif + } + + Steinberg::Vst::IEditController* edit_controller() + { + return static_cast(controller); + } + + void computeSize() + { + const int n = edit_controller()->getParameterCount(); + const int rows = (n + cols - 1) / cols; + m_width = cols * cell_w + margin * 2; + m_height = (rows > 0 ? rows : 1) * cell_h + margin * 2; + if(m_width < 240) m_width = 240; + if(m_height < 140) m_height = 140; + } + + void buildControls() + { + auto* ec = edit_controller(); + const int n = ec->getParameterCount(); + int col = 0, row = 0; + for(int i = 0; i < n; ++i) + { + Steinberg::Vst::ParameterInfo info{}; + if(ec->getParameterInfo(i, info) != Steinberg::kResultTrue) + continue; + + const VSTGUI::CCoord x = margin + col * cell_w; + const VSTGUI::CCoord y = margin + row * cell_h; + + VSTGUI::CControl* ctl = nullptr; + if(info.stepCount == 1) + { + // Toggle -> self-drawing checkbox. + VSTGUI::CRect rc(x + 8, y + 24, x + cell_w - 8, y + 48); + ctl = new VSTGUI::CCheckBox(rc, this, info.id, title_of(info).data()); + } + else + { + // Continuous -> self-drawing knob. + VSTGUI::CRect rc(x + (cell_w - 56) / 2, y + 20, x + (cell_w - 56) / 2 + 56, y + 76); + auto* knob = new VSTGUI::CKnob(rc, this, info.id, nullptr, nullptr); + knob->setDrawStyle(VSTGUI::CKnob::kCoronaDrawing | VSTGUI::CKnob::kHandleCircleDrawing); + ctl = knob; + + // Name label above the knob. + VSTGUI::CRect lr(x, y + 2, x + cell_w, y + 18); + auto* lbl = new VSTGUI::CTextLabel(lr, VSTGUI::UTF8String{title_of(info)}); + lbl->setFontColor(VSTGUI::kWhiteCColor); + lbl->setBackColor(VSTGUI::kTransparentCColor); + lbl->setFrameColor(VSTGUI::kTransparentCColor); + frame->addView(lbl); + } + + ctl->setValueNormalized(static_cast(ec->getParamNormalized(info.id))); + frame->addView(ctl); + + if(++col >= cols) { col = 0; ++row; } + } + } + + static std::string title_of(const Steinberg::Vst::ParameterInfo& info) + { + Steinberg::String s(info.title); + s.toMultiByte(Steinberg::kCP_Utf8); + return std::string(s.text8() ? s.text8() : ""); + } + + ControllerCommon* controller{}; + VSTGUI::CFrame* frame{}; + Steinberg::IPlugFrame* plugFrame{}; + std::atomic m_ref{1}; + + static constexpr int cols = 4; + static constexpr int cell_w = 90; + static constexpr int cell_h = 96; + static constexpr int margin = 12; + int m_width = 240; + int m_height = 140; +}; + +} + +#endif From b58522544b477adf01ccf71348222bd8ae20396f Mon Sep 17 00:00:00 2001 From: Edu Meneses Date: Wed, 1 Jul 2026 17:43:23 -0400 Subject: [PATCH 02/24] vst3: enable OBJC/OBJCXX for VSTGUI on macOS --- cmake/avendish.vst3.cmake | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cmake/avendish.vst3.cmake b/cmake/avendish.vst3.cmake index a87c5b9a..11836986 100644 --- a/cmake/avendish.vst3.cmake +++ b/cmake/avendish.vst3.cmake @@ -10,6 +10,13 @@ endif() # VST3 SDK configured with SMTG_ADD_VSTGUI=ON so the `vstgui` target exists. option(AVND_ENABLE_VST3_VSTGUI "Embed a VSTGUI editor in VST3 plug-ins" ON) +# VSTGUI on macOS compiles Objective-C++ sources, so the enclosing project must +# enable those languages (a plain `project(Foo CXX)` does not). +if(APPLE AND AVND_ENABLE_VST3_VSTGUI) + enable_language(OBJC) + enable_language(OBJCXX) +endif() + set(VST3_SDK_ROOT "" CACHE PATH "VST3 SDK path") if(NOT VST3_SDK_ROOT) function(avnd_make_vst3) From d66bce3aa8e10c2e49b83398c1b26a7f29a5c176 Mon Sep 17 00:00:00 2001 From: Edu Meneses Date: Wed, 1 Jul 2026 17:47:04 -0400 Subject: [PATCH 03/24] vst3: add VSTGUI include root for editor sources --- cmake/avendish.vst3.cmake | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cmake/avendish.vst3.cmake b/cmake/avendish.vst3.cmake index 11836986..ad337acc 100644 --- a/cmake/avendish.vst3.cmake +++ b/cmake/avendish.vst3.cmake @@ -154,6 +154,13 @@ function(avnd_make_vst3) if(AVND_ENABLE_VST3_VSTGUI AND TARGET vstgui) target_link_libraries(${AVND_FX_TARGET} PRIVATE vstgui) target_compile_definitions(${AVND_FX_TARGET} PRIVATE AVND_VST3_VSTGUI=1) + # VSTGUI headers are included as ; the include root is the + # vstgui4 dir (the `vstgui` target does not export it). + if(SMTG_VSTGUI_SOURCE_DIR) + target_include_directories(${AVND_FX_TARGET} PRIVATE "${SMTG_VSTGUI_SOURCE_DIR}") + else() + target_include_directories(${AVND_FX_TARGET} PRIVATE "${VST3_SDK_ROOT}/vstgui4") + endif() endif() avnd_common_setup("${AVND_TARGET}" "${AVND_FX_TARGET}") From 3a0ac8a77f68ee70dd19343117fcde5e8339243f Mon Sep 17 00:00:00 2001 From: Edu Meneses Date: Wed, 1 Jul 2026 17:51:40 -0400 Subject: [PATCH 04/24] vst3: fix VSTGUI control construction (CFrame/CKnob/CTextLabel APIs) --- include/avnd/binding/vst3/vstgui_editor.hpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/include/avnd/binding/vst3/vstgui_editor.hpp b/include/avnd/binding/vst3/vstgui_editor.hpp index 4eb228e1..9d1159e9 100644 --- a/include/avnd/binding/vst3/vstgui_editor.hpp +++ b/include/avnd/binding/vst3/vstgui_editor.hpp @@ -69,7 +69,7 @@ class VstGuiEditor final Steinberg::tresult PLUGIN_API attached(void* parent, Steinberg::FIDString /*type*/) override { computeSize(); - frame = new VSTGUI::CFrame(VSTGUI::CRect(0, 0, m_width, m_height), this); + frame = new VSTGUI::CFrame(VSTGUI::CRect(0, 0, m_width, m_height), nullptr); frame->setBackgroundColor(VSTGUI::CColor{32, 32, 34, 255}); VSTGUI::PlatformType pt = @@ -183,13 +183,15 @@ class VstGuiEditor final { // Continuous -> self-drawing knob. VSTGUI::CRect rc(x + (cell_w - 56) / 2, y + 20, x + (cell_w - 56) / 2 + 56, y + 76); - auto* knob = new VSTGUI::CKnob(rc, this, info.id, nullptr, nullptr); - knob->setDrawStyle(VSTGUI::CKnob::kCoronaDrawing | VSTGUI::CKnob::kHandleCircleDrawing); + auto* knob = new VSTGUI::CKnob( + rc, this, info.id, nullptr, nullptr, VSTGUI::CPoint(0, 0), + VSTGUI::CKnob::kCoronaDrawing | VSTGUI::CKnob::kHandleCircleDrawing); ctl = knob; // Name label above the knob. VSTGUI::CRect lr(x, y + 2, x + cell_w, y + 18); - auto* lbl = new VSTGUI::CTextLabel(lr, VSTGUI::UTF8String{title_of(info)}); + const std::string lbl_txt = title_of(info); + auto* lbl = new VSTGUI::CTextLabel(lr, lbl_txt.c_str()); lbl->setFontColor(VSTGUI::kWhiteCColor); lbl->setBackColor(VSTGUI::kTransparentCColor); lbl->setFrameColor(VSTGUI::kTransparentCColor); From 7f9446ca1921f3c5f1a3d2b435a1cd4cf4c29ac3 Mon Sep 17 00:00:00 2001 From: Edu Meneses Date: Wed, 1 Jul 2026 18:02:20 -0400 Subject: [PATCH 05/24] vst3: strip VSTGUI's macOS -Werror so its internal warnings don't fail builds --- cmake/avendish.vst3.cmake | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/cmake/avendish.vst3.cmake b/cmake/avendish.vst3.cmake index ad337acc..cca57f7b 100644 --- a/cmake/avendish.vst3.cmake +++ b/cmake/avendish.vst3.cmake @@ -161,6 +161,17 @@ function(avnd_make_vst3) else() target_include_directories(${AVND_FX_TARGET} PRIVATE "${VST3_SDK_ROOT}/vstgui4") endif() + + # VSTGUI compiles its own sources with -Werror on macOS, which trips on + # recent AppleClang/macOS SDKs (e.g. unused-variable in cgbitmap.cpp). + # Don't let VSTGUI's internal warnings fail the addon build. + if(APPLE) + get_target_property(_avnd_vstgui_opts vstgui COMPILE_OPTIONS) + if(_avnd_vstgui_opts) + list(REMOVE_ITEM _avnd_vstgui_opts "-Werror") + set_target_properties(vstgui PROPERTIES COMPILE_OPTIONS "${_avnd_vstgui_opts}") + endif() + endif() endif() avnd_common_setup("${AVND_TARGET}" "${AVND_FX_TARGET}") From 2d4a1a9baa46f292a45a4b7929c8f6c3c07a6a4d Mon Sep 17 00:00:00 2001 From: Edu Meneses Date: Wed, 1 Jul 2026 18:06:51 -0400 Subject: [PATCH 06/24] vst3: set MSVC /Zc:preprocessor on the vstgui + module targets --- cmake/avendish.vst3.cmake | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cmake/avendish.vst3.cmake b/cmake/avendish.vst3.cmake index cca57f7b..19db9fd8 100644 --- a/cmake/avendish.vst3.cmake +++ b/cmake/avendish.vst3.cmake @@ -172,6 +172,13 @@ function(avnd_make_vst3) set_target_properties(vstgui PROPERTIES COMPILE_OPTIONS "${_avnd_vstgui_opts}") endif() endif() + + # VSTGUI's variadic macros (e.g. vstgui_assert with ##__VA_ARGS__) need + # MSVC's conformant preprocessor. Set it on the VSTGUI lib and the module. + if(MSVC) + target_compile_options(vstgui PRIVATE /Zc:preprocessor) + target_compile_options(${AVND_FX_TARGET} PRIVATE /Zc:preprocessor) + endif() endif() avnd_common_setup("${AVND_TARGET}" "${AVND_FX_TARGET}") From 326bd888837a6f214da04c6cb4f3189e4dc55203 Mon Sep 17 00:00:00 2001 From: Edu Meneses Date: Wed, 1 Jul 2026 18:12:02 -0400 Subject: [PATCH 07/24] vst3: add /Zc:preprocessor at directory scope before the SDK subdir (MSVC) --- cmake/avendish.vst3.cmake | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cmake/avendish.vst3.cmake b/cmake/avendish.vst3.cmake index 19db9fd8..cceb4ca8 100644 --- a/cmake/avendish.vst3.cmake +++ b/cmake/avendish.vst3.cmake @@ -62,6 +62,14 @@ if(MSVC AND (AVND_ENABLE_MAX set(SMTG_USE_STATIC_CRT ON CACHE BOOL "" FORCE) endif() +# VSTGUI's variadic macros (vstgui_assert uses ,##__VA_ARGS__) require MSVC's +# conformant preprocessor. Set it at directory scope before the SDK/VSTGUI +# subdirectory so every SDK target inherits it (a per-target flag doesn't stick +# reliably with the Visual Studio generator, and CMAKE_CXX_FLAGS gets clobbered). +if(MSVC AND AVND_ENABLE_VST3_VSTGUI) + add_compile_options(/Zc:preprocessor) +endif() + add_subdirectory("${VST3_SDK_ROOT}" "${CMAKE_BINARY_DIR}/vst3_sdk") function(avnd_make_vst3) From 38e07e05b1c5fdc0f2960158ba78f51c82fffc75 Mon Sep 17 00:00:00 2001 From: Edu Meneses Date: Wed, 1 Jul 2026 18:22:35 -0400 Subject: [PATCH 08/24] vst3: force-include vstguidebug.h on MSVC to define vstgui_assert --- cmake/avendish.vst3.cmake | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cmake/avendish.vst3.cmake b/cmake/avendish.vst3.cmake index cceb4ca8..0cc4ec4b 100644 --- a/cmake/avendish.vst3.cmake +++ b/cmake/avendish.vst3.cmake @@ -68,6 +68,13 @@ endif() # reliably with the Visual Studio generator, and CMAKE_CXX_FLAGS gets clobbered). if(MSVC AND AVND_ENABLE_VST3_VSTGUI) add_compile_options(/Zc:preprocessor) + # VSTGUI's vstgui_assert macro is defined via a vstguibase.h<->vstguidebug.h + # include cycle that leaves it undefined for some TUs under MSVC (malloc.h: + # "'vstgui_assert': identifier not found"). Force-include vstguidebug.h so the + # macro exists before any TU is compiled. + if(EXISTS "${VST3_SDK_ROOT}/vstgui4/vstgui/lib/vstguidebug.h") + add_compile_options("/FI${VST3_SDK_ROOT}/vstgui4/vstgui/lib/vstguidebug.h") + endif() endif() add_subdirectory("${VST3_SDK_ROOT}" "${CMAKE_BINARY_DIR}/vst3_sdk") From 5e80f1c44a15ddcfaab02a79e32b06e003fa4795 Mon Sep 17 00:00:00 2001 From: Edu Meneses Date: Thu, 2 Jul 2026 08:42:01 -0400 Subject: [PATCH 09/24] vst3: force-include a vstgui_assert shim on MSVC (fixes C3861 in VSTGUI) --- cmake/avendish.vst3.cmake | 11 ++++++----- include/avnd/binding/vst3/vstgui_msvc_assert_shim.h | 13 +++++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) create mode 100644 include/avnd/binding/vst3/vstgui_msvc_assert_shim.h diff --git a/cmake/avendish.vst3.cmake b/cmake/avendish.vst3.cmake index 0cc4ec4b..8c7c570e 100644 --- a/cmake/avendish.vst3.cmake +++ b/cmake/avendish.vst3.cmake @@ -70,11 +70,12 @@ if(MSVC AND AVND_ENABLE_VST3_VSTGUI) add_compile_options(/Zc:preprocessor) # VSTGUI's vstgui_assert macro is defined via a vstguibase.h<->vstguidebug.h # include cycle that leaves it undefined for some TUs under MSVC (malloc.h: - # "'vstgui_assert': identifier not found"). Force-include vstguidebug.h so the - # macro exists before any TU is compiled. - if(EXISTS "${VST3_SDK_ROOT}/vstgui4/vstgui/lib/vstguidebug.h") - add_compile_options("/FI${VST3_SDK_ROOT}/vstgui4/vstgui/lib/vstguidebug.h") - endif() + # "'vstgui_assert': identifier not found"). Force-include a shim that always + # defines it, and silence the redefinition warning from VSTGUI's own later + # definition (C4005). + add_compile_options( + "/FI${AVND_SOURCE_DIR}/include/avnd/binding/vst3/vstgui_msvc_assert_shim.h" + /wd4005) endif() add_subdirectory("${VST3_SDK_ROOT}" "${CMAKE_BINARY_DIR}/vst3_sdk") diff --git a/include/avnd/binding/vst3/vstgui_msvc_assert_shim.h b/include/avnd/binding/vst3/vstgui_msvc_assert_shim.h new file mode 100644 index 00000000..76d003f9 --- /dev/null +++ b/include/avnd/binding/vst3/vstgui_msvc_assert_shim.h @@ -0,0 +1,13 @@ +#pragma once + +/* SPDX-License-Identifier: GPL-3.0-or-later */ + +// Work around a VSTGUI header-ordering issue that only bites MSVC: for some +// translation units vstgui_assert is used (e.g. in malloc.h's Buffer) before +// vstguibase.h/vstguidebug.h have defined it, giving "C3861: 'vstgui_assert': +// identifier not found". Force-including this header (see avendish.vst3.cmake) +// guarantees the macro is always defined. VSTGUI's own later definition simply +// redefines it (that C4005 warning is silenced alongside this). +#ifndef vstgui_assert +#define vstgui_assert(...) ((void)0) +#endif From 4ec593213fe06bbe3c6a7985d40f5575a5842e54 Mon Sep 17 00:00:00 2001 From: Edu Meneses Date: Thu, 2 Jul 2026 09:11:35 -0400 Subject: [PATCH 10/24] vst3/vstgui: wire host IRunLoop into VSTGUI on Linux (fixes X11 editor crash) --- include/avnd/binding/vst3/vstgui_editor.hpp | 92 +++++++++++++++++++-- 1 file changed, 87 insertions(+), 5 deletions(-) diff --git a/include/avnd/binding/vst3/vstgui_editor.hpp b/include/avnd/binding/vst3/vstgui_editor.hpp index 9d1159e9..2c34bd49 100644 --- a/include/avnd/binding/vst3/vstgui_editor.hpp +++ b/include/avnd/binding/vst3/vstgui_editor.hpp @@ -22,12 +22,93 @@ #include #include +#if defined(__linux__) +#include +#include +#include +#include +#include +#endif + #include #include namespace stv3 { +#if defined(__linux__) +// Bridges the host's Steinberg::Linux::IRunLoop (from IPlugFrame) to VSTGUI's +// X11 run loop, required for VSTGUI to handle events inside a VST3 host on +// Linux. Adapted from VSTGUI's own vst3editor.cpp. +class Vst3X11RunLoop final + : public VSTGUI::IRunLoop + , public VSTGUI::AtomicReferenceCounted +{ +public: + struct EventHandler : Steinberg::Linux::IEventHandler, public Steinberg::FObject + { + VSTGUI::IEventHandler* handler{nullptr}; + void PLUGIN_API onFDIsSet(Steinberg::Linux::FileDescriptor) override + { if(handler) handler->onEvent(); } + DELEGATE_REFCOUNT(Steinberg::FObject) + DEFINE_INTERFACES + DEF_INTERFACE(Steinberg::Linux::IEventHandler) + END_DEFINE_INTERFACES(Steinberg::FObject) + }; + struct TimerHandler : Steinberg::Linux::ITimerHandler, public Steinberg::FObject + { + VSTGUI::ITimerHandler* handler{nullptr}; + void PLUGIN_API onTimer() final { if(handler) handler->onTimer(); } + DELEGATE_REFCOUNT(Steinberg::FObject) + DEFINE_INTERFACES + DEF_INTERFACE(Steinberg::Linux::ITimerHandler) + END_DEFINE_INTERFACES(Steinberg::FObject) + }; + + bool registerEventHandler(int fd, VSTGUI::IEventHandler* handler) final + { + if(!runLoop) return false; + auto h = Steinberg::owned(new EventHandler()); + h->handler = handler; + if(runLoop->registerEventHandler(h, fd) == Steinberg::kResultTrue) + { eventHandlers.push_back(h); return true; } + return false; + } + bool unregisterEventHandler(VSTGUI::IEventHandler* handler) final + { + if(!runLoop) return false; + for(auto it = eventHandlers.begin(); it != eventHandlers.end(); ++it) + if((*it)->handler == handler) + { runLoop->unregisterEventHandler(*it); eventHandlers.erase(it); return true; } + return false; + } + bool registerTimer(uint64_t interval, VSTGUI::ITimerHandler* handler) final + { + if(!runLoop) return false; + auto h = Steinberg::owned(new TimerHandler()); + h->handler = handler; + if(runLoop->registerTimer(h, interval) == Steinberg::kResultTrue) + { timerHandlers.push_back(h); return true; } + return false; + } + bool unregisterTimer(VSTGUI::ITimerHandler* handler) final + { + if(!runLoop) return false; + for(auto it = timerHandlers.begin(); it != timerHandlers.end(); ++it) + if((*it)->handler == handler) + { runLoop->unregisterTimer(*it); timerHandlers.erase(it); return true; } + return false; + } + + explicit Vst3X11RunLoop(Steinberg::FUnknown* rl) : runLoop(rl) {} + +private: + std::vector> eventHandlers; + std::vector> timerHandlers; + Steinberg::FUnknownPtr runLoop; +}; +#endif + class VstGuiEditor final : public Steinberg::IPlugView , public VSTGUI::IControlListener @@ -72,15 +153,16 @@ class VstGuiEditor final frame = new VSTGUI::CFrame(VSTGUI::CRect(0, 0, m_width, m_height), nullptr); frame->setBackgroundColor(VSTGUI::CColor{32, 32, 34, 255}); - VSTGUI::PlatformType pt = #if defined(_WIN32) - VSTGUI::PlatformType::kHWND; + frame->open(parent, VSTGUI::PlatformType::kHWND); #elif defined(__APPLE__) - VSTGUI::PlatformType::kNSView; + frame->open(parent, VSTGUI::PlatformType::kNSView); #else - VSTGUI::PlatformType::kX11EmbedWindowID; + // X11 needs the host run loop wired into VSTGUI, or event handling crashes. + VSTGUI::X11::FrameConfig x11config; + x11config.runLoop = VSTGUI::owned(new Vst3X11RunLoop(plugFrame)); + frame->open(parent, VSTGUI::PlatformType::kX11EmbedWindowID, &x11config); #endif - frame->open(parent, pt); buildControls(); return Steinberg::kResultTrue; } From c17f250073248bf5802dbbb402067e1433342f7a Mon Sep 17 00:00:00 2001 From: Edu Meneses Date: Thu, 2 Jul 2026 09:16:17 -0400 Subject: [PATCH 11/24] vst3/vstgui: include platform_linux.h for VSTGUI IRunLoop types --- include/avnd/binding/vst3/vstgui_editor.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/include/avnd/binding/vst3/vstgui_editor.hpp b/include/avnd/binding/vst3/vstgui_editor.hpp index 2c34bd49..0e80a28e 100644 --- a/include/avnd/binding/vst3/vstgui_editor.hpp +++ b/include/avnd/binding/vst3/vstgui_editor.hpp @@ -23,6 +23,7 @@ #include #if defined(__linux__) +#include #include #include #include From 8dce65c86fc8103607f7d20824f2f42ab9d76fd4 Mon Sep 17 00:00:00 2001 From: Edu Meneses Date: Thu, 2 Jul 2026 09:20:04 -0400 Subject: [PATCH 12/24] vst3/vstgui: qualify IRunLoop types with VSTGUI::X11 (matches VSTGUI 3.7.x) --- include/avnd/binding/vst3/vstgui_editor.hpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/include/avnd/binding/vst3/vstgui_editor.hpp b/include/avnd/binding/vst3/vstgui_editor.hpp index 0e80a28e..ab7290ed 100644 --- a/include/avnd/binding/vst3/vstgui_editor.hpp +++ b/include/avnd/binding/vst3/vstgui_editor.hpp @@ -23,7 +23,6 @@ #include #if defined(__linux__) -#include #include #include #include @@ -42,13 +41,13 @@ namespace stv3 // X11 run loop, required for VSTGUI to handle events inside a VST3 host on // Linux. Adapted from VSTGUI's own vst3editor.cpp. class Vst3X11RunLoop final - : public VSTGUI::IRunLoop + : public VSTGUI::X11::IRunLoop , public VSTGUI::AtomicReferenceCounted { public: struct EventHandler : Steinberg::Linux::IEventHandler, public Steinberg::FObject { - VSTGUI::IEventHandler* handler{nullptr}; + VSTGUI::X11::IEventHandler* handler{nullptr}; void PLUGIN_API onFDIsSet(Steinberg::Linux::FileDescriptor) override { if(handler) handler->onEvent(); } DELEGATE_REFCOUNT(Steinberg::FObject) @@ -58,7 +57,7 @@ class Vst3X11RunLoop final }; struct TimerHandler : Steinberg::Linux::ITimerHandler, public Steinberg::FObject { - VSTGUI::ITimerHandler* handler{nullptr}; + VSTGUI::X11::ITimerHandler* handler{nullptr}; void PLUGIN_API onTimer() final { if(handler) handler->onTimer(); } DELEGATE_REFCOUNT(Steinberg::FObject) DEFINE_INTERFACES @@ -66,7 +65,7 @@ class Vst3X11RunLoop final END_DEFINE_INTERFACES(Steinberg::FObject) }; - bool registerEventHandler(int fd, VSTGUI::IEventHandler* handler) final + bool registerEventHandler(int fd, VSTGUI::X11::IEventHandler* handler) final { if(!runLoop) return false; auto h = Steinberg::owned(new EventHandler()); @@ -75,7 +74,7 @@ class Vst3X11RunLoop final { eventHandlers.push_back(h); return true; } return false; } - bool unregisterEventHandler(VSTGUI::IEventHandler* handler) final + bool unregisterEventHandler(VSTGUI::X11::IEventHandler* handler) final { if(!runLoop) return false; for(auto it = eventHandlers.begin(); it != eventHandlers.end(); ++it) @@ -83,7 +82,7 @@ class Vst3X11RunLoop final { runLoop->unregisterEventHandler(*it); eventHandlers.erase(it); return true; } return false; } - bool registerTimer(uint64_t interval, VSTGUI::ITimerHandler* handler) final + bool registerTimer(uint64_t interval, VSTGUI::X11::ITimerHandler* handler) final { if(!runLoop) return false; auto h = Steinberg::owned(new TimerHandler()); @@ -92,7 +91,7 @@ class Vst3X11RunLoop final { timerHandlers.push_back(h); return true; } return false; } - bool unregisterTimer(VSTGUI::ITimerHandler* handler) final + bool unregisterTimer(VSTGUI::X11::ITimerHandler* handler) final { if(!runLoop) return false; for(auto it = timerHandlers.begin(); it != timerHandlers.end(); ++it) From 62955c3acabd7a3aadd36fd8f4d5acc7273d7351 Mon Sep 17 00:00:00 2001 From: Edu Meneses Date: Thu, 2 Jul 2026 09:56:36 -0400 Subject: [PATCH 13/24] vst3/vstgui: init VSTGUI platform from the module handle on Linux (ModuleEntry) --- include/avnd/binding/vst3/prototype.cpp.in | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/include/avnd/binding/vst3/prototype.cpp.in b/include/avnd/binding/vst3/prototype.cpp.in index 6e271b2a..adc2ea2b 100644 --- a/include/avnd/binding/vst3/prototype.cpp.in +++ b/include/avnd/binding/vst3/prototype.cpp.in @@ -4,6 +4,10 @@ #include #include +#if defined(AVND_VST3_VSTGUI) +#include +#endif + // clang-format off #include <@AVND_MAIN_FILE@> @@ -19,8 +23,22 @@ extern "C" AVND_EXPORTED_SYMBOL bool bundleExit() { return true; } extern "C" AVND_EXPORTED_SYMBOL bool InitDll() { return true; } extern "C" AVND_EXPORTED_SYMBOL bool ExitDll() { return true; } #elif __linux__ -extern "C" AVND_EXPORTED_SYMBOL bool ModuleEntry() { return true; } -extern "C" AVND_EXPORTED_SYMBOL bool ModuleExit() { return true; } +// The host passes the plugin's dlopen handle; VSTGUI needs it to initialise its +// Linux (X11/fontconfig) platform, or creating an editor CFrame crashes. +extern "C" AVND_EXPORTED_SYMBOL bool ModuleEntry(void* sharedLibraryHandle) +{ +#if defined(AVND_VST3_VSTGUI) + VSTGUI::initPlatform(sharedLibraryHandle); +#endif + return true; +} +extern "C" AVND_EXPORTED_SYMBOL bool ModuleExit() +{ +#if defined(AVND_VST3_VSTGUI) + VSTGUI::exitPlatform(); +#endif + return true; +} #endif extern "C" AVND_EXPORTED_SYMBOL Steinberg::IPluginFactory* GetPluginFactory() From a3cb16d0a7e3a391d55ece1c00715805b30022c1 Mon Sep 17 00:00:00 2001 From: Edu Meneses Date: Thu, 2 Jul 2026 17:11:49 -0400 Subject: [PATCH 14/24] vst3/vstgui: use VSTGUI::init/exit in module entries on all platforms VSTGUI::init = initPlatform + CFontDesc::init (global fonts). Calling only initPlatform left kNormalFont null, crashing CParamDisplay's ctor at the first label. Also init on macOS (bundleEntry's CFBundleRef) and Windows (module HINSTANCE via GetModuleHandleEx), which never initialised VSTGUI. --- include/avnd/binding/vst3/prototype.cpp.in | 52 ++++++++++++++++++---- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/include/avnd/binding/vst3/prototype.cpp.in b/include/avnd/binding/vst3/prototype.cpp.in index adc2ea2b..52216f7e 100644 --- a/include/avnd/binding/vst3/prototype.cpp.in +++ b/include/avnd/binding/vst3/prototype.cpp.in @@ -5,7 +5,14 @@ #include #if defined(AVND_VST3_VSTGUI) -#include +// VSTGUI::init is the required library init: platform layer + global fonts +// (kNormalFont & co.). Constructing any VSTGUI view without it crashes. +#include +#if defined(_WIN32) +#include +#elif defined(__APPLE__) +#include +#endif #endif // clang-format off @@ -17,25 +24,54 @@ bool DeinitModule() { return true; } #if __APPLE__ extern "C" AVND_EXPORTED_SYMBOL bool BundleEntry() { return true; } extern "C" AVND_EXPORTED_SYMBOL bool BundleExit() { return true; } -extern "C" AVND_EXPORTED_SYMBOL bool bundleEntry() { return true; } -extern "C" AVND_EXPORTED_SYMBOL bool bundleExit() { return true; } +extern "C" AVND_EXPORTED_SYMBOL bool bundleEntry(CFBundleRef ref) +{ +#if defined(AVND_VST3_VSTGUI) + VSTGUI::init(ref); +#endif + return true; +} +extern "C" AVND_EXPORTED_SYMBOL bool bundleExit() +{ +#if defined(AVND_VST3_VSTGUI) + VSTGUI::exit(); +#endif + return true; +} #elif _WIN32 -extern "C" AVND_EXPORTED_SYMBOL bool InitDll() { return true; } -extern "C" AVND_EXPORTED_SYMBOL bool ExitDll() { return true; } +extern "C" AVND_EXPORTED_SYMBOL bool InitDll() +{ +#if defined(AVND_VST3_VSTGUI) + HMODULE mod{}; + GetModuleHandleExW( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS + | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(&InitDll), &mod); + VSTGUI::init(mod); +#endif + return true; +} +extern "C" AVND_EXPORTED_SYMBOL bool ExitDll() +{ +#if defined(AVND_VST3_VSTGUI) + VSTGUI::exit(); +#endif + return true; +} #elif __linux__ // The host passes the plugin's dlopen handle; VSTGUI needs it to initialise its -// Linux (X11/fontconfig) platform, or creating an editor CFrame crashes. +// Linux (X11/fontconfig) platform. extern "C" AVND_EXPORTED_SYMBOL bool ModuleEntry(void* sharedLibraryHandle) { #if defined(AVND_VST3_VSTGUI) - VSTGUI::initPlatform(sharedLibraryHandle); + VSTGUI::init(sharedLibraryHandle); #endif return true; } extern "C" AVND_EXPORTED_SYMBOL bool ModuleExit() { #if defined(AVND_VST3_VSTGUI) - VSTGUI::exitPlatform(); + VSTGUI::exit(); #endif return true; } From ec4d2e5eefb7f3de04230cbf072c5e89839c6340 Mon Sep 17 00:00:00 2001 From: Edu Meneses Date: Thu, 2 Jul 2026 17:22:12 -0400 Subject: [PATCH 15/24] vst3: define DEVELOPMENT only in Debug (SIGTRAP asserts crashed release plug-ins) --- cmake/avendish.vst3.cmake | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cmake/avendish.vst3.cmake b/cmake/avendish.vst3.cmake index 8c7c570e..67ddbb2f 100644 --- a/cmake/avendish.vst3.cmake +++ b/cmake/avendish.vst3.cmake @@ -40,7 +40,11 @@ endif() set(SMTG_ADD_VST3_HOSTING_SAMPLES 0) set(SMTG_ADD_VST3_HOSTING_SAMPLES 0 CACHE INTERNAL "") -add_definitions(-DDEVELOPMENT) +# The SDK wants exactly one of DEVELOPMENT / RELEASE. DEVELOPMENT enables its +# FDebugBreak assertions, which raise SIGTRAP inside the host process -- an +# unconditional -DDEVELOPMENT made *release* plug-ins crash hosts on teardown +# (e.g. VSTGUI editor close). Define it for Debug builds only. +add_compile_definitions($,DEVELOPMENT=1,RELEASE=1>) include_directories("${VST3_SDK_ROOT}") # VST3 uses COM APIs which require no virtual dtors in interfaces From 3e4983fbad8d255f8445e612d085262684c78052 Mon Sep 17 00:00:00 2001 From: Edu Meneses Date: Thu, 2 Jul 2026 17:28:55 -0400 Subject: [PATCH 16/24] vst3/vstgui: compute editor size in ctor (hosts call getSize before attached) --- include/avnd/binding/vst3/vstgui_editor.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/avnd/binding/vst3/vstgui_editor.hpp b/include/avnd/binding/vst3/vstgui_editor.hpp index ab7290ed..c3f9b595 100644 --- a/include/avnd/binding/vst3/vstgui_editor.hpp +++ b/include/avnd/binding/vst3/vstgui_editor.hpp @@ -117,6 +117,8 @@ class VstGuiEditor final explicit VstGuiEditor(ControllerCommon* c) : controller{c} { + // Hosts call getSize() before attached() to size the window: compute now. + computeSize(); } // ---------------- IUnknown ---------------- From 1598b280780e231dd1195954ffa628efc1ca7046 Mon Sep 17 00:00:00 2001 From: Edu Meneses Date: Fri, 3 Jul 2026 09:52:54 -0400 Subject: [PATCH 17/24] =?UTF-8?q?vst3/vstgui:=20nicer=20default=20editor?= =?UTF-8?q?=20=E2=80=94=20title=20bar,=20on/off=20buttons,=20corona=20knob?= =?UTF-8?q?s=20with=20labels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Plugin name (avnd::get_name) shown in a title bar on top. - Toggles render as on/off text buttons instead of a tiny checkbox. - Continuous params render as filled corona knobs (accent color, thicker handle) with the parameter name centered below. --- include/avnd/binding/vst3/controller.hpp | 2 +- include/avnd/binding/vst3/vstgui_editor.hpp | 90 +++++++++++++++------ 2 files changed, 65 insertions(+), 27 deletions(-) diff --git a/include/avnd/binding/vst3/controller.hpp b/include/avnd/binding/vst3/controller.hpp index 0c31914b..2fb5e732 100644 --- a/include/avnd/binding/vst3/controller.hpp +++ b/include/avnd/binding/vst3/controller.hpp @@ -74,7 +74,7 @@ class Controller final Steinberg::IPlugView* createView(const char* name) override { if(name && std::strcmp(name, Steinberg::Vst::ViewType::kEditor) == 0) - return new stv3::VstGuiEditor{this}; + return new stv3::VstGuiEditor{this, std::string{avnd::get_name()}}; return nullptr; } #endif diff --git a/include/avnd/binding/vst3/vstgui_editor.hpp b/include/avnd/binding/vst3/vstgui_editor.hpp index c3f9b595..16c675db 100644 --- a/include/avnd/binding/vst3/vstgui_editor.hpp +++ b/include/avnd/binding/vst3/vstgui_editor.hpp @@ -22,6 +22,9 @@ #include #include +#include +#include + #if defined(__linux__) #include #include @@ -114,8 +117,9 @@ class VstGuiEditor final , public VSTGUI::IControlListener { public: - explicit VstGuiEditor(ControllerCommon* c) + explicit VstGuiEditor(ControllerCommon* c, std::string title = {}) : controller{c} + , m_title{std::move(title)} { // Hosts call getSize() before attached() to size the window: compute now. computeSize(); @@ -235,57 +239,89 @@ class VstGuiEditor final void computeSize() { const int n = edit_controller()->getParameterCount(); - const int rows = (n + cols - 1) / cols; - m_width = cols * cell_w + margin * 2; - m_height = (rows > 0 ? rows : 1) * cell_h + margin * 2; - if(m_width < 240) m_width = 240; - if(m_height < 140) m_height = 140; + const int c = n < cols ? (n > 0 ? n : 1) : cols; + const int rows = (n + c - 1) / c; + m_width = c * cell_w + margin * 2; + m_height = title_h + rows * cell_h + margin * 2; + if(m_width < 260) m_width = 260; } void buildControls() { auto* ec = edit_controller(); const int n = ec->getParameterCount(); - int col = 0, row = 0; + const int c = n < cols ? (n > 0 ? n : 1) : cols; + + const VSTGUI::CColor accent{86, 158, 255, 255}; + const VSTGUI::CColor labelCol{205, 205, 212, 255}; + + // Title bar with the plugin name. + { + VSTGUI::CRect tr(0, 0, m_width, title_h); + auto* t = new VSTGUI::CTextLabel( + tr, m_title.empty() ? "Avendish" : m_title.c_str()); + t->setFrameColor(VSTGUI::kTransparentCColor); + t->setBackColor(VSTGUI::CColor{24, 24, 26, 255}); + t->setFontColor(VSTGUI::CColor{235, 235, 240, 255}); + t->setFont(VSTGUI::kNormalFontBig); + t->setHoriAlign(VSTGUI::kCenterText); + frame->addView(t); + } + + int idx = 0; for(int i = 0; i < n; ++i) { Steinberg::Vst::ParameterInfo info{}; if(ec->getParameterInfo(i, info) != Steinberg::kResultTrue) continue; + const int col = idx % c, row = idx / c; const VSTGUI::CCoord x = margin + col * cell_w; - const VSTGUI::CCoord y = margin + row * cell_h; + const VSTGUI::CCoord y = title_h + margin + row * cell_h; + const std::string name = title_of(info); VSTGUI::CControl* ctl = nullptr; if(info.stepCount == 1) { - // Toggle -> self-drawing checkbox. - VSTGUI::CRect rc(x + 8, y + 24, x + cell_w - 8, y + 48); - ctl = new VSTGUI::CCheckBox(rc, this, info.id, title_of(info).data()); + // Toggle -> on/off text button that shows the parameter name. + VSTGUI::CRect rc(x + 10, y + 22, x + cell_w - 10, y + 54); + auto* b = new VSTGUI::CTextButton( + rc, this, info.id, name.c_str(), VSTGUI::CTextButton::kOnOffStyle); + b->setRoundRadius(5.); + b->setFrameColor(accent); + b->setTextColor(labelCol); + b->setTextColorHighlighted(VSTGUI::CColor{16, 16, 18, 255}); + ctl = b; } else { - // Continuous -> self-drawing knob. - VSTGUI::CRect rc(x + (cell_w - 56) / 2, y + 20, x + (cell_w - 56) / 2 + 56, y + 76); + // Continuous -> corona knob with the name label below it. + const VSTGUI::CCoord kw = 58; + const VSTGUI::CCoord kx = x + (cell_w - kw) / 2; + VSTGUI::CRect rc(kx, y + 12, kx + kw, y + 12 + kw); auto* knob = new VSTGUI::CKnob( rc, this, info.id, nullptr, nullptr, VSTGUI::CPoint(0, 0), - VSTGUI::CKnob::kCoronaDrawing | VSTGUI::CKnob::kHandleCircleDrawing); + VSTGUI::CKnob::kCoronaDrawing | VSTGUI::CKnob::kCoronaOutline + | VSTGUI::CKnob::kHandleCircleDrawing); + knob->setCoronaColor(accent); + knob->setColorHandle(accent); + knob->setColorShadowHandle(VSTGUI::CColor{16, 16, 18, 255}); + knob->setCoronaInset(4.); + knob->setHandleLineWidth(2.5); ctl = knob; - // Name label above the knob. - VSTGUI::CRect lr(x, y + 2, x + cell_w, y + 18); - const std::string lbl_txt = title_of(info); - auto* lbl = new VSTGUI::CTextLabel(lr, lbl_txt.c_str()); - lbl->setFontColor(VSTGUI::kWhiteCColor); - lbl->setBackColor(VSTGUI::kTransparentCColor); + VSTGUI::CRect lr(x, y + cell_h - 26, x + cell_w, y + cell_h - 8); + auto* lbl = new VSTGUI::CTextLabel(lr, name.c_str()); lbl->setFrameColor(VSTGUI::kTransparentCColor); + lbl->setBackColor(VSTGUI::kTransparentCColor); + lbl->setFontColor(labelCol); + lbl->setHoriAlign(VSTGUI::kCenterText); frame->addView(lbl); } ctl->setValueNormalized(static_cast(ec->getParamNormalized(info.id))); frame->addView(ctl); - - if(++col >= cols) { col = 0; ++row; } + ++idx; } } @@ -301,12 +337,14 @@ class VstGuiEditor final Steinberg::IPlugFrame* plugFrame{}; std::atomic m_ref{1}; + std::string m_title; static constexpr int cols = 4; - static constexpr int cell_w = 90; - static constexpr int cell_h = 96; + static constexpr int cell_w = 96; + static constexpr int cell_h = 104; static constexpr int margin = 12; - int m_width = 240; - int m_height = 140; + static constexpr int title_h = 34; + int m_width = 260; + int m_height = 180; }; } From 61e33008ff930f7e11311a24d7bf015aba317c21 Mon Sep 17 00:00:00 2001 From: Edu Meneses Date: Fri, 3 Jul 2026 14:10:29 -0400 Subject: [PATCH 18/24] vst3: report boolean controls as stepCount=1 (toggle, not knob) Toggles reported stepCount=0 (no range.step), so hosts and the VSTGUI editor drew them as continuous knobs. Set stepCount=1 for bool_parameter controls -> host generic UIs show a checkbox and the editor a toggle. --- include/avnd/binding/vst3/controller.hpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/include/avnd/binding/vst3/controller.hpp b/include/avnd/binding/vst3/controller.hpp index 2fb5e732..517b0346 100644 --- a/include/avnd/binding/vst3/controller.hpp +++ b/include/avnd/binding/vst3/controller.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -109,6 +110,10 @@ class Controller final if constexpr(requires { range.step; }) info.stepCount = avnd::get_range().step; } + // A boolean control is a stepped (on/off) parameter: hosts then show a + // checkbox and our VSTGUI editor a toggle button instead of a knob. + if constexpr(avnd::bool_parameter) + info.stepCount = 1; }); info.unitId = 1; From 8ac25e99eb644a5729349c0843a7627d7fe783f6 Mon Sep 17 00:00:00 2001 From: Edu Meneses Date: Mon, 6 Jul 2026 17:39:05 -0400 Subject: [PATCH 19/24] vst3/vstgui: set toggle button fill gradients so the label stays readable The on/off button used the default (light) gradient, so the light 'off' label was invisible on a white fill. Set explicit dark (off) and accent (on) gradients with contrasting text. --- include/avnd/binding/vst3/vstgui_editor.hpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/include/avnd/binding/vst3/vstgui_editor.hpp b/include/avnd/binding/vst3/vstgui_editor.hpp index 16c675db..ee316ab9 100644 --- a/include/avnd/binding/vst3/vstgui_editor.hpp +++ b/include/avnd/binding/vst3/vstgui_editor.hpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -289,8 +290,16 @@ class VstGuiEditor final rc, this, info.id, name.c_str(), VSTGUI::CTextButton::kOnOffStyle); b->setRoundRadius(5.); b->setFrameColor(accent); - b->setTextColor(labelCol); - b->setTextColorHighlighted(VSTGUI::CColor{16, 16, 18, 255}); + // Explicit fills so the label stays readable in both states (the + // default gradient is light, which hid light text when off). + auto offGrad = VSTGUI::owned(VSTGUI::CGradient::create( + 0., 1., VSTGUI::CColor{54, 54, 60, 255}, VSTGUI::CColor{38, 38, 42, 255})); + auto onGrad = VSTGUI::owned(VSTGUI::CGradient::create( + 0., 1., VSTGUI::CColor{104, 170, 255, 255}, VSTGUI::CColor{74, 142, 235, 255})); + b->setGradient(offGrad.get()); + b->setGradientHighlighted(onGrad.get()); + b->setTextColor(VSTGUI::CColor{225, 225, 230, 255}); // off: light on dark + b->setTextColorHighlighted(VSTGUI::CColor{16, 16, 18, 255}); // on: dark on accent ctl = b; } else From 1e374a0636210269b99f22eeff09848ed7d473c7 Mon Sep 17 00:00:00 2001 From: Edu Meneses Date: Mon, 6 Jul 2026 17:40:58 -0400 Subject: [PATCH 20/24] vst3/vstgui: render momentary buttons (bangs) as kick-style buttons ParameterInfo has no notion of a momentary control, so the generic VSTGUI editor drew every stepCount==1 parameter as a latched on/off toggle. Add ControllerCommon::isMomentaryParameter(), overridden by the typed Controller from the control's widget enumerators (pushbutton / bang / impulse), and use it in the editor to pick CTextButton kKickStyle: 1 while pressed, 0 on release. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01K8rKjEZfGtrHLkU5iXR4gn --- include/avnd/binding/vst3/controller.hpp | 16 ++++++++++++++++ include/avnd/binding/vst3/controller_base.hpp | 10 ++++++++++ include/avnd/binding/vst3/vstgui_editor.hpp | 8 ++++++-- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/include/avnd/binding/vst3/controller.hpp b/include/avnd/binding/vst3/controller.hpp index 517b0346..b1ce665c 100644 --- a/include/avnd/binding/vst3/controller.hpp +++ b/include/avnd/binding/vst3/controller.hpp @@ -82,6 +82,22 @@ class Controller final int32 getParameterCount() override { return inputs_info_t::size; } + bool isMomentaryParameter(Steinberg::Vst::ParamID id) const noexcept override + { + bool momentary = false; + inputs_info_t::for_nth_raw( + id, [&](avnd::field_reflection) { + // Momentary widgets expose a `pushbutton` (maintained button) or + // `bang` / `impulse` (impulse button) enumerator; latched toggles + // don't. + if constexpr( + requires { C::pushbutton; } || requires { C::bang; } + || requires { C::impulse; }) + momentary = true; + }); + return momentary; + } + Steinberg::tresult getParameterInfo(int32 paramIndex, ParameterInfo& info) override { if(paramIndex < 0 || paramIndex >= inputs_info_t::size) diff --git a/include/avnd/binding/vst3/controller_base.hpp b/include/avnd/binding/vst3/controller_base.hpp index 351d62f4..c4b49d11 100644 --- a/include/avnd/binding/vst3/controller_base.hpp +++ b/include/avnd/binding/vst3/controller_base.hpp @@ -24,6 +24,16 @@ struct ControllerCommon public: ControllerCommon() { Steinberg::UpdateHandler::instance(); } + // True for momentary (bang / push-button) parameters: pressed = 1, + // released = 0, no latched state. The generic VSTGUI editor uses this to + // draw a kick-style button instead of an on/off toggle. ParameterInfo has + // no such notion, so the typed Controller overrides this from the control + // type's widget. + virtual bool isMomentaryParameter(Steinberg::Vst::ParamID) const noexcept + { + return false; + } + Steinberg::tresult initialize(Steinberg::FUnknown* context) final override { { diff --git a/include/avnd/binding/vst3/vstgui_editor.hpp b/include/avnd/binding/vst3/vstgui_editor.hpp index ee316ab9..9544382f 100644 --- a/include/avnd/binding/vst3/vstgui_editor.hpp +++ b/include/avnd/binding/vst3/vstgui_editor.hpp @@ -284,10 +284,14 @@ class VstGuiEditor final VSTGUI::CControl* ctl = nullptr; if(info.stepCount == 1) { - // Toggle -> on/off text button that shows the parameter name. + // Momentary (bang) -> kick button: 1 while pressed, 0 on release. + // Latched toggle -> on/off button. Both show the parameter name. + const bool momentary = controller->isMomentaryParameter(info.id); VSTGUI::CRect rc(x + 10, y + 22, x + cell_w - 10, y + 54); auto* b = new VSTGUI::CTextButton( - rc, this, info.id, name.c_str(), VSTGUI::CTextButton::kOnOffStyle); + rc, this, info.id, name.c_str(), + momentary ? VSTGUI::CTextButton::kKickStyle + : VSTGUI::CTextButton::kOnOffStyle); b->setRoundRadius(5.); b->setFrameColor(accent); // Explicit fills so the label stays readable in both states (the From 5fb25716dc495ce90c0a109606a0752dad8cfbb7 Mon Sep 17 00:00:00 2001 From: Edu Meneses Date: Wed, 8 Jul 2026 11:45:17 -0400 Subject: [PATCH 21/24] vst3/vstgui: show each knob's value under it, live on edit Adds a value readout label per continuous parameter (formatted via the controller's getParamStringByValue), refreshed in valueChanged as the user turns the knob. --- include/avnd/binding/vst3/vstgui_editor.hpp | 45 ++++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/include/avnd/binding/vst3/vstgui_editor.hpp b/include/avnd/binding/vst3/vstgui_editor.hpp index 9544382f..1704ab10 100644 --- a/include/avnd/binding/vst3/vstgui_editor.hpp +++ b/include/avnd/binding/vst3/vstgui_editor.hpp @@ -25,6 +25,7 @@ #include #include +#include #if defined(__linux__) #include @@ -218,6 +219,13 @@ class VstGuiEditor final controller->componentHandler->endEdit(tag); } edit_controller()->setParamNormalized(tag, v); + + for(auto& [t, lbl] : m_valueLabels) + if(t == tag && lbl) + { + lbl->setText(value_string(tag, v).c_str()); + lbl->invalid(); + } } private: @@ -252,6 +260,7 @@ class VstGuiEditor final auto* ec = edit_controller(); const int n = ec->getParameterCount(); const int c = n < cols ? (n > 0 ? n : 1) : cols; + m_valueLabels.clear(); // labels belong to the (recreated) frame const VSTGUI::CColor accent{86, 158, 255, 255}; const VSTGUI::CColor labelCol{205, 205, 212, 255}; @@ -308,10 +317,11 @@ class VstGuiEditor final } else { - // Continuous -> corona knob with the name label below it. - const VSTGUI::CCoord kw = 58; + // Continuous -> corona knob, with a live value readout and the name + // label below it. + const VSTGUI::CCoord kw = 52; const VSTGUI::CCoord kx = x + (cell_w - kw) / 2; - VSTGUI::CRect rc(kx, y + 12, kx + kw, y + 12 + kw); + VSTGUI::CRect rc(kx, y + 8, kx + kw, y + 8 + kw); auto* knob = new VSTGUI::CKnob( rc, this, info.id, nullptr, nullptr, VSTGUI::CPoint(0, 0), VSTGUI::CKnob::kCoronaDrawing | VSTGUI::CKnob::kCoronaOutline @@ -323,7 +333,19 @@ class VstGuiEditor final knob->setHandleLineWidth(2.5); ctl = knob; - VSTGUI::CRect lr(x, y + cell_h - 26, x + cell_w, y + cell_h - 8); + // Value readout (refreshed on user edits, see valueChanged). + VSTGUI::CRect vr(x, y + cell_h - 44, x + cell_w, y + cell_h - 26); + auto* vlbl = new VSTGUI::CTextLabel( + vr, value_string(info.id, ec->getParamNormalized(info.id)).c_str()); + vlbl->setFrameColor(VSTGUI::kTransparentCColor); + vlbl->setBackColor(VSTGUI::kTransparentCColor); + vlbl->setFontColor(accent); + vlbl->setHoriAlign(VSTGUI::kCenterText); + frame->addView(vlbl); + m_valueLabels.push_back({info.id, vlbl}); + + // Name label. + VSTGUI::CRect lr(x, y + cell_h - 24, x + cell_w, y + cell_h - 6); auto* lbl = new VSTGUI::CTextLabel(lr, name.c_str()); lbl->setFrameColor(VSTGUI::kTransparentCColor); lbl->setBackColor(VSTGUI::kTransparentCColor); @@ -338,6 +360,18 @@ class VstGuiEditor final } } + // Formatted plain-value string for a normalized value (reuses the + // controller's getParamStringByValue, e.g. "1.00", "0.50", "2.00"). + std::string value_string(Steinberg::Vst::ParamID tag, double norm) + { + Steinberg::Vst::String128 s{}; + if(edit_controller()->getParamStringByValue(tag, norm, s) != Steinberg::kResultTrue) + return {}; + Steinberg::String str(s); + str.toMultiByte(Steinberg::kCP_Utf8); + return std::string(str.text8() ? str.text8() : ""); + } + static std::string title_of(const Steinberg::Vst::ParameterInfo& info) { Steinberg::String s(info.title); @@ -351,9 +385,10 @@ class VstGuiEditor final std::atomic m_ref{1}; std::string m_title; + std::vector> m_valueLabels; static constexpr int cols = 4; static constexpr int cell_w = 96; - static constexpr int cell_h = 104; + static constexpr int cell_h = 118; static constexpr int margin = 12; static constexpr int title_h = 34; int m_width = 260; From 667a6ddef530bd210eedab2f0a364fca5822a47d Mon Sep 17 00:00:00 2001 From: Edu Meneses Date: Wed, 8 Jul 2026 16:13:04 -0400 Subject: [PATCH 22/24] vst3: don't lose momentary button presses within one process block processControl only applied the last point of each parameter queue. VSTGUI's kick-style buttons emit press (1) and release (0) back-to-back on mouse-up, so both landed in the same block and the processor only ever saw 0 - momentary buttons never fired at all. For bool parameters, apply the maximum point of the block's queue instead (latching the press for the whole block) and, when the queue ends released, defer the false assignment to the start of the next block. Continuous parameters keep the last-point behavior. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01K8rKjEZfGtrHLkU5iXR4gn --- include/avnd/binding/vst3/component.hpp | 68 +++++++++++++++++++++---- 1 file changed, 59 insertions(+), 9 deletions(-) diff --git a/include/avnd/binding/vst3/component.hpp b/include/avnd/binding/vst3/component.hpp index a5932998..daf9c406 100644 --- a/include/avnd/binding/vst3/component.hpp +++ b/include/avnd/binding/vst3/component.hpp @@ -11,6 +11,8 @@ #include #include #include +#include +#include namespace stv3 { @@ -83,10 +85,17 @@ struct Component final using inputs_info_t = avnd::parameter_input_introspection; static const constexpr int32_t parameter_count = inputs_info_t::size; + // Bool parameters whose same-block press+release was latched; released at + // the start of the next block (see processControl / processControls). + std::vector m_pendingButtonReleases; + Component() { using namespace Steinberg::Vst; + // Never allocates on the audio thread: at most one entry per parameter. + m_pendingButtonReleases.reserve(parameter_count); + currentProcessMode = -1; processSetup.maxSamplesPerBlock = 1024; processSetup.processMode = kRealtime; @@ -351,17 +360,43 @@ struct Component final int32 numPoints = queue.getPointCount(); int id = queue.getParameterId(); - if(queue.getPoint(numPoints - 1, sampleOffset, value) == Steinberg::kResultTrue) + if(queue.getPoint(numPoints - 1, sampleOffset, value) != Steinberg::kResultTrue) + return; + + // Momentary buttons can emit press + release within one block (VSTGUI's + // kick style even fires both edits back-to-back on mouse-up); keeping + // only the last point would drop the press entirely. For bool parameters, + // latch a pressed point so the processor sees it for this whole block, + // and apply the release at the start of the next one. + ParamValue max_value = value; + for(int32 i = 0; i < numPoints - 1; i++) { - // Apply the host parameter change to every (per-channel) instance so all - // voices of a polyphonic effect track automation, not just instance 0. - for(auto state : this->effect.full_state()) - avnd::parameter_input_introspection::for_nth_raw( - state.inputs, id, [&](C& ctl) { - if constexpr(requires { avnd::map_control_from_01(value); }) + ParamValue v; + int32 off; + if(queue.getPoint(i, off, v) == Steinberg::kResultTrue && v > max_value) + max_value = v; + } + + // Apply the host parameter change to every (per-channel) instance so all + // voices of a polyphonic effect track automation, not just instance 0. + for(auto state : this->effect.full_state()) + avnd::parameter_input_introspection::for_nth_raw( + state.inputs, id, [&](C& ctl) { + if constexpr(requires { avnd::map_control_from_01(value); }) + { + if constexpr(std::is_same_v, bool>) + { + const bool pressed = max_value >= 0.5; + assign_if_assignable(ctl.value, pressed); + if(pressed && value < 0.5) + m_pendingButtonReleases.push_back(id); + } + else + { assign_if_assignable(ctl.value, avnd::map_control_from_01(value)); - }); - }; + } + } + }); } void processControls(ProcessData& data) @@ -369,6 +404,21 @@ struct Component final using namespace Steinberg; using namespace Steinberg::Vst; + // Releases latched from the previous block (see processControl): the + // button was pressed and released within one block, the press was held + // for that block, now let it go back to false (a new press in this + // block's queues will simply re-latch it below). + for(int id : m_pendingButtonReleases) + { + for(auto state : this->effect.full_state()) + avnd::parameter_input_introspection::for_nth_raw( + state.inputs, id, [&](C& ctl) { + if constexpr(std::is_same_v, bool>) + assign_if_assignable(ctl.value, false); + }); + } + m_pendingButtonReleases.clear(); + if(auto paramChanges = data.inputParameterChanges) { int32 numParamsChanged = paramChanges->getParameterCount(); From b8bf94dfa7c4638bfac51c6d36f5db9485f9cfe2 Mon Sep 17 00:00:00 2001 From: Edu Meneses Date: Thu, 9 Jul 2026 11:02:36 -0400 Subject: [PATCH 23/24] fix: getParamNormalized always returned 0 (map_control_to_01 overload disabled) The control-object overload of map_control_to_01 gated itself on an unqualified map_control_to_01(t.value), which has no viable overload (the value overloads need explicit ), so the requires never held and the overload was always disabled. Every caller (VST3 getParamNormalized, ...) then got 0. Qualify the requires with . --- include/avnd/wrappers/controls.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/avnd/wrappers/controls.hpp b/include/avnd/wrappers/controls.hpp index e6f04c82..b4881f1d 100644 --- a/include/avnd/wrappers/controls.hpp +++ b/include/avnd/wrappers/controls.hpp @@ -284,7 +284,7 @@ static constexpr auto map_control_to_01(const auto& value) = delete; // } template - requires requires(T t) { map_control_to_01(t.value); } + requires requires(T t) { map_control_to_01(t.value); } static constexpr auto map_control_to_01(const T& ctl) { return map_control_to_01(ctl.value); From 9eb394ddf4d6b8ac00823b7b721f9ec275446f0d Mon Sep 17 00:00:00 2001 From: Edu Meneses Date: Fri, 10 Jul 2026 09:16:55 -0400 Subject: [PATCH 24/24] vst3/vstgui: widen the editor grid for parameter-heavy plugins Use 6 columns instead of 4 when a plugin exposes more than 20 parameters, keeping the window height manageable (e.g. 40 parameters: 7 rows instead of 10). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01K8rKjEZfGtrHLkU5iXR4gn --- include/avnd/binding/vst3/vstgui_editor.hpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/include/avnd/binding/vst3/vstgui_editor.hpp b/include/avnd/binding/vst3/vstgui_editor.hpp index 1704ab10..45358e76 100644 --- a/include/avnd/binding/vst3/vstgui_editor.hpp +++ b/include/avnd/binding/vst3/vstgui_editor.hpp @@ -248,6 +248,7 @@ class VstGuiEditor final void computeSize() { const int n = edit_controller()->getParameterCount(); + const int cols = colsFor(n); const int c = n < cols ? (n > 0 ? n : 1) : cols; const int rows = (n + c - 1) / c; m_width = c * cell_w + margin * 2; @@ -259,6 +260,7 @@ class VstGuiEditor final { auto* ec = edit_controller(); const int n = ec->getParameterCount(); + const int cols = colsFor(n); const int c = n < cols ? (n > 0 ? n : 1) : cols; m_valueLabels.clear(); // labels belong to the (recreated) frame @@ -386,7 +388,8 @@ class VstGuiEditor final std::string m_title; std::vector> m_valueLabels; - static constexpr int cols = 4; + // Wider grid for parameter-heavy plugins so the window stays usable. + static constexpr int colsFor(int n) noexcept { return n > 20 ? 6 : 4; } static constexpr int cell_w = 96; static constexpr int cell_h = 118; static constexpr int margin = 12;