From b92209de7468e5cddf70fe9b986087293f852170 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Mon, 31 Aug 2020 05:47:37 +0200 Subject: [PATCH] Support UI in LV2 --- .travis/prepare_osx.sh | 3 +- editor/CMakeLists.txt | 7 +- editor/cmake/Vstgui.cmake | 11 + {vst => editor}/resources/logo.png | Bin {vst => editor}/resources/logo.svg | 0 editor/src/editor/Editor.cpp | 2 +- lv2/CMakeLists.txt | 50 ++- lv2/lv2ui.version | 4 + lv2/manifest.ttl.in | 8 +- lv2/sfizz.c | 61 ++-- lv2/sfizz.ttl.in | 69 ++++ lv2/sfizz_lv2.h | 43 +++ lv2/sfizz_ui.cpp | 492 +++++++++++++++++++++++++++++ lv2/sfizz_ui.ttl.in | 14 + lv2/vstgui_helpers.cpp | 170 ++++++++++ lv2/vstgui_helpers.h | 68 ++++ vst/CMakeLists.txt | 7 +- 17 files changed, 966 insertions(+), 43 deletions(-) rename {vst => editor}/resources/logo.png (100%) rename {vst => editor}/resources/logo.svg (100%) create mode 100644 lv2/lv2ui.version create mode 100644 lv2/sfizz_lv2.h create mode 100644 lv2/sfizz_ui.cpp create mode 100644 lv2/sfizz_ui.ttl.in create mode 100644 lv2/vstgui_helpers.cpp create mode 100644 lv2/vstgui_helpers.h diff --git a/.travis/prepare_osx.sh b/.travis/prepare_osx.sh index 8fc891c9..8de6d08e 100755 --- a/.travis/prepare_osx.sh +++ b/.travis/prepare_osx.sh @@ -15,7 +15,8 @@ buildenv make DESTDIR=${PWD}/${INSTALL_DIR} install # Bundle LV2 dependencies cd "${INSTALL_DIR}"/Library/Audio/Plug-Ins/LV2 -dylibbundler -od -b -x sfizz.lv2/sfizz.so -d sfizz.lv2/libs/ -p @loader_path/libs/ +dylibbundler -od -b -x sfizz.lv2/Contents/Binary/sfizz.so -d sfizz.lv2/Contents/libs/ -p @loader_path/../libs/ +dylibbundler -od -b -x sfizz.lv2/Contents/Binary/sfizz_ui.so -d sfizz.lv2/Contents/libs/ -p @loader_path/../libs/ cd "${TRAVIS_BUILD_DIR}/build" # Bundle VST3 dependencies diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 977f36a4..777939d4 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -1,6 +1,10 @@ set(VSTGUI_BASEDIR "${CMAKE_CURRENT_SOURCE_DIR}/external/vstgui4") include("cmake/Vstgui.cmake") +set(EDITOR_RESOURCES + logo.png + PARENT_SCOPE) + # editor add_library(sfizz_editor STATIC EXCLUDE_FROM_ALL src/editor/EditIds.h @@ -13,4 +17,5 @@ add_library(sfizz_editor STATIC EXCLUDE_FROM_ALL src/editor/utility/vstgui_after.h src/editor/utility/vstgui_before.h) target_include_directories(sfizz_editor PUBLIC "src") -target_link_libraries(sfizz_editor PRIVATE sfizz-vstgui absl::strings) +target_link_libraries(sfizz_editor PRIVATE sfizz-vstgui) +target_link_libraries(sfizz_editor PUBLIC absl::strings absl::variant) diff --git a/editor/cmake/Vstgui.cmake b/editor/cmake/Vstgui.cmake index 92f85f03..e853b091 100644 --- a/editor/cmake/Vstgui.cmake +++ b/editor/cmake/Vstgui.cmake @@ -207,3 +207,14 @@ endif() if(${CMAKE_BUILD_TYPE} MATCHES "Release") target_compile_definitions(sfizz-vstgui PRIVATE "RELEASE") endif() + +if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(sfizz-vstgui PRIVATE + "-Wno-deprecated-copy" + "-Wno-ignored-qualifiers" + "-Wno-reorder" + "-Wno-sign-compare" + "-Wno-unused-function" + "-Wno-unused-parameter" + "-Wno-unused-variable") +endif() diff --git a/vst/resources/logo.png b/editor/resources/logo.png similarity index 100% rename from vst/resources/logo.png rename to editor/resources/logo.png diff --git a/vst/resources/logo.svg b/editor/resources/logo.svg similarity index 100% rename from vst/resources/logo.svg rename to editor/resources/logo.svg diff --git a/editor/src/editor/Editor.cpp b/editor/src/editor/Editor.cpp index c4d247c8..1024c27d 100644 --- a/editor/src/editor/Editor.cpp +++ b/editor/src/editor/Editor.cpp @@ -288,7 +288,7 @@ void Editor::Impl::createFrameContents() view->setBackgroundColor(CColor(0xff, 0xff, 0xff)); - SharedPointer logo = new CBitmap("logo.png"); + SharedPointer logo { new CBitmap("logo.png") }; CRect bottomRow = bounds; bottomRow.top = bottomRow.bottom - 30; diff --git a/lv2/CMakeLists.txt b/lv2/CMakeLists.txt index 4104eb25..c45f964f 100644 --- a/lv2/CMakeLists.txt +++ b/lv2/CMakeLists.txt @@ -11,6 +11,10 @@ set (LV2PLUGIN_TTL_SRC_FILES manifest.ttl.in ${PROJECT_NAME}.ttl.in ) +if (SFIZZ_LV2_UI) + list(APPEND LV2PLUGIN_TTL_SRC_FILES + ${PROJECT_NAME}_ui.ttl.in) +endif() source_group("Turtle Files" FILES ${LV2PLUGIN_TTL_SRC_FILES} ) @@ -19,31 +23,61 @@ add_library (${LV2PLUGIN_PRJ_NAME} MODULE atomic_compat.h ${LV2PLUGIN_TTL_SRC_FILES}) target_link_libraries (${LV2PLUGIN_PRJ_NAME} ${PROJECT_NAME}::${PROJECT_NAME}) + +if (SFIZZ_LV2_UI) + add_library (${LV2PLUGIN_PRJ_NAME}_ui MODULE + ${PROJECT_NAME}_ui.cpp + vstgui_helpers.h + vstgui_helpers.cpp) + target_link_libraries (${LV2PLUGIN_PRJ_NAME}_ui sfizz_editor sfizz-vstgui) +endif() + # Explicitely strip all symbols on Linux but lv2_descriptor() # MacOS linker does not support this apparently https://bugs.webkit.org/show_bug.cgi?id=144555 if (${CMAKE_SYSTEM_NAME} MATCHES "Linux") file(COPY lv2.version DESTINATION ${CMAKE_BINARY_DIR}/lv2) target_link_libraries(${LV2PLUGIN_PRJ_NAME} "-Wl,--version-script=lv2.version") # target_link_libraries(${LV2PLUGIN_PRJ_NAME} "-Wl,-u,lv2_descriptor") + if (SFIZZ_LV2_UI) + file(COPY lv2ui.version DESTINATION ${CMAKE_BINARY_DIR}/lv2) + target_link_libraries(${LV2PLUGIN_PRJ_NAME}_ui "-Wl,--version-script=lv2ui.version") + # target_link_libraries(${LV2PLUGIN_PRJ_NAME}_ui "-Wl,-u,lv2ui_descriptor") + endif() endif() target_include_directories(${LV2PLUGIN_PRJ_NAME} PRIVATE . external/ardour) sfizz_enable_lto_if_needed (${LV2PLUGIN_PRJ_NAME}) if (MINGW) set_target_properties (${LV2PLUGIN_PRJ_NAME} PROPERTIES LINK_FLAGS "-static") endif() +if (SFIZZ_LV2_UI) + target_include_directories(${LV2PLUGIN_PRJ_NAME}_ui PRIVATE . external/ardour) + sfizz_enable_lto_if_needed (${LV2PLUGIN_PRJ_NAME}_ui) + if (MINGW) + set_target_properties (${LV2PLUGIN_PRJ_NAME}_ui PROPERTIES LINK_FLAGS "-static") + endif() +endif() # Remove the "lib" prefix, rename the target name and build it in the .lv build dir # /lv2/_lv2. to # /lv2/.lv2/. set_target_properties (${LV2PLUGIN_PRJ_NAME} PROPERTIES PREFIX "") set_target_properties (${LV2PLUGIN_PRJ_NAME} PROPERTIES OUTPUT_NAME "${PROJECT_NAME}") -set_target_properties (${LV2PLUGIN_PRJ_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}") +set_target_properties (${LV2PLUGIN_PRJ_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/Contents/Binary") + +if (SFIZZ_LV2_UI) + set_target_properties (${LV2PLUGIN_PRJ_NAME}_ui PROPERTIES PREFIX "") + set_target_properties (${LV2PLUGIN_PRJ_NAME}_ui PROPERTIES OUTPUT_NAME "${PROJECT_NAME}_ui") + set_target_properties (${LV2PLUGIN_PRJ_NAME}_ui PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/Contents/Binary") +endif() # Generate *.ttl files from *.in sources, # create the destination directory if it doesn't exists and copy needed files file (MAKE_DIRECTORY ${PROJECT_BINARY_DIR}) configure_file (manifest.ttl.in ${PROJECT_BINARY_DIR}/manifest.ttl) configure_file (${PROJECT_NAME}.ttl.in ${PROJECT_BINARY_DIR}/${PROJECT_NAME}.ttl) +if (SFIZZ_LV2_UI) + configure_file (${PROJECT_NAME}_ui.ttl.in ${PROJECT_BINARY_DIR}/${PROJECT_NAME}_ui.ttl) +endif() configure_file (LICENSE.md.in ${PROJECT_BINARY_DIR}/LICENSE.md) if (SFIZZ_USE_VCPKG OR SFIZZ_STATIC_DEPENDENCIES OR CMAKE_CXX_COMPILER_ID MATCHES "MSVC") file(COPY "lgpl-3.0.txt" DESTINATION ${PROJECT_BINARY_DIR}) @@ -54,12 +88,22 @@ set(LV2_RESOURCES DefaultInstrument.sfz DefaultScale.scl) execute_process( - COMMAND "${CMAKE_COMMAND}" -E make_directory "${PROJECT_BINARY_DIR}/Resources") + COMMAND "${CMAKE_COMMAND}" -E make_directory "${PROJECT_BINARY_DIR}/Contents/Resources") foreach(res ${LV2_RESOURCES}) file (COPY "${CMAKE_CURRENT_SOURCE_DIR}/resources/${res}" - DESTINATION "${PROJECT_BINARY_DIR}/Resources") + DESTINATION "${PROJECT_BINARY_DIR}/Contents/Resources") endforeach() +# Copy editor resources +if (SFIZZ_LV2_UI) + execute_process ( + COMMAND "${CMAKE_COMMAND}" -E make_directory "${PROJECT_BINARY_DIR}/Contents/Resources") + foreach(res ${EDITOR_RESOURCES}) + file (COPY "${CMAKE_CURRENT_SOURCE_DIR}/../editor/resources/${res}" + DESTINATION "${PROJECT_BINARY_DIR}/Contents/Resources") + endforeach() +endif() + # Installation if (NOT MSVC) install (DIRECTORY ${PROJECT_BINARY_DIR} DESTINATION ${LV2PLUGIN_INSTALL_DIR} diff --git a/lv2/lv2ui.version b/lv2/lv2ui.version new file mode 100644 index 00000000..95c55e1b --- /dev/null +++ b/lv2/lv2ui.version @@ -0,0 +1,4 @@ +LV2UIABI_1.0 { + global: *lv2ui_descriptor*; + local: *; +}; diff --git a/lv2/manifest.ttl.in b/lv2/manifest.ttl.in index 3ea4e883..ab407d33 100644 --- a/lv2/manifest.ttl.in +++ b/lv2/manifest.ttl.in @@ -1,7 +1,13 @@ @prefix lv2: . @prefix rdfs: . +@prefix ui: . <@LV2PLUGIN_URI@> a lv2:Plugin ; - lv2:binary <@PROJECT_NAME@@CMAKE_SHARED_MODULE_SUFFIX@> ; + lv2:binary ; rdfs:seeAlso <@PROJECT_NAME@.ttl> . + +@LV2PLUGIN_IF_ENABLE_UI@<@LV2PLUGIN_URI@#ui> +@LV2PLUGIN_IF_ENABLE_UI@ a ui:@LV2_UI_TYPE@ ; +@LV2PLUGIN_IF_ENABLE_UI@ ui:binary ; +@LV2PLUGIN_IF_ENABLE_UI@ rdfs:seeAlso <@PROJECT_NAME@_ui.ttl> . diff --git a/lv2/sfizz.c b/lv2/sfizz.c index ef07343e..b68c527f 100644 --- a/lv2/sfizz.c +++ b/lv2/sfizz.c @@ -32,6 +32,9 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ +#include "sfizz_lv2.h" + +#include #include #include #include @@ -58,23 +61,11 @@ #include "atomic_compat.h" -#define SFIZZ_URI "http://sfztools.github.io/sfizz" -#define SFIZZ_PREFIX SFIZZ_URI "#" -#define SFIZZ__sfzFile SFIZZ_URI ":" "sfzfile" -#define SFIZZ__tuningfile SFIZZ_URI ":" "tuningfile" -#define SFIZZ__numVoices SFIZZ_URI ":" "numvoices" -#define SFIZZ__preloadSize SFIZZ_URI ":" "preload_size" -#define SFIZZ__oversampling SFIZZ_URI ":" "oversampling" -// These ones are just for the worker -#define SFIZZ__logStatus SFIZZ_URI ":" "log_status" -#define SFIZZ__checkModification SFIZZ_URI ":" "check_modification" - #define CHANNEL_MASK 0x0F #define MIDI_CHANNEL(byte) (byte & CHANNEL_MASK) #define MIDI_STATUS(byte) (byte & ~CHANNEL_MASK) #define PITCH_BUILD_AND_CENTER(first_byte, last_byte) (int)(((unsigned int)last_byte << 7) + (unsigned int)first_byte) - 8192 #define MAX_BLOCK_SIZE 8192 -#define MAX_PATH_SIZE 1024 #define MAX_VOICES 256 #define DEFAULT_VOICES 64 #define DEFAULT_OVERSAMPLING SFIZZ_OVERSAMPLING_X1 @@ -82,8 +73,8 @@ #define LOG_SAMPLE_COUNT 48000 #define UNUSED(x) (void)(x) -#define DEFAULT_SCALA_FILE "Resources/DefaultScale.scl" -#define DEFAULT_SFZ_FILE "Resources/DefaultInstrument.sfz" +#define DEFAULT_SCALA_FILE "Contents/Resources/DefaultScale.scl" +#define DEFAULT_SFZ_FILE "Contents/Resources/DefaultInstrument.sfz" // This assumes that the longest path is the default sfz file; if not, change it #define MAX_BUNDLE_PATH_SIZE (MAX_PATH_SIZE - sizeof(DEFAULT_SFZ_FILE)) @@ -115,6 +106,11 @@ typedef struct const float *tuning_frequency_port; const float *stretch_tuning_port; float *active_voices_port; + float *num_curves_port; + float *num_masters_port; + float *num_groups_port; + float *num_regions_port; + float *num_samples_port; // Atom forge LV2_Atom_Forge forge; ///< Forge for writing atoms in run thread @@ -187,23 +183,6 @@ typedef struct char bundle_path[MAX_BUNDLE_PATH_SIZE]; } sfizz_plugin_t; -enum -{ - SFIZZ_CONTROL = 0, - SFIZZ_NOTIFY = 1, - SFIZZ_LEFT = 2, - SFIZZ_RIGHT = 3, - SFIZZ_VOLUME = 4, - SFIZZ_POLYPHONY = 5, - SFIZZ_OVERSAMPLING = 6, - SFIZZ_PRELOAD = 7, - SFIZZ_FREEWHEELING = 8, - SFIZZ_SCALA_ROOT_KEY = 9, - SFIZZ_TUNING_FREQUENCY = 10, - SFIZZ_STRETCH_TUNING = 11, - SFIZZ_ACTIVE_VOICES = 12, -}; - enum { SFIZZ_TIMEINFO_POSITION = 1 << 0, @@ -369,6 +348,21 @@ connect_port(LV2_Handle instance, case SFIZZ_ACTIVE_VOICES: self->active_voices_port = (float *)data; break; + case SFIZZ_NUM_CURVES: + self->num_curves_port = (float *)data; + break; + case SFIZZ_NUM_MASTERS: + self->num_masters_port = (float *)data; + break; + case SFIZZ_NUM_GROUPS: + self->num_groups_port = (float *)data; + break; + case SFIZZ_NUM_REGIONS: + self->num_regions_port = (float *)data; + break; + case SFIZZ_NUM_SAMPLES: + self->num_samples_port = (float *)data; + break; default: break; } @@ -977,6 +971,11 @@ run(LV2_Handle instance, uint32_t sample_count) sfizz_lv2_check_oversampling(self); sfizz_lv2_check_num_voices(self); *(self->active_voices_port) = sfizz_get_num_active_voices(self->synth); + *(self->num_curves_port) = sfizz_get_num_curves(self->synth); + *(self->num_masters_port) = sfizz_get_num_masters(self->synth); + *(self->num_groups_port) = sfizz_get_num_groups(self->synth); + *(self->num_regions_port) = sfizz_get_num_regions(self->synth); + *(self->num_samples_port) = sfizz_get_num_preloaded_samples(self->synth); // Log the buffer usage self->sample_counter += (int)sample_count; diff --git a/lv2/sfizz.ttl.in b/lv2/sfizz.ttl.in index b0934ca6..f4090158 100644 --- a/lv2/sfizz.ttl.in +++ b/lv2/sfizz.ttl.in @@ -13,6 +13,7 @@ @prefix rdfs: . @prefix state: . @prefix time: . +@prefix ui: . @prefix units: . @prefix urid: . @prefix work: . @@ -83,6 +84,8 @@ midnam:update a lv2:Feature . opts:supportedOption param:sampleRate ; opts:supportedOption bufsize:maxBlockLength, bufsize:nominalBlockLength ; + @LV2PLUGIN_IF_ENABLE_UI@ui:ui <@LV2PLUGIN_URI@#ui> ; + patch:writable <@LV2PLUGIN_URI@:sfzfile> , <@LV2PLUGIN_URI@:tuningfile> ; @@ -310,4 +313,70 @@ midnam:update a lv2:Feature . lv2:default 0 ; lv2:minimum 0 ; lv2:maximum 256 ; + ] , [ + a lv2:OutputPort, lv2:ControlPort ; + lv2:index 12 ; + lv2:symbol "active_voices" ; + lv2:name "Active voices", + "Voix utilisées"@fr ; + pg:group <@LV2PLUGIN_URI@#status> ; + lv2:portProperty lv2:integer ; + lv2:default 0 ; + lv2:minimum 0 ; + lv2:maximum 256 ; + ] , [ + a lv2:OutputPort, lv2:ControlPort ; + lv2:index 13 ; + lv2:symbol "num_curves" ; + lv2:name "Number of curves", + "Nombre de courbes"@fr ; + pg:group <@LV2PLUGIN_URI@#status> ; + lv2:portProperty lv2:integer ; + lv2:default 0 ; + lv2:minimum 0 ; + lv2:maximum 65535 ; + ] , [ + a lv2:OutputPort, lv2:ControlPort ; + lv2:index 14 ; + lv2:symbol "num_masters" ; + lv2:name "Number of masters", + "Nombre de maîtres"@fr ; + pg:group <@LV2PLUGIN_URI@#status> ; + lv2:portProperty lv2:integer ; + lv2:default 0 ; + lv2:minimum 0 ; + lv2:maximum 65535 ; + ] , [ + a lv2:OutputPort, lv2:ControlPort ; + lv2:index 15 ; + lv2:symbol "num_groups" ; + lv2:name "Number of groups", + "Nombre de groupes"@fr ; + pg:group <@LV2PLUGIN_URI@#status> ; + lv2:portProperty lv2:integer ; + lv2:default 0 ; + lv2:minimum 0 ; + lv2:maximum 65535 ; + ] , [ + a lv2:OutputPort, lv2:ControlPort ; + lv2:index 16 ; + lv2:symbol "num_regions" ; + lv2:name "Number of regions", + "Nombre de regions"@fr ; + pg:group <@LV2PLUGIN_URI@#status> ; + lv2:portProperty lv2:integer ; + lv2:default 0 ; + lv2:minimum 0 ; + lv2:maximum 65535 ; + ] , [ + a lv2:OutputPort, lv2:ControlPort ; + lv2:index 17 ; + lv2:symbol "num_samples" ; + lv2:name "Number of samples", + "Nombre d'échantillons"@fr ; + pg:group <@LV2PLUGIN_URI@#status> ; + lv2:portProperty lv2:integer ; + lv2:default 0 ; + lv2:minimum 0 ; + lv2:maximum 65535 ; ] . diff --git a/lv2/sfizz_lv2.h b/lv2/sfizz_lv2.h new file mode 100644 index 00000000..b0eafe00 --- /dev/null +++ b/lv2/sfizz_lv2.h @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once + +#define MAX_PATH_SIZE 1024 + +#define SFIZZ_URI "http://sfztools.github.io/sfizz" +#define SFIZZ_UI_URI "http://sfztools.github.io/sfizz#ui" +#define SFIZZ_PREFIX SFIZZ_URI "#" +#define SFIZZ__sfzFile SFIZZ_URI ":" "sfzfile" +#define SFIZZ__tuningfile SFIZZ_URI ":" "tuningfile" +#define SFIZZ__numVoices SFIZZ_URI ":" "numvoices" +#define SFIZZ__preloadSize SFIZZ_URI ":" "preload_size" +#define SFIZZ__oversampling SFIZZ_URI ":" "oversampling" +// These ones are just for the worker +#define SFIZZ__logStatus SFIZZ_URI ":" "log_status" +#define SFIZZ__checkModification SFIZZ_URI ":" "check_modification" + +enum +{ + SFIZZ_CONTROL = 0, + SFIZZ_NOTIFY = 1, + SFIZZ_LEFT = 2, + SFIZZ_RIGHT = 3, + SFIZZ_VOLUME = 4, + SFIZZ_POLYPHONY = 5, + SFIZZ_OVERSAMPLING = 6, + SFIZZ_PRELOAD = 7, + SFIZZ_FREEWHEELING = 8, + SFIZZ_SCALA_ROOT_KEY = 9, + SFIZZ_TUNING_FREQUENCY = 10, + SFIZZ_STRETCH_TUNING = 11, + SFIZZ_ACTIVE_VOICES = 12, + SFIZZ_NUM_CURVES = 13, + SFIZZ_NUM_MASTERS = 14, + SFIZZ_NUM_GROUPS = 15, + SFIZZ_NUM_REGIONS = 16, + SFIZZ_NUM_SAMPLES = 17, +}; diff --git a/lv2/sfizz_ui.cpp b/lv2/sfizz_ui.cpp new file mode 100644 index 00000000..1cbb49a2 --- /dev/null +++ b/lv2/sfizz_ui.cpp @@ -0,0 +1,492 @@ +/* + SPDX-License-Identifier: ISC + + Sfizz LV2 plugin + + Copyright 2019-2020, Paul Ferrand + + This file was based on skeleton and example code from the LV2 plugin + distribution available at http://lv2plug.in/ + + The LV2 sample plugins have the following copyright and notice, which are + extended to the current work: + Copyright 2011-2016 David Robillard + Copyright 2011 Gabriel M. Beddingfield + Copyright 2011 James Morris + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + Compiling this plugin statically against libsndfile implies distributing it + under the terms of the LGPL v3 license. See the LICENSE.md file for more + information. If you did not receive a LICENSE.md file, inform the current + maintainer. + + THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +#include "sfizz_lv2.h" + +#include "editor/Editor.h" +#include "editor/EditorController.h" +#include "editor/EditIds.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "vstgui_helpers.h" + +#include "editor/utility/vstgui_before.h" +#include "vstgui/lib/cframe.h" +#include "vstgui/lib/platform/iplatformframe.h" +#include "editor/utility/vstgui_after.h" +using namespace VSTGUI; + +/// +struct FrameHolderDeleter { + void operator()(CFrame* frame) const + { + if (frame->getNbReference() != 1) + frame->forget(); + else + frame->close(); + } +}; +typedef std::unique_ptr FrameHolder; + +/// +struct sfizz_ui_t : EditorController, VSTGUIEditorInterface { + LV2UI_Write_Function write = nullptr; + LV2UI_Controller con = nullptr; + LV2_URID_Map *map = nullptr; + LV2_URID_Unmap *unmap = nullptr; + LV2UI_Resize *resize = nullptr; + LV2UI_Touch *touch = nullptr; + std::unique_ptr editor; + FrameHolder uiFrame; +#if LINUX + SharedPointer runLoop; +#endif + + /// VSTGUIEditorInterface + CFrame* getFrame() const override { return uiFrame.get(); } + + LV2_Atom_Forge atom_forge; + LV2_URID atom_event_transfer_uri; + LV2_URID atom_object_uri; + LV2_URID atom_path_uri; + LV2_URID atom_urid_uri; + LV2_URID midi_event_uri; + LV2_URID patch_get_uri; + LV2_URID patch_set_uri; + LV2_URID patch_property_uri; + LV2_URID patch_value_uri; + LV2_URID sfizz_sfz_file_uri; + LV2_URID sfizz_scala_file_uri; + +protected: + void uiSendValue(EditId id, const EditValue& v) override; + void uiBeginSend(EditId id) override; + void uiEndSend(EditId id) override; + void uiSendMIDI(const uint8_t* msg, uint32_t len) override; + +private: + void uiTouch(EditId id, bool t); +}; + +static LV2UI_Handle +instantiate(const LV2UI_Descriptor *descriptor, + const char *plugin_uri, + const char *bundle_path, + LV2UI_Write_Function write_function, + LV2UI_Controller controller, + LV2UI_Widget *widget, + const LV2_Feature * const *features) +{ + std::unique_ptr self { new sfizz_ui_t }; + + (void)descriptor; + (void)plugin_uri; + (void)bundle_path; + + self->write = write_function; + self->con = controller; + + void *parentWindowId = nullptr; + + LV2_URID_Map *map = nullptr; + LV2_URID_Unmap *unmap = nullptr; + + for (const LV2_Feature *const *f = features; *f; f++) + { + if (!strcmp((**f).URI, LV2_URID__map)) + self->map = map = (LV2_URID_Map *)(**f).data; + else if (!strcmp((**f).URI, LV2_URID__unmap)) + self->unmap = unmap = (LV2_URID_Unmap *)(**f).data; + else if (!strcmp((**f).URI, LV2_UI__resize)) + self->resize = (LV2UI_Resize *)(**f).data; + else if (!strcmp((**f).URI, LV2_UI__touch)) + self->touch = (LV2UI_Touch*)(**f).data; + else if (!strcmp((**f).URI, LV2_UI__parent)) + parentWindowId = (**f).data; + } + + // The map feature is required + if (!map || !unmap) + return nullptr; + + LV2_Atom_Forge *forge = &self->atom_forge; + lv2_atom_forge_init(forge, map); + self->atom_event_transfer_uri = map->map(map->handle, LV2_ATOM__eventTransfer); + self->atom_object_uri = map->map(map->handle, LV2_ATOM__Object); + self->atom_path_uri = map->map(map->handle, LV2_ATOM__Path); + self->atom_urid_uri = map->map(map->handle, LV2_ATOM__URID); + self->midi_event_uri = map->map(map->handle, LV2_MIDI__MidiEvent); + self->patch_get_uri = map->map(map->handle, LV2_PATCH__Get); + self->patch_set_uri = map->map(map->handle, LV2_PATCH__Set); + self->patch_property_uri = map->map(map->handle, LV2_PATCH__property); + self->patch_value_uri = map->map(map->handle, LV2_PATCH__value); + self->sfizz_sfz_file_uri = map->map(map->handle, SFIZZ__sfzFile); + self->sfizz_scala_file_uri = map->map(map->handle, SFIZZ__tuningfile); + + // set up the resource path + // * on Linux, this is determined by going 2 folders back from the SO path + // name, and appending "Contents/Resources" (not overridable) + // * on Windows, the folder is set programmatically + // * on macOS, resource files are looked up using CFBundle APIs +#if _WIN32 + IWin32PlatformFrame::setResourceBasePath((std::string(bundle_path) + "\\Contents\\Resources\\").c_str()); +#elif __APPLE__ + #pragma message("TODO: make resources work on macOS using bundles") +#endif + + // makes labels refresh correctly + CView::kDirtyCallAlwaysOnMainThread = true; + + const CRect uiBounds(0, 0, Editor::viewWidth, Editor::viewHeight); + CFrame* uiFrame = new CFrame(uiBounds, self.get()); + self->uiFrame.reset(uiFrame); + + IPlatformFrameConfig* config = nullptr; +#if LINUX + SharedPointer runLoop = new Lv2IdleRunLoop; + self->runLoop = runLoop; + VSTGUI::X11::FrameConfig x11Config; + x11Config.runLoop = runLoop; + config = &x11Config; +#endif + + if (!uiFrame->open(parentWindowId, kDefaultNative, config)) + return nullptr; + + Editor *editor = new Editor(*self); + self->editor.reset(editor); + editor->open(*uiFrame); + + *widget = reinterpret_cast(uiFrame->getPlatformFrame()->getPlatformRepresentation()); + + if (self->resize) + self->resize->ui_resize(self->resize->handle, Editor::viewWidth, Editor::viewHeight); + + // send a request to receive all parameters + uint8_t buffer[256]; + lv2_atom_forge_set_buffer(forge, buffer, sizeof(buffer)); + LV2_Atom_Forge_Frame frame; + LV2_Atom *msg = (LV2_Atom *)lv2_atom_forge_object(forge, &frame, 0, self->patch_get_uri); + lv2_atom_forge_pop(forge, &frame); + write_function(controller, 0, lv2_atom_total_size(msg), self->atom_event_transfer_uri, msg); + + return self.release(); +} + +static void +cleanup(LV2UI_Handle ui) +{ + sfizz_ui_t *self = (sfizz_ui_t *)ui; + delete self; +} + +static void +port_event(LV2UI_Handle ui, + uint32_t port_index, + uint32_t buffer_size, + uint32_t format, + const void *buffer) +{ + sfizz_ui_t *self = (sfizz_ui_t *)ui; + + if (format == 0) { + const float v = *reinterpret_cast(buffer); + + switch (port_index) { + case SFIZZ_VOLUME: + self->uiReceiveValue(EditId::Volume, v); + break; + case SFIZZ_POLYPHONY: + self->uiReceiveValue(EditId::Polyphony, v); + break; + case SFIZZ_OVERSAMPLING: + self->uiReceiveValue(EditId::Oversampling, v); + break; + case SFIZZ_PRELOAD: + self->uiReceiveValue(EditId::PreloadSize, v); + break; + case SFIZZ_SCALA_ROOT_KEY: + self->uiReceiveValue(EditId::ScalaRootKey, v); + break; + case SFIZZ_TUNING_FREQUENCY: + self->uiReceiveValue(EditId::TuningFrequency, v); + break; + case SFIZZ_STRETCH_TUNING: + self->uiReceiveValue(EditId::StretchTuning, v); + break; + case SFIZZ_ACTIVE_VOICES: + self->uiReceiveValue(EditId::UINumActiveVoices, v); + break; + case SFIZZ_NUM_CURVES: + self->uiReceiveValue(EditId::UINumCurves, v); + break; + case SFIZZ_NUM_MASTERS: + self->uiReceiveValue(EditId::UINumMasters, v); + break; + case SFIZZ_NUM_GROUPS: + self->uiReceiveValue(EditId::UINumGroups, v); + break; + case SFIZZ_NUM_REGIONS: + self->uiReceiveValue(EditId::UINumRegions, v); + break; + case SFIZZ_NUM_SAMPLES: + self->uiReceiveValue(EditId::UINumPreloadedSamples, v); + break; + } + } + else if (format == self->atom_event_transfer_uri) { + auto *atom = reinterpret_cast(buffer); + + if (atom->type == self->atom_object_uri) { + const LV2_Atom *prop = nullptr; + const LV2_Atom *value = nullptr; + + lv2_atom_object_get( + reinterpret_cast(atom), + self->patch_property_uri, &prop, self->patch_value_uri, &value, 0); + + if (prop && value && prop->type == self->atom_urid_uri) { + const LV2_URID prop_uri = reinterpret_cast(prop)->body; + auto *value_body = reinterpret_cast(LV2_ATOM_BODY_CONST(value)); + + if (prop_uri == self->sfizz_sfz_file_uri && value->type == self->atom_path_uri) { + std::string path(value_body, strnlen(value_body, value->size)); + self->uiReceiveValue(EditId::SfzFile, path); + } + else if (prop_uri == self->sfizz_scala_file_uri && value->type == self->atom_path_uri) { + std::string path(value_body, strnlen(value_body, value->size)); + self->uiReceiveValue(EditId::ScalaFile, path); + } + } + } + } + + (void)buffer_size; +} + +static int +idle(LV2UI_Handle ui) +{ + sfizz_ui_t *self = (sfizz_ui_t *)ui; + +#if LINUX + self->runLoop->execIdle(); +#endif + + return 0; +} + +static const LV2UI_Idle_Interface idle_interface = { + &idle, +}; + +static int +show(LV2UI_Handle ui) +{ + sfizz_ui_t *self = (sfizz_ui_t *)ui; + self->uiFrame->setVisible(true); + return 0; +} + +static int +hide(LV2UI_Handle ui) +{ + sfizz_ui_t *self = (sfizz_ui_t *)ui; + self->uiFrame->setVisible(false); + return 0; +} + +static const LV2UI_Show_Interface show_interface = { + &show, + &hide, +}; + +const void * +extension_data(const char *uri) +{ + if (!strcmp(uri, LV2_UI__idleInterface)) + return &idle_interface; + + if (!strcmp(uri, LV2_UI__showInterface)) + return &show_interface; + + return nullptr; +} + +static const LV2UI_Descriptor descriptor = { + SFIZZ_UI_URI, + instantiate, + cleanup, + port_event, + extension_data, +}; + +LV2_SYMBOL_EXPORT +const LV2UI_Descriptor * +lv2ui_descriptor(uint32_t index) +{ +#if LINUX + VSTGUI::initializeSoHandle(); +#endif + + switch (index) + { + case 0: + return &descriptor; + default: + return nullptr; + } +} + +/// +void sfizz_ui_t::uiSendValue(EditId id, const EditValue& v) +{ + auto sendFloat = [this](int port, float value) { + write(con, port, sizeof(float), 0, &value); + }; + + auto sendPath = [this](LV2_URID property, const std::string& value) { + LV2_Atom_Forge *forge = &atom_forge; + LV2_Atom_Forge_Frame frame; + alignas(LV2_Atom) uint8_t buffer[MAX_PATH_SIZE + 512]; + auto *atom = reinterpret_cast(buffer); + lv2_atom_forge_set_buffer(forge, (uint8_t *)&buffer, sizeof(buffer)); + if (lv2_atom_forge_object(forge, &frame, 0, patch_set_uri) && + lv2_atom_forge_key(forge, patch_property_uri) && + lv2_atom_forge_urid(forge, property) && + lv2_atom_forge_key(forge, patch_value_uri) && + lv2_atom_forge_path(forge, value.data(), value.size())) + { + lv2_atom_forge_pop(forge, &frame); + write(con, SFIZZ_CONTROL, lv2_atom_total_size(atom), atom_event_transfer_uri, atom); + } + }; + + switch (id) { + case EditId::Volume: + sendFloat(SFIZZ_VOLUME, absl::get(v)); + break; + case EditId::Polyphony: + sendFloat(SFIZZ_POLYPHONY, absl::get(v)); + break; + case EditId::Oversampling: + sendFloat(SFIZZ_OVERSAMPLING, absl::get(v)); + break; + case EditId::PreloadSize: + sendFloat(SFIZZ_PRELOAD, absl::get(v)); + break; + case EditId::ScalaRootKey: + sendFloat(SFIZZ_SCALA_ROOT_KEY, absl::get(v)); + break; + case EditId::TuningFrequency: + sendFloat(SFIZZ_TUNING_FREQUENCY, absl::get(v)); + break; + case EditId::StretchTuning: + sendFloat(SFIZZ_STRETCH_TUNING, absl::get(v)); + break; + case EditId::SfzFile: + sendPath(sfizz_sfz_file_uri, absl::get(v)); + break; + case EditId::ScalaFile: + sendPath(sfizz_scala_file_uri, absl::get(v)); + break; + default: + break; + } +} + +void sfizz_ui_t::uiBeginSend(EditId id) +{ + uiTouch(id, true); +} + +void sfizz_ui_t::uiEndSend(EditId id) +{ + uiTouch(id, false); +} + +void sfizz_ui_t::uiTouch(EditId id, bool t) +{ + if (!touch) + return; + + switch (id) { + case EditId::Volume: + touch->touch(touch->handle, SFIZZ_VOLUME, t); + break; + case EditId::Polyphony: + touch->touch(touch->handle, SFIZZ_POLYPHONY, t); + break; + case EditId::Oversampling: + touch->touch(touch->handle, SFIZZ_OVERSAMPLING, t); + break; + case EditId::PreloadSize: + touch->touch(touch->handle, SFIZZ_PRELOAD, t); + break; + case EditId::ScalaRootKey: + touch->touch(touch->handle, SFIZZ_SCALA_ROOT_KEY, t); + break; + case EditId::TuningFrequency: + touch->touch(touch->handle, SFIZZ_TUNING_FREQUENCY, t); + break; + case EditId::StretchTuning: + touch->touch(touch->handle, SFIZZ_STRETCH_TUNING, t); + break; + default: + break; + } +} + +void sfizz_ui_t::uiSendMIDI(const uint8_t* msg, uint32_t len) +{ + LV2_Atom_Forge *forge = &atom_forge; + alignas(LV2_Atom) uint8_t buffer[512]; + auto *atom = reinterpret_cast(buffer); + lv2_atom_forge_set_buffer(forge, (uint8_t *)&buffer, sizeof(buffer)); + if (lv2_atom_forge_atom(forge, len, midi_event_uri) && + lv2_atom_forge_write(forge, msg, len)) + { + write(con, SFIZZ_CONTROL, lv2_atom_total_size(atom), atom_event_transfer_uri, atom); + } +} diff --git a/lv2/sfizz_ui.ttl.in b/lv2/sfizz_ui.ttl.in new file mode 100644 index 00000000..b5fb3470 --- /dev/null +++ b/lv2/sfizz_ui.ttl.in @@ -0,0 +1,14 @@ +@prefix lv2: . +@prefix ui: . +@prefix urid: . + +<@LV2PLUGIN_URI@#ui> + lv2:extensionData ui:idleInterface ; + lv2:extensionData ui:showInterface ; + lv2:requiredFeature ui:idleInterface ; + lv2:optionalFeature ui:noUserResize ; + lv2:optionalFeature ui:resize ; + lv2:optionalFeature ui:parent ; + lv2:optionalFeature ui:touch ; + lv2:requiredFeature urid:map ; + lv2:requiredFeature urid:unmap . diff --git a/lv2/vstgui_helpers.cpp b/lv2/vstgui_helpers.cpp new file mode 100644 index 00000000..4e8969f6 --- /dev/null +++ b/lv2/vstgui_helpers.cpp @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +// Note(jpc) same code as used in Surge LV2, I am the original author + +#include "vstgui_helpers.h" +#include +#include +#include +#include +#if LINUX +#include +#include +#endif + +#if LINUX +void Lv2IdleRunLoop::execIdle() +{ + std::chrono::steady_clock::time_point tick = std::chrono::steady_clock::now(); + + for (Event& ev : _events) + { + if (!ev.alive) + continue; + +// TODO LV2: fix me, XCB descriptor polling not working at this point +#if 0 + pollfd pfd = {}; + pfd.fd = ev.fd; + pfd.events = POLLIN|POLLERR|POLLHUP; + if (poll(&pfd, 1, 0) > 0) +#endif + { + ev.handler->onEvent(); + } + } + + for (Timer& tm : _timers) + { + if (!tm.alive) + continue; + + if (tm.lastTickValid) + { + std::chrono::steady_clock::duration duration = tick - tm.lastTick; + tm.counter += std::chrono::duration_cast(duration); + if (tm.counter >= tm.interval) + { + tm.handler->onTimer(); + tm.counter = std::min(tm.counter - tm.interval, tm.interval); + } + } + tm.lastTick = tick; + tm.lastTickValid = true; + } + + garbageCollectDeadHandlers(_events); + garbageCollectDeadHandlers(_timers); +} + +bool Lv2IdleRunLoop::registerEventHandler(int fd, VSTGUI::X11::IEventHandler* handler) +{ + // fprintf(stderr, "registerEventHandler %d %p\n", fd, handler); + + Event ev; + ev.fd = fd; + ev.handler = handler; + ev.alive = true; + _events.push_back(ev); + + return true; +} + +bool Lv2IdleRunLoop::unregisterEventHandler(VSTGUI::X11::IEventHandler* handler) +{ + // fprintf(stderr, "unregisterEventHandler %p\n", handler); + + auto it = std::find_if(_events.begin(), _events.end(), [handler](const Event& ev) -> bool { + return ev.handler == handler && ev.alive; + }); + + if (it != _events.end()) + it->alive = false; + + return true; +} + +bool Lv2IdleRunLoop::registerTimer(uint64_t interval, VSTGUI::X11::ITimerHandler* handler) +{ + // fprintf(stderr, "registerTimer %lu %p\n", interval, handler); + + Timer tm; + tm.interval = std::chrono::milliseconds(interval); + tm.counter = std::chrono::microseconds(0); + tm.lastTickValid = false; + tm.handler = handler; + tm.alive = true; + _timers.push_back(tm); + + return true; +} + +bool Lv2IdleRunLoop::unregisterTimer(VSTGUI::X11::ITimerHandler* handler) +{ + // fprintf(stderr, "unregisterTimer %p\n", handler); + + auto it = std::find_if(_timers.begin(), _timers.end(), [handler](const Timer& tm) -> bool { + return tm.handler == handler && tm.alive; + }); + + if (it != _timers.end()) + it->alive = false; + + return true; +} + +template void Lv2IdleRunLoop::garbageCollectDeadHandlers(std::list& handlers) +{ + auto pos = handlers.begin(); + auto end = handlers.end(); + + while (pos != end) + { + auto curPos = pos++; + if (!curPos->alive) + handlers.erase(curPos); + } +} +#endif + +/// +#if LINUX +namespace VSTGUI +{ +void* soHandle = nullptr; + +static volatile bool soHandleInitialized = false; +static std::mutex soHandleMutex; + +struct Dl_handle_deleter +{ + void operator()(void* x) const noexcept + { + dlclose(x); + } +}; +static std::unique_ptr soHandlePointer; +} // namespace VSTGUI + +void VSTGUI::initializeSoHandle() +{ + if (VSTGUI::soHandleInitialized) + return; + + std::lock_guard lock(VSTGUI::soHandleMutex); + if (VSTGUI::soHandleInitialized) + return; + + Dl_info info; + if (dladdr((void*)&lv2ui_descriptor, &info)) + { + VSTGUI::soHandle = dlopen(info.dli_fname, RTLD_LAZY); + VSTGUI::soHandlePointer.reset(VSTGUI::soHandle); + } + VSTGUI::soHandleInitialized = true; +} +#endif diff --git a/lv2/vstgui_helpers.h b/lv2/vstgui_helpers.h new file mode 100644 index 00000000..bb48936e --- /dev/null +++ b/lv2/vstgui_helpers.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +// Note(jpc) same code as used in Surge LV2, I am the original author + +#pragma once +#include +#include + +#include "editor/utility/vstgui_before.h" +#include "vstgui/lib/vstguibase.h" +#if LINUX +#include "vstgui/lib/platform/platform_x11.h" +#include "vstgui/lib/platform/linux/x11platform.h" +#endif +#include "editor/utility/vstgui_after.h" + +#if LINUX +class Lv2IdleRunLoop : public VSTGUI::X11::IRunLoop +{ +public: + void execIdle(); + + bool registerEventHandler(int fd, VSTGUI::X11::IEventHandler* handler) override; + bool unregisterEventHandler(VSTGUI::X11::IEventHandler* handler) override; + bool registerTimer(uint64_t interval, VSTGUI::X11::ITimerHandler* handler) override; + bool unregisterTimer(VSTGUI::X11::ITimerHandler* handler) override; + + void forget() override + {} + void remember() override + {} + +private: + struct Event + { + int fd; + VSTGUI::X11::IEventHandler* handler; + bool alive; + }; + struct Timer + { + std::chrono::microseconds interval; + std::chrono::microseconds counter; + bool lastTickValid; + std::chrono::steady_clock::time_point lastTick; + VSTGUI::X11::ITimerHandler* handler; + bool alive; + }; + +private: + template static void garbageCollectDeadHandlers(std::list& handlers); + +private: + std::list _events; + std::list _timers; +}; +#endif + +#if LINUX +namespace VSTGUI +{ +void initializeSoHandle(); +} +#endif diff --git a/vst/CMakeLists.txt b/vst/CMakeLists.txt index 5d0301e6..f96abd80 100644 --- a/vst/CMakeLists.txt +++ b/vst/CMakeLists.txt @@ -27,9 +27,6 @@ set(VSTPLUGIN_HEADERS SfizzVstState.h X11RunLoop.h) -set(VSTPLUGIN_RESOURCES - logo.png) - add_library(${VSTPLUGIN_PRJ_NAME} MODULE ${VSTPLUGIN_HEADERS} ${VSTPLUGIN_SOURCES}) @@ -71,8 +68,8 @@ endif() # Create the bundle (see "VST 3 Locations / Format") execute_process ( COMMAND "${CMAKE_COMMAND}" -E make_directory "${PROJECT_BINARY_DIR}/${VSTPLUGIN_BUNDLE_NAME}/Contents/Resources") -foreach(res ${VSTPLUGIN_RESOURCES}) - file (COPY "${CMAKE_CURRENT_SOURCE_DIR}/resources/${res}" +foreach(res ${EDITOR_RESOURCES}) + file (COPY "${CMAKE_CURRENT_SOURCE_DIR}/../editor/resources/${res}" DESTINATION "${PROJECT_BINARY_DIR}/${VSTPLUGIN_BUNDLE_NAME}/Contents/Resources") endforeach() if(WIN32)