Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5f86954
vst3: optional self-contained VSTGUI editor
edumeneses Jul 1, 2026
b585225
vst3: enable OBJC/OBJCXX for VSTGUI on macOS
edumeneses Jul 1, 2026
d66bce3
vst3: add VSTGUI include root for editor sources
edumeneses Jul 1, 2026
3a0ac8a
vst3: fix VSTGUI control construction (CFrame/CKnob/CTextLabel APIs)
edumeneses Jul 1, 2026
7f9446c
vst3: strip VSTGUI's macOS -Werror so its internal warnings don't fai…
edumeneses Jul 1, 2026
2d4a1a9
vst3: set MSVC /Zc:preprocessor on the vstgui + module targets
edumeneses Jul 1, 2026
326bd88
vst3: add /Zc:preprocessor at directory scope before the SDK subdir (…
edumeneses Jul 1, 2026
38e07e0
vst3: force-include vstguidebug.h on MSVC to define vstgui_assert
edumeneses Jul 1, 2026
5e80f1c
vst3: force-include a vstgui_assert shim on MSVC (fixes C3861 in VSTGUI)
edumeneses Jul 2, 2026
4ec5932
vst3/vstgui: wire host IRunLoop into VSTGUI on Linux (fixes X11 edito…
edumeneses Jul 2, 2026
c17f250
vst3/vstgui: include platform_linux.h for VSTGUI IRunLoop types
edumeneses Jul 2, 2026
8dce65c
vst3/vstgui: qualify IRunLoop types with VSTGUI::X11 (matches VSTGUI …
edumeneses Jul 2, 2026
62955c3
vst3/vstgui: init VSTGUI platform from the module handle on Linux (Mo…
edumeneses Jul 2, 2026
a3cb16d
vst3/vstgui: use VSTGUI::init/exit in module entries on all platforms
edumeneses Jul 2, 2026
ec4d2e5
vst3: define DEVELOPMENT only in Debug (SIGTRAP asserts crashed relea…
edumeneses Jul 2, 2026
3e4983f
vst3/vstgui: compute editor size in ctor (hosts call getSize before a…
edumeneses Jul 2, 2026
1598b28
vst3/vstgui: nicer default editor — title bar, on/off buttons, corona…
edumeneses Jul 3, 2026
61e3300
vst3: report boolean controls as stepCount=1 (toggle, not knob)
edumeneses Jul 3, 2026
8ac25e9
vst3/vstgui: set toggle button fill gradients so the label stays read…
edumeneses Jul 6, 2026
1e374a0
vst3/vstgui: render momentary buttons (bangs) as kick-style buttons
edumeneses Jul 6, 2026
5fb2571
vst3/vstgui: show each knob's value under it, live on edit
edumeneses Jul 8, 2026
667a6dd
vst3: don't lose momentary button presses within one process block
edumeneses Jul 8, 2026
b8bf94d
fix: getParamNormalized always returned 0 (map_control_to_01 overload…
edumeneses Jul 9, 2026
9eb394d
vst3/vstgui: widen the editor grid for parameter-heavy plugins
edumeneses Jul 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 64 additions & 1 deletion cmake/avendish.vst3.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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($<IF:$<CONFIG:Debug>,DEVELOPMENT=1,RELEASE=1>)
include_directories("${VST3_SDK_ROOT}")

# VST3 uses COM APIs which require no virtual dtors in interfaces
Expand All @@ -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)
Expand Down Expand Up @@ -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 <vstgui/...>; 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()

Expand Down
68 changes: 59 additions & 9 deletions include/avnd/binding/vst3/component.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
#include <avnd/introspection/output.hpp>
#include <avnd/wrappers/controls.hpp>
#include <avnd/wrappers/process_adapter.hpp>
#include <type_traits>
#include <vector>

namespace stv3
{
Expand Down Expand Up @@ -83,10 +85,17 @@ struct Component final
using inputs_info_t = avnd::parameter_input_introspection<T>;
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<int> 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;
Expand Down Expand Up @@ -351,24 +360,65 @@ 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<T>::for_nth_raw(
state.inputs, id, [&]<typename C>(C& ctl) {
if constexpr(requires { avnd::map_control_from_01<C>(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<T>::for_nth_raw(
state.inputs, id, [&]<typename C>(C& ctl) {
if constexpr(requires { avnd::map_control_from_01<C>(value); })
{
if constexpr(std::is_same_v<std::decay_t<decltype(ctl.value)>, 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<C>(value));
});
};
}
}
});
}

void processControls(ProcessData& data)
{
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<T>::for_nth_raw(
state.inputs, id, [&]<typename C>(C& ctl) {
if constexpr(std::is_same_v<std::decay_t<decltype(ctl.value)>, bool>)
assign_if_assignable(ctl.value, false);
});
}
m_pendingButtonReleases.clear();

if(auto paramChanges = data.inputParameterChanges)
{
int32 numParamsChanged = paramChanges->getParameterCount();
Expand Down
31 changes: 31 additions & 0 deletions include/avnd/binding/vst3/controller.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
#include <avnd/binding/vst3/controller_base.hpp>
#include <avnd/binding/vst3/programs.hpp>
#include <avnd/binding/vst3/refcount.hpp>
#include <avnd/binding/vst3/vstgui_editor.hpp>
#include <avnd/common/widechar.hpp>
#include <avnd/concepts/parameter.hpp>
#include <avnd/introspection/input.hpp>
#include <avnd/introspection/output.hpp>
#include <avnd/wrappers/control_display.hpp>
Expand Down Expand Up @@ -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<T>()}};
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, [&]<std::size_t Index, typename C>(avnd::field_reflection<Index, C>) {
// 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)
Expand Down Expand Up @@ -99,6 +126,10 @@ class Controller final
if constexpr(requires { range.step; })
info.stepCount = avnd::get_range<C>().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<C>)
info.stepCount = 1;
});

info.unitId = 1;
Expand Down
10 changes: 10 additions & 0 deletions include/avnd/binding/vst3/controller_base.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
{
Expand Down
66 changes: 60 additions & 6 deletions include/avnd/binding/vst3/prototype.cpp.in
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@
#include <avnd/binding/vst3/configure.hpp>
#include <avnd/common/export.hpp>

#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 <vstgui/lib/vstguiinit.h>
#if defined(_WIN32)
#include <windows.h>
#elif defined(__APPLE__)
#include <CoreFoundation/CoreFoundation.h>
#endif
#endif

// clang-format off
#include <@AVND_MAIN_FILE@>

Expand All @@ -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);

@jcelerier jcelerier Jul 3, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems to use the official vstgui backend from steinberg which is really terrible for making UIs (and won't help us when we want to use CLAP etc.)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Real Edu here: What do you propose as a cross-platform UI tool for plugins?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, the decision was based on portability. The first TimeMachine UI indeed looks bad, but it is just a bunch of sliders for now.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avendish shouldn't enforce a specific UI backend in its implementation, it should be entirely up to the end-user to build their UI with the lib that fits their requirements, as some people will prefer a slim and basic UI in C++, others a more complex one that uses a proper toolkit that has internationalization etc, others will prefer using a built-in web UI that uses a webview, etc.
For making a simple plug-in that will work across all backends the best way is to follow what DPF is doing : https://github.com/DISTRHO/DPF

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

other good options for context usecase:
https://github.com/thorvg/thorvg https://github.com/sammycage/plutovg
my main requirement for the one I'm using for examples is that it can work all the way from esp32 to wasm as those are all avendish-supported platforms, so if someone wants to make "an ui for my algorithm" then the "default choice" should not cause build errors on any of those (which restricts to simple, pixel-graphics-only framework)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, that makes sense.

#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<LPCWSTR>(&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()
Expand Down
Loading
Loading