Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 16 additions & 4 deletions .github/workflows/build_cmake.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ jobs:
chmod +x ./llvm.sh ; \
sudo ./llvm.sh 22 ; \
sudo apt update ; \
sudo apt install ninja-build clang-22 lld-22 libc++-22-dev libc++abi-22-dev",
sudo apt install ninja-build clang-22 lld-22 libc++-22-dev libc++abi-22-dev \
libx11-dev libxrandr-dev libxext-dev libxcursor-dev xvfb",
sdk: "/opt/ossia-sdk-x86_64",
pre_build: ""
}
Expand All @@ -61,7 +62,8 @@ jobs:
sudo apt install lsb-release wget software-properties-common ; \
sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test ; \
sudo apt update ; \
sudo apt install ninja-build gcc-16 g++-16 libgstreamer1.0-dev",
sudo apt install ninja-build gcc-16 g++-16 libgstreamer1.0-dev \
libx11-dev libxrandr-dev libxext-dev libxcursor-dev xvfb",
sdk: "/opt/ossia-sdk-x86_64",
pre_build: ""
}
Expand Down Expand Up @@ -164,7 +166,12 @@ jobs:
export PATH="${{ matrix.config.path }}:$PATH"
fi

cmake --build . --target test
# The editor tests open real windows: give them an X server.
if [[ "$RUNNER_OS" == "Linux" ]]; then
xvfb-run -a -s "-screen 0 1280x960x24" cmake --build . --target test
else
cmake --build . --target test
fi

- name: Build release
shell: bash
Expand Down Expand Up @@ -205,4 +212,9 @@ jobs:
export PATH="${{ matrix.config.path }}:$PATH"
fi

cmake --build . --target test
# The editor tests open real windows: give them an X server.
if [[ "$RUNNER_OS" == "Linux" ]]; then
xvfb-run -a -s "-screen 0 1280x960x24" cmake --build . --target test
else
cmake --build . --target test
fi
502 changes: 502 additions & 0 deletions CUSTOM_UI_PLAN.md

Large diffs are not rendered by default.

27 changes: 25 additions & 2 deletions book/src/advanced/ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
We have seen so far that we can specify widgets for our controls. Multiple back-ends may render these widgets in various ways.
This is already a good start for making user interfaces, but most media systems generally have more specific user interface needs.

Avendish allows three levels of UI definition:
Avendish allows four levels of UI definition:

1. Automatic: nothing to do, all the widgets corresponding to inputs and outputs of the processor will be generated automatically in a list. This is not pretty but sufficient for many simple cases. For instance, here is how some Avendish plug-ins render in *ossia score*.

Expand All @@ -22,4 +22,27 @@ Avendish allows three levels of UI definition:

> Supported bindings: ossia

![Basic UI](images/ui-image.gif)
![Basic UI](images/ui-image.gif)

4. Shipping an entirely custom editor written with **any UI framework**, through
the `ui::window` escape hatch:

```cpp
struct ui {
struct window {
void open(avnd::gui_parent parent, avnd::gui_host host); // host's native window handle
void close();
void idle(); // host UI-thread tick
std::pair<int, int> size() const;
};
};
```

The bindings hand `open()` the host-provided parent window (HWND / NSView /
X11 window) and a set of automation-gesture callbacks
(`begin_edit` / `perform_edit` / `end_edit`); anything can be created inside —
Dear ImGui, Qt, JUCE, or raw platform code. See
`examples/Advanced/UI/CustomUiWindow.hpp` for a complete plug-in whose editor
is a plain Win32 window, built as CLAP, VST2 and VST3 from the same file.

> Supported bindings: CLAP, VST2, VST3
4 changes: 3 additions & 1 deletion cmake/avendish.clap.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ target_include_directories(

target_link_libraries(Avendish_clap_pch
PUBLIC
concurrentqueue
DisableExceptions
)

Expand Down Expand Up @@ -130,14 +131,15 @@ function(avnd_make_clap)
${AVND_FX_TARGET}
PUBLIC
Avendish::Avendish_clap
concurrentqueue
DisableExceptions
)

avnd_common_setup("${AVND_TARGET}" "${AVND_FX_TARGET}")
endfunction()

add_library(Avendish_clap INTERFACE)
target_link_libraries(Avendish_clap INTERFACE Avendish)
target_link_libraries(Avendish_clap INTERFACE Avendish concurrentqueue)
add_library(Avendish::Avendish_clap ALIAS Avendish_clap)

