diff --git a/cmake/avendish.vst3.cmake b/cmake/avendish.vst3.cmake index 3a1ad192..67ddbb2f 100644 --- a/cmake/avendish.vst3.cmake +++ b/cmake/avendish.vst3.cmake @@ -6,6 +6,17 @@ 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) + +# 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) @@ -29,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 @@ -51,6 +66,22 @@ 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) + # 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 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") function(avnd_make_vst3) @@ -138,6 +169,38 @@ 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) + # 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() + + # 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() + + # 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}") endfunction() 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(); diff --git a/include/avnd/binding/vst3/controller.hpp b/include/avnd/binding/vst3/controller.hpp index 4aed48ea..b1ce665c 100644 --- a/include/avnd/binding/vst3/controller.hpp +++ b/include/avnd/binding/vst3/controller.hpp @@ -2,7 +2,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -69,8 +71,33 @@ 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, std::string{avnd::get_name()}}; + return nullptr; + } +#endif + 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) @@ -99,6 +126,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; 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/prototype.cpp.in b/include/avnd/binding/vst3/prototype.cpp.in index 6e271b2a..52216f7e 100644 --- a/include/avnd/binding/vst3/prototype.cpp.in +++ b/include/avnd/binding/vst3/prototype.cpp.in @@ -4,6 +4,17 @@ #include #include +#if defined(AVND_VST3_VSTGUI) +// 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 #include <@AVND_MAIN_FILE@> @@ -13,14 +24,57 @@ 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__ -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. +extern "C" AVND_EXPORTED_SYMBOL bool ModuleEntry(void* sharedLibraryHandle) +{ +#if defined(AVND_VST3_VSTGUI) + VSTGUI::init(sharedLibraryHandle); +#endif + return true; +} +extern "C" AVND_EXPORTED_SYMBOL bool ModuleExit() +{ +#if defined(AVND_VST3_VSTGUI) + VSTGUI::exit(); +#endif + return true; +} #endif extern "C" AVND_EXPORTED_SYMBOL Steinberg::IPluginFactory* GetPluginFactory() diff --git a/include/avnd/binding/vst3/vstgui_editor.hpp b/include/avnd/binding/vst3/vstgui_editor.hpp new file mode 100644 index 00000000..45358e76 --- /dev/null +++ b/include/avnd/binding/vst3/vstgui_editor.hpp @@ -0,0 +1,403 @@ +#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 +#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::X11::IRunLoop + , public VSTGUI::AtomicReferenceCounted +{ +public: + struct EventHandler : Steinberg::Linux::IEventHandler, public Steinberg::FObject + { + VSTGUI::X11::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::X11::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::X11::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::X11::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::X11::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::X11::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 +{ +public: + 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(); + } + + // ---------------- 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), nullptr); + frame->setBackgroundColor(VSTGUI::CColor{32, 32, 34, 255}); + +#if defined(_WIN32) + frame->open(parent, VSTGUI::PlatformType::kHWND); +#elif defined(__APPLE__) + frame->open(parent, VSTGUI::PlatformType::kNSView); +#else + // 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 + 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); + + for(auto& [t, lbl] : m_valueLabels) + if(t == tag && lbl) + { + lbl->setText(value_string(tag, v).c_str()); + lbl->invalid(); + } + } + +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 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; + 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(); + const int cols = colsFor(n); + 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}; + + // 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 = title_h + margin + row * cell_h; + const std::string name = title_of(info); + + VSTGUI::CControl* ctl = nullptr; + if(info.stepCount == 1) + { + // 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(), + momentary ? VSTGUI::CTextButton::kKickStyle + : VSTGUI::CTextButton::kOnOffStyle); + b->setRoundRadius(5.); + b->setFrameColor(accent); + // 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 + { + // 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 + 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 + | 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; + + // 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); + lbl->setFontColor(labelCol); + lbl->setHoriAlign(VSTGUI::kCenterText); + frame->addView(lbl); + } + + ctl->setValueNormalized(static_cast(ec->getParamNormalized(info.id))); + frame->addView(ctl); + ++idx; + } + } + + // 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); + 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}; + + std::string m_title; + std::vector> m_valueLabels; + // 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; + static constexpr int title_h = 34; + int m_width = 260; + int m_height = 180; +}; + +} + +#endif 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 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);