target_sources(Avendish PRIVATE
Expand Down
36 changes: 36 additions & 0 deletions cmake/avendish.examples.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,42 @@ avnd_make_audioplug(
C_NAME avnd_midi
)

# Tier C custom-UI example: the plug-in ships its own editor (raw Win32 /
# raw Xlib) through the avnd::gui_windowed_ui seam instead of the soft editor.
avnd_make_audioplug(
TARGET CustomUiWindow
MAIN_FILE examples/Advanced/UI/CustomUiWindow.hpp
MAIN_CLASS examples::CustomUiWindow
C_NAME avnd_custom_ui_window
)
# The editor's two platform implementations live in their own translation
# units (behind the pimpl in CustomUiWindow.hpp). Compile the matching one into
# each plug-in module, and link its windowing library: a host has no reason to
# have libX11 loaded when it dlopens the module. Without an implementation for
# the platform (or, on Linux, without the X11 headers) the example simply has
# no editor, matching the header's own guard.
set(_avnd_customui_impl "")
set(_avnd_customui_x11 0)
if(WIN32)
set(_avnd_customui_impl "${CMAKE_CURRENT_SOURCE_DIR}/examples/Advanced/UI/CustomUiWindow.win32.cpp")
elseif(UNIX AND NOT APPLE)
find_package(X11 QUIET)
if(X11_FOUND)
set(_avnd_customui_impl "${CMAKE_CURRENT_SOURCE_DIR}/examples/Advanced/UI/CustomUiWindow.x11.cpp")
set(_avnd_customui_x11 1)
endif()
endif()
if(_avnd_customui_impl)
foreach(_avnd_backend clap vst3 vintage)
if(TARGET CustomUiWindow_${_avnd_backend})
target_sources(CustomUiWindow_${_avnd_backend} PRIVATE "${_avnd_customui_impl}")
if(_avnd_customui_x11)
target_link_libraries(CustomUiWindow_${_avnd_backend} PRIVATE X11::X11)
endif()
endif()
endforeach()
endif()

# GCC segfaults with those two...
if(NOT "${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU")
avnd_make_all(
Expand Down
35 changes: 35 additions & 0 deletions cmake/avendish.tests.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ if(BUILD_TESTING)
avnd_add_static_test(test_introspection_rec tests/test_introspection_rec.cpp)
avnd_add_static_test(test_predicate tests/test_predicate.cpp)
avnd_add_static_test(test_vintage tests/tests_vintage.cpp)
if(TARGET concurrentqueue) # vintage gui glue's bus transport
target_link_libraries(test_vintage PRIVATE concurrentqueue)
endif()
avnd_add_static_test(test_channels tests/tests_channels.cpp)
avnd_add_static_test(test_function_reflection tests/tests_function_reflection.cpp)
avnd_add_static_test(test_audioprocessor tests/test_audioprocessor.cpp)
Expand All @@ -48,5 +51,37 @@ if(BUILD_TESTING)

avnd_add_catch_test(test_gain tests/objects/gain.cpp)
avnd_add_catch_test(test_patternal tests/objects/patternal.cpp)

# The end-to-end editor tests need a real windowing system: Win32, or
# X11 (run under Xvfb in headless CI).
if(WIN32)
set(AVND_GUI_TESTS 1)
elseif(UNIX AND NOT APPLE)
find_package(X11 QUIET)
if(X11_FOUND)
set(AVND_GUI_TESTS 1)
else()
set(AVND_GUI_TESTS 0)
endif()
else()
set(AVND_GUI_TESTS 0)
endif()

# Editor test hosts drive the plug-in's window from outside on X11.
function(avnd_gui_test_platform_libs theTarget)
if(UNIX AND NOT APPLE)
target_link_libraries("${theTarget}" PRIVATE X11::X11)
endif()
endfunction()

# Tier C: the CustomUiWindow example's author-provided editor.
if(AVND_GUI_TESTS AND TARGET CustomUiWindow_clap)
avnd_add_catch_test(test_custom_ui_window tests/ui/test_custom_ui_window.cpp)
add_dependencies(test_custom_ui_window CustomUiWindow_clap)
target_include_directories(test_custom_ui_window PRIVATE ${CLAP_HEADER})
avnd_gui_test_platform_libs(test_custom_ui_window)
target_compile_definitions(test_custom_ui_window PRIVATE
"AVND_TEST_CUSTOM_UI_CLAP_PATH=\"$<TARGET_FILE:CustomUiWindow_clap>\"")
endif()
endif()

4 changes: 3 additions & 1 deletion cmake/avendish.vintage.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ target_precompile_headers(Avendish_vintage_pch

target_link_libraries(Avendish_vintage_pch
PUBLIC
concurrentqueue
DisableExceptions
)
avnd_common_setup("" "Avendish_vintage_pch")
Expand Down Expand Up @@ -101,14 +102,15 @@ function(avnd_make_vintage)
${AVND_FX_TARGET}
PUBLIC
Avendish::Avendish_vintage
concurrentqueue
DisableExceptions
)

avnd_common_setup("${AVND_TARGET}" "${AVND_FX_TARGET}")
endfunction()

add_library(Avendish_vintage INTERFACE)
target_link_libraries(Avendish_vintage INTERFACE Avendish)
target_link_libraries(Avendish_vintage INTERFACE Avendish concurrentqueue)
add_library(Avendish::Avendish_vintage ALIAS Avendish_vintage)

target_sources(Avendish PRIVATE
Expand Down
2 changes: 2 additions & 0 deletions cmake/avendish.vst3.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,10 @@ function(avnd_make_vst3)
PUBLIC
Avendish::Avendish_vst3
sdk_common pluginterfaces
concurrentqueue
DisableExceptions
)

if(APPLE)
find_library(COREFOUNDATION_FK CoreFoundation)
target_link_libraries(
Expand Down
143 changes: 143 additions & 0 deletions examples/Advanced/UI/CustomUiWindow.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#pragma once

/* SPDX-License-Identifier: GPL-3.0-or-later */

/**
* Tier C custom-UI example (CUSTOM_UI_PLAN.md §5): instead of the
* declarative `struct ui` rendered by the reference soft editor, the plug-in
* ships its own editor implementing the avnd::gui_windowed_ui shape:
*
* struct ui {
* struct window {
* void open(avnd::gui_parent, avnd::gui_host);
* void close();
* void idle();
* std::pair<int, int> size() const;
* };
* };
*
* Any UI stack works behind this seam -- Dear ImGui, Qt, JUCE, ... This
* example deliberately uses no library at all: two hand-rolled implementations
* of the same contract, a raw Win32 child window painted with GDI and a raw
* Xlib window. So the contract itself is the whole story:
*
* - open() receives the host's parent window handle and creates whatever
* the framework needs inside it;
* - parameter *reads* poll the effect model (live processor instance in
* CLAP/VST2, controller-side model in VST3), so host automation shows up
* without any extra plumbing;
* - parameter *writes* set the field, then drive the automation-gesture
* hooks on avnd::gui_host (flat parameter index, [0; 1] normalized) --
* the bindings translate to CLAP gesture events / audioMasterAutomate /
* IComponentHandler;
* - ticks come from the host (clap.timer-support, effEditIdle) and from
* the host's message pump for frameworks with their own timers.
*
* The two platform implementations live in the sibling translation units
* CustomUiWindow.win32.cpp and CustomUiWindow.x11.cpp, reached through a
* forward-declared pimpl so this header pulls in no windowing headers at all
* (a real toolkit would hide the same way behind its own opaque handle). On
* platforms with neither backend the plug-in simply has no editor: the
* concept is not satisfied and the bindings skip it.
*/

#include <avnd/concepts/gui_window.hpp>
#include <avnd/wrappers/effect_container.hpp>
#include <halp/audio.hpp>
#include <halp/controls.hpp>
#include <halp/meta.hpp>

#include <functional>
#include <memory>
#include <utility>

namespace examples
{
struct CustomUiWindow
{
halp_meta(name, "Custom UI window")
halp_meta(c_name, "avnd_custom_ui_window")
halp_meta(category, "Demo")
halp_meta(author, "Avendish")
halp_meta(description, "Gain with a hand-rolled (raw Win32 / raw Xlib) editor")
halp_meta(uuid, "aef14f10-fb50-45e9-9dc6-c40c5e5fe790")

struct ins
{
halp::knob_f32<"Gain", halp::range{.min = 0., .max = 1., .init = 0.5}> gain;
halp::dynamic_audio_bus<"In", float> audio;
} inputs;

struct outs
{
halp::dynamic_audio_bus<"Out", float> audio;
} outputs;

void operator()(int frames)
{
for(int c = 0; c < outputs.audio.channels; c++)
for(int i = 0; i < frames; i++)
outputs.audio.samples[c][i] = inputs.audio.samples[c][i] * inputs.gain.value;
}

// Message bus for Tier C: the window itself is the UI-side endpoint --
// the bindings wire window.send_message and call window.process_message.
struct ui_to_processor
{
float value;
};
struct processor_to_ui
{
float applied;
};

void process_message(const ui_to_processor& msg)
{
// Deliberately not the same as the direct edit (msg.value / 2) so the
// bus path is observable in tests.
inputs.gain.value = msg.value * 0.5f;
if(send_message)
send_message(processor_to_ui{.applied = inputs.gain.value});
}
std::function<void(processor_to_ui)> send_message;

// The editor exists only where a windowing implementation backs it: raw Win32
// on Windows, raw Xlib on Linux when the X11 headers are present. Both are
// defined out-of-line in CustomUiWindow.{win32,x11}.cpp behind the pimpl below.
#if defined(_WIN32) || (defined(__linux__) && __has_include(<X11/Xlib.h>))
struct ui
{
struct window
{
static constexpr int width = 360, height = 120;
static constexpr int gain_param = 0; // flat index in parameter order

// The bindings pass the model when the constructor accepts it
// (avnd::make_ui_editor); in CLAP/VST2 this is the live processor
// instance, in VST3 the controller-side model.
explicit window(avnd::effect_container<CustomUiWindow>& fx);
~window();

// avnd::gui_windowed_ui contract, forwarded to the platform impl.
void open(avnd::gui_parent parent, avnd::gui_host h);
void close();
void idle();
std::pair<int, int> size() const { return {width, height}; }

// ---- Message bus endpoints (wired by the bindings) ----
// UI thread -> processor; the impl calls it on mouse release.
std::function<void(ui_to_processor)> send_message;
// processor -> UI thread
void process_message(processor_to_ui msg) { feedback = msg.applied; }
float feedback{-1.f};

private:
// Platform window + its paint/event code, defined per-OS in the matching
// .cpp; forward-declared here so no windowing header reaches this file.
struct impl;
std::unique_ptr<impl> self;
};
};
#endif
};
}
Loading
Loading