From b9fe5853538a2381e688d3e605cccf76be6a238c Mon Sep 17 00:00:00 2001 From: falkTX Date: Fri, 29 Sep 2023 11:10:50 +0200 Subject: [PATCH 1/8] Implement plugin hosting via carla utils library Signed-off-by: falkTX --- build-aux/com.obsproject.Studio.json | 9 +- build-aux/modules/90-carla.json | 24 + cmake/Modules/FindCarlaUtils.cmake | 106 ++ cmake/finders/FindCarlaUtils.cmake | 157 +++ plugins/CMakeLists.txt | 2 + plugins/carla/CMakeLists.txt | 92 ++ plugins/carla/carla-bridge-wrapper.cpp | 577 ++++++++ plugins/carla/carla-bridge.cpp | 1404 +++++++++++++++++++ plugins/carla/carla-bridge.hpp | 204 +++ plugins/carla/carla-patchbay-wrapper.c | 517 +++++++ plugins/carla/carla-wrapper.h | 73 + plugins/carla/carla.c | 530 +++++++ plugins/carla/cmake/macos/Info.plist.in | 28 + plugins/carla/common.c | 150 ++ plugins/carla/common.h | 47 + plugins/carla/pluginlistdialog.cpp | 1667 +++++++++++++++++++++++ plugins/carla/pluginlistdialog.hpp | 91 ++ plugins/carla/pluginlistdialog.ui | 765 +++++++++++ plugins/carla/pluginrefreshdialog.hpp | 77 ++ plugins/carla/pluginrefreshdialog.ui | 183 +++ plugins/carla/qtutils.cpp | 158 +++ plugins/carla/qtutils.h | 135 ++ 22 files changed, 6994 insertions(+), 2 deletions(-) create mode 100644 build-aux/modules/90-carla.json create mode 100644 cmake/Modules/FindCarlaUtils.cmake create mode 100644 cmake/finders/FindCarlaUtils.cmake create mode 100644 plugins/carla/CMakeLists.txt create mode 100644 plugins/carla/carla-bridge-wrapper.cpp create mode 100644 plugins/carla/carla-bridge.cpp create mode 100644 plugins/carla/carla-bridge.hpp create mode 100644 plugins/carla/carla-patchbay-wrapper.c create mode 100644 plugins/carla/carla-wrapper.h create mode 100644 plugins/carla/carla.c create mode 100644 plugins/carla/cmake/macos/Info.plist.in create mode 100644 plugins/carla/common.c create mode 100644 plugins/carla/common.h create mode 100644 plugins/carla/pluginlistdialog.cpp create mode 100644 plugins/carla/pluginlistdialog.hpp create mode 100644 plugins/carla/pluginlistdialog.ui create mode 100644 plugins/carla/pluginrefreshdialog.hpp create mode 100644 plugins/carla/pluginrefreshdialog.ui create mode 100644 plugins/carla/qtutils.cpp create mode 100644 plugins/carla/qtutils.h diff --git a/build-aux/com.obsproject.Studio.json b/build-aux/com.obsproject.Studio.json index 00b1a8b344aca9..8145f8beeac10c 100644 --- a/build-aux/com.obsproject.Studio.json +++ b/build-aux/com.obsproject.Studio.json @@ -19,7 +19,11 @@ "--talk-name=org.a11y.Bus", "--own-name=org.kde.StatusNotifierItem-2-2", "--system-talk-name=org.freedesktop.Avahi", - "--env=VST_PATH=/app/extensions/Plugins/vst" + "--env=CLAP_PATH=/app/extensions/Plugins/clap", + "--env=LADSPA_PATH=/app/extensions/Plugins/ladspa", + "--env=LV2_PATH=/app/extensions/Plugins/lv2:/app/lib/lv2", + "--env=VST_PATH=/app/extensions/Plugins/vst", + "--env=VST3_PATH=/app/extensions/Plugins/vst3" ], "add-extensions": { "com.obsproject.Studio.Plugin": { @@ -34,7 +38,7 @@ "directory": "extensions/Plugins", "version": "22.08", "add-ld-path": "lib", - "merge-dirs": "vst", + "merge-dirs": "clap;ladspa;lv2;vst;vst3", "subdirectories": true, "no-autodownload": true } @@ -67,6 +71,7 @@ "modules/50-swig.json", "modules/50-v4l-utils.json", "modules/90-asio.json", + "modules/90-carla.json", "modules/90-nlohmann-json.json", "modules/90-websocketpp.json", "modules/99-cef.json", diff --git a/build-aux/modules/90-carla.json b/build-aux/modules/90-carla.json new file mode 100644 index 00000000000000..33af40260dbc60 --- /dev/null +++ b/build-aux/modules/90-carla.json @@ -0,0 +1,24 @@ +{ + "name": "carla", + "buildsystem": "cmake-ninja", + "builddir": true, + "subdir": "cmake", + "config-opts": [ + "-DCMAKE_BUILD_TYPE=Release", + "-DCARLA_USE_JACK:BOOL=OFF", + "-DCARLA_USE_OSC:BOOL=OFF" + ], + "cleanup": [ + "/include", + "/lib/carla/libcarla_native-plugin.so", + "/lib/carla/libcarla_standalone2.so", + "/lib/pkgconfig" + ], + "sources": [ + { + "type": "git", + "url": "https://github.com/falkTX/Carla.git", + "commit": "070d734cbdc0c64dfea245470f313c81f5d1ca26" + } + ] +} diff --git a/cmake/Modules/FindCarlaUtils.cmake b/cmake/Modules/FindCarlaUtils.cmake new file mode 100644 index 00000000000000..89cc567e9186c5 --- /dev/null +++ b/cmake/Modules/FindCarlaUtils.cmake @@ -0,0 +1,106 @@ +# Once done these will be defined: +# +# CarlaUtils_FOUND CarlaUtils_INCLUDE_DIRS CarlaUtils_LIBRARIES + +find_package(PkgConfig QUIET) +if(PKG_CONFIG_FOUND) + pkg_check_modules(PC_CarlaUtils QUIET carla-utils) +endif() + +find_path( + CarlaUtils_INCLUDE_DIR + NAMES utils/CarlaBridgeUtils.hpp + HINTS ${PC_CarlaUtils_INCLUDE_DIRS} + PATHS /usr/include /usr/local/include /app/include + PATH_SUFFIXES carla) + +find_library( + CarlaUtils_LIBRARY + NAMES carla_utils libcarla_utils + HINTS ${PC_CarlaUtils_LIBRARY_DIRS} + PATHS /usr/lib /usr/local/lib /app/lib + PATH_SUFFIXES carla) + +find_program( + CarlaUtils_BRIDGE_LV2_GTK2 + NAMES carla-bridge-lv2-gtk2 + HINTS ${PC_CarlaUtils_LIBRARY_DIRS} + PATHS /usr/lib /usr/local/lib /app/lib + PATH_SUFFIXES carla) + +find_program( + CarlaUtils_BRIDGE_LV2_GTK3 + NAMES carla-bridge-lv2-gtk3 + HINTS ${PC_CarlaUtils_LIBRARY_DIRS} + PATHS /usr/lib /usr/local/lib /app/lib + PATH_SUFFIXES carla) + +find_program( + CarlaUtils_BRIDGE_NATIVE + NAMES carla-bridge-native + HINTS ${PC_CarlaUtils_LIBRARY_DIRS} + PATHS /usr/lib /usr/local/lib /app/lib + PATH_SUFFIXES carla) + +find_program( + CarlaUtils_DISCOVERY_NATIVE + NAMES carla-discovery-native + HINTS ${PC_CarlaUtils_LIBRARY_DIRS} + PATHS /usr/lib /usr/local/lib /app/lib + PATH_SUFFIXES carla) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args( + CarlaUtils + FOUND_VAR CarlaUtils_FOUND + REQUIRED_VARS CarlaUtils_INCLUDE_DIR CarlaUtils_LIBRARY CarlaUtils_BRIDGE_NATIVE CarlaUtils_DISCOVERY_NATIVE) +mark_as_advanced(CarlaUtils_INCLUDE_DIR CarlaUtils_LIBRARY CarlaUtils_BRIDGE_NATIVE CarlaUtils_DISCOVERY_NATIVE) + +if(CarlaUtils_FOUND) + set(CarlaUtils_INCLUDE_DIRS ${CarlaUtils_INCLUDE_DIR} ${CarlaUtils_INCLUDE_DIR}/includes + ${CarlaUtils_INCLUDE_DIR}/utils) + set(CarlaUtils_LIBRARIES ${CarlaUtils_LIBRARY}) + + if(NOT TARGET carla::utils) + if(IS_ABSOLUTE "${CarlaUtils_LIBRARIES}") + add_library(carla::utils UNKNOWN IMPORTED GLOBAL) + set_target_properties(carla::utils PROPERTIES IMPORTED_LOCATION "${CarlaUtils_LIBRARIES}") + else() + add_library(carla::utils INTERFACE IMPORTED GLOBAL) + set_target_properties(carla::utils PROPERTIES IMPORTED_LIBNAME "${CarlaUtils_LIBRARIES}") + endif() + + if(PC_CarlaUtils_FOUND) + message("DEBUG: using carla-utils pkg-config | ${PC_CarlaUtils_FOUND} | ${PC_CarlaUtils_LDFLAGS}") + set_target_properties(carla::utils PROPERTIES INTERFACE_LINK_OPTIONS ${PC_CarlaUtils_LDFLAGS}) + else() + message("DEBUG: NOT using carla-utils pkg-config | ${PC_CarlaUtils_FOUND} | ${PC_CarlaUtils_LDFLAGS}") + endif() + + set_target_properties(carla::utils PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${CarlaUtils_INCLUDE_DIRS}") + endif() + + if(NOT TARGET carla::bridge-lv2-gtk2) + add_executable(carla::bridge-lv2-gtk2 IMPORTED GLOBAL) + set_target_properties(carla::bridge-lv2-gtk2 PROPERTIES IMPORTED_LOCATION "${CarlaUtils_BRIDGE_LV2_GTK2}") + add_dependencies(carla::utils carla::bridge-lv2-gtk2) + endif() + + if(NOT TARGET carla::bridge-lv2-gtk3) + add_executable(carla::bridge-lv2-gtk3 IMPORTED GLOBAL) + set_target_properties(carla::bridge-lv2-gtk3 PROPERTIES IMPORTED_LOCATION "${CarlaUtils_BRIDGE_LV2_GTK3}") + add_dependencies(carla::utils carla::bridge-lv2-gtk3) + endif() + + if(NOT TARGET carla::bridge-native) + add_executable(carla::bridge-native IMPORTED GLOBAL) + set_target_properties(carla::bridge-native PROPERTIES IMPORTED_LOCATION "${CarlaUtils_BRIDGE_NATIVE}") + add_dependencies(carla::utils carla::bridge-native) + endif() + + if(NOT TARGET carla::discovery-native) + add_executable(carla::discovery-native IMPORTED GLOBAL) + set_target_properties(carla::discovery-native PROPERTIES IMPORTED_LOCATION "${CarlaUtils_DISCOVERY_NATIVE}") + add_dependencies(carla::utils carla::discovery-native) + endif() +endif() diff --git a/cmake/finders/FindCarlaUtils.cmake b/cmake/finders/FindCarlaUtils.cmake new file mode 100644 index 00000000000000..566aca066adba5 --- /dev/null +++ b/cmake/finders/FindCarlaUtils.cmake @@ -0,0 +1,157 @@ +#[=======================================================================[.rst +FindCarlaUtils +-------------- + +FindModule for carla-utils and associated libraries + +Result Variables +^^^^^^^^^^^^^^^^ + +This module sets the following variables: + +``CarlaUtils_FOUND`` + True, if all required components and the core library were found. + +Cache variables +^^^^^^^^^^^^^^^ + +The following cache variables may also be set: + +``CarlaUtils_LIBRARIES`` + Path to the library component of carla-utils +``CarlaUtils_INCLUDE_DIRS`` + Directories used by carla-utils. + +#]=======================================================================] + +include(FindPackageHandleStandardArgs) + +find_package(PkgConfig QUIET) +if(PKG_CONFIG_FOUND) + pkg_check_modules(PC_CarlaUtils QUIET carla-utils) +endif() + +if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin" AND NOT PC_CarlaUtils_FOUND) + message("DEBUG: using carla-utils macos framework | ${PC_CarlaUtils_FOUND} | ${PC_CarlaUtils_LDFLAGS}") + set(CarlaUtils_USE_MACOS_FRAMEWORK TRUE) +else() + message("DEBUG: NOT using carla-utils macos framework | ${PC_CarlaUtils_FOUND} | ${PC_CarlaUtils_LDFLAGS}") + # set(CarlaUtils_USE_MACOS_FRAMEWORK FALSE) +endif() + +find_library( + CarlaUtils_LIBRARY + NAMES carla-utils carla_utils libcarla_utils + HINTS ${PC_CarlaUtils_LIBRARY_DIRS} + PATHS /usr/lib /usr/local/lib /app/lib + PATH_SUFFIXES carla) + +find_path( + CarlaUtils_INCLUDE_DIR + NAMES utils/CarlaBridgeUtils.hpp + HINTS ${PC_CarlaUtils_INCLUDE_DIRS} ${CarlaUtils_LIBRARY} + PATHS /usr/include /usr/local/include /app/include + PATH_SUFFIXES carla Headers) + +find_program( + CarlaUtils_BRIDGE_LV2_GTK2 + NAMES carla-bridge-lv2-gtk2 + HINTS ${PC_CarlaUtils_LIBRARY_DIRS} ${CarlaUtils_LIBRARY} + PATHS /usr/lib /usr/local/lib /app/lib + PATH_SUFFIXES carla) + +find_program( + CarlaUtils_BRIDGE_LV2_GTK3 + NAMES carla-bridge-lv2-gtk3 + HINTS ${PC_CarlaUtils_LIBRARY_DIRS} ${CarlaUtils_LIBRARY} + PATHS /usr/lib /usr/local/lib /app/lib + PATH_SUFFIXES carla) + +find_program( + CarlaUtils_BRIDGE_NATIVE + NAMES carla-bridge-native + HINTS ${PC_CarlaUtils_LIBRARY_DIRS} ${CarlaUtils_LIBRARY} + PATHS /usr/lib /usr/local/lib /app/lib + PATH_SUFFIXES carla) + +find_program( + CarlaUtils_DISCOVERY_NATIVE + NAMES carla-discovery-native + HINTS ${PC_CarlaUtils_LIBRARY_DIRS} ${CarlaUtils_LIBRARY} + PATHS /usr/lib /usr/local/lib /app/lib + PATH_SUFFIXES carla) + +if(CMAKE_HOST_SYSTEM_NAME MATCHES "Darwin|Windows") + set(CarlaUtils_ERROR_REASON "Ensure that obs-deps is provided as part of CMAKE_PREFIX_PATH.") +elseif(CMAKE_HOST_SYSTEM_NAME MATCHES "Linux|FreeBSD") + set(CarlaUtils_ERROR_REASON "Ensure that carla is installed on the system.") +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args( + CarlaUtils + FOUND_VAR CarlaUtils_FOUND + REQUIRED_VARS CarlaUtils_LIBRARY CarlaUtils_INCLUDE_DIR CarlaUtils_BRIDGE_NATIVE CarlaUtils_DISCOVERY_NATIVE + REASON_FAILURE_MESSAGE "${CarlaUtils_ERROR_REASON}") +mark_as_advanced(CarlaUtils_LIBRARY CarlaUtils_INCLUDE_DIR CarlaUtils_BRIDGE_NATIVE CarlaUtils_DISCOVERY_NATIVE) +unset(CarlaUtils_ERROR_REASON) + +if(CarlaUtils_FOUND) + set(CarlaUtils_INCLUDE_DIRS ${CarlaUtils_INCLUDE_DIR} ${CarlaUtils_INCLUDE_DIR}/includes + ${CarlaUtils_INCLUDE_DIR}/utils) + set(CarlaUtils_LIBRARIES ${CarlaUtils_LIBRARY}) + + if(NOT TARGET carla::utils) + if(${CarlaUtils_USE_MACOS_FRAMEWORK}) + add_library(carla::utils INTERFACE IMPORTED GLOBAL) + set_target_properties(carla::utils PROPERTIES IMPORTED_LOCATION "${CarlaUtils_LIBRARIES}") + set_target_properties(carla::utils PROPERTIES INTERFACE_LINK_LIBRARIES $) + elseif(IS_ABSOLUTE "${CarlaUtils_LIBRARIES}") + add_library(carla::utils UNKNOWN IMPORTED GLOBAL) + set_target_properties(carla::utils PROPERTIES IMPORTED_LOCATION "${CarlaUtils_LIBRARIES}") + else() + add_library(carla::utils INTERFACE IMPORTED GLOBAL) + set_target_properties(carla::utils PROPERTIES IMPORTED_LIBNAME "${CarlaUtils_LIBRARIES}") + endif() + + if(PC_CarlaUtils_FOUND) + set_target_properties(carla::utils PROPERTIES INTERFACE_LINK_OPTIONS ${PC_CarlaUtils_LDFLAGS}) + # elseif(${CarlaUtils_USE_MACOS_FRAMEWORK}) + # set_target_properties(carla::utils PROPERTIES INTERFACE_LINK_LIBRARIES $) + endif() + + set_target_properties(carla::utils PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${CarlaUtils_INCLUDE_DIRS}") + endif() + + if(NOT TARGET carla::bridge-lv2-gtk2) + add_executable(carla::bridge-lv2-gtk2 IMPORTED GLOBAL) + set_target_properties(carla::bridge-lv2-gtk2 PROPERTIES IMPORTED_LOCATION "${CarlaUtils_BRIDGE_LV2_GTK2}") + add_dependencies(carla::utils carla::bridge-lv2-gtk2) + endif() + + if(NOT TARGET carla::bridge-lv2-gtk3) + add_executable(carla::bridge-lv2-gtk3 IMPORTED GLOBAL) + set_target_properties(carla::bridge-lv2-gtk3 PROPERTIES IMPORTED_LOCATION "${CarlaUtils_BRIDGE_LV2_GTK3}") + add_dependencies(carla::utils carla::bridge-lv2-gtk3) + endif() + + if(NOT TARGET carla::bridge-native) + add_executable(carla::bridge-native IMPORTED GLOBAL) + set_target_properties(carla::bridge-native PROPERTIES IMPORTED_LOCATION "${CarlaUtils_BRIDGE_NATIVE}") + add_dependencies(carla::utils carla::bridge-native) + endif() + + if(NOT TARGET carla::discovery-native) + add_executable(carla::discovery-native IMPORTED GLOBAL) + set_target_properties(carla::discovery-native PROPERTIES IMPORTED_LOCATION "${CarlaUtils_DISCOVERY_NATIVE}") + add_dependencies(carla::utils carla::discovery-native) + endif() +endif() + +unset(CarlaUtils_USE_MACOS_FRAMEWORK) + +include(FeatureSummary) +set_package_properties( + CarlaUtils PROPERTIES + URL "https://kx.studio/Applications:Carla" + DESCRIPTION "Carla Plugin Host") diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 90bea35be27345..100dc0937586e8 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -35,6 +35,7 @@ if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0) # Add plugins in alphabetical order to retain order in IDE projects add_subdirectory(aja) + add_subdirectory(carla) if(OS_WINDOWS OR OS_MACOS) add_subdirectory(coreaudio-encoder) endif() @@ -185,3 +186,4 @@ add_subdirectory(rtmp-services) add_subdirectory(text-freetype2) add_subdirectory(aja) add_subdirectory(obs-webrtc) +add_subdirectory(carla) diff --git a/plugins/carla/CMakeLists.txt b/plugins/carla/CMakeLists.txt new file mode 100644 index 00000000000000..f07c3bc41efc40 --- /dev/null +++ b/plugins/carla/CMakeLists.txt @@ -0,0 +1,92 @@ +cmake_minimum_required(VERSION 3.16...3.25) + +option(ENABLE_CARLA "Enable building OBS with carla plugin host" ON) + +if(NOT ENABLE_CARLA) + if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0) + target_disable(carla) + else() + message(STATUS "OBS: DISABLED carla") + endif() + return() +endif() + +# Find carla utils +find_package(CarlaUtils) +if(NOT CarlaUtils_FOUND) + if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0) + target_disable(carla) + else() + message(STATUS "OBS: DISABLED carla (carla-utils library not found)") + endif() + return() +endif() + +# Find Qt +find_qt(COMPONENTS Core Widgets) + +# Setup carla-bridge target +add_library(carla-bridge MODULE) +add_library(OBS::carla-bridge ALIAS carla-bridge) + +if(OS_MACOS) + # needed for using Qt as framework + target_compile_options(carla-bridge PRIVATE -Wno-quoted-include-in-framework-header) +endif() + +target_compile_definitions(carla-bridge PRIVATE CARLA_MODULE_ID="carla-bridge" CARLA_MODULE_NAME="Audio Plugin" + CARLA_UTILS_USE_QT) + +target_link_libraries(carla-bridge PRIVATE carla::utils OBS::libobs Qt::Core Qt::Widgets $<$:dl>) + +target_sources( + carla-bridge + PRIVATE carla.c + carla-bridge.cpp + carla-bridge-wrapper.cpp + common.c + pluginlistdialog.cpp + pluginrefreshdialog.hpp + qtutils.cpp) + +if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0) + set_target_properties_obs( + carla-bridge + PROPERTIES AUTOMOC ON + AUTOUIC ON + AUTORCC ON + FOLDER plugins + PREFIX "") +else() + set_target_properties( + carla-bridge + PROPERTIES AUTOMOC ON + AUTOUIC ON + AUTORCC ON + FOLDER plugins + PREFIX "") + setup_plugin_target(carla-bridge) +endif() + +# Setup carla-patchbay target (only available for certain systems) +if(PKGCONFIG_FOUND AND NOT (OS_MACOS OR OS_WINDOWS)) + pkg_check_modules(carla-host-plugin IMPORTED_TARGET QUIET carla-host-plugin) + if(carla-host-plugin_FOUND) + add_library(carla-patchbay MODULE) + add_library(OBS::carla-patchbay ALIAS carla-patchbay) + + target_compile_definitions(carla-patchbay PRIVATE CARLA_MODULE_ID="carla-patchbay" + CARLA_MODULE_NAME="Carla Patchbay") + + target_link_libraries(carla-patchbay PRIVATE OBS::libobs Qt::Core Qt::Widgets PkgConfig::carla-host-plugin) + + target_sources(carla-patchbay PRIVATE carla.c carla-patchbay-wrapper.c common.c qtutils.cpp) + + if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0) + set_target_properties_obs(carla-patchbay PROPERTIES FOLDER plugins PREFIX "") + else() + set_target_properties(carla-patchbay PROPERTIES FOLDER plugins PREFIX "") + setup_plugin_target(carla-patchbay) + endif() + endif() +endif() diff --git a/plugins/carla/carla-bridge-wrapper.cpp b/plugins/carla/carla-bridge-wrapper.cpp new file mode 100644 index 00000000000000..0a7d91dec25ba4 --- /dev/null +++ b/plugins/carla/carla-bridge-wrapper.cpp @@ -0,0 +1,577 @@ +/* + * Carla plugin for OBS + * Copyright (C) 2023 Filipe Coelho + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +// needed for strcasestr +#if !defined(_GNU_SOURCE) && !defined(_WIN32) +#define _GNU_SOURCE +#endif + +#include +#include + +#include +#include + +#include + +#include "carla-bridge.hpp" +#include "carla-wrapper.h" +#include "common.h" +#include "qtutils.h" + +#if CARLA_VERSION_HEX >= 0x020591 +#define CARLA_2_6_FEATURES +#endif + +// ---------------------------------------------------------------------------- +// private data methods + +struct carla_priv : carla_bridge_callback { + obs_source_t *source = nullptr; + uint32_t bufferSize = 0; + double sampleRate = 0; + + // update properties when timeout is reached, 0 means do nothing + uint64_t update_request = 0; + + carla_bridge bridge; + + void bridge_parameter_changed(uint index, float value) override + { + char pname[PARAM_NAME_SIZE] = PARAM_NAME_INIT; + param_index_to_name(index, pname); + + obs_data_t *settings = obs_source_get_settings(source); + + /**/ if (bridge.paramDetails[index].hints & + PARAMETER_IS_BOOLEAN) + obs_data_set_bool(settings, pname, value > 0.5f); + else if (bridge.paramDetails[index].hints & + PARAMETER_IS_INTEGER) + obs_data_set_int(settings, pname, value); + else + obs_data_set_double(settings, pname, value); + + obs_data_release(settings); + + postpone_update_request(&update_request); + } +}; + +// ---------------------------------------------------------------------------- +// carla + obs integration methods + +struct carla_priv *carla_priv_create(obs_source_t *source, + enum buffer_size_mode bufsize, + uint32_t srate) +{ + struct carla_priv *priv = new struct carla_priv; + if (priv == NULL) + return NULL; + + priv->bridge.callback = priv; + priv->source = source; + priv->bufferSize = bufsize_mode_to_frames(bufsize); + priv->sampleRate = srate; + + return priv; +} + +void carla_priv_destroy(struct carla_priv *priv) +{ + priv->bridge.cleanup(); + delete priv; +} + +// ---------------------------------------------------------------------------- + +void carla_priv_activate(struct carla_priv *priv) +{ + priv->bridge.activate(); +} + +void carla_priv_deactivate(struct carla_priv *priv) +{ + priv->bridge.deactivate(); +} + +void carla_priv_process_audio(struct carla_priv *priv, + float *buffers[MAX_AV_PLANES], uint32_t frames) +{ + priv->bridge.process(buffers, frames); +} + +void carla_priv_idle(struct carla_priv *priv) +{ + priv->bridge.idle(); + handle_update_request(priv->source, &priv->update_request); +} + +// ---------------------------------------------------------------------------- + +void carla_priv_save(struct carla_priv *priv, obs_data_t *settings) +{ + priv->bridge.save_and_wait(); + + obs_data_set_string(settings, "btype", + getBinaryTypeAsString(priv->bridge.info.btype)); + obs_data_set_string(settings, "ptype", + getPluginTypeAsString(priv->bridge.info.ptype)); + obs_data_set_string(settings, "filename", priv->bridge.info.filename); + obs_data_set_string(settings, "label", priv->bridge.info.label); + obs_data_set_int(settings, "uniqueId", + static_cast(priv->bridge.info.uniqueId)); + + if (!priv->bridge.customData.empty()) { + obs_data_array_t *array = obs_data_array_create(); + + for (CustomData &cdata : priv->bridge.customData) { + obs_data_t *data = obs_data_create(); + obs_data_set_string(data, "type", cdata.type); + obs_data_set_string(data, "key", cdata.key); + obs_data_set_string(data, "value", cdata.value); + obs_data_array_push_back(array, data); + obs_data_release(data); + } + + obs_data_set_array(settings, PROP_CUSTOM_DATA, array); + obs_data_array_release(array); + } else { + obs_data_erase(settings, PROP_CUSTOM_DATA); + } + + char pname[PARAM_NAME_SIZE] = PARAM_NAME_INIT; + + if ((priv->bridge.info.options & PLUGIN_OPTION_USE_CHUNKS) && + !priv->bridge.chunk.isEmpty()) { + char *b64ptr = CarlaString::asBase64(priv->bridge.chunk.data(), + priv->bridge.chunk.size()) + .releaseBufferPointer(); + const CarlaString b64chunk(b64ptr, false); + obs_data_set_string(settings, PROP_CHUNK, b64chunk.buffer()); + + for (uint32_t i = 0; + i < priv->bridge.paramCount && i < MAX_PARAMS; ++i) { + const carla_param_data ¶m( + priv->bridge.paramDetails[i]); + + if ((param.hints & PARAMETER_IS_ENABLED) == 0) + continue; + + param_index_to_name(i, pname); + obs_data_erase(settings, pname); + } + } else { + obs_data_erase(settings, PROP_CHUNK); + + for (uint32_t i = 0; + i < priv->bridge.paramCount && i < MAX_PARAMS; ++i) { + const carla_param_data ¶m( + priv->bridge.paramDetails[i]); + + if ((param.hints & PARAMETER_IS_ENABLED) == 0) + continue; + + param_index_to_name(i, pname); + + if (param.hints & PARAMETER_IS_BOOLEAN) { + obs_data_set_bool(settings, pname, + carla_isEqual(param.value, + param.max)); + } else if (param.hints & PARAMETER_IS_INTEGER) { + obs_data_set_int(settings, pname, param.value); + } else { + obs_data_set_double(settings, pname, + param.value); + } + } + } +} + +void carla_priv_load(struct carla_priv *priv, obs_data_t *settings) +{ + const BinaryType btype = + getBinaryTypeFromString(obs_data_get_string(settings, "btype")); + const PluginType ptype = + getPluginTypeFromString(obs_data_get_string(settings, "ptype")); + + // abort early if both of these are null, likely from an empty config + if (btype == BINARY_NONE && ptype == PLUGIN_NONE) + return; + + const char *const filename = obs_data_get_string(settings, "filename"); + const char *const label = obs_data_get_string(settings, "label"); + const int64_t uniqueId = + static_cast(obs_data_get_int(settings, "uniqueId")); + + priv->bridge.cleanup(); + priv->bridge.init(priv->bufferSize, priv->sampleRate); + + if (!priv->bridge.start(btype, ptype, label, filename, uniqueId)) { + carla_show_error_dialog("Failed to load plugin", + priv->bridge.get_last_error()); + return; + } + + obs_data_array_t *array = + obs_data_get_array(settings, PROP_CUSTOM_DATA); + if (array) { + const size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; ++i) { + obs_data_t *data = obs_data_array_item(array, i); + const char *type = obs_data_get_string(data, "type"); + const char *key = obs_data_get_string(data, "key"); + const char *value = obs_data_get_string(data, "value"); + priv->bridge.add_custom_data(type, key, value); + } + priv->bridge.custom_data_loaded(); + } + + if (priv->bridge.info.options & PLUGIN_OPTION_USE_CHUNKS) { + const char *b64chunk = + obs_data_get_string(settings, PROP_CHUNK); + priv->bridge.load_chunk(b64chunk); + } else { + for (uint32_t i = 0; i < priv->bridge.paramCount; ++i) { + const carla_param_data ¶m( + priv->bridge.paramDetails[i]); + + priv->bridge.set_value(i, param.value); + } + } + + char pname[PARAM_NAME_SIZE] = PARAM_NAME_INIT; + + for (uint32_t i = 0; i < priv->bridge.paramCount && i < MAX_PARAMS; + ++i) { + const carla_param_data ¶m(priv->bridge.paramDetails[i]); + + if ((param.hints & PARAMETER_IS_ENABLED) == 0) + continue; + + param_index_to_name(i, pname); + + if (param.hints & PARAMETER_IS_BOOLEAN) { + obs_data_set_bool(settings, pname, + carla_isEqual(param.value, + param.max)); + } else if (param.hints & PARAMETER_IS_INTEGER) { + obs_data_set_int(settings, pname, param.value); + } else { + obs_data_set_double(settings, pname, param.value); + } + } + + priv->bridge.activate(); +} + +// ---------------------------------------------------------------------------- + +uint32_t carla_priv_get_num_channels(struct carla_priv *priv) +{ + return std::max(priv->bridge.info.numAudioIns, + priv->bridge.info.numAudioOuts); +} + +void carla_priv_set_buffer_size(struct carla_priv *priv, + enum buffer_size_mode bufsize) +{ + priv->bridge.set_buffer_size(bufsize_mode_to_frames(bufsize)); +} + +// ---------------------------------------------------------------------------- + +static bool carla_post_load_callback(struct carla_priv *priv, + obs_properties_t *props) +{ + obs_source_t *source = priv->source; + obs_data_t *settings = obs_source_get_settings(source); + remove_all_props(props, settings); + carla_priv_readd_properties(priv, props, true); + obs_data_release(settings); + return true; +} + +static bool carla_priv_load_file_callback(obs_properties_t *props, + obs_property_t *property, void *data) +{ + UNUSED_PARAMETER(property); + + struct carla_priv *priv = static_cast(data); + + char *filename = carla_qt_file_dialog( + false, false, obs_module_text("Load File"), NULL); + + if (filename == NULL) + return false; + +#ifndef _WIN32 + // truncate plug.vst3/Contents//plug.so -> plug.vst3 + if (char *const vst3str = strcasestr(filename, ".vst3/Contents/")) + vst3str[5] = '\0'; +#endif + + BinaryType btype; + PluginType ptype; + + { + const QFileInfo fileInfo(QString::fromUtf8(filename)); + const QString extension(fileInfo.suffix()); + +#if defined(CARLA_OS_MAC) + if (extension == "vst") + ptype = PLUGIN_VST2; +#else + if (extension == "dll" || extension == "so") + ptype = PLUGIN_VST2; +#endif + else if (extension == "vst3") + ptype = PLUGIN_VST3; +#ifdef CARLA_2_6_FEATURES + else if (extension == "clap") + ptype = PLUGIN_CLAP; +#endif + else { + carla_show_error_dialog("Failed to load file", + "Unknown file type"); + return false; + } + + btype = getBinaryTypeFromFile(filename); + } + + priv->bridge.cleanup(); + priv->bridge.init(priv->bufferSize, priv->sampleRate); + + if (priv->bridge.start(btype, ptype, "(none)", filename, 0)) + priv->bridge.activate(); + else + carla_show_error_dialog("Failed to load file", + priv->bridge.get_last_error()); + + return carla_post_load_callback(priv, props); +} + +static bool carla_priv_select_plugin_callback(obs_properties_t *props, + obs_property_t *property, + void *data) +{ + UNUSED_PARAMETER(property); + + struct carla_priv *priv = static_cast(data); + + const PluginListDialogResults *plugin = carla_exec_plugin_list_dialog(); + + if (plugin == NULL) + return false; + + priv->bridge.cleanup(); + priv->bridge.init(priv->bufferSize, priv->sampleRate); + + if (priv->bridge.start(static_cast(plugin->build), + static_cast(plugin->type), + plugin->label, plugin->filename, + plugin->uniqueId)) + priv->bridge.activate(); + else + carla_show_error_dialog("Failed to load plugin", + priv->bridge.get_last_error()); + + return carla_post_load_callback(priv, props); +} + +static bool carla_priv_reload_callback(obs_properties_t *props, + obs_property_t *property, void *data) +{ + UNUSED_PARAMETER(property); + + struct carla_priv *priv = static_cast(data); + + if (priv->bridge.is_running()) { + priv->bridge.reload(); + return true; + } + + if (priv->bridge.info.btype == BINARY_NONE) + return false; + + // cache relevant information for later + const BinaryType btype = priv->bridge.info.btype; + const PluginType ptype = priv->bridge.info.ptype; + const int64_t uniqueId = priv->bridge.info.uniqueId; + char *const label = priv->bridge.info.label.releaseBufferPointer(); + char *const filename = + priv->bridge.info.filename.releaseBufferPointer(); + + priv->bridge.cleanup(false); + priv->bridge.init(priv->bufferSize, priv->sampleRate); + + if (priv->bridge.start(btype, ptype, label, filename, uniqueId)) { + priv->bridge.restore_state(); + priv->bridge.activate(); + } else { + carla_show_error_dialog("Failed to reload plugin", + priv->bridge.get_last_error()); + } + + std::free(label); + std::free(filename); + + return carla_post_load_callback(priv, props); +} + +static bool carla_priv_show_gui_callback(obs_properties_t *props, + obs_property_t *property, void *data) +{ + UNUSED_PARAMETER(props); + UNUSED_PARAMETER(property); + + struct carla_priv *priv = static_cast(data); + + priv->bridge.show_ui(); + + return false; +} + +static bool carla_priv_param_changed(void *data, obs_properties_t *props, + obs_property_t *property, + obs_data_t *settings) +{ + UNUSED_PARAMETER(props); + + struct carla_priv *priv = static_cast(data); + + const char *const pname = obs_property_name(property); + if (pname == NULL) + return false; + + const char *pname2 = pname + 1; + while (*pname2 == '0') + ++pname2; + + const int pindex = atoi(pname2); + + if (pindex < 0 || pindex >= (int)priv->bridge.paramCount) + return false; + + const uint index = static_cast(pindex); + + const float min = priv->bridge.paramDetails[index].min; + const float max = priv->bridge.paramDetails[index].max; + + float value; + switch (obs_property_get_type(property)) { + case OBS_PROPERTY_BOOL: + value = obs_data_get_bool(settings, pname) ? max : min; + break; + case OBS_PROPERTY_INT: + value = obs_data_get_int(settings, pname); + if (value < min) + value = min; + else if (value > max) + value = max; + break; + case OBS_PROPERTY_FLOAT: + value = obs_data_get_double(settings, pname); + if (value < min) + value = min; + else if (value > max) + value = max; + break; + default: + return false; + } + + priv->bridge.set_value(index, value); + + return false; +} + +void carla_priv_readd_properties(struct carla_priv *priv, + obs_properties_t *props, bool reset) +{ + if (!reset) { + obs_properties_add_button2(props, PROP_SELECT_PLUGIN, + obs_module_text("Select plugin..."), + carla_priv_select_plugin_callback, + priv); + + obs_properties_add_button2(props, PROP_LOAD_FILE, + obs_module_text("Load file..."), + carla_priv_load_file_callback, priv); + } + + if (priv->bridge.info.ptype != PLUGIN_NONE) + obs_properties_add_button2(props, PROP_RELOAD_PLUGIN, + obs_module_text("Reload"), + carla_priv_reload_callback, priv); + + if (priv->bridge.info.hints & PLUGIN_HAS_CUSTOM_UI) + obs_properties_add_button2(props, PROP_SHOW_GUI, + obs_module_text("Show custom GUI"), + carla_priv_show_gui_callback, priv); + + obs_data_t *settings = obs_source_get_settings(priv->source); + + char pname[PARAM_NAME_SIZE] = PARAM_NAME_INIT; + + for (uint32_t i = 0; i < priv->bridge.paramCount && i < MAX_PARAMS; + ++i) { + const carla_param_data ¶m(priv->bridge.paramDetails[i]); + + if ((param.hints & PARAMETER_IS_ENABLED) == 0) + continue; + + obs_property_t *prop; + param_index_to_name(i, pname); + + if (param.hints & PARAMETER_IS_BOOLEAN) { + prop = obs_properties_add_bool(props, pname, + param.name); + + obs_data_set_default_bool(settings, pname, + carla_isEqual(param.def, + param.max)); + + if (reset) + obs_data_set_bool(settings, pname, + carla_isEqual(param.value, + param.max)); + } else if (param.hints & PARAMETER_IS_INTEGER) { + prop = obs_properties_add_int_slider( + props, pname, param.name, param.min, param.max, + param.step); + + obs_data_set_default_int(settings, pname, param.def); + + if (param.unit.isNotEmpty()) + obs_property_int_set_suffix(prop, param.unit); + + if (reset) + obs_data_set_int(settings, pname, param.value); + } else { + prop = obs_properties_add_float_slider( + props, pname, param.name, param.min, param.max, + param.step); + + obs_data_set_default_double(settings, pname, param.def); + + if (param.unit.isNotEmpty()) + obs_property_float_set_suffix(prop, param.unit); + + if (reset) + obs_data_set_double(settings, pname, + param.value); + } + + obs_property_set_modified_callback2( + prop, carla_priv_param_changed, priv); + } + + obs_data_release(settings); +} + +// ---------------------------------------------------------------------------- diff --git a/plugins/carla/carla-bridge.cpp b/plugins/carla/carla-bridge.cpp new file mode 100644 index 00000000000000..3d336ec76cf14b --- /dev/null +++ b/plugins/carla/carla-bridge.cpp @@ -0,0 +1,1404 @@ +/* + * Carla plugin for OBS + * Copyright (C) 2023 Filipe Coelho + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include +#include +#include + +#ifdef __APPLE__ +#include +#endif + +#include +#include +#include +#include +#include + +#include + +#include "carla-bridge.hpp" + +#include "common.h" +#include "qtutils.h" + +#if defined(__APPLE__) && defined(__aarch64__) +// ---------------------------------------------------------------------------- +// check the header of a plugin binary to see if it matches mach 64bit + intel + +static bool isIntel64BitPlugin(const char *const pluginBundle) +{ + const char *const pluginBinary = findBinaryInBundle(pluginBundle); + CARLA_SAFE_ASSERT_RETURN(pluginBinary != nullptr, false); + + FILE *const f = fopen(pluginBinary, "r"); + CARLA_SAFE_ASSERT_RETURN(f != nullptr, false); + + bool match = false; + uint8_t buf[8]; + if (fread(buf, sizeof(buf), 1, f) == 1) { + const uint32_t magic = *(uint32_t *)buf; + if (magic == 0xfeedfacf && buf[4] == 0x07) + match = true; + } + + fclose(f); + return match; +} +#endif + +// ---------------------------------------------------------------------------- +// utility class for reading and deleting incoming bridge text in RAII fashion + +struct BridgeTextReader { + char *text = nullptr; + + BridgeTextReader(BridgeNonRtServerControl &nonRtServerCtrl) + { + const uint32_t size = nonRtServerCtrl.readUInt(); + CARLA_SAFE_ASSERT_RETURN(size != 0, ); + + text = new char[size + 1]; + nonRtServerCtrl.readCustomData(text, size); + text[size] = '\0'; + } + + BridgeTextReader(BridgeNonRtServerControl &nonRtServerCtrl, + const uint32_t size) + { + text = new char[size + 1]; + + if (size != 0) + nonRtServerCtrl.readCustomData(text, size); + + text[size] = '\0'; + } + + ~BridgeTextReader() noexcept { delete[] text; } + + CARLA_DECLARE_NON_COPYABLE(BridgeTextReader) +}; + +// ---------------------------------------------------------------------------- +// custom bridge process implementation + +BridgeProcess::BridgeProcess(const char *const shmIds) +{ + // move object to the correct/expected thread + moveToThread(qApp->thread()); + + // setup environment for client side + QProcessEnvironment env(QProcessEnvironment::systemEnvironment()); + env.insert("ENGINE_BRIDGE_SHM_IDS", shmIds); + setProcessEnvironment(env); +} + +void BridgeProcess::start() +{ + // pass-through all bridge output + setInputChannelMode(QProcess::ForwardedInputChannel); + setProcessChannelMode(QProcess::ForwardedChannels); + QProcess::start(QIODevice::Unbuffered | QIODevice::ReadOnly); +} + +// NOTE: process instance cannot be used after this! +void BridgeProcess::stop() +{ + if (state() != QProcess::NotRunning) { + terminate(); + waitForFinished(2000); + + if (state() != QProcess::NotRunning) { + blog(LOG_INFO, + "[carla] bridge refused to close, force kill now"); + kill(); + } + } + + deleteLater(); +} + +// ---------------------------------------------------------------------------- + +bool carla_bridge::init(uint32_t maxBufferSize, double sampleRate) +{ + // add entropy to rand calls, used for finding unused paths + std::srand(static_cast(os_gettime_ns() / 1000000)); + + // initialize the several communication channels + if (!audiopool.initializeServer()) { + blog(LOG_WARNING, + "[carla] Failed to initialize shared memory audio pool"); + goto fail1; + } + + if (!rtClientCtrl.initializeServer()) { + blog(LOG_WARNING, + "[carla] Failed to initialize RT client control"); + goto fail2; + } + + if (!nonRtClientCtrl.initializeServer()) { + blog(LOG_WARNING, + "[carla] Failed to initialize Non-RT client control"); + goto fail3; + } + + if (!nonRtServerCtrl.initializeServer()) { + blog(LOG_WARNING, + "[carla] Failed to initialize Non-RT server control"); + goto fail4; + } + + // resize audiopool data to be as large as needed + audiopool.resize(maxBufferSize, MAX_AV_PLANES, MAX_AV_PLANES); + + // clear realtime data + rtClientCtrl.data->procFlags = 0; + carla_zeroStruct(rtClientCtrl.data->timeInfo); + carla_zeroBytes(rtClientCtrl.data->midiOut, + kBridgeRtClientDataMidiOutSize); + + // clear ringbuffers + rtClientCtrl.clearData(); + nonRtClientCtrl.clearData(); + nonRtServerCtrl.clearData(); + + // first ever message is bridge API version + nonRtClientCtrl.writeOpcode(kPluginBridgeNonRtClientVersion); + nonRtClientCtrl.writeUInt(CARLA_PLUGIN_BRIDGE_API_VERSION_CURRENT); + + // then expected size for each data channel + nonRtClientCtrl.writeUInt( + static_cast(sizeof(BridgeRtClientData))); + nonRtClientCtrl.writeUInt( + static_cast(sizeof(BridgeNonRtClientData))); + nonRtClientCtrl.writeUInt( + static_cast(sizeof(BridgeNonRtServerData))); + + // and finally the initial buffer size and sample rate + nonRtClientCtrl.writeOpcode(kPluginBridgeNonRtClientInitialSetup); + nonRtClientCtrl.writeUInt(maxBufferSize); + nonRtClientCtrl.writeDouble(sampleRate); + + nonRtClientCtrl.commitWrite(); + + // report audiopool size to client side + rtClientCtrl.writeOpcode(kPluginBridgeRtClientSetAudioPool); + rtClientCtrl.writeULong(static_cast(audiopool.dataSize)); + rtClientCtrl.commitWrite(); + + // FIXME + rtClientCtrl.writeOpcode(kPluginBridgeRtClientSetBufferSize); + rtClientCtrl.writeUInt(maxBufferSize); + rtClientCtrl.commitWrite(); + + bufferSize = maxBufferSize; + return true; + +fail4: + nonRtClientCtrl.clear(); + +fail3: + rtClientCtrl.clear(); + +fail2: + audiopool.clear(); + +fail1: + setLastError("Failed to initialize shared memory"); + return false; +} + +void carla_bridge::cleanup(const bool clearPluginData) +{ + // signal to stop processing audio + const bool wasActivated = activated; + ready = activated = false; + + // stop bridge process + if (childprocess != nullptr) { + // make `childprocess` null first + BridgeProcess *proc = childprocess; + childprocess = nullptr; + + // if process is running, ask nicely for it to close + if (proc->state() != QProcess::NotRunning) { + { + const CarlaMutexLocker cml( + nonRtClientCtrl.mutex); + + if (wasActivated) { + nonRtClientCtrl.writeOpcode( + kPluginBridgeNonRtClientDeactivate); + nonRtClientCtrl.commitWrite(); + } + + nonRtClientCtrl.writeOpcode( + kPluginBridgeNonRtClientQuit); + nonRtClientCtrl.commitWrite(); + } + + rtClientCtrl.writeOpcode(kPluginBridgeRtClientQuit); + rtClientCtrl.commitWrite(); + + if (!timedErr && !timedOut) + wait("stopping", 3000); + } else { + // log warning in case plugin process crashed + if (proc->exitStatus() == QProcess::CrashExit) { + blog(LOG_WARNING, "[carla] bridge crashed"); + + if (!clearPluginData) { + carla_show_error_dialog( + "A plugin bridge has crashed", + info.name); + } + } + } + + // let Qt do the final cleanup on the main thread + QMetaObject::invokeMethod(proc, "stop"); + } + + // cleanup shared memory bits + nonRtServerCtrl.clear(); + nonRtClientCtrl.clear(); + rtClientCtrl.clear(); + audiopool.clear(); + + // clear cached plugin data if requested + if (clearPluginData) { + info.clear(); + chunk.clear(); + clear_custom_data(); + } +} + +bool carla_bridge::start(const BinaryType btype, const PluginType ptype, + const char *label, const char *filename, + const int64_t uniqueId) +{ + // make sure we are trying to load something valid + if (btype == BINARY_NONE || ptype == PLUGIN_NONE) { + setLastError("Invalid plugin state"); + return false; + } + + // find path to bridge binary + QString bridgeBinary(QString::fromUtf8(get_carla_bin_path())); + + if (btype == BINARY_NATIVE) { + bridgeBinary += CARLA_OS_SEP_STR "carla-bridge-native"; + } else { + switch (btype) { + case BINARY_POSIX32: + bridgeBinary += CARLA_OS_SEP_STR "carla-bridge-posix32"; + break; + case BINARY_POSIX64: + bridgeBinary += CARLA_OS_SEP_STR "carla-bridge-posix64"; + break; + case BINARY_WIN32: + bridgeBinary += CARLA_OS_SEP_STR + "carla-bridge-win32.exe"; + break; + case BINARY_WIN64: + bridgeBinary += CARLA_OS_SEP_STR + "carla-bridge-win64.exe"; + break; + default: + bridgeBinary.clear(); + break; + } + } + + if (bridgeBinary.isEmpty() || !QFileInfo(bridgeBinary).isExecutable()) { + setLastError("Required plugin bridge is not available"); + return false; + } + + // create string of shared memory ids to pass into the bridge process + char shmIdsStr[6 * 4 + 1] = {}; + + size_t len = audiopool.filename.length(); + CARLA_SAFE_ASSERT_RETURN(len > 6, false); + std::strncpy(shmIdsStr, &audiopool.filename[len - 6], 6); + + len = rtClientCtrl.filename.length(); + CARLA_SAFE_ASSERT_RETURN(len > 6, false); + std::strncpy(shmIdsStr + 6, &rtClientCtrl.filename[len - 6], 6); + + len = nonRtClientCtrl.filename.length(); + CARLA_SAFE_ASSERT_RETURN(len > 6, false); + std::strncpy(shmIdsStr + 12, &nonRtClientCtrl.filename[len - 6], 6); + + len = nonRtServerCtrl.filename.length(); + CARLA_SAFE_ASSERT_RETURN(len > 6, false); + std::strncpy(shmIdsStr + 18, &nonRtServerCtrl.filename[len - 6], 6); + + // create bridge process and setup arguments + BridgeProcess *proc = new BridgeProcess(shmIdsStr); + + QStringList arguments; + +#if defined(__APPLE__) && defined(__aarch64__) + // see if this binary needs special help (x86_64 plugins under arm64 systems) + switch (ptype) { + case PLUGIN_VST2: + case PLUGIN_VST3: + case PLUGIN_CLAP: + if (isIntel64BitPlugin(filename)) { + // TODO we need to hook into qprocess for: + // posix_spawnattr_setbinpref_np + CPU_TYPE_X86_64 + arguments.append("-arch"); + arguments.append("x86_64"); + arguments.append(bridgeBinary); + bridgeBinary = "arch"; + } + default: + break; + } +#endif + + // do not use null strings for label and filename + if (label == nullptr || label[0] == '\0') + label = "(none)"; + if (filename == nullptr || filename[0] == '\0') + filename = "(none)"; + + // arg 1: plugin type + arguments.append(QString::fromUtf8(getPluginTypeAsString(ptype))); + + // arg 2: filename + arguments.append(QString::fromUtf8(filename)); + + // arg 3: label + arguments.append(QString::fromUtf8(label)); + + // arg 4: uniqueId + arguments.append(QString::number(uniqueId)); + + proc->setProgram(bridgeBinary); + proc->setArguments(arguments); + + // start process on main thread + QMetaObject::invokeMethod(proc, "start"); + + // check if it started correctly + const bool started = proc->waitForStarted(5000); + + if (!started) { + QMetaObject::invokeMethod(proc, "stop"); + setLastError("Plugin bridge failed to start"); + return false; + } + + // wait for plugin process to start talking to us + ready = false; + timedErr = false; + timedOut = false; + + const uint64_t start_time = os_gettime_ns(); + + // NOTE: we cannot rely on `proc->state() == QProcess::Running` here + // as Qt only updates QProcess state on main thread + while (proc != nullptr && !ready && !timedErr) { + os_sleep_ms(5); + + // timeout after 5s + if (os_gettime_ns() - start_time > 5 * 1000000000ULL) + break; + + readMessages(); + } + + if (!ready) { + QMetaObject::invokeMethod(proc, "stop"); + if (!timedErr) + setLastError( + "Timeout while waiting for plugin bridge to start"); + return false; + } + + // refuse to load plugin with incompatible IO + if (info.hasCV || info.numAudioIns > MAX_AV_PLANES || + info.numAudioOuts > MAX_AV_PLANES) { + // tell bridge process to quit + nonRtClientCtrl.writeOpcode(kPluginBridgeNonRtClientQuit); + nonRtClientCtrl.commitWrite(); + rtClientCtrl.writeOpcode(kPluginBridgeRtClientQuit); + rtClientCtrl.commitWrite(); + wait("stopping", 3000); + QMetaObject::invokeMethod(proc, "stop"); + + // cleanup shared memory bits + nonRtServerCtrl.clear(); + nonRtClientCtrl.clear(); + rtClientCtrl.clear(); + audiopool.clear(); + + // also clear cached info + info.clear(); + chunk.clear(); + clear_custom_data(); + delete[] paramDetails; + paramDetails = nullptr; + paramCount = 0; + + setLastError("Selected plugin has IO incompatible with OBS"); + return false; + } + + // cache relevant information for later + info.btype = btype; + info.ptype = ptype; + info.filename = filename; + info.label = label; + info.uniqueId = uniqueId; + + // finally assign childprocess and set active + childprocess = proc; + + return true; +} + +bool carla_bridge::is_running() const +{ + return childprocess != nullptr && + childprocess->state() == QProcess::Running; +} + +bool carla_bridge::idle() +{ + if (childprocess == nullptr) + return false; + + switch (childprocess->state()) { + case QProcess::Running: + if (!pendingPing) { + pendingPing = true; + + const CarlaMutexLocker cml(nonRtClientCtrl.mutex); + + nonRtClientCtrl.writeOpcode( + kPluginBridgeNonRtClientPing); + nonRtClientCtrl.commitWrite(); + } + break; + case QProcess::NotRunning: + activated = false; + timedErr = true; + cleanup(false); + return false; + default: + return false; + } + + if (timedOut && activated) { + deactivate(); + return idle(); + } + + try { + readMessages(); + } + CARLA_SAFE_EXCEPTION("readMessages"); + + return true; +} + +bool carla_bridge::wait(const char *const action, const uint msecs) +{ + CARLA_SAFE_ASSERT_RETURN(!timedErr, false); + CARLA_SAFE_ASSERT_RETURN(!timedOut, false); + + if (rtClientCtrl.waitForClient(msecs)) + return true; + + timedOut = true; + blog(LOG_WARNING, "[carla] wait(%s) timed out", action); + return false; +} + +// ---------------------------------------------------------------------------- + +void carla_bridge::set_value(uint index, float value) +{ + CARLA_SAFE_ASSERT_UINT2_RETURN(index < paramCount, index, paramCount, ); + + paramDetails[index].value = value; + + if (is_running()) { + const CarlaMutexLocker cml(nonRtClientCtrl.mutex); + + nonRtClientCtrl.writeOpcode( + kPluginBridgeNonRtClientSetParameterValue); + nonRtClientCtrl.writeUInt(index); + nonRtClientCtrl.writeFloat(value); + nonRtClientCtrl.commitWrite(); + + if (info.hints & PLUGIN_HAS_CUSTOM_UI) { + nonRtClientCtrl.writeOpcode( + kPluginBridgeNonRtClientUiParameterChange); + nonRtClientCtrl.writeUInt(index); + nonRtClientCtrl.writeFloat(value); + nonRtClientCtrl.commitWrite(); + } + + nonRtClientCtrl.waitIfDataIsReachingLimit(); + } +} + +void carla_bridge::show_ui() +{ + if (is_running()) { + const CarlaMutexLocker cml(nonRtClientCtrl.mutex); + + nonRtClientCtrl.writeOpcode(kPluginBridgeNonRtClientShowUI); + nonRtClientCtrl.commitWrite(); + } +} + +bool carla_bridge::is_active() const noexcept +{ + return activated; +} + +void carla_bridge::activate() +{ + if (activated) + return; + + if (is_running()) { + { + const CarlaMutexLocker cml(nonRtClientCtrl.mutex); + + nonRtClientCtrl.writeOpcode( + kPluginBridgeNonRtClientActivate); + nonRtClientCtrl.commitWrite(); + } + + try { + wait("activate", 2000); + } + CARLA_SAFE_EXCEPTION("activate - waitForClient"); + + activated = true; + } +} + +void carla_bridge::deactivate() +{ + CARLA_SAFE_ASSERT_RETURN(activated, ); + + activated = false; + timedErr = false; + timedOut = false; + + if (is_running()) { + { + const CarlaMutexLocker cml(nonRtClientCtrl.mutex); + + nonRtClientCtrl.writeOpcode( + kPluginBridgeNonRtClientDeactivate); + nonRtClientCtrl.commitWrite(); + } + + try { + wait("deactivate", 2000); + } + CARLA_SAFE_EXCEPTION("deactivate - waitForClient"); + } +} + +void carla_bridge::reload() +{ + ready = false; + timedErr = false; + timedOut = false; + + if (activated) + deactivate(); + + if (is_running()) { + { + const CarlaMutexLocker cml(nonRtClientCtrl.mutex); + + nonRtClientCtrl.writeOpcode( + kPluginBridgeNonRtClientReload); + nonRtClientCtrl.commitWrite(); + } + } + + activate(); + + if (is_running()) { + try { + wait("reload", 2000); + } + CARLA_SAFE_EXCEPTION("reload - waitForClient"); + } + + // wait for plugin process to start talking back to us + const uint64_t start_time = os_gettime_ns(); + + while (childprocess != nullptr && !ready) { + os_sleep_ms(5); + + // timeout after 1s + if (os_gettime_ns() - start_time > 1000000000ULL) + break; + + readMessages(); + } +} + +void carla_bridge::restore_state() +{ + const uint32_t maxLocalValueLen = clientBridgeVersion >= 10 ? 4096 + : 16384; + + const CarlaMutexLocker cml(nonRtClientCtrl.mutex); + + for (CustomData &cdata : customData) { + const uint32_t typeLen = + static_cast(std::strlen(cdata.type)); + const uint32_t keyLen = + static_cast(std::strlen(cdata.key)); + const uint32_t valueLen = + static_cast(std::strlen(cdata.value)); + + nonRtClientCtrl.writeOpcode( + kPluginBridgeNonRtClientSetCustomData); + + nonRtClientCtrl.writeUInt(typeLen); + nonRtClientCtrl.writeCustomData(cdata.type, typeLen); + + nonRtClientCtrl.writeUInt(keyLen); + nonRtClientCtrl.writeCustomData(cdata.key, keyLen); + + nonRtClientCtrl.writeUInt(valueLen); + + if (valueLen > 0) { + if (valueLen > maxLocalValueLen) { + QString filePath(QDir::tempPath()); + + filePath += CARLA_OS_SEP_STR + ".CarlaCustomData_"; + filePath += audiopool.getFilenameSuffix(); + + QFile file(filePath); + if (file.open(QIODevice::WriteOnly) && + file.write(cdata.value) != + static_cast(valueLen)) { + const uint32_t ulength = + static_cast( + filePath.length()); + + nonRtClientCtrl.writeUInt(ulength); + nonRtClientCtrl.writeCustomData( + filePath.toUtf8().constData(), + ulength); + } else { + nonRtClientCtrl.writeUInt(0); + } + } else { + nonRtClientCtrl.writeCustomData(cdata.value, + valueLen); + } + } + + nonRtClientCtrl.commitWrite(); + + nonRtClientCtrl.waitIfDataIsReachingLimit(); + } + + if (info.ptype == PLUGIN_LV2) { + nonRtClientCtrl.writeOpcode( + kPluginBridgeNonRtClientRestoreLV2State); + nonRtClientCtrl.commitWrite(); + } + + if (info.options & PLUGIN_OPTION_USE_CHUNKS) { + QString filePath(QDir::tempPath()); + + filePath += CARLA_OS_SEP_STR ".CarlaChunk_"; + filePath += audiopool.getFilenameSuffix(); + + QFile file(filePath); + if (file.open(QIODevice::WriteOnly) && + file.write(CarlaString::asBase64(chunk.data(), chunk.size()) + .buffer()) != 0) { + file.close(); + + const uint32_t ulength = + static_cast(filePath.length()); + + nonRtClientCtrl.writeOpcode( + kPluginBridgeNonRtClientSetChunkDataFile); + nonRtClientCtrl.writeUInt(ulength); + nonRtClientCtrl.writeCustomData( + filePath.toUtf8().constData(), ulength); + nonRtClientCtrl.commitWrite(); + + nonRtClientCtrl.waitIfDataIsReachingLimit(); + } + } else { + for (uint32_t i = 0; i < paramCount; ++i) { + const carla_param_data ¶m(paramDetails[i]); + + nonRtClientCtrl.writeOpcode( + kPluginBridgeNonRtClientSetParameterValue); + nonRtClientCtrl.writeUInt(i); + nonRtClientCtrl.writeFloat(param.value); + nonRtClientCtrl.commitWrite(); + + nonRtClientCtrl.writeOpcode( + kPluginBridgeNonRtClientUiParameterChange); + nonRtClientCtrl.writeUInt(i); + nonRtClientCtrl.writeFloat(param.value); + nonRtClientCtrl.commitWrite(); + + nonRtClientCtrl.waitIfDataIsReachingLimit(); + } + } +} + +void carla_bridge::process(float *buffers[MAX_AV_PLANES], const uint32_t frames) +{ + if (!ready || !activated) + return; + + rtClientCtrl.data->timeInfo.usecs = os_gettime_ns() / 1000; + + for (uint32_t c = 0; c < MAX_AV_PLANES; ++c) + carla_copyFloats(audiopool.data + (c * bufferSize), buffers[c], + frames); + + rtClientCtrl.writeOpcode(kPluginBridgeRtClientProcess); + rtClientCtrl.writeUInt(frames); + rtClientCtrl.commitWrite(); + + if (wait("process", 1000)) { + for (uint32_t c = 0; c < MAX_AV_PLANES; ++c) + carla_copyFloats( + buffers[c], + audiopool.data + + ((c + info.numAudioIns) * bufferSize), + frames); + } +} + +void carla_bridge::add_custom_data(const char *const type, + const char *const key, + const char *const value, + const bool sendToPlugin) +{ + CARLA_SAFE_ASSERT_RETURN(type != nullptr && type[0] != '\0', ); + CARLA_SAFE_ASSERT_RETURN(key != nullptr && key[0] != '\0', ); + CARLA_SAFE_ASSERT_RETURN(value != nullptr, ); + + // Check if we already have this key + bool found = false; + for (CustomData &cdata : customData) { + if (std::strcmp(cdata.key, key) == 0) { + bfree(const_cast(cdata.value)); + cdata.value = bstrdup(value); + found = true; + break; + } + } + + // Otherwise store it + if (!found) { + CustomData cdata = {}; + cdata.type = bstrdup(type); + cdata.key = bstrdup(key); + cdata.value = bstrdup(value); + customData.push_back(cdata); + } + + if (sendToPlugin) { + const uint32_t maxLocalValueLen = + clientBridgeVersion >= 10 ? 4096 : 16384; + + const uint32_t typeLen = + static_cast(std::strlen(type)); + const uint32_t keyLen = static_cast(std::strlen(key)); + const uint32_t valueLen = + static_cast(std::strlen(value)); + + const CarlaMutexLocker cml(nonRtClientCtrl.mutex); + + if (valueLen > maxLocalValueLen) + nonRtClientCtrl.waitIfDataIsReachingLimit(); + + nonRtClientCtrl.writeOpcode( + kPluginBridgeNonRtClientSetCustomData); + + nonRtClientCtrl.writeUInt(typeLen); + nonRtClientCtrl.writeCustomData(type, typeLen); + + nonRtClientCtrl.writeUInt(keyLen); + nonRtClientCtrl.writeCustomData(key, keyLen); + + nonRtClientCtrl.writeUInt(valueLen); + + if (valueLen > 0) { + if (valueLen > maxLocalValueLen) { + QString filePath(QDir::tempPath()); + + filePath += CARLA_OS_SEP_STR + ".CarlaCustomData_"; + filePath += audiopool.getFilenameSuffix(); + + QFile file(filePath); + if (file.open(QIODevice::WriteOnly) && + file.write(value) != + static_cast(valueLen)) { + const uint32_t ulength = + static_cast( + filePath.length()); + + nonRtClientCtrl.writeUInt(ulength); + nonRtClientCtrl.writeCustomData( + filePath.toUtf8().constData(), + ulength); + } else { + nonRtClientCtrl.writeUInt(0); + } + } else { + nonRtClientCtrl.writeCustomData(value, + valueLen); + } + } + + nonRtClientCtrl.commitWrite(); + + nonRtClientCtrl.waitIfDataIsReachingLimit(); + } +} + +void carla_bridge::custom_data_loaded() +{ + if (info.ptype != PLUGIN_LV2) + return; + + const CarlaMutexLocker cml(nonRtClientCtrl.mutex); + + nonRtClientCtrl.writeOpcode(kPluginBridgeNonRtClientRestoreLV2State); + nonRtClientCtrl.commitWrite(); +} + +void carla_bridge::clear_custom_data() +{ + for (CustomData &cdata : customData) { + bfree(const_cast(cdata.type)); + bfree(const_cast(cdata.key)); + bfree(const_cast(cdata.value)); + } + customData.clear(); +} + +void carla_bridge::load_chunk(const char *b64chunk) +{ + chunk = QByteArray::fromBase64(b64chunk); + + QString filePath(QDir::tempPath()); + + filePath += CARLA_OS_SEP_STR ".CarlaChunk_"; + filePath += audiopool.getFilenameSuffix(); + + QFile file(filePath); + if (file.open(QIODevice::WriteOnly) && file.write(b64chunk) != 0) { + file.close(); + + const uint32_t ulength = + static_cast(filePath.length()); + + const CarlaMutexLocker cml(nonRtClientCtrl.mutex); + + nonRtClientCtrl.writeOpcode( + kPluginBridgeNonRtClientSetChunkDataFile); + nonRtClientCtrl.writeUInt(ulength); + nonRtClientCtrl.writeCustomData(filePath.toUtf8().constData(), + ulength); + nonRtClientCtrl.commitWrite(); + + nonRtClientCtrl.waitIfDataIsReachingLimit(); + } +} + +void carla_bridge::save_and_wait() +{ + if (!is_running()) + return; + + saved = false; + pendingPing = false; + + { + const CarlaMutexLocker cml(nonRtClientCtrl.mutex); + + // deactivate bridge client-side ping check + // some plugins block during save, preventing regular ping timings + nonRtClientCtrl.writeOpcode(kPluginBridgeNonRtClientPingOnOff); + nonRtClientCtrl.writeBool(false); + nonRtClientCtrl.commitWrite(); + + // tell plugin bridge to save and report any pending data + nonRtClientCtrl.writeOpcode( + kPluginBridgeNonRtClientPrepareForSave); + nonRtClientCtrl.commitWrite(); + } + + // wait for "saved" reply + const uint64_t start_time = os_gettime_ns(); + + while (is_running() && !saved) { + os_sleep_ms(5); + + // timeout after 10s + if (os_gettime_ns() - start_time > 10 * 1000000000ULL) + break; + + readMessages(); + + // deactivate plugin if we timeout during save + if (timedOut && activated) { + activated = false; + + const CarlaMutexLocker cml(nonRtClientCtrl.mutex); + + nonRtClientCtrl.writeOpcode( + kPluginBridgeNonRtClientDeactivate); + nonRtClientCtrl.commitWrite(); + } + } + + if (is_running()) { + const CarlaMutexLocker cml(nonRtClientCtrl.mutex); + + // reactivate ping check + nonRtClientCtrl.writeOpcode(kPluginBridgeNonRtClientPingOnOff); + nonRtClientCtrl.writeBool(true); + nonRtClientCtrl.commitWrite(); + } +} + +void carla_bridge::set_buffer_size(const uint32_t maxBufferSize) +{ + if (bufferSize == maxBufferSize) + return; + + bufferSize = maxBufferSize; + + if (is_running()) { + audiopool.resize(maxBufferSize, MAX_AV_PLANES, MAX_AV_PLANES); + + rtClientCtrl.writeOpcode(kPluginBridgeRtClientSetAudioPool); + rtClientCtrl.writeULong( + static_cast(audiopool.dataSize)); + rtClientCtrl.commitWrite(); + + rtClientCtrl.writeOpcode(kPluginBridgeRtClientSetBufferSize); + rtClientCtrl.writeUInt(maxBufferSize); + rtClientCtrl.commitWrite(); + } +} + +const char *carla_bridge::get_last_error() const noexcept +{ + return lastError; +} + +// ---------------------------------------------------------------------------- + +void carla_bridge::readMessages() +{ + while (nonRtServerCtrl.isDataAvailableForReading()) { + const PluginBridgeNonRtServerOpcode opcode = + nonRtServerCtrl.readOpcode(); + + // #ifdef DEBUG + if (opcode != kPluginBridgeNonRtServerPong && + opcode != kPluginBridgeNonRtServerParameterValue2) { + blog(LOG_DEBUG, "[carla] got opcode: %s", + PluginBridgeNonRtServerOpcode2str(opcode)); + } + // #endif + + switch (opcode) { + case kPluginBridgeNonRtServerNull: + break; + + case kPluginBridgeNonRtServerPong: + pendingPing = false; + break; + + // uint/version + case kPluginBridgeNonRtServerVersion: + clientBridgeVersion = nonRtServerCtrl.readUInt(); + break; + + // uint/category, uint/hints, uint/optionsAvailable, uint/optionsEnabled, long/uniqueId + case kPluginBridgeNonRtServerPluginInfo1: { + // const uint32_t category = + nonRtServerCtrl.readUInt(); + info.hints = nonRtServerCtrl.readUInt() | + PLUGIN_IS_BRIDGE; + // const uint32_t optionAv = + nonRtServerCtrl.readUInt(); + info.options = nonRtServerCtrl.readUInt(); + const int64_t uniqueId = nonRtServerCtrl.readLong(); + + if (info.uniqueId != 0) { + CARLA_SAFE_ASSERT_INT2(info.uniqueId == + uniqueId, + info.uniqueId, uniqueId); + } + } break; + + // uint/size, str[] (realName), uint/size, str[] (label), uint/size, str[] (maker), uint/size, str[] (copyright) + case kPluginBridgeNonRtServerPluginInfo2: { + // realName + const BridgeTextReader name(nonRtServerCtrl); + info.name = name.text; + + // label + if (const uint32_t size = nonRtServerCtrl.readUInt()) + nonRtServerCtrl.skipRead(size); + + // maker + if (const uint32_t size = nonRtServerCtrl.readUInt()) + nonRtServerCtrl.skipRead(size); + + // copyright + if (const uint32_t size = nonRtServerCtrl.readUInt()) + nonRtServerCtrl.skipRead(size); + } break; + + // uint/ins, uint/outs + case kPluginBridgeNonRtServerAudioCount: + info.numAudioIns = nonRtServerCtrl.readUInt(); + info.numAudioOuts = nonRtServerCtrl.readUInt(); + break; + + // uint/ins, uint/outs + case kPluginBridgeNonRtServerMidiCount: + nonRtServerCtrl.readUInt(); + nonRtServerCtrl.readUInt(); + break; + + // uint/ins, uint/outs + case kPluginBridgeNonRtServerCvCount: { + const uint32_t cvIns = nonRtServerCtrl.readUInt(); + const uint32_t cvOuts = nonRtServerCtrl.readUInt(); + info.hasCV = cvIns + cvOuts != 0; + } break; + + // uint/count + case kPluginBridgeNonRtServerParameterCount: { + paramCount = nonRtServerCtrl.readUInt(); + + delete[] paramDetails; + + if (paramCount != 0) + paramDetails = new carla_param_data[paramCount]; + else + paramDetails = nullptr; + } break; + + // uint/count + case kPluginBridgeNonRtServerProgramCount: + nonRtServerCtrl.readUInt(); + break; + + // uint/count + case kPluginBridgeNonRtServerMidiProgramCount: + nonRtServerCtrl.readUInt(); + break; + + // byte/type, uint/index, uint/size, str[] (name) + case kPluginBridgeNonRtServerPortName: { + nonRtServerCtrl.readByte(); + nonRtServerCtrl.readUInt(); + + // name + if (const uint32_t size = nonRtServerCtrl.readUInt()) + nonRtServerCtrl.skipRead(size); + + } break; + + // uint/index, int/rindex, uint/type, uint/hints, short/cc + case kPluginBridgeNonRtServerParameterData1: { + const uint32_t index = nonRtServerCtrl.readUInt(); + nonRtServerCtrl.readInt(); + const uint32_t type = nonRtServerCtrl.readUInt(); + const uint32_t hints = nonRtServerCtrl.readUInt(); + nonRtServerCtrl.readShort(); + + CARLA_SAFE_ASSERT_UINT2_BREAK(index < paramCount, index, + paramCount); + + if (type != PARAMETER_INPUT) + break; + if ((hints & PARAMETER_IS_ENABLED) == 0) + break; + if (hints & + (PARAMETER_IS_READ_ONLY | PARAMETER_IS_NOT_SAVED)) + break; + + paramDetails[index].hints = hints; + } break; + + // uint/index, uint/size, str[] (name), uint/size, str[] (unit) + case kPluginBridgeNonRtServerParameterData2: { + const uint32_t index = nonRtServerCtrl.readUInt(); + + // name + const BridgeTextReader name(nonRtServerCtrl); + + // symbol + const BridgeTextReader symbol(nonRtServerCtrl); + + // unit + const BridgeTextReader unit(nonRtServerCtrl); + + CARLA_SAFE_ASSERT_UINT2_BREAK(index < paramCount, index, + paramCount); + + if (paramDetails[index].hints & PARAMETER_IS_ENABLED) { + paramDetails[index].name = name.text; + paramDetails[index].symbol = symbol.text; + paramDetails[index].unit = unit.text; + } + } break; + + // uint/index, float/def, float/min, float/max, float/step, float/stepSmall, float/stepLarge + case kPluginBridgeNonRtServerParameterRanges: { + const uint32_t index = nonRtServerCtrl.readUInt(); + const float def = nonRtServerCtrl.readFloat(); + const float min = nonRtServerCtrl.readFloat(); + const float max = nonRtServerCtrl.readFloat(); + const float step = nonRtServerCtrl.readFloat(); + nonRtServerCtrl.readFloat(); + nonRtServerCtrl.readFloat(); + + CARLA_SAFE_ASSERT_BREAK(min < max); + CARLA_SAFE_ASSERT_BREAK(def >= min); + CARLA_SAFE_ASSERT_BREAK(def <= max); + CARLA_SAFE_ASSERT_UINT2_BREAK(index < paramCount, index, + paramCount); + + if (paramDetails[index].hints & PARAMETER_IS_ENABLED) { + paramDetails[index].def = + paramDetails[index].value = def; + paramDetails[index].min = min; + paramDetails[index].max = max; + paramDetails[index].step = step; + } + } break; + + // uint/index, float/value + case kPluginBridgeNonRtServerParameterValue: { + const uint32_t index = nonRtServerCtrl.readUInt(); + const float value = nonRtServerCtrl.readFloat(); + + if (index < paramCount) { + const float fixedValue = carla_fixedValue( + paramDetails[index].min, + paramDetails[index].max, value); + + if (carla_isNotEqual(paramDetails[index].value, + fixedValue)) { + paramDetails[index].value = fixedValue; + + if (callback != nullptr) { + // skip parameters that we do not show + if ((paramDetails[index].hints & + PARAMETER_IS_ENABLED) == 0) + break; + + callback->bridge_parameter_changed( + index, fixedValue); + } + } + } + } break; + + // uint/index, float/value + case kPluginBridgeNonRtServerParameterValue2: { + const uint32_t index = nonRtServerCtrl.readUInt(); + const float value = nonRtServerCtrl.readFloat(); + + if (index < paramCount) { + const float fixedValue = carla_fixedValue( + paramDetails[index].min, + paramDetails[index].max, value); + paramDetails[index].value = fixedValue; + } + } break; + + // uint/index, bool/touch + case kPluginBridgeNonRtServerParameterTouch: + nonRtServerCtrl.readUInt(); + nonRtServerCtrl.readBool(); + break; + + // uint/index, float/value + case kPluginBridgeNonRtServerDefaultValue: { + const uint32_t index = nonRtServerCtrl.readUInt(); + const float value = nonRtServerCtrl.readFloat(); + + if (index < paramCount) + paramDetails[index].def = value; + } break; + + // int/index + case kPluginBridgeNonRtServerCurrentProgram: + nonRtServerCtrl.readInt(); + break; + + // int/index + case kPluginBridgeNonRtServerCurrentMidiProgram: + nonRtServerCtrl.readInt(); + break; + + // uint/index, uint/size, str[] (name) + case kPluginBridgeNonRtServerProgramName: { + nonRtServerCtrl.readUInt(); + + if (const uint32_t size = nonRtServerCtrl.readUInt()) + nonRtServerCtrl.skipRead(size); + } break; + + // uint/index, uint/bank, uint/program, uint/size, str[] (name) + case kPluginBridgeNonRtServerMidiProgramData: { + nonRtServerCtrl.readUInt(); + nonRtServerCtrl.readUInt(); + nonRtServerCtrl.readUInt(); + + // name + if (const uint32_t size = nonRtServerCtrl.readUInt()) + nonRtServerCtrl.skipRead(size); + } break; + + // uint/size, str[], uint/size, str[], uint/size, str[] + case kPluginBridgeNonRtServerSetCustomData: { + const uint32_t maxLocalValueLen = + clientBridgeVersion >= 10 ? 4096 : 16384; + + // type + const BridgeTextReader type(nonRtServerCtrl); + + // key + const BridgeTextReader key(nonRtServerCtrl); + + // value + const uint32_t valueSize = nonRtServerCtrl.readUInt(); + + // special case for big values + if (valueSize > maxLocalValueLen) { + const BridgeTextReader bigValueFilePath( + nonRtServerCtrl, valueSize); + + QString realBigValueFilePath(QString::fromUtf8( + bigValueFilePath.text)); + + QFile bigValueFile(realBigValueFilePath); + CARLA_SAFE_ASSERT_BREAK(bigValueFile.exists()); + + if (bigValueFile.open(QIODevice::ReadOnly)) { + add_custom_data(type.text, key.text, + bigValueFile.readAll() + .constData(), + false); + bigValueFile.remove(); + } + } else { + const BridgeTextReader value(nonRtServerCtrl, + valueSize); + + add_custom_data(type.text, key.text, value.text, + false); + } + + } break; + + // uint/size, str[] (filename, base64 content) + case kPluginBridgeNonRtServerSetChunkDataFile: { + // chunkFilePath + const BridgeTextReader chunkFilePath(nonRtServerCtrl); + + QString realChunkFilePath( + QString::fromUtf8(chunkFilePath.text)); + + QFile chunkFile(realChunkFilePath); + CARLA_SAFE_ASSERT_BREAK(chunkFile.exists()); + + if (chunkFile.open(QIODevice::ReadOnly)) { + chunk = QByteArray::fromBase64( + chunkFile.readAll()); + chunkFile.remove(); + } + } break; + + // uint/latency + case kPluginBridgeNonRtServerSetLatency: + nonRtServerCtrl.readUInt(); + break; + + // uint/index, uint/size, str[] (name) + case kPluginBridgeNonRtServerSetParameterText: { + nonRtServerCtrl.readInt(); + + if (const uint32_t size = nonRtServerCtrl.readUInt()) + nonRtServerCtrl.skipRead(size); + } break; + + case kPluginBridgeNonRtServerReady: + ready = true; + break; + + case kPluginBridgeNonRtServerSaved: + saved = true; + break; + + // ulong/window-id + case kPluginBridgeNonRtServerRespEmbedUI: + nonRtServerCtrl.readULong(); + break; + + // uint/width, uint/height + case kPluginBridgeNonRtServerResizeEmbedUI: + nonRtServerCtrl.readUInt(); + nonRtServerCtrl.readUInt(); + break; + + case kPluginBridgeNonRtServerUiClosed: + break; + + // uint/size, str[] + case kPluginBridgeNonRtServerError: { + const BridgeTextReader error(nonRtServerCtrl); + timedErr = true; + blog(LOG_ERROR, "[carla] %s", error.text); + setLastError(error.text); + } break; + } + } +} + +// ---------------------------------------------------------------------------- + +void carla_bridge::setLastError(const char *const error) +{ + bfree(lastError); + lastError = bstrdup(error); +} + +// ---------------------------------------------------------------------------- diff --git a/plugins/carla/carla-bridge.hpp b/plugins/carla/carla-bridge.hpp new file mode 100644 index 00000000000000..b354a2d57b1416 --- /dev/null +++ b/plugins/carla/carla-bridge.hpp @@ -0,0 +1,204 @@ +/* + * Carla plugin for OBS + * Copyright (C) 2023 Filipe Coelho + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#include +#include + +#include +#include +#include + +#include + +#include + +CARLA_BACKEND_USE_NAMESPACE + +// ---------------------------------------------------------------------------- +// custom class for allowing QProcess usage outside the main thread + +class BridgeProcess : public QProcess { + Q_OBJECT + +public: + BridgeProcess(const char *shmIds); + +public Q_SLOTS: + void start(); + void stop(); +}; + +// ---------------------------------------------------------------------------- +// relevant information for an exposed plugin parameter + +struct carla_param_data { + uint32_t hints = 0; + float value = 0.f; + float def = 0.f; + float min = 0.f; + float max = 1.f; + float step = 0.01f; + CarlaString name; + CarlaString symbol; + CarlaString unit; +}; + +// ---------------------------------------------------------------------------- +// information about the currently active plugin + +struct carla_bridge_info { + BinaryType btype = BINARY_NONE; + PluginType ptype = PLUGIN_NONE; + uint32_t hints = 0; + uint32_t options = PLUGIN_OPTIONS_NULL; + bool hasCV = false; + uint32_t numAudioIns = 0; + uint32_t numAudioOuts = 0; + int64_t uniqueId = 0; + CarlaString filename; + CarlaString label; + CarlaString name; + + void clear() + { + btype = BINARY_NONE; + ptype = PLUGIN_NONE; + hints = 0; + options = PLUGIN_OPTIONS_NULL; + hasCV = false; + numAudioIns = numAudioOuts = 0; + uniqueId = 0; + filename.clear(); + label.clear(); + name.clear(); + } +}; + +// ---------------------------------------------------------------------------- +// bridge callbacks, triggered during carla_bridge::idle() + +struct carla_bridge_callback { + virtual ~carla_bridge_callback(){}; + virtual void bridge_parameter_changed(uint index, float value) = 0; +}; + +// ---------------------------------------------------------------------------- +// bridge implementation + +struct carla_bridge { + carla_bridge_callback *callback = nullptr; + + // cached parameter info + uint32_t paramCount = 0; + carla_param_data *paramDetails = nullptr; + + // cached plugin info + carla_bridge_info info; + QByteArray chunk; + std::vector customData; + + ~carla_bridge() + { + delete[] paramDetails; + clear_custom_data(); + bfree(lastError); + } + + // initialize bridge shared memory details + bool init(uint32_t maxBufferSize, double sampleRate); + + // stop bridge process and cleanup shared memory + void cleanup(bool clearPluginData = true); + + // start plugin bridge + bool start(BinaryType btype, PluginType ptype, const char *label, + const char *filename, int64_t uniqueId); + + // check if plugin bridge process is running + // return status might be wrong when called outside the main thread + bool is_running() const; + + // to be called at regular intervals, from the main thread + // returns false if bridge process is not running + bool idle(); + + // wait on RT client, making sure it is still active + // returns true on success + // NOTE: plugin will be deactivated on next `idle()` if timed out + bool wait(const char *action, uint msecs); + + // change a plugin parameter value + void set_value(uint index, float value); + + // show the plugin's custom UI + void show_ui(); + + // [de]activate, a deactivated plugin does not process any audio + bool is_active() const noexcept; + void activate(); + void deactivate(); + + // reactivate and reload plugin information + void reload(); + + // restore current state from known info, useful when bridge crashes + void restore_state(); + + // process plugin audio + // frames must be <= `maxBufferSize` as passed during `init` + void process(float *buffers[MAX_AV_PLANES], uint32_t frames); + + // add or replace custom data (non-parameter plugin values) + void add_custom_data(const char *type, const char *key, + const char *value, bool sendToPlugin = true); + + // inform plugin that all custom data has been loaded + // required after loading plugin state + void custom_data_loaded(); + + // clear all custom data stored so far + void clear_custom_data(); + + // load plugin state as base64 chunk + // NOTE: do not save parameter values for plugins using "chunks" + void load_chunk(const char *b64chunk); + + // request plugin bridge to save and report back its internal state + // must be called just before saving plugin state + void save_and_wait(); + + // change the maximum expected buffer size + // plugin is temporarily deactivated during the change + void set_buffer_size(uint32_t maxBufferSize); + + // get last known error, e.g. reason for last bridge start to fail + const char *get_last_error() const noexcept; + +private: + bool activated = false; + bool pendingPing = false; + bool ready = false; + bool saved = false; + bool timedErr = false; + bool timedOut = false; + uint32_t bufferSize = 0; + uint32_t clientBridgeVersion = 0; + char *lastError = nullptr; + + BridgeAudioPool audiopool; + BridgeRtClientControl rtClientCtrl; + BridgeNonRtClientControl nonRtClientCtrl; + BridgeNonRtServerControl nonRtServerCtrl; + + BridgeProcess *childprocess = nullptr; + + void readMessages(); + void setLastError(const char *error); +}; + +// ---------------------------------------------------------------------------- diff --git a/plugins/carla/carla-patchbay-wrapper.c b/plugins/carla/carla-patchbay-wrapper.c new file mode 100644 index 00000000000000..d3f2a6f1851ebe --- /dev/null +++ b/plugins/carla/carla-patchbay-wrapper.c @@ -0,0 +1,517 @@ +/* + * Carla plugin for OBS + * Copyright (C) 2023 Filipe Coelho + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "carla-wrapper.h" +#include "common.h" +#include "qtutils.h" + +#include + +#include "CarlaNativePlugin.h" + +// If this changes we need to adapt Carla side for matching port count +_Static_assert(MAX_AV_PLANES == 8, "expected 8 IO"); + +// ---------------------------------------------------------------------------- +// helper methods + +struct carla_main_thread_param_change { + const NativePluginDescriptor *descriptor; + NativePluginHandle handle; + uint32_t index; + float value; +}; + +static void carla_main_thread_param_change(void *data) +{ + struct carla_main_thread_param_change *priv = data; + priv->descriptor->ui_set_parameter_value(priv->handle, priv->index, + priv->value); + bfree(data); +} + +// ---------------------------------------------------------------------------- +// private data methods + +struct carla_param_data { + uint32_t hints; + float min, max; +}; + +struct carla_priv { + obs_source_t *source; + uint32_t bufferSize; + double sampleRate; + const NativePluginDescriptor *descriptor; + NativePluginHandle handle; + NativeHostDescriptor host; + NativeTimeInfo timeInfo; + + // cached parameter info + uint32_t paramCount; + struct carla_param_data *paramDetails; + + // update properties when timeout is reached, 0 means do nothing + uint64_t update_request; + + // keep track of active state + volatile bool activated; +}; + +// ---------------------------------------------------------------------------- +// carla host methods + +static uint32_t host_get_buffer_size(NativeHostHandle handle) +{ + const struct carla_priv *priv = handle; + return priv->bufferSize; +} + +static double host_get_sample_rate(NativeHostHandle handle) +{ + const struct carla_priv *priv = handle; + return priv->sampleRate; +} + +static bool host_is_offline(NativeHostHandle handle) +{ + UNUSED_PARAMETER(handle); + return false; +} + +static const NativeTimeInfo *host_get_time_info(NativeHostHandle handle) +{ + const struct carla_priv *priv = handle; + return &priv->timeInfo; +} + +static bool host_write_midi_event(NativeHostHandle handle, + const NativeMidiEvent *event) +{ + UNUSED_PARAMETER(handle); + UNUSED_PARAMETER(event); + return false; +} + +static void host_ui_parameter_changed(NativeHostHandle handle, uint32_t index, + float value) +{ + struct carla_priv *priv = handle; + + if (index >= priv->paramCount) + return; + + // skip parameters that we do not show + const uint32_t hints = priv->paramDetails[index].hints; + if ((hints & NATIVE_PARAMETER_IS_ENABLED) == 0) + return; + if (hints & NATIVE_PARAMETER_IS_OUTPUT) + return; + + char pname[PARAM_NAME_SIZE] = PARAM_NAME_INIT; + param_index_to_name(index, pname); + + obs_source_t *source = priv->source; + obs_data_t *settings = obs_source_get_settings(source); + + /**/ if (hints & NATIVE_PARAMETER_IS_BOOLEAN) + obs_data_set_bool(settings, pname, value > 0.5f ? 1.f : 0.f); + else if (hints & NATIVE_PARAMETER_IS_INTEGER) + obs_data_set_int(settings, pname, (int)value); + else + obs_data_set_double(settings, pname, value); + + obs_data_release(settings); + + postpone_update_request(&priv->update_request); +} + +static void host_ui_midi_program_changed(NativeHostHandle handle, + uint8_t channel, uint32_t bank, + uint32_t program) +{ + UNUSED_PARAMETER(handle); + UNUSED_PARAMETER(channel); + UNUSED_PARAMETER(bank); + UNUSED_PARAMETER(program); +} + +static void host_ui_custom_data_changed(NativeHostHandle handle, + const char *key, const char *value) +{ + UNUSED_PARAMETER(handle); + UNUSED_PARAMETER(key); + UNUSED_PARAMETER(value); +} + +static void host_ui_closed(NativeHostHandle handle) +{ + UNUSED_PARAMETER(handle); +} + +static const char *host_ui_open_file(NativeHostHandle handle, bool isDir, + const char *title, const char *filter) +{ + UNUSED_PARAMETER(handle); + return carla_qt_file_dialog(false, isDir, title, filter); +} + +static const char *host_ui_save_file(NativeHostHandle handle, bool isDir, + const char *title, const char *filter) +{ + UNUSED_PARAMETER(handle); + return carla_qt_file_dialog(true, isDir, title, filter); +} + +static intptr_t host_dispatcher(NativeHostHandle handle, + NativeHostDispatcherOpcode opcode, + int32_t index, intptr_t value, void *ptr, + float opt) +{ + UNUSED_PARAMETER(index); + UNUSED_PARAMETER(value); + UNUSED_PARAMETER(ptr); + UNUSED_PARAMETER(opt); + + struct carla_priv *priv = handle; + + switch (opcode) { + case NATIVE_HOST_OPCODE_NULL: + case NATIVE_HOST_OPCODE_RELOAD_MIDI_PROGRAMS: + case NATIVE_HOST_OPCODE_UPDATE_MIDI_PROGRAM: + break; + case NATIVE_HOST_OPCODE_UPDATE_PARAMETER: + case NATIVE_HOST_OPCODE_RELOAD_PARAMETERS: + case NATIVE_HOST_OPCODE_RELOAD_ALL: + postpone_update_request(&priv->update_request); + break; + case NATIVE_HOST_OPCODE_GET_FILE_PATH: + case NATIVE_HOST_OPCODE_HOST_IDLE: + case NATIVE_HOST_OPCODE_INTERNAL_PLUGIN: + case NATIVE_HOST_OPCODE_PREVIEW_BUFFER_DATA: + case NATIVE_HOST_OPCODE_QUEUE_INLINE_DISPLAY: + case NATIVE_HOST_OPCODE_REQUEST_IDLE: + case NATIVE_HOST_OPCODE_UI_UNAVAILABLE: + case NATIVE_HOST_OPCODE_UI_RESIZE: + case NATIVE_HOST_OPCODE_UI_TOUCH_PARAMETER: + break; + } + + return 0; +} + +// ---------------------------------------------------------------------------- +// carla + obs integration methods + +struct carla_priv *carla_priv_create(obs_source_t *source, + enum buffer_size_mode bufsize, + uint32_t srate) +{ + const NativePluginDescriptor *descriptor = + carla_get_native_patchbay_obs_plugin(); + if (descriptor == NULL) + return NULL; + + struct carla_priv *priv = bzalloc(sizeof(struct carla_priv)); + if (priv == NULL) + return NULL; + + priv->source = source; + priv->bufferSize = bufsize_mode_to_frames(bufsize); + priv->sampleRate = srate; + priv->descriptor = descriptor; + + { + // resource dir swaps .../lib/carla for .../share/carla/resources + const char *const binpath = get_carla_bin_path(); + const size_t binlen = strlen(binpath); + char *const respath = bmalloc(binlen + 13); + memcpy(respath, binpath, binlen - 9); + memcpy(respath + (binlen - 9), "share/carla/resources", 22); + + NativeHostDescriptor host = { + .handle = priv, + .resourceDir = respath, + .uiName = "Carla-OBS", + .uiParentId = 0, + .get_buffer_size = host_get_buffer_size, + .get_sample_rate = host_get_sample_rate, + .is_offline = host_is_offline, + .get_time_info = host_get_time_info, + .write_midi_event = host_write_midi_event, + .ui_parameter_changed = host_ui_parameter_changed, + .ui_midi_program_changed = host_ui_midi_program_changed, + .ui_custom_data_changed = host_ui_custom_data_changed, + .ui_closed = host_ui_closed, + .ui_open_file = host_ui_open_file, + .ui_save_file = host_ui_save_file, + .dispatcher = host_dispatcher}; + priv->host = host; + } + + { + NativeTimeInfo timeInfo = { + .usecs = os_gettime_ns() / 1000, + }; + priv->timeInfo = timeInfo; + } + + priv->handle = descriptor->instantiate(&priv->host); + if (priv->handle == NULL) { + bfree(priv); + return NULL; + } + + return priv; +} + +void carla_priv_destroy(struct carla_priv *priv) +{ + if (priv->activated) + carla_priv_deactivate(priv); + + priv->descriptor->cleanup(priv->handle); + bfree(priv->paramDetails); + bfree((char *)priv->host.resourceDir); + bfree(priv); +} + +// ---------------------------------------------------------------------------- + +void carla_priv_activate(struct carla_priv *priv) +{ + priv->descriptor->activate(priv->handle); + priv->activated = true; +} + +void carla_priv_deactivate(struct carla_priv *priv) +{ + priv->activated = false; + priv->descriptor->deactivate(priv->handle); +} + +void carla_priv_process_audio(struct carla_priv *priv, + float *buffers[MAX_AV_PLANES], uint32_t frames) +{ + priv->timeInfo.usecs = os_gettime_ns() / 1000; + priv->descriptor->process(priv->handle, buffers, buffers, frames, NULL, + 0); +} + +void carla_priv_idle(struct carla_priv *priv) +{ + priv->descriptor->ui_idle(priv->handle); + handle_update_request(priv->source, &priv->update_request); +} + +void carla_priv_save(struct carla_priv *priv, obs_data_t *settings) +{ + char *state = priv->descriptor->get_state(priv->handle); + if (state) { + obs_data_set_string(settings, "state", state); + free(state); + } +} + +void carla_priv_load(struct carla_priv *priv, obs_data_t *settings) +{ + const char *state = obs_data_get_string(settings, "state"); + if (state) + priv->descriptor->set_state(priv->handle, state); +} + +// ---------------------------------------------------------------------------- + +uint32_t carla_priv_get_num_channels(struct carla_priv *priv) +{ + UNUSED_PARAMETER(priv); + return 8; +} + +void carla_priv_set_buffer_size(struct carla_priv *priv, + enum buffer_size_mode bufsize) +{ + const uint32_t new_buffer_size = bufsize_mode_to_frames(bufsize); + const bool activated = priv->activated; + + if (activated) + carla_priv_deactivate(priv); + + priv->bufferSize = new_buffer_size; + priv->descriptor->dispatcher(priv->handle, + NATIVE_PLUGIN_OPCODE_BUFFER_SIZE_CHANGED, + new_buffer_size, 0, NULL, 0.f); + + if (activated) + carla_priv_activate(priv); +} + +// ---------------------------------------------------------------------------- + +static bool carla_priv_param_changed(void *data, obs_properties_t *props, + obs_property_t *property, + obs_data_t *settings) +{ + UNUSED_PARAMETER(props); + + struct carla_priv *priv = data; + + const char *const pname = obs_property_name(property); + if (pname == NULL) + return false; + + const char *pname2 = pname + 1; + while (*pname2 == '0') + ++pname2; + + const int pindex = atoi(pname2); + + if (pindex < 0 || pindex >= (int)priv->paramCount) + return false; + + const float min = priv->paramDetails[pindex].min; + const float max = priv->paramDetails[pindex].max; + + float value; + switch (obs_property_get_type(property)) { + case OBS_PROPERTY_BOOL: + value = obs_data_get_bool(settings, pname) ? max : min; + break; + case OBS_PROPERTY_INT: + value = (float)obs_data_get_int(settings, pname); + if (value < min) + value = min; + else if (value > max) + value = max; + break; + case OBS_PROPERTY_FLOAT: + value = (float)obs_data_get_double(settings, pname); + if (value < min) + value = min; + else if (value > max) + value = max; + break; + default: + return false; + } + + priv->descriptor->set_parameter_value(priv->handle, pindex, value); + + // UI param change notification needs to happen on main thread + struct carla_main_thread_param_change mchange = { + .descriptor = priv->descriptor, + .handle = priv->handle, + .index = pindex, + .value = value}; + struct carla_main_thread_param_change *mchangeptr = + bmalloc(sizeof(mchange)); + *mchangeptr = mchange; + carla_qt_callback_on_main_thread(carla_main_thread_param_change, + mchangeptr); + + return false; +} + +static bool carla_priv_show_gui_callback(obs_properties_t *props, + obs_property_t *property, void *data) +{ + UNUSED_PARAMETER(props); + UNUSED_PARAMETER(property); + + struct carla_priv *priv = data; + + priv->descriptor->ui_show(priv->handle, true); + + return false; +} + +void carla_priv_readd_properties(struct carla_priv *priv, + obs_properties_t *props, bool reset) +{ + obs_data_t *settings = obs_source_get_settings(priv->source); + + if (priv->descriptor->hints & NATIVE_PLUGIN_HAS_UI) { + obs_properties_add_button2(props, PROP_SHOW_GUI, + obs_module_text("Show custom GUI"), + carla_priv_show_gui_callback, priv); + } + + uint32_t params = priv->descriptor->get_parameter_count(priv->handle); + if (params > MAX_PARAMS) + params = MAX_PARAMS; + + bfree(priv->paramDetails); + priv->paramCount = params; + priv->paramDetails = bzalloc(sizeof(struct carla_param_data) * params); + + char pname[PARAM_NAME_SIZE] = PARAM_NAME_INIT; + + for (uint32_t i = 0; i < params; ++i) { + const NativeParameter *const info = + priv->descriptor->get_parameter_info(priv->handle, i); + + if ((info->hints & NATIVE_PARAMETER_IS_ENABLED) == 0) + continue; + if (info->hints & NATIVE_PARAMETER_IS_OUTPUT) + continue; + + param_index_to_name(i, pname); + priv->paramDetails[i].hints = info->hints; + priv->paramDetails[i].min = info->ranges.min; + priv->paramDetails[i].max = info->ranges.max; + + obs_property_t *prop; + + if (info->hints & NATIVE_PARAMETER_IS_BOOLEAN) { + prop = obs_properties_add_bool(props, pname, + info->name); + + obs_data_set_default_bool(settings, pname, + info->ranges.def == + info->ranges.max); + + if (reset) + obs_data_set_bool(settings, pname, + info->ranges.def == + info->ranges.max); + } else if (info->hints & NATIVE_PARAMETER_IS_INTEGER) { + prop = obs_properties_add_int_slider( + props, pname, info->name, (int)info->ranges.min, + (int)info->ranges.max, (int)info->ranges.step); + + obs_data_set_default_int(settings, pname, + (int)info->ranges.def); + + if (info->unit && *info->unit) + obs_property_int_set_suffix(prop, info->unit); + + if (reset) + obs_data_set_int(settings, pname, + (int)info->ranges.def); + } else { + prop = obs_properties_add_float_slider( + props, pname, info->name, info->ranges.min, + info->ranges.max, info->ranges.step); + + obs_data_set_default_double(settings, pname, + info->ranges.def); + + if (info->unit && *info->unit) + obs_property_float_set_suffix(prop, info->unit); + + if (reset) + obs_data_set_double(settings, pname, + info->ranges.def); + } + + obs_property_set_modified_callback2( + prop, carla_priv_param_changed, priv); + } + + obs_data_release(settings); +} + +// ---------------------------------------------------------------------------- diff --git a/plugins/carla/carla-wrapper.h b/plugins/carla/carla-wrapper.h new file mode 100644 index 00000000000000..f7dd37c9aba433 --- /dev/null +++ b/plugins/carla/carla-wrapper.h @@ -0,0 +1,73 @@ +/* + * Carla plugin for OBS + * Copyright (C) 2023 Filipe Coelho + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#include + +// maximum buffer used, can be smaller +#define MAX_AUDIO_BUFFER_SIZE 512 + +enum buffer_size_mode { + buffer_size_direct, + buffer_size_buffered_128, + buffer_size_buffered_256, + buffer_size_buffered_512, + buffer_size_buffered_max = buffer_size_buffered_512 +}; + +// ---------------------------------------------------------------------------- +// helper methods + +static inline uint32_t bufsize_mode_to_frames(enum buffer_size_mode bufsize) +{ + switch (bufsize) { + case buffer_size_buffered_128: + return 128; + case buffer_size_buffered_256: + return 256; + default: + return MAX_AUDIO_BUFFER_SIZE; + } +} + +// ---------------------------------------------------------------------------- +// carla + obs integration methods + +#ifdef __cplusplus +extern "C" { +#endif + +struct carla_priv; + +struct carla_priv *carla_priv_create(obs_source_t *source, + enum buffer_size_mode bufsize, + uint32_t srate); +void carla_priv_destroy(struct carla_priv *carla); + +void carla_priv_activate(struct carla_priv *carla); +void carla_priv_deactivate(struct carla_priv *carla); +void carla_priv_process_audio(struct carla_priv *carla, + float *buffers[MAX_AV_PLANES], uint32_t frames); + +void carla_priv_idle(struct carla_priv *carla); + +void carla_priv_save(struct carla_priv *carla, obs_data_t *settings); +void carla_priv_load(struct carla_priv *carla, obs_data_t *settings); + +uint32_t carla_priv_get_num_channels(struct carla_priv *carla); + +void carla_priv_set_buffer_size(struct carla_priv *carla, + enum buffer_size_mode bufsize); + +void carla_priv_readd_properties(struct carla_priv *carla, + obs_properties_t *props, bool reset); + +#ifdef __cplusplus +} +#endif + +// ---------------------------------------------------------------------------- diff --git a/plugins/carla/carla.c b/plugins/carla/carla.c new file mode 100644 index 00000000000000..f02470721d689f --- /dev/null +++ b/plugins/carla/carla.c @@ -0,0 +1,530 @@ +/* + * Carla plugin for OBS + * Copyright (C) 2023 Filipe Coelho + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +// for audio generator thread +#include + +#include +#include + +#include "carla-wrapper.h" +#include "common.h" + +#ifndef CARLA_MODULE_ID +#error CARLA_MODULE_ID undefined +#endif + +#ifndef CARLA_MODULE_NAME +#error CARLA_MODULE_NAME undefined +#endif + +// -------------------------------------------------------------------------------------------------------------------- + +struct carla_data { + // carla host details, intentionally kept private so we can easily swap internals + struct carla_priv *priv; + + // current OBS config + bool activated; + uint32_t sample_rate; + obs_source_t *source; + + // filter related options + size_t channels; + + // audio generator thread + bool audiogen_enabled; + volatile bool audiogen_running; + pthread_t audiogen_thread; + + // internal buffering + float *buffers[MAX_AV_PLANES]; + uint16_t buffer_head; + uint16_t buffer_tail; + enum buffer_size_mode buffer_size_mode; + + // dummy buffer for unused audio channels + float *dummybuffer; +}; + +// -------------------------------------------------------------------------------------------------------------------- +// private methods + +static enum speaker_layout carla_obs_channels_to_speakers(const size_t channels) +{ + switch (channels) { + case 1: + return SPEAKERS_MONO; + case 2: + return SPEAKERS_STEREO; + case 3: + return SPEAKERS_2POINT1; + case 4: + return SPEAKERS_4POINT0; + case 5: + return SPEAKERS_4POINT1; + case 6: + return SPEAKERS_5POINT1; + // FIXME missing case for 7 channels + case 8: + return SPEAKERS_7POINT1; + // use stereo as fallback + default: + return SPEAKERS_STEREO; + } +} + +static void *carla_obs_audio_gen_thread(void *data) +{ + struct carla_data *carla = data; + + struct obs_source_audio out = { + .format = AUDIO_FORMAT_FLOAT_PLANAR, + .samples_per_sec = carla->sample_rate, + }; + + for (uint8_t c = 0; c < MAX_AV_PLANES; ++c) + out.data[c] = (const uint8_t *)carla->buffers[c]; + + const uint32_t sample_rate = carla->sample_rate; + const uint64_t start_time = out.timestamp = os_gettime_ns(); + uint64_t total_samples = 0; + + while (carla->audiogen_running) { + const uint32_t buffer_size = + bufsize_mode_to_frames(carla->buffer_size_mode); + + out.frames = buffer_size; + out.speakers = carla_obs_channels_to_speakers( + carla_priv_get_num_channels(carla->priv)); + carla_priv_process_audio(carla->priv, carla->buffers, + buffer_size); + obs_source_output_audio(carla->source, &out); + + if (!carla->audiogen_running) + break; + + total_samples += buffer_size; + out.timestamp = start_time + + audio_frames_to_ns(sample_rate, total_samples); + + os_sleepto_ns_fast(out.timestamp); + } + + return NULL; +} + +static void carla_obs_idle_callback(void *data, float unused) +{ + UNUSED_PARAMETER(unused); + struct carla_data *carla = data; + carla_priv_idle(carla->priv); +} + +// -------------------------------------------------------------------------------------------------------------------- +// obs plugin methods + +static void carla_obs_deactivate(void *data); + +static const char *carla_obs_get_name(void *data) +{ + return !strcmp(data, "filter") + ? obs_module_text(CARLA_MODULE_NAME " Filter") + : obs_module_text(CARLA_MODULE_NAME " Generator/Source"); +} + +static void *carla_obs_create(obs_data_t *settings, obs_source_t *source, + bool isFilter) +{ + UNUSED_PARAMETER(settings); + + const audio_t *audio = obs_get_audio(); + const size_t channels = audio_output_get_channels(audio); + const uint32_t sample_rate = audio_output_get_sample_rate(audio); + + if (sample_rate == 0 || (isFilter && channels == 0)) + return NULL; + + struct carla_data *carla = bzalloc(sizeof(*carla)); + if (carla == NULL) + return NULL; + + for (uint8_t c = 0; c < MAX_AV_PLANES; ++c) { + carla->buffers[c] = + bzalloc(sizeof(float) * MAX_AUDIO_BUFFER_SIZE); + if (carla->buffers[c] == NULL) + goto fail1; + } + + carla->dummybuffer = bzalloc(sizeof(float) * MAX_AUDIO_BUFFER_SIZE); + if (carla->dummybuffer == NULL) + goto fail2; + + // prefer no-latency mode for filter, lowest latency for generator + const enum buffer_size_mode bufsize = + isFilter ? buffer_size_direct : buffer_size_buffered_128; + + struct carla_priv *priv = + carla_priv_create(source, bufsize, sample_rate); + if (carla == NULL) + goto fail3; + + carla->priv = priv; + carla->source = source; + carla->channels = channels; + carla->sample_rate = sample_rate; + + carla->buffer_head = 0; + carla->buffer_tail = UINT16_MAX; + carla->buffer_size_mode = bufsize; + + // audio generator, aka input source + carla->audiogen_enabled = !isFilter; + + obs_add_tick_callback(carla_obs_idle_callback, carla); + + return carla; + +fail3: + bfree(carla->dummybuffer); + +fail2: + for (uint8_t c = 0; c < MAX_AV_PLANES; ++c) + bfree(carla->buffers[c]); + +fail1: + bfree(carla); + return NULL; +} + +static void *carla_obs_create_filter(obs_data_t *settings, obs_source_t *source) +{ + return carla_obs_create(settings, source, true); +} + +static void *carla_obs_create_input(obs_data_t *settings, obs_source_t *source) +{ + return carla_obs_create(settings, source, false); +} + +static void carla_obs_destroy(void *data) +{ + struct carla_data *carla = data; + + if (carla->activated) + carla_obs_deactivate(carla); + + obs_remove_tick_callback(carla_obs_idle_callback, carla); + + carla_priv_destroy(carla->priv); + + bfree(carla->dummybuffer); + for (uint8_t c = 0; c < MAX_AV_PLANES; ++c) + bfree(carla->buffers[c]); + bfree(carla); +} + +static bool carla_obs_bufsize_callback(void *data, obs_properties_t *props, + obs_property_t *list, + obs_data_t *settings) +{ + UNUSED_PARAMETER(props); + UNUSED_PARAMETER(list); + + struct carla_data *carla = data; + + enum buffer_size_mode bufsize; + const char *const value = + obs_data_get_string(settings, PROP_BUFFER_SIZE); + + /**/ if (!strcmp(value, "direct")) + bufsize = buffer_size_direct; + else if (!strcmp(value, "128")) + bufsize = buffer_size_buffered_128; + else if (!strcmp(value, "256")) + bufsize = buffer_size_buffered_256; + else if (!strcmp(value, "512")) + bufsize = buffer_size_buffered_512; + else + return false; + + if (carla->buffer_size_mode == bufsize) + return false; + + // deactivate first, to stop audio from processing + carla_priv_deactivate(carla->priv); + + // safely change to new buffer size + carla->buffer_size_mode = bufsize; + carla_priv_set_buffer_size(carla->priv, bufsize); + + // activate again + carla_priv_activate(carla->priv); + + return false; +} + +static obs_properties_t *carla_obs_get_properties(void *data) +{ + struct carla_data *carla = data; + + obs_properties_t *props = obs_properties_create(); + + obs_property_t *list = obs_properties_add_list( + props, PROP_BUFFER_SIZE, obs_module_text("Buffer Size"), + OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + + if (carla->audiogen_enabled) { + obs_property_list_add_string( + list, obs_module_text("128 samples"), "128"); + obs_property_list_add_string( + list, obs_module_text("256 samples"), "256"); + obs_property_list_add_string( + list, obs_module_text("512 samples"), "512"); + } else { + obs_property_list_add_string( + list, obs_module_text("Direct (variable buffer)"), + "direct"); + obs_property_list_add_string( + list, + obs_module_text( + "128 samples (fixed buffer with latency)"), + "128"); + obs_property_list_add_string( + list, + obs_module_text( + "256 samples (fixed buffer with latency)"), + "256"); + obs_property_list_add_string( + list, + obs_module_text( + "512 samples (fixed buffer with latency)"), + "512"); + } + + obs_property_set_modified_callback2(list, carla_obs_bufsize_callback, + carla); + + carla_priv_readd_properties(carla->priv, props, false); + + return props; +} + +static void carla_obs_activate(void *data) +{ + struct carla_data *carla = data; + assert(!carla->activated); + + if (carla->activated) + return; + + carla->activated = true; + + carla_priv_activate(carla->priv); + + if (carla->audiogen_enabled) { + assert(!carla->audiogen_running); + carla->audiogen_running = true; + pthread_create(&carla->audiogen_thread, NULL, + carla_obs_audio_gen_thread, carla); + } +} + +static void carla_obs_deactivate(void *data) +{ + struct carla_data *carla = data; + assert(carla->activated); + + if (!carla->activated) + return; + + carla->activated = false; + + if (carla->audiogen_running) { + carla->audiogen_running = false; + pthread_join(carla->audiogen_thread, NULL); + } + + carla_priv_deactivate(carla->priv); +} + +static void carla_obs_filter_audio_direct(struct carla_data *carla, + struct obs_audio_data *audio) +{ + uint32_t frames = audio->frames; + float *obsbuffers[MAX_AV_PLANES]; + + // process in blocks up to MAX_AUDIO_BUFFER_SIZE + for (uint32_t i = 0; frames != 0;) { + const uint32_t stepframes = frames >= MAX_AUDIO_BUFFER_SIZE + ? MAX_AUDIO_BUFFER_SIZE + : frames; + + for (uint8_t c = 0; c < MAX_AV_PLANES; ++c) + obsbuffers[c] = audio->data[c] + ? ((float *)audio->data[c] + i) + : carla->dummybuffer; + + carla_priv_process_audio(carla->priv, obsbuffers, stepframes); + + memset(carla->dummybuffer, 0, sizeof(float) * stepframes); + + i += stepframes; + frames -= stepframes; + } +} + +static void carla_obs_filter_audio_buffered(struct carla_data *carla, + struct obs_audio_data *audio) +{ + const uint32_t buffer_size = + bufsize_mode_to_frames(carla->buffer_size_mode); + const size_t channels = carla->channels; + const uint32_t frames = audio->frames; + + // cast audio buffers to correct type + float *obsbuffers[MAX_AV_PLANES]; + + for (uint8_t c = 0; c < MAX_AV_PLANES; ++c) + obsbuffers[c] = audio->data[c] ? (float *)audio->data[c] + : carla->dummybuffer; + + // preload some variables before looping section + uint16_t buffer_head = carla->buffer_head; + uint16_t buffer_tail = carla->buffer_tail; + + for (uint32_t i = 0, h, t; i < frames; ++i) { + // OBS -> plugin internal buffering + h = buffer_head++; + + for (uint8_t c = 0; c < channels; ++c) + carla->buffers[c][h] = obsbuffers[c][i]; + + // when we reach the target buffer size, do audio processing + if (buffer_head == buffer_size) { + buffer_head = 0; + carla_priv_process_audio(carla->priv, carla->buffers, + buffer_size); + memset(carla->dummybuffer, 0, + sizeof(float) * buffer_size); + + // we can now begin to copy back the buffer into OBS + if (buffer_tail == UINT16_MAX) + buffer_tail = 0; + } + + if (buffer_tail == UINT16_MAX) { + // buffering still taking place, skip until first audio cycle + for (uint8_t c = 0; c < channels; ++c) + obsbuffers[c][i] = 0.f; + } else { + // plugin -> OBS buffer copy + t = buffer_tail++; + + for (uint8_t c = 0; c < channels; ++c) + obsbuffers[c][i] = carla->buffers[c][t]; + + if (buffer_tail == buffer_size) + buffer_tail = 0; + } + } + + carla->buffer_head = buffer_head; + carla->buffer_tail = buffer_tail; +} + +static struct obs_audio_data * +carla_obs_filter_audio(void *data, struct obs_audio_data *audio) +{ + struct carla_data *carla = data; + + switch (carla->buffer_size_mode) { + case buffer_size_direct: + carla_obs_filter_audio_direct(carla, audio); + break; + case buffer_size_buffered_128: + case buffer_size_buffered_256: + case buffer_size_buffered_512: + carla_obs_filter_audio_buffered(carla, audio); + break; + } + + return audio; +} + +static void carla_obs_save(void *data, obs_data_t *settings) +{ + struct carla_data *carla = data; + carla_priv_save(carla->priv, settings); +} + +static void carla_obs_load(void *data, obs_data_t *settings) +{ + struct carla_data *carla = data; + carla_priv_load(carla->priv, settings); +} + +// -------------------------------------------------------------------------------------------------------------------- + +OBS_DECLARE_MODULE() +OBS_MODULE_USE_DEFAULT_LOCALE("carla", "en-US") +OBS_MODULE_AUTHOR("Filipe Coelho") +const char *obs_module_name(void) +{ + return CARLA_MODULE_NAME; +} + +bool obs_module_load(void) +{ + const char *carla_bin_path = get_carla_bin_path(); + if (!carla_bin_path) { + blog(LOG_WARNING, + "[" CARLA_MODULE_ID "]" + " failed to find binaries, will not load module"); + return false; + } + blog(LOG_INFO, "[" CARLA_MODULE_ID "] using binary path %s", + carla_bin_path); + + static const struct obs_source_info filter = { + .id = CARLA_MODULE_ID "-filter", + .type = OBS_SOURCE_TYPE_FILTER, + .output_flags = OBS_SOURCE_AUDIO, + .get_name = carla_obs_get_name, + .create = carla_obs_create_filter, + .destroy = carla_obs_destroy, + .get_properties = carla_obs_get_properties, + .activate = carla_obs_activate, + .deactivate = carla_obs_deactivate, + .filter_audio = carla_obs_filter_audio, + .save = carla_obs_save, + .load = carla_obs_load, + .type_data = "filter", + .icon_type = OBS_ICON_TYPE_PROCESS_AUDIO_OUTPUT, + }; + obs_register_source(&filter); + + static const struct obs_source_info input = { + .id = CARLA_MODULE_ID "-input", + .type = OBS_SOURCE_TYPE_INPUT, + .output_flags = OBS_SOURCE_AUDIO, + .get_name = carla_obs_get_name, + .create = carla_obs_create_input, + .destroy = carla_obs_destroy, + .get_properties = carla_obs_get_properties, + .activate = carla_obs_activate, + .deactivate = carla_obs_deactivate, + .save = carla_obs_save, + .load = carla_obs_load, + .type_data = "input", + .icon_type = OBS_ICON_TYPE_AUDIO_OUTPUT, + }; + obs_register_source(&input); + + return true; +} + +// -------------------------------------------------------------------------------------------------------------------- diff --git a/plugins/carla/cmake/macos/Info.plist.in b/plugins/carla/cmake/macos/Info.plist.in new file mode 100644 index 00000000000000..c2d597444a48cd --- /dev/null +++ b/plugins/carla/cmake/macos/Info.plist.in @@ -0,0 +1,28 @@ + + + + + CFBundleName + obs-carla + CFBundleIdentifier + com.obsproject.carla-bridge + CFBundleVersion + ${MACOSX_BUNDLE_BUNDLE_VERSION} + CFBundleShortVersionString + ${MACOSX_BUNDLE_SHORT_VERSION_STRING} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleExecutable + carla-bridge + CFBundlePackageType + BNDL + CFBundleSupportedPlatforms + + MacOSX + + LSMinimumSystemVersion + ${CMAKE_OSX_DEPLOYMENT_TARGET} + NSHumanReadableCopyright + (c) 2023 Filipe Coelho + + diff --git a/plugins/carla/common.c b/plugins/carla/common.c new file mode 100644 index 00000000000000..ee029643778bb0 --- /dev/null +++ b/plugins/carla/common.c @@ -0,0 +1,150 @@ +/* + * Carla plugin for OBS + * Copyright (C) 2023 Filipe Coelho + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#if !(defined(__APPLE__) || defined(_WIN32)) +// needed for libdl stuff and strcasestr +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include +#include +#include +#endif + +#if defined(__linux__) || defined(__FreeBSD__) +#include +#endif + +#include + +#include +#include + +#include "common.h" + +// ---------------------------------------------------------------------------- + +static char *carla_bin_path = NULL; + +const char *get_carla_bin_path(void) +{ + if (carla_bin_path != NULL) + return carla_bin_path; + + const char *const utilspath = carla_get_library_folder(); + const size_t utilslen = strlen(utilspath); + char *binpath; + + binpath = bmalloc(utilslen + 28); + memcpy(binpath, utilspath, utilslen); +#ifdef _WIN32 + memcpy(binpath + utilslen, "\\carla-discovery-native.exe", 28); +#else + memcpy(binpath + utilslen, "/carla-discovery-native", 24); +#endif + + if (os_file_exists(binpath)) { + binpath[utilslen] = '\0'; + carla_bin_path = binpath; + return carla_bin_path; + } + + free(binpath); + +#if !(defined(__APPLE__) || defined(_WIN32)) + // check path of this OBS plugin as fallback + Dl_info info; + dladdr(get_carla_bin_path, &info); + binpath = realpath(info.dli_fname, NULL); + + if (binpath == NULL) + return NULL; + + // truncate to last separator + char *lastsep = strrchr(binpath, '/'); + if (lastsep == NULL) + goto free; + *lastsep = '\0'; + + if (os_file_exists(binpath)) { + carla_bin_path = bstrdup(binpath); + free(binpath); + return carla_bin_path; + } + +free: + free(binpath); +#endif // !(__APPLE__ || _WIN32) + + return carla_bin_path; +} + +void param_index_to_name(uint32_t index, char name[PARAM_NAME_SIZE]) +{ + name[1] = '0' + ((index / 100) % 10); + name[2] = '0' + ((index / 10) % 10); + name[3] = '0' + ((index / 1) % 10); +} + +void remove_all_props(obs_properties_t *props, obs_data_t *settings) +{ + obs_data_erase(settings, PROP_RELOAD_PLUGIN); + obs_properties_remove_by_name(props, PROP_RELOAD_PLUGIN); + + obs_data_erase(settings, PROP_SHOW_GUI); + obs_properties_remove_by_name(props, PROP_SHOW_GUI); + + obs_data_erase(settings, PROP_CHUNK); + obs_properties_remove_by_name(props, PROP_CHUNK); + + obs_data_erase(settings, PROP_CUSTOM_DATA); + obs_properties_remove_by_name(props, PROP_CUSTOM_DATA); + + char pname[PARAM_NAME_SIZE] = PARAM_NAME_INIT; + + for (uint32_t i = 0; i < MAX_PARAMS; ++i) { + param_index_to_name(i, pname); + obs_data_unset_default_value(settings, pname); + obs_data_erase(settings, pname); + obs_properties_remove_by_name(props, pname); + } +} + +void postpone_update_request(uint64_t *update_req) +{ + *update_req = os_gettime_ns(); +} + +void handle_update_request(obs_source_t *source, uint64_t *update_req) +{ + const uint64_t old_update_req = *update_req; + + if (old_update_req == 0) + return; + + const uint64_t now = os_gettime_ns(); + + // request in the future? + if (now < old_update_req) { + *update_req = now; + return; + } + + if (now - old_update_req >= 100000000ULL) // 100ms + { + *update_req = 0; + signal_handler_signal(obs_source_get_signal_handler(source), + "update_properties", NULL); + } +} + +void obs_module_unload(void) +{ + bfree(carla_bin_path); + carla_bin_path = NULL; +} + +// ---------------------------------------------------------------------------- diff --git a/plugins/carla/common.h b/plugins/carla/common.h new file mode 100644 index 00000000000000..42659702fee16a --- /dev/null +++ b/plugins/carla/common.h @@ -0,0 +1,47 @@ +/* + * Carla plugin for OBS + * Copyright (C) 2023 Filipe Coelho + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#include + +#define MAX_PARAMS 100 + +#define PARAM_NAME_SIZE 5 +#define PARAM_NAME_INIT \ + { \ + 'p', '0', '0', '0', '\0' \ + } + +// property names +#define PROP_LOAD_FILE "load-file" +#define PROP_SELECT_PLUGIN "select-plugin" +#define PROP_RELOAD_PLUGIN "reload" +#define PROP_BUFFER_SIZE "buffer-size" +#define PROP_SHOW_GUI "show-gui" + +#define PROP_CHUNK "chunk" +#define PROP_CUSTOM_DATA "customdata" + +// ---------------------------------------------------------------------------- + +#ifdef __cplusplus +extern "C" { +#endif + +const char *get_carla_bin_path(void); + +void param_index_to_name(uint32_t index, char name[PARAM_NAME_SIZE]); +void remove_all_props(obs_properties_t *props, obs_data_t *settings); + +void postpone_update_request(uint64_t *update_req); +void handle_update_request(obs_source_t *source, uint64_t *update_req); + +#ifdef __cplusplus +} +#endif + +// ---------------------------------------------------------------------------- diff --git a/plugins/carla/pluginlistdialog.cpp b/plugins/carla/pluginlistdialog.cpp new file mode 100644 index 00000000000000..2e94486b8281b5 --- /dev/null +++ b/plugins/carla/pluginlistdialog.cpp @@ -0,0 +1,1667 @@ +/* + * Carla plugin host, adjusted for OBS + * Copyright (C) 2011-2023 Filipe Coelho + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include +#include + +#include +#include +#include + +#include "pluginlistdialog.hpp" +#include "pluginrefreshdialog.hpp" + +#include "common.h" +#include "qtutils.h" + +CARLA_BACKEND_USE_NAMESPACE + +// ---------------------------------------------------------------------------- +// check if the plugin IO makes sense for OBS + +template static bool isSupportedIO(const T &info) +{ + return info.cvIns == 0 && info.cvOuts == 0 && + info.audioIns <= MAX_AV_PLANES && + info.audioOuts <= MAX_AV_PLANES; +} + +// ---------------------------------------------------------------------------- +// getenv with a fallback value if unset + +static inline const char *getEnvWithFallback(const char *const env, + const char *const fallback) +{ + if (const char *const value = std::getenv(env)) + return value; + + return fallback; +} + +// ---------------------------------------------------------------------------- +// Plugin paths (from env vars first, then default locations) + +struct PluginPaths { + QUtf8String ladspa; + QUtf8String lv2; + QUtf8String vst2; + QUtf8String vst3; + QUtf8String clap; + QUtf8String jsfx; + + PluginPaths() + { + // get common env vars first + const QString HOME = QDir::toNativeSeparators(QDir::homePath()); + +#if defined(Q_OS_WINDOWS) + const char *const envAPPDATA = std::getenv("APPDATA"); + const char *const envLOCALAPPDATA = + getEnvWithFallback("LOCALAPPDATA", envAPPDATA); + const char *const envPROGRAMFILES = std::getenv("PROGRAMFILES"); + const char *const envCOMMONPROGRAMFILES = + std::getenv("COMMONPROGRAMFILES"); + + // small integrity tests + if (envAPPDATA == nullptr) { + qFatal("APPDATA variable not set, cannot continue"); + abort(); + } + + if (envPROGRAMFILES == nullptr) { + qFatal("PROGRAMFILES variable not set, cannot continue"); + abort(); + } + + if (envCOMMONPROGRAMFILES == nullptr) { + qFatal("COMMONPROGRAMFILES variable not set, cannot continue"); + abort(); + } + + const QUtf8String APPDATA(envAPPDATA); + const QUtf8String LOCALAPPDATA(envLOCALAPPDATA); + const QUtf8String PROGRAMFILES(envPROGRAMFILES); + const QUtf8String COMMONPROGRAMFILES(envCOMMONPROGRAMFILES); +#elif !defined(Q_OS_DARWIN) + const QUtf8String CONFIG_HOME(getEnvWithFallback( + "XDG_CONFIG_HOME", (HOME + "/.config").toUtf8())); +#endif + + // now we set paths, listing format path spec if available + if (const char *const envLADSPA = std::getenv("LADSPA_PATH")) { + ladspa = envLADSPA; + } else { + // no official spec for LADSPA, use most common paths +#if defined(Q_OS_WINDOWS) + ladspa = APPDATA + "\\LADSPA"; + ladspa += ";" + PROGRAMFILES + "\\LADSPA"; +#elif defined(Q_OS_DARWIN) + ladspa = HOME + "/Library/Audio/Plug-Ins/LADSPA"; + ladspa += ":/Library/Audio/Plug-Ins/LADSPA"; +#else + ladspa = HOME + "/.ladspa"; + ladspa += ":/usr/local/lib/ladspa"; + ladspa += ":/usr/lib/ladspa"; +#endif + } + + if (const char *const envLV2 = std::getenv("LV2_PATH")) { + lv2 = envLV2; + } else { + // use path spec as defined in: + // https://lv2plug.in/pages/filesystem-hierarchy-standard.html +#if defined(Q_OS_WINDOWS) + lv2 = APPDATA + "\\LV2"; + lv2 += ";" + COMMONPROGRAMFILES + "\\LV2"; +#elif defined(Q_OS_DARWIN) + lv2 = HOME + "/Library/Audio/Plug-Ins/LV2"; + lv2 += ":/Library/Audio/Plug-Ins/LV2"; +#else + lv2 = HOME + "/.lv2"; + lv2 += ":/usr/local/lib/lv2"; + lv2 += ":/usr/lib/lv2"; +#endif + } + + if (const char *const envVST2 = std::getenv("VST_PATH")) { + vst2 = envVST2; + } else { +#if defined(Q_OS_WINDOWS) + // use path spec as defined in: + // https://helpcenter.steinberg.de/hc/en-us/articles/115000177084 + vst2 = PROGRAMFILES + "\\VSTPlugins"; + vst2 += ";" + PROGRAMFILES + "\\Steinberg\\VSTPlugins"; + vst2 += ";" + COMMONPROGRAMFILES + "\\VST2"; + vst2 += ";" + COMMONPROGRAMFILES + "\\Steinberg\\VST2"; +#elif defined(Q_OS_DARWIN) + // use path spec as defined in: + // https://helpcenter.steinberg.de/hc/en-us/articles/115000171310 + vst2 = HOME + "/Library/Audio/Plug-Ins/VST"; + vst2 += ":/Library/Audio/Plug-Ins/VST"; +#else + // no official spec for VST2 on non-win/mac, use most common paths + vst2 = HOME + "/.vst"; + vst2 += ":" + HOME + "/.lxvst"; + vst2 += ":/usr/local/lib/vst"; + vst2 += ":/usr/local/lib/lxvst"; + vst2 += ":/usr/lib/vst"; + vst2 += ":/usr/lib/lxvst"; +#endif + } + + if (const char *const envVST3 = std::getenv("VST3_PATH")) { + vst3 = envVST3; + } else { + // use path spec as defined in: + // https://steinbergmedia.github.io/vst3_dev_portal/pages/Technical+Documentation/Locations+Format/Plugin+Locations.html +#if defined(Q_OS_WINDOWS) + vst3 = LOCALAPPDATA + "\\Programs\\Common\\VST3"; + vst3 += ";" + COMMONPROGRAMFILES + "\\VST3"; +#elif defined(Q_OS_DARWIN) + vst3 = HOME + "/Library/Audio/Plug-Ins/VST3"; + vst3 += ":/Library/Audio/Plug-Ins/VST3"; +#else + vst3 = HOME + "/.vst3"; + vst3 += ":/usr/local/lib/vst3"; + vst3 += ":/usr/lib/vst3"; +#endif + } + + if (const char *const envCLAP = std::getenv("CLAP_PATH")) { + clap = envCLAP; + } else { + // use path spec as defined in: + // https://github.com/free-audio/clap/blob/main/include/clap/entry.h +#if defined(Q_OS_WINDOWS) + clap = LOCALAPPDATA + "\\Programs\\Common\\CLAP"; + clap += ";" + COMMONPROGRAMFILES + "\\CLAP"; +#elif defined(Q_OS_DARWIN) + clap = HOME + "/Library/Audio/Plug-Ins/CLAP"; + clap += ":/Library/Audio/Plug-Ins/CLAP"; +#else + clap = HOME + "/.clap"; + clap += ":/usr/local/lib/clap"; + clap += ":/usr/lib/clap"; +#endif + } + + if (const char *const envJSFX = std::getenv("JSFX_PATH")) { + jsfx = envJSFX; + } else { + // there is no path spec, use REAPER's user data directory +#if defined(Q_OS_WINDOWS) + jsfx = APPDATA + "\\REAPER\\Effects"; +#elif defined(Q_OS_DARWIN) + jsfx = HOME + + "/Library/Application Support/REAPER/Effects"; +#else + jsfx = CONFIG_HOME + "/REAPER/Effects"; +#endif + } + } +}; + +// ---------------------------------------------------------------------------- +// Qt-compatible plugin info (convert to and from QVariant) + +// base details, nicely packed and POD-only so we can directly use as binary +struct PluginInfoHeader { + uint16_t build; + uint16_t type; + uint32_t hints; + uint64_t uniqueId; + uint16_t audioIns; + uint16_t audioOuts; + uint16_t cvIns; + uint16_t cvOuts; + uint16_t midiIns; + uint16_t midiOuts; + uint16_t parameterIns; + uint16_t parameterOuts; +}; + +// full details, now with non-POD types +struct PluginInfo : PluginInfoHeader { + QString category; + QString filename; + QString name; + QString label; + QString maker; +}; + +// convert PluginInfo to Qt types +static QVariant asByteArray(const PluginInfo &info) +{ + QByteArray qdata; + + // start with the POD data, stored as-is + qdata.append( + static_cast(static_cast(&info)), + sizeof(PluginInfoHeader)); + + // then all the strings, with a null terminating byte + { + const QByteArray qcategory(info.category.toUtf8()); + qdata += qcategory.constData(); + qdata += '\0'; + } + + { + const QByteArray qfilename(info.filename.toUtf8()); + qdata += qfilename.constData(); + qdata += '\0'; + } + + { + const QByteArray qname(info.name.toUtf8()); + qdata += qname.constData(); + qdata += '\0'; + } + + { + const QByteArray qlabel(info.label.toUtf8()); + qdata += qlabel.constData(); + qdata += '\0'; + } + + { + const QByteArray qmaker(info.maker.toUtf8()); + qdata += qmaker.constData(); + qdata += '\0'; + } + + return qdata; +} + +static QVariant asVariant(const PluginInfo &info) +{ + return QVariant(asByteArray(info)); +} + +// convert Qt types to PluginInfo +static PluginInfo asPluginInfo(const QByteArray &qdata) +{ + // make sure data is big enough to fit POD data + 5 strings + CARLA_SAFE_ASSERT_RETURN(static_cast(qdata.size()) >= + sizeof(PluginInfoHeader) + + sizeof(char) * 5, + {}); + + // read POD data first + const PluginInfoHeader *const data = + static_cast( + static_cast(qdata.constData())); + PluginInfo info = {data->build, + data->type, + data->hints, + data->uniqueId, + data->audioIns, + data->audioOuts, + data->cvIns, + data->cvOuts, + data->midiIns, + data->midiOuts, + data->parameterIns, + data->parameterOuts, + {}, + {}, + {}, + {}, + {}}; + + // then all the strings, keeping the same order as in `asVariant` + const char *sdata = + static_cast(static_cast(data + 1)); + + info.category = QString::fromUtf8(sdata); + sdata += info.category.size() + 1; + + info.filename = QString::fromUtf8(sdata); + sdata += info.filename.size() + 1; + + info.name = QString::fromUtf8(sdata); + sdata += info.name.size() + 1; + + info.label = QString::fromUtf8(sdata); + sdata += info.label.size() + 1; + + info.maker = QString::fromUtf8(sdata); + sdata += info.maker.size() + 1; + + return info; +} + +static PluginInfo asPluginInfo(const QVariant &var) +{ + return asPluginInfo(var.toByteArray()); +} + +static QList asPluginInfoList(const QVariant &var) +{ + QCompatByteArray qdata(var.toByteArray()); + + QList plist; + + while (!qdata.isEmpty()) { + const PluginInfo info = asPluginInfo(qdata); + CARLA_SAFE_ASSERT_RETURN(info.build != BINARY_NONE, {}); + + plist.append(info); + qdata = qdata.sliced(sizeof(PluginInfoHeader) + + info.category.size() + + info.filename.size() + info.name.size() + + info.label.size() + info.maker.size() + 5); + } + + return plist; +} + +#ifndef CARLA_2_6_FEATURES +// convert cached plugin stuff to PluginInfo +static PluginInfo asPluginInfo(const CarlaCachedPluginInfo *const desc, + const PluginType ptype) +{ + PluginInfo pinfo = {}; + pinfo.build = BINARY_NATIVE; + pinfo.type = ptype; + pinfo.hints = desc->hints; + pinfo.name = desc->name; + pinfo.label = desc->label; + pinfo.maker = desc->maker; + pinfo.category = getPluginCategoryAsString(desc->category); + + pinfo.audioIns = desc->audioIns; + pinfo.audioOuts = desc->audioOuts; + + pinfo.cvIns = desc->cvIns; + pinfo.cvOuts = desc->cvOuts; + + pinfo.midiIns = desc->midiIns; + pinfo.midiOuts = desc->midiOuts; + + pinfo.parameterIns = desc->parameterIns; + pinfo.parameterOuts = desc->parameterOuts; + + if (ptype == PLUGIN_LV2) { + const QString label(desc->label); + pinfo.filename = label.split(CARLA_OS_SEP).first(); + pinfo.label = label.section(CARLA_OS_SEP, 1); + } + + return pinfo; +} +#endif + +// ---------------------------------------------------------------------------- +// Qt-compatible plugin favorite (convert to and from QVariant) + +// base details, nicely packed and POD-only so we can directly use as binary +struct PluginFavoriteHeader { + uint16_t type; + uint64_t uniqueId; +}; + +// full details, now with non-POD types +struct PluginFavorite : PluginFavoriteHeader { + QString filename; + QString label; + + bool operator==(const PluginFavorite &other) const + { + return type == other.type && uniqueId == other.uniqueId && + filename == other.filename && label == other.label; + } +}; + +// convert PluginFavorite to Qt types +static QByteArray asByteArray(const PluginFavorite &fav) +{ + QByteArray qdata; + + // start with the POD data, stored as-is + qdata.append(static_cast(static_cast(&fav)), + sizeof(PluginFavoriteHeader)); + + // then all the strings, with a null terminating byte + { + const QByteArray qfilename(fav.filename.toUtf8()); + qdata += qfilename.constData(); + qdata += '\0'; + } + + { + const QByteArray qlabel(fav.label.toUtf8()); + qdata += qlabel.constData(); + qdata += '\0'; + } + + return qdata; +} + +static QVariant asVariant(const QList &favlist) +{ + QByteArray qdata; + + for (const PluginFavorite &fav : favlist) + qdata += asByteArray(fav); + + return QVariant(qdata); +} + +// convert Qt types to PluginInfo +static PluginFavorite asPluginFavorite(const QByteArray &qdata) +{ + // make sure data is big enough to fit POD data + 3 strings + CARLA_SAFE_ASSERT_RETURN(static_cast(qdata.size()) >= + sizeof(PluginFavoriteHeader) + + sizeof(char) * 3, + {}); + + // read POD data first + const PluginFavoriteHeader *const data = + static_cast( + static_cast(qdata.constData())); + PluginFavorite fav = {data->type, data->uniqueId, {}, {}}; + + // then all the strings, keeping the same order as in `asVariant` + const char *sdata = + static_cast(static_cast(data + 1)); + + fav.filename = QString::fromUtf8(sdata); + sdata += fav.filename.size() + 1; + + fav.label = QString::fromUtf8(sdata); + sdata += fav.label.size() + 1; + + return fav; +} + +static QList asPluginFavoriteList(const QVariant &var) +{ + QCompatByteArray qdata(var.toByteArray()); + + QList favlist; + + while (!qdata.isEmpty()) { + const PluginFavorite fav = asPluginFavorite(qdata); + CARLA_SAFE_ASSERT_RETURN(fav.type != PLUGIN_NONE, {}); + + favlist.append(fav); + qdata = qdata.sliced(sizeof(PluginFavoriteHeader) + + fav.filename.size() + fav.label.size() + + 2); + } + + return favlist; +} + +// create PluginFavorite from PluginInfo data +static PluginFavorite asPluginFavorite(const PluginInfo &info) +{ + return PluginFavorite{info.type, info.uniqueId, info.filename, + info.label}; +} + +#ifdef CARLA_2_6_FEATURES +// ---------------------------------------------------------------------------- +// discovery callbacks + +static void discoveryCallback(void *const ptr, + const CarlaPluginDiscoveryInfo *const info, + const char *const sha1sum) +{ + static_cast(ptr)->addPluginInfo(info, sha1sum); +} + +static bool checkCacheCallback(void *const ptr, const char *const filename, + const char *const sha1sum) +{ + if (sha1sum == nullptr) + return false; + + return static_cast(ptr)->checkPluginCache(filename, + sha1sum); +} +#endif // CARLA_2_6_FEATURES + +// ---------------------------------------------------------------------------- + +struct PluginListDialog::PrivateData { + int lastTableWidgetIndex = 0; + int timerId = 0; + PluginInfo retPlugin; + + struct Discovery { + PluginType ptype = PLUGIN_NONE; + bool firstInit = true; +#ifdef CARLA_2_6_FEATURES + bool ignoreCache = false; + bool checkInvalid = false; + CarlaPluginDiscoveryHandle handle = nullptr; + QUtf8String tool; + QPointer dialog; + Discovery() + { + tool = get_carla_bin_path(); +#ifdef Q_OS_WINDOWS + tool += "\\carla-discovery-native.exe"; +#else + tool += "/carla-discovery-native"; +#endif + } + + ~Discovery() + { + if (handle != nullptr) + carla_plugin_discovery_stop(handle); + } +#endif + } discovery; + + PluginPaths paths; + + struct { + std::vector internal; + std::vector lv2; + std::vector jsfx; +#ifdef CARLA_2_6_FEATURES + std::vector ladspa; + std::vector vst2; + std::vector vst3; + std::vector clap; + QMap> cache; +#endif + QList favorites; + + bool add(const PluginInfo &pinfo) + { + switch (pinfo.type) { + case PLUGIN_INTERNAL: + internal.push_back(pinfo); + return true; + case PLUGIN_LV2: + lv2.push_back(pinfo); + return true; + case PLUGIN_JSFX: + jsfx.push_back(pinfo); + return true; +#ifdef CARLA_2_6_FEATURES + case PLUGIN_LADSPA: + ladspa.push_back(pinfo); + return true; + case PLUGIN_VST2: + vst2.push_back(pinfo); + return true; + case PLUGIN_VST3: + vst3.push_back(pinfo); + return true; + case PLUGIN_CLAP: + clap.push_back(pinfo); + return true; +#endif + default: + return false; + } + } + } plugins; +}; + +// ---------------------------------------------------------------------------- + +PluginListDialog::PluginListDialog(QWidget *const parent) + : QDialog(parent), + p(new PrivateData) +{ + ui.setupUi(this); + + // -------------------------------------------------------------------- + // Set-up GUI + + ui.b_load->setEnabled(false); + + // do not resize info frame so much + const QLayout *const infoLayout = ui.frame_info->layout(); + const QMargins infoMargins = infoLayout->contentsMargins(); + ui.frame_info->setMinimumWidth( + infoMargins.left() + infoMargins.right() + + infoLayout->spacing() * 3 + + ui.la_id->fontMetrics().horizontalAdvance( + "Has Custom GUI: 9999999999")); + +#ifndef CARLA_2_6_FEATURES + ui.ch_ladspa->hide(); + ui.ch_vst->hide(); + ui.ch_vst3->hide(); + ui.ch_clap->hide(); +#endif + + // start with no plugin selected + checkPlugin(-1); + + // custom action that listens for Ctrl+F shortcut + addAction(ui.act_focus_search); + + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); +#ifdef Q_OS_DARWIN + setWindowModality(Qt::WindowModal); +#endif + + // -------------------------------------------------------------------- + // Load settings + + loadSettings(); + + // -------------------------------------------------------------------- + // Set-up Icons + + ui.b_clear_filters->setProperty("themeID", "clearIconSmall"); + ui.b_refresh->setProperty("themeID", "refreshIconSmall"); + + /* FIXME get a star/bookmark/favorite icon + QTableWidgetItem* const hhi = ui.tableWidget->horizontalHeaderItem(TW_FAVORITE); + hhi->setProperty("themeID", "starIconSmall"); + */ + + // -------------------------------------------------------------------- + // Set-up connections + + QObject::connect(this, &QDialog::finished, this, + &PluginListDialog::saveSettings); + QObject::connect(ui.b_load, &QPushButton::clicked, this, + &QDialog::accept); + QObject::connect(ui.b_cancel, &QPushButton::clicked, this, + &QDialog::reject); + + QObject::connect(ui.b_refresh, &QPushButton::clicked, this, + &PluginListDialog::refreshPlugins); + QObject::connect(ui.b_clear_filters, &QPushButton::clicked, this, + &PluginListDialog::clearFilters); + QObject::connect(ui.lineEdit, &QLineEdit::textChanged, this, + &PluginListDialog::checkFilters); + QObject::connect(ui.tableWidget, &QTableWidget::currentCellChanged, + this, &PluginListDialog::checkPlugin); + QObject::connect(ui.tableWidget, &QTableWidget::cellClicked, this, + &PluginListDialog::cellClicked); + QObject::connect(ui.tableWidget, &QTableWidget::cellDoubleClicked, this, + &PluginListDialog::cellDoubleClicked); + + QObject::connect(ui.ch_internal, &QCheckBox::clicked, this, + &PluginListDialog::checkFilters); + QObject::connect(ui.ch_ladspa, &QCheckBox::clicked, this, + &PluginListDialog::checkFilters); + QObject::connect(ui.ch_lv2, &QCheckBox::clicked, this, + &PluginListDialog::checkFilters); + QObject::connect(ui.ch_vst, &QCheckBox::clicked, this, + &PluginListDialog::checkFilters); + QObject::connect(ui.ch_vst3, &QCheckBox::clicked, this, + &PluginListDialog::checkFilters); + QObject::connect(ui.ch_clap, &QCheckBox::clicked, this, + &PluginListDialog::checkFilters); + QObject::connect(ui.ch_jsfx, &QCheckBox::clicked, this, + &PluginListDialog::checkFilters); + QObject::connect(ui.ch_effects, &QCheckBox::clicked, this, + &PluginListDialog::checkFilters); + QObject::connect(ui.ch_instruments, &QCheckBox::clicked, this, + &PluginListDialog::checkFilters); + QObject::connect(ui.ch_midi, &QCheckBox::clicked, this, + &PluginListDialog::checkFilters); + QObject::connect(ui.ch_other, &QCheckBox::clicked, this, + &PluginListDialog::checkFilters); + QObject::connect(ui.ch_favorites, &QCheckBox::clicked, this, + &PluginListDialog::checkFilters); + QObject::connect(ui.ch_gui, &QCheckBox::clicked, this, + &PluginListDialog::checkFilters); + QObject::connect(ui.ch_stereo, &QCheckBox::clicked, this, + &PluginListDialog::checkFilters); + QObject::connect(ui.ch_cat_all, &QCheckBox::clicked, this, + &PluginListDialog::checkFiltersCategoryAll); + QObject::connect(ui.ch_cat_delay, &QCheckBox::clicked, this, + &PluginListDialog::checkFiltersCategorySpecific); + QObject::connect(ui.ch_cat_distortion, &QCheckBox::clicked, this, + &PluginListDialog::checkFiltersCategorySpecific); + QObject::connect(ui.ch_cat_dynamics, &QCheckBox::clicked, this, + &PluginListDialog::checkFiltersCategorySpecific); + QObject::connect(ui.ch_cat_eq, &QCheckBox::clicked, this, + &PluginListDialog::checkFiltersCategorySpecific); + QObject::connect(ui.ch_cat_filter, &QCheckBox::clicked, this, + &PluginListDialog::checkFiltersCategorySpecific); + QObject::connect(ui.ch_cat_modulator, &QCheckBox::clicked, this, + &PluginListDialog::checkFiltersCategorySpecific); + QObject::connect(ui.ch_cat_synth, &QCheckBox::clicked, this, + &PluginListDialog::checkFiltersCategorySpecific); + QObject::connect(ui.ch_cat_utility, &QCheckBox::clicked, this, + &PluginListDialog::checkFiltersCategorySpecific); + QObject::connect(ui.ch_cat_other, &QCheckBox::clicked, this, + &PluginListDialog::checkFiltersCategorySpecific); + + QObject::connect(ui.act_focus_search, &QAction::triggered, this, + &PluginListDialog::focusSearchFieldAndSelectAllText); +} + +PluginListDialog::~PluginListDialog() +{ + if (p->timerId != 0) + killTimer(p->timerId); + + delete p; +} + +// ---------------------------------------------------------------------------- +// public methods + +const PluginInfo &PluginListDialog::getSelectedPluginInfo() const +{ + return p->retPlugin; +} + +#ifdef CARLA_2_6_FEATURES +void PluginListDialog::addPluginInfo(const CarlaPluginDiscoveryInfo *const info, + const char *const sha1sum) +{ + if (info == nullptr) { + if (sha1sum != nullptr) { + QSafeSettings settings; + settings.setValue( + QString("PluginCache/%1").arg(sha1sum), + QByteArray()); + + p->plugins.cache[QString(sha1sum)] = {}; + } + return; + } + + const PluginInfo pinfo = { + static_cast(info->btype), + static_cast(info->ptype), + info->metadata.hints, + info->uniqueId, + static_cast(info->io.audioIns), + static_cast(info->io.audioOuts), + static_cast(info->io.cvIns), + static_cast(info->io.cvOuts), + static_cast(info->io.midiIns), + static_cast(info->io.midiOuts), + static_cast(info->io.parameterIns), + static_cast(info->io.parameterOuts), + getPluginCategoryAsString(info->metadata.category), + QString::fromUtf8(info->filename), + QString::fromUtf8(info->metadata.name), + QString::fromUtf8(info->label), + QString::fromUtf8(info->metadata.maker), + }; + + if (sha1sum != nullptr) { + QSafeSettings settings; + const QString qsha1sum(sha1sum); + const QString key = QString("PluginCache/%1").arg(sha1sum); + + // single sha1sum can contain >1 plugin + QByteArray qdata; + if (p->plugins.cache.contains(qsha1sum)) + qdata = settings.valueByteArray(key); + qdata += asVariant(pinfo).toByteArray(); + + settings.setValue(key, qdata); + + p->plugins.cache[qsha1sum].append(pinfo); + } + + if (isSupportedIO(pinfo)) + p->plugins.add(pinfo); +} + +bool PluginListDialog::checkPluginCache(const char *const filename, + const char *const sha1sum) +{ + // sha1sum is always valid for this call + const QString qsha1sum(sha1sum); + + if (filename != nullptr) + p->discovery.dialog->progressBar->setFormat(filename); + + if (!p->plugins.cache.contains(qsha1sum)) + return false; + + const QList &plist(p->plugins.cache[qsha1sum]); + + if (plist.isEmpty()) + return p->discovery.ignoreCache || !p->discovery.checkInvalid; + + // if filename does not match, abort (hash collision?) + if (filename == nullptr || plist.first().filename != filename) { + p->plugins.cache.remove(qsha1sum); + return false; + } + + for (const PluginInfo &info : plist) { + if (isSupportedIO(info)) + p->plugins.add(info); + } + + return true; +} +#endif + +// ---------------------------------------------------------------------------- +// protected methods + +void PluginListDialog::done(const int r) +{ + if (r == QDialog::Accepted && ui.tableWidget->currentRow() >= 0) { + p->retPlugin = asPluginInfo( + ui.tableWidget + ->item(ui.tableWidget->currentRow(), TW_NAME) + ->data(Qt::UserRole + UR_PLUGIN_INFO)); + } else { + p->retPlugin = {}; + } + + QDialog::done(r); +} + +void PluginListDialog::showEvent(QShowEvent *const event) +{ + focusSearchFieldAndSelectAllText(); + QDialog::showEvent(event); + + // Set up initial discovery + if (p->discovery.firstInit) { + p->discovery.firstInit = false; + +#ifdef CARLA_2_6_FEATURES + p->discovery.dialog = new PluginRefreshDialog(this); + p->discovery.dialog->b_start->setEnabled(false); + p->discovery.dialog->b_skip->setEnabled(true); + p->discovery.dialog->ch_updated->setChecked(true); + p->discovery.dialog->ch_invalid->setChecked(false); + p->discovery.dialog->group->setEnabled(false); + p->discovery.dialog->progressBar->setFormat( + "Starting initial discovery..."); + p->discovery.dialog->show(); + + QObject::connect(p->discovery.dialog->b_skip, + &QPushButton::clicked, this, + &PluginListDialog::refreshPluginsSkip); + QObject::connect(p->discovery.dialog, &QDialog::finished, this, + &PluginListDialog::refreshPluginsStop); +#endif + + p->timerId = startTimer(0); + } +} + +void PluginListDialog::timerEvent(QTimerEvent *const event) +{ + if (event->timerId() == p->timerId) { + do { +#ifdef CARLA_2_6_FEATURES + // discovery in progress, keep it going + if (p->discovery.handle != nullptr) { + if (!carla_plugin_discovery_idle( + p->discovery.handle)) { + carla_plugin_discovery_stop( + p->discovery.handle); + p->discovery.handle = nullptr; + } + break; + } +#endif + // start next discovery + QUtf8String path; + switch (p->discovery.ptype) { + case PLUGIN_NONE: + ui.label->setText( + tr("Discovering internal plugins...")); + p->discovery.ptype = PLUGIN_INTERNAL; + break; + case PLUGIN_INTERNAL: + ui.label->setText( + tr("Discovering LV2 plugins...")); + path = p->paths.lv2; + p->discovery.ptype = PLUGIN_LV2; + break; + case PLUGIN_LV2: + if (p->paths.jsfx.isNotEmpty()) { + ui.label->setText(tr( + "Discovering JSFX plugins...")); + path = p->paths.jsfx; + p->discovery.ptype = PLUGIN_JSFX; + break; + } + [[fallthrough]]; +#ifdef CARLA_2_6_FEATURES + case PLUGIN_JSFX: + ui.label->setText( + tr("Discovering LADSPA plugins...")); + path = p->paths.ladspa; + p->discovery.ptype = PLUGIN_LADSPA; + break; + case PLUGIN_LADSPA: + ui.label->setText( + tr("Discovering VST2 plugins...")); + path = p->paths.vst2; + p->discovery.ptype = PLUGIN_VST2; + break; + case PLUGIN_VST2: + ui.label->setText( + tr("Discovering VST3 plugins...")); + path = p->paths.vst3; + p->discovery.ptype = PLUGIN_VST3; + break; + case PLUGIN_VST3: + ui.label->setText( + tr("Discovering CLAP plugins...")); + path = p->paths.clap; + p->discovery.ptype = PLUGIN_CLAP; + break; +#endif + default: + // discovery complete + refreshPluginsStop(); + } + + if (p->timerId == 0) + break; + +#ifdef CARLA_2_6_FEATURES + p->discovery.handle = carla_plugin_discovery_start( + p->discovery.tool.toUtf8().constData(), + BINARY_NATIVE, p->discovery.ptype, + path.toUtf8().constData(), discoveryCallback, + checkCacheCallback, this); +#else + if (const uint count = carla_get_cached_plugin_count( + p->discovery.ptype, + path.toUtf8().constData())) { + for (uint i = 0; i < count; ++i) { + const CarlaCachedPluginInfo *const info = + carla_get_cached_plugin_info( + p->discovery.ptype, i); + + if (!info || !info->valid) + continue; + + // ignore plugins with non-compatible IO + if (isSupportedIO(*info)) + p->plugins.add(asPluginInfo( + info, + p->discovery.ptype)); + } + } +#endif + } while (false); + } + + QDialog::timerEvent(event); +} + +// ---------------------------------------------------------------------------- +// private methods + +void PluginListDialog::addPluginsToTable() +{ + // -------------------------------------------------------------------- + // sum plugins first, creating all needed rows in advance + + ui.tableWidget->setSortingEnabled(false); + ui.tableWidget->clearContents(); + +#ifdef CARLA_2_6_FEATURES + ui.tableWidget->setRowCount( + int(p->plugins.internal.size() + p->plugins.ladspa.size() + + p->plugins.lv2.size() + p->plugins.vst2.size() + + p->plugins.vst3.size() + p->plugins.clap.size() + + p->plugins.jsfx.size())); + + ui.label->setText( + tr("Have %1 Internal, %2 LADSPA, %3 LV2, %4 VST2, %5 VST3, %6 CLAP and %7 JSFX plugins") + .arg(QString::number(p->plugins.internal.size())) + .arg(QString::number(p->plugins.ladspa.size())) + .arg(QString::number(p->plugins.lv2.size())) + .arg(QString::number(p->plugins.vst2.size())) + .arg(QString::number(p->plugins.vst3.size())) + .arg(QString::number(p->plugins.clap.size())) + .arg(QString::number(p->plugins.jsfx.size()))); +#else + ui.tableWidget->setRowCount(int(p->plugins.internal.size() + + p->plugins.lv2.size() + + p->plugins.jsfx.size())); + + ui.label->setText( + tr("Have %1 Internal, %2 LV2 and %3 JSFX plugins") + .arg(QString::number(p->plugins.internal.size())) + .arg(QString::number(p->plugins.lv2.size())) + .arg(QString::number(p->plugins.jsfx.size()))); +#endif + + // -------------------------------------------------------------------- + // now add all plugins to the table + + auto addPluginToTable = [=](const PluginInfo &info) { + const int index = p->lastTableWidgetIndex++; + const bool isFav = + p->plugins.favorites.contains(asPluginFavorite(info)); + + QTableWidgetItem *const itemFav = new QTableWidgetItem; + itemFav->setCheckState(isFav ? Qt::Checked : Qt::Unchecked); + itemFav->setText(isFav ? " " : " "); + + const QString pluginText = + (info.name + info.label + info.maker + info.filename) + .toLower(); + ui.tableWidget->setItem(index, TW_FAVORITE, itemFav); + ui.tableWidget->setItem(index, TW_NAME, + new QTableWidgetItem(info.name)); + ui.tableWidget->setItem(index, TW_LABEL, + new QTableWidgetItem(info.label)); + ui.tableWidget->setItem(index, TW_MAKER, + new QTableWidgetItem(info.maker)); + ui.tableWidget->setItem( + index, TW_BINARY, + new QTableWidgetItem( + QFileInfo(info.filename).fileName())); + + QTableWidgetItem *const itemName = + ui.tableWidget->item(index, TW_NAME); + itemName->setData(Qt::UserRole + UR_PLUGIN_INFO, + asVariant(info)); + itemName->setData(Qt::UserRole + UR_SEARCH_TEXT, pluginText); + }; + + p->lastTableWidgetIndex = 0; + + for (const PluginInfo &plugin : p->plugins.internal) + addPluginToTable(plugin); + + for (const PluginInfo &plugin : p->plugins.lv2) + addPluginToTable(plugin); + + for (const PluginInfo &plugin : p->plugins.jsfx) + addPluginToTable(plugin); + +#ifdef CARLA_2_6_FEATURES + for (const PluginInfo &plugin : p->plugins.ladspa) + addPluginToTable(plugin); + + for (const PluginInfo &plugin : p->plugins.vst2) + addPluginToTable(plugin); + + for (const PluginInfo &plugin : p->plugins.vst3) + addPluginToTable(plugin); + + for (const PluginInfo &plugin : p->plugins.clap) + addPluginToTable(plugin); +#endif + + CARLA_SAFE_ASSERT_INT2( + p->lastTableWidgetIndex == ui.tableWidget->rowCount(), + p->lastTableWidgetIndex, ui.tableWidget->rowCount()); + + // -------------------------------------------------------------------- + // and reenable sorting + filtering + + ui.tableWidget->setSortingEnabled(true); + + checkFilters(); + checkPlugin(ui.tableWidget->currentRow()); +} + +void PluginListDialog::loadSettings() +{ + const QSafeSettings settings; + + restoreGeometry(settings.valueByteArray("PluginListDialog/Geometry")); + ui.ch_effects->setChecked( + settings.valueBool("PluginListDialog/ShowEffects", true)); + ui.ch_instruments->setChecked( + settings.valueBool("PluginListDialog/ShowInstruments", true)); + ui.ch_midi->setChecked( + settings.valueBool("PluginListDialog/ShowMIDI", true)); + ui.ch_other->setChecked( + settings.valueBool("PluginListDialog/ShowOther", true)); + ui.ch_internal->setChecked( + settings.valueBool("PluginListDialog/ShowInternal", true)); + ui.ch_ladspa->setChecked( + settings.valueBool("PluginListDialog/ShowLADSPA", true)); + ui.ch_lv2->setChecked( + settings.valueBool("PluginListDialog/ShowLV2", true)); + ui.ch_vst->setChecked( + settings.valueBool("PluginListDialog/ShowVST2", true)); + ui.ch_vst3->setChecked( + settings.valueBool("PluginListDialog/ShowVST3", true)); + ui.ch_clap->setChecked( + settings.valueBool("PluginListDialog/ShowCLAP", true)); + ui.ch_jsfx->setChecked( + settings.valueBool("PluginListDialog/ShowJSFX", true)); + ui.ch_favorites->setChecked( + settings.valueBool("PluginListDialog/ShowFavorites", false)); + ui.ch_gui->setChecked( + settings.valueBool("PluginListDialog/ShowHasGUI", false)); + ui.ch_stereo->setChecked( + settings.valueBool("PluginListDialog/ShowStereoOnly", false)); + ui.lineEdit->setText( + settings.valueString("PluginListDialog/SearchText", "")); + + const QString categories = + settings.valueString("PluginListDialog/ShowCategory", "all"); + if (categories == "all" or categories.length() < 2) { + ui.ch_cat_all->setChecked(true); + ui.ch_cat_delay->setChecked(false); + ui.ch_cat_distortion->setChecked(false); + ui.ch_cat_dynamics->setChecked(false); + ui.ch_cat_eq->setChecked(false); + ui.ch_cat_filter->setChecked(false); + ui.ch_cat_modulator->setChecked(false); + ui.ch_cat_synth->setChecked(false); + ui.ch_cat_utility->setChecked(false); + ui.ch_cat_other->setChecked(false); + } else { + ui.ch_cat_all->setChecked(false); + ui.ch_cat_delay->setChecked(categories.contains(":delay:")); + ui.ch_cat_distortion->setChecked( + categories.contains(":distortion:")); + ui.ch_cat_dynamics->setChecked( + categories.contains(":dynamics:")); + ui.ch_cat_eq->setChecked(categories.contains(":eq:")); + ui.ch_cat_filter->setChecked(categories.contains(":filter:")); + ui.ch_cat_modulator->setChecked( + categories.contains(":modulator:")); + ui.ch_cat_synth->setChecked(categories.contains(":synth:")); + ui.ch_cat_utility->setChecked(categories.contains(":utility:")); + ui.ch_cat_other->setChecked(categories.contains(":other:")); + } + + const QByteArray tableGeometry = + settings.valueByteArray("PluginListDialog/TableGeometry"); + QHeaderView *const horizontalHeader = + ui.tableWidget->horizontalHeader(); + if (!tableGeometry.isNull()) { + horizontalHeader->restoreState(tableGeometry); + } else { + ui.tableWidget->setColumnWidth(TW_NAME, 250); + ui.tableWidget->setColumnWidth(TW_LABEL, 200); + ui.tableWidget->setColumnWidth(TW_MAKER, 150); + ui.tableWidget->sortByColumn(TW_NAME, Qt::AscendingOrder); + } + + horizontalHeader->setSectionResizeMode(TW_FAVORITE, QHeaderView::Fixed); + ui.tableWidget->setColumnWidth(TW_FAVORITE, 24); + ui.tableWidget->setSortingEnabled(true); + + p->plugins.favorites = asPluginFavoriteList( + settings.valueByteArray("PluginListDialog/Favorites")); + +#ifdef CARLA_2_6_FEATURES + // load entire plugin cache + const QStringList keys = settings.allKeys(); + for (const QUtf8String key : keys) { + if (!key.startsWith("PluginCache/")) + continue; + + const QByteArray data(settings.valueByteArray(key)); + + if (data.isEmpty()) + p->plugins.cache.insert(key.sliced(12), {}); + else + p->plugins.cache.insert(key.sliced(12), + asPluginInfoList(data)); + } +#endif +} + +// ---------------------------------------------------------------------------- +// private slots + +void PluginListDialog::cellClicked(const int row, const int column) +{ + if (column != TW_FAVORITE) + return; + + const PluginInfo info = + asPluginInfo(ui.tableWidget->item(row, TW_NAME) + ->data(Qt::UserRole + UR_PLUGIN_INFO)); + const PluginFavorite fav = asPluginFavorite(info); + const bool isFavorite = p->plugins.favorites.contains(fav); + + if (ui.tableWidget->item(row, TW_FAVORITE)->checkState() == + Qt::Checked) { + if (!isFavorite) + p->plugins.favorites.append(fav); + } else if (isFavorite) { + p->plugins.favorites.removeAll(fav); + } + + QSafeSettings settings; + settings.setValue("PluginListDialog/Favorites", + asVariant(p->plugins.favorites)); +} + +void PluginListDialog::cellDoubleClicked(int, const int column) +{ + if (column != TW_FAVORITE) + done(QDialog::Accepted); +} + +void PluginListDialog::focusSearchFieldAndSelectAllText() +{ + ui.lineEdit->setFocus(); + ui.lineEdit->selectAll(); +} + +void PluginListDialog::checkFilters() +{ + const QUtf8String text = ui.lineEdit->text().toLower(); + + const bool hideEffects = !ui.ch_effects->isChecked(); + const bool hideInstruments = !ui.ch_instruments->isChecked(); + const bool hideMidi = !ui.ch_midi->isChecked(); + const bool hideOther = !ui.ch_other->isChecked(); + + const bool hideInternal = !ui.ch_internal->isChecked(); + const bool hideLV2 = !ui.ch_lv2->isChecked(); + const bool hideJSFX = !ui.ch_jsfx->isChecked(); +#ifdef CARLA_2_6_FEATURES + const bool hideLadspa = !ui.ch_ladspa->isChecked(); + const bool hideVST2 = !ui.ch_vst->isChecked(); + const bool hideVST3 = !ui.ch_vst3->isChecked(); + const bool hideCLAP = !ui.ch_clap->isChecked(); +#endif + + const bool hideNonFavs = ui.ch_favorites->isChecked(); + const bool hideNonGui = ui.ch_gui->isChecked(); + const bool hideNonStereo = ui.ch_stereo->isChecked(); + + for (int i = 0, c = ui.tableWidget->rowCount(); i < c; ++i) { + const PluginInfo info = asPluginInfo( + ui.tableWidget->item(i, TW_NAME) + ->data(Qt::UserRole + UR_PLUGIN_INFO)); + const QString ptext = + ui.tableWidget->item(i, TW_NAME) + ->data(Qt::UserRole + UR_SEARCH_TEXT) + .toString(); + const uint16_t ptype = info.type; + const uint32_t phints = info.hints; + const uint16_t aIns = info.audioIns; + const uint16_t aOuts = info.audioOuts; + const uint16_t mIns = info.midiIns; + const uint16_t mOuts = info.midiOuts; + const QString categ = info.category; + const bool isSynth = phints & PLUGIN_IS_SYNTH; + const bool isEffect = aIns > 0 && aOuts > 0 && !isSynth; + const bool isMidi = aIns == 0 && aOuts == 0 && mIns > 0 && + mOuts > 0; + const bool isOther = !(isEffect || isSynth || isMidi); + const bool isStereo = (aIns == 2 && aOuts == 2) || + (isSynth && aOuts == 2); + const bool hasGui = phints & PLUGIN_HAS_CUSTOM_UI; + + const auto hasText = [text, ptext]() { + const QStringList textSplit = text.strip().split(' '); + for (const QString &t : textSplit) + if (ptext.contains(t)) + return true; + return false; + }; + + /**/ if (hideEffects && isEffect) + ui.tableWidget->hideRow(i); + else if (hideInstruments && isSynth) + ui.tableWidget->hideRow(i); + else if (hideMidi && isMidi) + ui.tableWidget->hideRow(i); + else if (hideOther && isOther) + ui.tableWidget->hideRow(i); + else if (hideInternal && ptype == PLUGIN_INTERNAL) + ui.tableWidget->hideRow(i); + else if (hideLV2 && ptype == PLUGIN_LV2) + ui.tableWidget->hideRow(i); + else if (hideJSFX && ptype == PLUGIN_JSFX) + ui.tableWidget->hideRow(i); +#ifdef CARLA_2_6_FEATURES + else if (hideLadspa && ptype == PLUGIN_LADSPA) + ui.tableWidget->hideRow(i); + else if (hideVST2 && ptype == PLUGIN_VST2) + ui.tableWidget->hideRow(i); + else if (hideVST3 && ptype == PLUGIN_VST3) + ui.tableWidget->hideRow(i); + else if (hideCLAP && ptype == PLUGIN_CLAP) + ui.tableWidget->hideRow(i); +#endif + else if (hideNonGui && not hasGui) + ui.tableWidget->hideRow(i); + else if (hideNonStereo && not isStereo) + ui.tableWidget->hideRow(i); + else if (text.isNotEmpty() && !hasText()) + ui.tableWidget->hideRow(i); + else if (hideNonFavs && + !p->plugins.favorites.contains(asPluginFavorite(info))) + ui.tableWidget->hideRow(i); + else if (ui.ch_cat_all->isChecked() or + (ui.ch_cat_delay->isChecked() && categ == "delay") or + (ui.ch_cat_distortion->isChecked() && + categ == "distortion") or + (ui.ch_cat_dynamics->isChecked() && + categ == "dynamics") or + (ui.ch_cat_eq->isChecked() && categ == "eq") or + (ui.ch_cat_filter->isChecked() && categ == "filter") or + (ui.ch_cat_modulator->isChecked() && + categ == "modulator") or + (ui.ch_cat_synth->isChecked() && categ == "synth") or + (ui.ch_cat_utility->isChecked() && + categ == "utility") or + (ui.ch_cat_other->isChecked() && categ == "other")) + ui.tableWidget->showRow(i); + else + ui.tableWidget->hideRow(i); + } +} + +void PluginListDialog::checkFiltersCategoryAll(const bool clicked) +{ + const bool notClicked = !clicked; + ui.ch_cat_delay->setChecked(notClicked); + ui.ch_cat_distortion->setChecked(notClicked); + ui.ch_cat_dynamics->setChecked(notClicked); + ui.ch_cat_eq->setChecked(notClicked); + ui.ch_cat_filter->setChecked(notClicked); + ui.ch_cat_modulator->setChecked(notClicked); + ui.ch_cat_synth->setChecked(notClicked); + ui.ch_cat_utility->setChecked(notClicked); + ui.ch_cat_other->setChecked(notClicked); + checkFilters(); +} + +void PluginListDialog::checkFiltersCategorySpecific(bool clicked) +{ + if (clicked) { + ui.ch_cat_all->setChecked(false); + } else if (!(ui.ch_cat_delay->isChecked() || + ui.ch_cat_distortion->isChecked() || + ui.ch_cat_dynamics->isChecked() || + ui.ch_cat_eq->isChecked() || + ui.ch_cat_filter->isChecked() || + ui.ch_cat_modulator->isChecked() || + ui.ch_cat_synth->isChecked() || + ui.ch_cat_utility->isChecked() || + ui.ch_cat_other->isChecked())) { + ui.ch_cat_all->setChecked(true); + } + checkFilters(); +} + +void PluginListDialog::clearFilters() +{ + auto setCheckedWithoutSignaling = [](auto &w, bool checked) { + w->blockSignals(true); + w->setChecked(checked); + w->blockSignals(false); + }; + + setCheckedWithoutSignaling(ui.ch_internal, true); + setCheckedWithoutSignaling(ui.ch_ladspa, true); + setCheckedWithoutSignaling(ui.ch_lv2, true); + setCheckedWithoutSignaling(ui.ch_vst, true); + setCheckedWithoutSignaling(ui.ch_vst3, true); + setCheckedWithoutSignaling(ui.ch_clap, true); + setCheckedWithoutSignaling(ui.ch_jsfx, true); + + setCheckedWithoutSignaling(ui.ch_instruments, true); + setCheckedWithoutSignaling(ui.ch_effects, true); + setCheckedWithoutSignaling(ui.ch_midi, true); + setCheckedWithoutSignaling(ui.ch_other, true); + + setCheckedWithoutSignaling(ui.ch_favorites, false); + setCheckedWithoutSignaling(ui.ch_stereo, false); + setCheckedWithoutSignaling(ui.ch_gui, false); + + setCheckedWithoutSignaling(ui.ch_cat_all, true); + setCheckedWithoutSignaling(ui.ch_cat_delay, false); + setCheckedWithoutSignaling(ui.ch_cat_distortion, false); + setCheckedWithoutSignaling(ui.ch_cat_dynamics, false); + setCheckedWithoutSignaling(ui.ch_cat_eq, false); + setCheckedWithoutSignaling(ui.ch_cat_filter, false); + setCheckedWithoutSignaling(ui.ch_cat_modulator, false); + setCheckedWithoutSignaling(ui.ch_cat_synth, false); + setCheckedWithoutSignaling(ui.ch_cat_utility, false); + setCheckedWithoutSignaling(ui.ch_cat_other, false); + + ui.lineEdit->blockSignals(true); + ui.lineEdit->clear(); + ui.lineEdit->blockSignals(false); + + checkFilters(); +} + +// ---------------------------------------------------------------------------- + +void PluginListDialog::checkPlugin(const int row) +{ + if (row >= 0) { + ui.b_load->setEnabled(true); + + const PluginInfo info = asPluginInfo( + ui.tableWidget->item(row, TW_NAME) + ->data(Qt::UserRole + UR_PLUGIN_INFO)); + + const bool isSynth = info.hints & PLUGIN_IS_SYNTH; + const bool isEffect = info.audioIns > 0 && info.audioOuts > 0 && + !isSynth; + const bool isMidi = info.audioIns == 0 && info.audioOuts == 0 && + info.midiIns > 0 && info.midiOuts > 0; + + QString ptype; + /**/ if (isSynth) + ptype = "Instrument"; + else if (isEffect) + ptype = "Effect"; + else if (isMidi) + ptype = "MIDI Plugin"; + else + ptype = "Other"; + + ui.l_format->setText(getPluginTypeAsString( + static_cast(info.type))); + + ui.l_type->setText(ptype); + ui.l_id->setText(QString::number(info.uniqueId)); + ui.l_ains->setText(QString::number(info.audioIns)); + ui.l_aouts->setText(QString::number(info.audioOuts)); + ui.l_mins->setText(QString::number(info.midiIns)); + ui.l_mouts->setText(QString::number(info.midiOuts)); + ui.l_pins->setText(QString::number(info.parameterIns)); + ui.l_pouts->setText(QString::number(info.parameterOuts)); + ui.l_gui->setText(info.hints & PLUGIN_HAS_CUSTOM_UI ? tr("Yes") + : tr("No")); + ui.l_synth->setText(isSynth ? tr("Yes") : tr("No")); + } else { + ui.b_load->setEnabled(false); + ui.l_format->setText("---"); + ui.l_type->setText("---"); + ui.l_id->setText("---"); + ui.l_ains->setText("---"); + ui.l_aouts->setText("---"); + ui.l_mins->setText("---"); + ui.l_mouts->setText("---"); + ui.l_pins->setText("---"); + ui.l_pouts->setText("---"); + ui.l_gui->setText("---"); + ui.l_synth->setText("---"); + } +} + +// ---------------------------------------------------------------------------- + +void PluginListDialog::refreshPlugins() +{ + refreshPluginsStop(); + +#ifdef CARLA_2_6_FEATURES + p->discovery.dialog = new PluginRefreshDialog(this); + p->discovery.dialog->show(); + + QObject::connect(p->discovery.dialog->b_start, &QPushButton::clicked, + this, &PluginListDialog::refreshPluginsStart); + QObject::connect(p->discovery.dialog->b_skip, &QPushButton::clicked, + this, &PluginListDialog::refreshPluginsSkip); + QObject::connect(p->discovery.dialog, &QDialog::finished, this, + &PluginListDialog::refreshPluginsStop); +#else + refreshPluginsStart(); +#endif +} + +void PluginListDialog::refreshPluginsStart() +{ + // remove old plugins + p->plugins.internal.clear(); + p->plugins.lv2.clear(); + p->plugins.jsfx.clear(); +#ifdef CARLA_2_6_FEATURES + p->plugins.ladspa.clear(); + p->plugins.vst2.clear(); + p->plugins.vst3.clear(); + p->plugins.clap.clear(); + p->discovery.dialog->b_start->setEnabled(false); + p->discovery.dialog->b_skip->setEnabled(true); + p->discovery.ignoreCache = p->discovery.dialog->ch_all->isChecked(); + p->discovery.checkInvalid = + p->discovery.dialog->ch_invalid->isChecked(); + if (p->discovery.ignoreCache) + p->plugins.cache.clear(); +#endif + + // start discovery again + p->discovery.ptype = PLUGIN_NONE; + + if (p->timerId == 0) + p->timerId = startTimer(0); +} + +void PluginListDialog::refreshPluginsStop() +{ +#ifdef CARLA_2_6_FEATURES + // stop previous discovery if still running + if (p->discovery.handle != nullptr) { + carla_plugin_discovery_stop(p->discovery.handle); + p->discovery.handle = nullptr; + } + + if (p->discovery.dialog) { + p->discovery.dialog->close(); + p->discovery.dialog = nullptr; + } +#endif + + if (p->timerId != 0) { + killTimer(p->timerId); + p->timerId = 0; + addPluginsToTable(); + } +} + +void PluginListDialog::refreshPluginsSkip() +{ +#ifdef CARLA_2_6_FEATURES + if (p->discovery.handle != nullptr) + carla_plugin_discovery_skip(p->discovery.handle); +#endif +} + +// ---------------------------------------------------------------------------- + +void PluginListDialog::saveSettings() +{ + QSafeSettings settings; + settings.setValue("PluginListDialog/Geometry", saveGeometry()); + settings.setValue("PluginListDialog/TableGeometry", + ui.tableWidget->horizontalHeader()->saveState()); + settings.setValue("PluginListDialog/ShowEffects", + ui.ch_effects->isChecked()); + settings.setValue("PluginListDialog/ShowInstruments", + ui.ch_instruments->isChecked()); + settings.setValue("PluginListDialog/ShowMIDI", ui.ch_midi->isChecked()); + settings.setValue("PluginListDialog/ShowOther", + ui.ch_other->isChecked()); + settings.setValue("PluginListDialog/ShowInternal", + ui.ch_internal->isChecked()); + settings.setValue("PluginListDialog/ShowLADSPA", + ui.ch_ladspa->isChecked()); + settings.setValue("PluginListDialog/ShowLV2", ui.ch_lv2->isChecked()); + settings.setValue("PluginListDialog/ShowVST2", ui.ch_vst->isChecked()); + settings.setValue("PluginListDialog/ShowVST3", ui.ch_vst3->isChecked()); + settings.setValue("PluginListDialog/ShowCLAP", ui.ch_clap->isChecked()); + settings.setValue("PluginListDialog/ShowJSFX", ui.ch_jsfx->isChecked()); + settings.setValue("PluginListDialog/ShowFavorites", + ui.ch_favorites->isChecked()); + settings.setValue("PluginListDialog/ShowHasGUI", + ui.ch_gui->isChecked()); + settings.setValue("PluginListDialog/ShowStereoOnly", + ui.ch_stereo->isChecked()); + settings.setValue("PluginListDialog/SearchText", ui.lineEdit->text()); + + if (ui.ch_cat_all->isChecked()) { + settings.setValue("PluginListDialog/ShowCategory", "all"); + } else { + QUtf8String categories; + if (ui.ch_cat_delay->isChecked()) + categories += ":delay"; + if (ui.ch_cat_distortion->isChecked()) + categories += ":distortion"; + if (ui.ch_cat_dynamics->isChecked()) + categories += ":dynamics"; + if (ui.ch_cat_eq->isChecked()) + categories += ":eq"; + if (ui.ch_cat_filter->isChecked()) + categories += ":filter"; + if (ui.ch_cat_modulator->isChecked()) + categories += ":modulator"; + if (ui.ch_cat_synth->isChecked()) + categories += ":synth"; + if (ui.ch_cat_utility->isChecked()) + categories += ":utility"; + if (ui.ch_cat_other->isChecked()) + categories += ":other"; + if (categories.isNotEmpty()) + categories += ":"; + settings.setValue("PluginListDialog/ShowCategory", categories); + } + + settings.setValue("PluginListDialog/Favorites", + asVariant(p->plugins.favorites)); +} + +// ---------------------------------------------------------------------------- + +const PluginListDialogResults *carla_exec_plugin_list_dialog() +{ + // create and keep dialog around, as recreating the dialog means doing + // a rescan. Qt will delete it later together with the main window + static PluginListDialog *const gui = + new PluginListDialog(carla_qt_get_main_window()); + + if (gui->exec()) { + static PluginListDialogResults ret; + static std::string filename; + static std::string label; + + const PluginInfo &plugin(gui->getSelectedPluginInfo()); + + filename = plugin.filename.toUtf8(); + label = plugin.label.toUtf8(); + + ret.build = plugin.build; + ret.type = plugin.type; + ret.filename = filename.c_str(); + ret.label = label.c_str(); + ret.uniqueId = plugin.uniqueId; + + return &ret; + } + + return nullptr; +} + +// ---------------------------------------------------------------------------- diff --git a/plugins/carla/pluginlistdialog.hpp b/plugins/carla/pluginlistdialog.hpp new file mode 100644 index 00000000000000..7d6ba8983054a1 --- /dev/null +++ b/plugins/carla/pluginlistdialog.hpp @@ -0,0 +1,91 @@ +/* + * Carla plugin host, adjusted for OBS + * Copyright (C) 2011-2023 Filipe Coelho + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#include + +#include "ui_pluginlistdialog.h" + +#if CARLA_VERSION_HEX >= 0x020591 +#define CARLA_2_6_FEATURES +#endif + +class QSafeSettings; +typedef struct _CarlaPluginDiscoveryInfo CarlaPluginDiscoveryInfo; +struct PluginInfo; + +// ---------------------------------------------------------------------------- +// Plugin List Dialog + +class PluginListDialog : public QDialog { + enum TableIndex { + TW_FAVORITE, + TW_NAME, + TW_LABEL, + TW_MAKER, + TW_BINARY, + }; + + enum UserRoles { + UR_PLUGIN_INFO = 1, + UR_SEARCH_TEXT, + }; + + struct PrivateData; + PrivateData *const p; + + Ui_PluginListDialog ui; + + // -------------------------------------------------------------------- + // public methods + +public: + explicit PluginListDialog(QWidget *parent); + ~PluginListDialog() override; + + const PluginInfo &getSelectedPluginInfo() const; +#ifdef CARLA_2_6_FEATURES + void addPluginInfo(const CarlaPluginDiscoveryInfo *info, + const char *sha1sum); + bool checkPluginCache(const char *filename, const char *sha1sum); +#endif + + // -------------------------------------------------------------------- + // protected methods + +protected: + void done(int) override; + void showEvent(QShowEvent *) override; + void timerEvent(QTimerEvent *) override; + + // -------------------------------------------------------------------- + // private methods + +private: + void addPluginsToTable(); + void loadSettings(); + + // -------------------------------------------------------------------- + // private slots + +private Q_SLOTS: + void cellClicked(int row, int column); + void cellDoubleClicked(int row, int column); + void focusSearchFieldAndSelectAllText(); + void checkFilters(); + void checkFiltersCategoryAll(bool clicked); + void checkFiltersCategorySpecific(bool clicked); + void clearFilters(); + void checkPlugin(int row); + void refreshPlugins(); + void refreshPluginsStart(); + void refreshPluginsStop(); + void refreshPluginsSkip(); + void saveSettings(); +}; + +// ---------------------------------------------------------------------------- diff --git a/plugins/carla/pluginlistdialog.ui b/plugins/carla/pluginlistdialog.ui new file mode 100644 index 00000000000000..fd6579ff3ad667 --- /dev/null +++ b/plugins/carla/pluginlistdialog.ui @@ -0,0 +1,765 @@ + + + PluginListDialog + + + + 0 + 0 + 1100 + 738 + + + + Plugin List + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + &Load Plugin + + + + + + + Cancel + + + + + + + + + + + true + + + + + + + Refresh + + + + + + + Reset filters + + + + + + + + + + 1 + 0 + + + + QAbstractItemView::NoEditTriggers + + + false + + + false + + + true + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + false + + + Qt::NoPen + + + true + + + false + + + 24 + + + true + + + false + + + 12 + + + 22 + + + + + + + + + + + + + + :/16x16/bookmarks.svgz:/16x16/bookmarks.svgz + + + + + Name + + + + + Label/Id/URI + + + + + Maker + + + + + Binary/Filename + + + + + + + + 0 + + + + + 0 + 0 + 141 + 241 + + + + Format + + + + + + Internal + + + + + + + LADSPA + + + + + + + LV2 + + + + + + + VST2 + + + + + + + VST3 + + + + + + + CLAP + + + + + + + JSFX + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + 0 + 0 + 141 + 168 + + + + Type + + + + + + Effects + + + + + + + Instruments + + + + + + + MIDI Plugins + + + + + + + Other/Misc + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + 0 + 0 + 141 + 305 + + + + Category + + + + + + All + + + + + + + Delay + + + + + + + Distortion + + + + + + + Dynamics + + + + + + + EQ + + + + + + + Filter + + + + + + + Modulator + + + + + + + Synth + + + + + + + Utility + + + + + + + Other + + + + + + + Qt::Vertical + + + + 20 + 23 + + + + + + + + + + + + + 0 + 0 + + + + + + + Stereo only + + + + + + + With Custom GUI + + + + + + + + 75 + true + + + + Requirements + + + + + + + Favorites only + + + + + + + + + + + + + Parameter Ins: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + UniqueID: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + TextLabel + + + + + + + Audio Ins: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + 0 + + + 1 + + + Qt::Horizontal + + + + + + + TextLabel + + + + + + + Parameter Outs: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + TextLabel + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + TextLabel + + + + + + + TextLabel + + + + + + + TextLabel + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + MIDI Ins: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + TextLabel + + + + + + + Is Synth: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 75 + true + + + + Information + + + Qt::AlignCenter + + + + + + + MIDI Outs: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + 0 + + + 1 + + + Qt::Horizontal + + + + + + + TextLabel + + + + + + + TextLabel + + + + + + + Audio Outs: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Has Custom GUI: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Type: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Format: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + TextLabel + + + + + + + TextLabel + + + + + + + + + + Focus Text Search + + + Ctrl+F + + + + + lineEdit + tableWidget + b_load + b_cancel + b_refresh + b_clear_filters + ch_internal + ch_ladspa + ch_lv2 + ch_vst + ch_vst3 + ch_clap + ch_effects + ch_instruments + ch_midi + ch_other + ch_stereo + ch_gui + frame_reqs + frame_info + + + + diff --git a/plugins/carla/pluginrefreshdialog.hpp b/plugins/carla/pluginrefreshdialog.hpp new file mode 100644 index 00000000000000..961e2361c34617 --- /dev/null +++ b/plugins/carla/pluginrefreshdialog.hpp @@ -0,0 +1,77 @@ +/* + * Carla plugin host, adjusted for OBS + * Copyright (C) 2011-2023 Filipe Coelho + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#include "ui_pluginrefreshdialog.h" + +#include "qtutils.h" + +// ---------------------------------------------------------------------------- +// Plugin Refresh Dialog + +struct PluginRefreshDialog : QDialog, Ui_PluginRefreshDialog { + explicit PluginRefreshDialog(QWidget *const parent) : QDialog(parent) + { + setupUi(this); + + setWindowFlags(windowFlags() & + ~Qt::WindowContextHelpButtonHint); +#ifdef __APPLE__ + setWindowModality(Qt::WindowModal); +#endif + + b_skip->setEnabled(false); + ch_invalid->setEnabled(false); + + // ------------------------------------------------------------ + // Load settings + + { + const QSafeSettings settings; + + restoreGeometry(settings.valueByteArray( + "PluginRefreshDialog/Geometry")); + + if (settings.valueBool("PluginRefreshDialog/RefreshAll", + false)) + ch_all->setChecked(true); + else + ch_updated->setChecked(true); + + ch_invalid->setChecked(settings.valueBool( + "PluginRefreshDialog/CheckInvalid", false)); + } + + // ------------------------------------------------------------ + // Set-up Icons + + b_start->setProperty("themeID", "playIcon"); + + // ------------------------------------------------------------ + // Set-up connections + + QObject::connect(this, &QDialog::finished, this, + &PluginRefreshDialog::saveSettings); + } + + // -------------------------------------------------------------------- + // private slots + +private Q_SLOTS: + void saveSettings() + { + QSafeSettings settings; + settings.setValue("PluginRefreshDialog/Geometry", + saveGeometry()); + settings.setValue("PluginRefreshDialog/RefreshAll", + ch_all->isChecked()); + settings.setValue("PluginRefreshDialog/CheckInvalid", + ch_invalid->isChecked()); + } +}; + +// ---------------------------------------------------------------------------- diff --git a/plugins/carla/pluginrefreshdialog.ui b/plugins/carla/pluginrefreshdialog.ui new file mode 100644 index 00000000000000..a47bc277031be7 --- /dev/null +++ b/plugins/carla/pluginrefreshdialog.ui @@ -0,0 +1,183 @@ + + + PluginRefreshDialog + + + + 0 + 0 + 873 + 179 + + + + Plugin Refresh + + + + + + + + Qt::Horizontal + + + QSizePolicy::Preferred + + + + 30 + 20 + + + + + + + + Searching for: + + + Qt::AlignCenter + + + + + + + + All plugins, ignoring cache + + + + + + + Updated plugins only + + + + + + + Check previously invalid plugins + + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Preferred + + + + 20 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 6 + + + + + + + + + + + 0 + 0 + + + + 100 + + + 0 + + + Press 'Scan' to begin the search + + + + + + + Scan + + + + + + + >> Skip + + + + + + + Close + + + + + + + + + + b_close + clicked() + PluginRefreshDialog + close() + + + 426 + 231 + + + 236 + 125 + + + + + ch_updated + toggled(bool) + ch_invalid + setEnabled(bool) + + + 436 + 78 + + + 436 + 105 + + + + + diff --git a/plugins/carla/qtutils.cpp b/plugins/carla/qtutils.cpp new file mode 100644 index 00000000000000..6c7cb250a776b4 --- /dev/null +++ b/plugins/carla/qtutils.cpp @@ -0,0 +1,158 @@ +/* + * Carla plugin for OBS + * Copyright (C) 2023 Filipe Coelho + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include +#include +#include +#include +#include + +#include + +#include "qtutils.h" + +//----------------------------------------------------------------------------- +// open a qt file dialog + +char *carla_qt_file_dialog(bool save, bool isDir, const char *title, + const char *filter) +{ + static QByteArray ret; + + QWidget *parent = carla_qt_get_main_window(); + QFileDialog::Options options; + + if (isDir) + options |= QFileDialog::ShowDirsOnly; + + ret = save ? QFileDialog::getSaveFileName(parent, title, {}, filter, + nullptr, options) + .toUtf8() + : QFileDialog::getOpenFileName(parent, title, {}, filter, + nullptr, options) + .toUtf8(); + + return ret.data(); +} + +//----------------------------------------------------------------------------- +// call a function on the main thread + +void carla_qt_callback_on_main_thread(void (*callback)(void *param), + void *param) +{ + if (QThread::currentThread() == qApp->thread()) { + callback(param); + return; + } + + QTimer *const maintimer = new QTimer; + maintimer->moveToThread(qApp->thread()); + maintimer->setSingleShot(true); + QObject::connect(maintimer, &QTimer::timeout, + [maintimer, callback, param]() { + callback(param); + maintimer->deleteLater(); + }); + QMetaObject::invokeMethod(maintimer, "start", Qt::QueuedConnection, + Q_ARG(int, 0)); +} + +//----------------------------------------------------------------------------- +// get the top-level qt main window + +QMainWindow *carla_qt_get_main_window(void) +{ + for (QWidget *w : QApplication::topLevelWidgets()) { + if (QMainWindow *mw = qobject_cast(w)) + return mw; + } + + return nullptr; +} + +//----------------------------------------------------------------------------- +// show an error dialog (on main thread and without blocking current scope) + +static void carla_show_error_dialog_later(void *const param) +{ + char **const texts = static_cast(param); + carla_show_error_dialog(texts[0], texts[1]); + bfree(texts[0]); + bfree(texts[1]); + bfree(texts); +} + +void carla_show_error_dialog(const char *const text1, const char *const text2) +{ + // there is no point showing incomplete error messages + if (text1 == nullptr || text2 == nullptr) + return; + + // we cannot do Qt gui stuff outside the main thread + // do a little dance so we call ourselves later on the main thread + if (QThread::currentThread() != qApp->thread()) { + char **const texts = + static_cast(bmalloc(sizeof(char *) * 2)); + texts[0] = bstrdup(text1); + texts[1] = bstrdup(text2); + carla_qt_callback_on_main_thread(carla_show_error_dialog_later, + texts); + return; + } + + QMessageBox *const box = new QMessageBox(carla_qt_get_main_window()); + box->setWindowTitle("Error"); + box->setText(QString("%1: %2").arg(text1).arg(text2)); + QObject::connect(box, &QDialog::finished, box, &QWidget::deleteLater); + QMetaObject::invokeMethod(box, "show", Qt::QueuedConnection); +} + +//----------------------------------------------------------------------------- + +#if QT_VERSION >= 0x60000 +static const auto q_meta_bool = QMetaType(QMetaType::Bool); +static const auto q_meta_bytearray = QMetaType(QMetaType::QByteArray); +static const auto q_meta_string = QMetaType(QMetaType::QString); +#else +constexpr auto q_meta_bool = QVariant::Bool; +constexpr auto q_meta_bytearray = QVariant::ByteArray; +constexpr auto q_meta_string = QVariant::String; +#endif + +bool QSafeSettings::valueBool(const QString &key, const bool defaultValue) const +{ + QVariant var(value(key, defaultValue)); + + if (!var.isNull() && var.convert(q_meta_bool) && var.isValid()) + return var.toBool(); + + return defaultValue; +} + +QString QSafeSettings::valueString(const QString &key, + const QString &defaultValue) const +{ + QVariant var(value(key, defaultValue)); + + if (!var.isNull() && var.convert(q_meta_string) && var.isValid()) + return var.toString(); + + return defaultValue; +} + +QByteArray QSafeSettings::valueByteArray(const QString &key, + const QByteArray defaultValue) const +{ + QVariant var(value(key, defaultValue)); + + if (!var.isNull() && var.convert(q_meta_bytearray) && var.isValid()) + return var.toByteArray(); + + return defaultValue; +} + +//----------------------------------------------------------------------------- diff --git a/plugins/carla/qtutils.h b/plugins/carla/qtutils.h new file mode 100644 index 00000000000000..4120ce387e1b95 --- /dev/null +++ b/plugins/carla/qtutils.h @@ -0,0 +1,135 @@ +/* + * Carla plugin for OBS + * Copyright (C) 2023 Filipe Coelho + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +//----------------------------------------------------------------------------- + +#ifdef __cplusplus +#include +#include +#include +extern "C" { +#else +#include +typedef struct QMainWindow QMainWindow; +#endif + +//----------------------------------------------------------------------------- + +typedef struct { + uint build; + uint type; + const char *filename; + const char *label; + uint64_t uniqueId; +} PluginListDialogResults; + +const PluginListDialogResults *carla_exec_plugin_list_dialog(); + +//----------------------------------------------------------------------------- +// open a qt file dialog + +char *carla_qt_file_dialog(bool save, bool isDir, const char *title, + const char *filter); + +//----------------------------------------------------------------------------- +// call a function on the main thread + +void carla_qt_callback_on_main_thread(void (*callback)(void *param), + void *param); + +//----------------------------------------------------------------------------- +// get the top-level qt main window + +QMainWindow *carla_qt_get_main_window(void); + +//----------------------------------------------------------------------------- +// show an error dialog (on main thread and without blocking current scope) + +void carla_show_error_dialog(const char *text1, const char *text2); + +//----------------------------------------------------------------------------- + +#ifdef __cplusplus +} // extern "C" + +//----------------------------------------------------------------------------- +// Safer QSettings class, which does not throw if type mismatches + +class QSafeSettings : public QSettings { +public: + inline QSafeSettings() : QSettings("obs-studio", "obs") {} + + bool valueBool(const QString &key, bool defaultValue) const; + QString valueString(const QString &key, + const QString &defaultValue) const; + QByteArray valueByteArray(const QString &key, + QByteArray defaultValue = {}) const; +}; + +//----------------------------------------------------------------------------- +// Custom QString class with default utf-8 mode and a few extra methods + +class QUtf8String : public QString { +public: + explicit inline QUtf8String() : QString() {} + + explicit inline QUtf8String(const char *const str) + : QString(fromUtf8(str)) + { + } + + inline QUtf8String(const QString &s) : QString(s) {} + + inline bool isNotEmpty() const { return !isEmpty(); } + + inline QUtf8String &operator=(const char *const str) + { + return (*this = fromUtf8(str)); + } + + inline QUtf8String strip() const { return simplified().remove(' '); } + +#if QT_VERSION < 0x60000 + explicit inline QUtf8String(const QChar *const str, const size_t size) + : QString(str, size) + { + } + + inline QUtf8String sliced(const size_t pos) const + { + return QUtf8String(data() + pos, size() - pos); + } +#endif +}; + +//----------------------------------------------------------------------------- +// Custom QByteArray class with a few extra methods for Qt5 compat + +#if QT_VERSION < 0x60000 +class QCompatByteArray : public QByteArray { +public: + explicit inline QCompatByteArray() : QByteArray() {} + + explicit inline QCompatByteArray(const char *const data, + const size_t size) + : QByteArray(data, size) + { + } + + inline QCompatByteArray(const QByteArray &b) : QByteArray(b) {} + + inline QCompatByteArray sliced(const size_t pos) const + { + return QCompatByteArray(data() + pos, size() - pos); + } +}; +#else +typedef QByteArray QCompatByteArray; +#endif + +#endif // __cplusplus From 8855c4ee663a44b05a33b07520d0ea75e260f7c4 Mon Sep 17 00:00:00 2001 From: falkTX Date: Fri, 29 Sep 2023 11:11:24 +0200 Subject: [PATCH 2/8] Build against latest carla Signed-off-by: falkTX --- buildspec.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/buildspec.json b/buildspec.json index f084b2c0eff113..4967b3c1631253 100644 --- a/buildspec.json +++ b/buildspec.json @@ -1,26 +1,26 @@ { "dependencies": { "prebuilt": { - "version": "2023-09-18", - "baseUrl": "https://github.com/obsproject/obs-deps/releases/download", + "version": "2023-09-28", + "baseUrl": "https://github.com/falkTX/obs-deps/releases/download", "label": "Pre-Built obs-deps", "hashes": { - "macos-universal": "aa9728e04390b4e21f37dbc64d0e182f6b84a6208af0872c7ef386e02be9d54a", - "windows-x64": "c46ba0bb7d5713a8ce1e5456211e4c6b919075cc6c645195f42f8927e1dab29b", - "windows-x86": "c5703c3e71dc2d3c8f8266977bb02f6c7e370cd19633968b192c639599522807", - "linux-x86_64": "40a52e8fc47b6eb02dfa4ba0b24ba79a9f4e34f3c30b3223d55b949618477af5" + "macos-universal": "7c3c88cd943a97e205045dd6545b0bf960a5646d44d86ff08590370a63f1bd6c", + "windows-x64": "7d5d168988a476d064e187e54d3839af8451fbbc2b68774960b270a1dd5ca553", + "windows-x86": "d5da4649d14dbec75b2c3623a163a254d7e3e5e8cad1499271c54b24406a5a4b", + "linux-x86_64": "299fb21599fbe1025c8411b8e9480eb202256667d27e8c313be9e76e246564f9" } }, "qt6": { - "version": "2023-09-18", - "baseUrl": "https://github.com/obsproject/obs-deps/releases/download", + "version": "2023-09-28", + "baseUrl": "https://github.com/falkTX/obs-deps/releases/download", "label": "Pre-Built Qt6", "hashes": { - "macos-universal": "223c112c085e0da3baf257ed3a0a7334b222b0fb411cd686d65eba9ebd3d74f4", - "windows-x64": "d57688e751dd6c3a839ee2ed962a6b78e3e162ecd1d2ab350c324a61964cc8ef" + "macos-universal": "a5762f7ee377858907b9106fa26be095a9bbd0a51c4eacb940827420ae1683a0", + "windows-x64": "7fbe00b068b72f61c58d6291352f6cf37353dd09e4d2cc19efef1a48233dfc1b" }, "debugSymbols": { - "windows-x64": "08ada0228d53d5f90e5032fd37c4b7d518e9243158e71ebfb1df7bcab6eb30c3" + "windows-x64": "5011605a96411629f6e489d2f42d1a30099628d8a92775ef0cf4644f3b92a4b0" } }, "cef": { From 9e9416860b774ef80400024a782e19b967096476 Mon Sep 17 00:00:00 2001 From: falkTX Date: Fri, 29 Sep 2023 11:11:36 +0200 Subject: [PATCH 3/8] Keep compat for msvc bins, needs working alternative.. Signed-off-by: falkTX --- cmake/Modules/CopyMSVCBins.cmake | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/cmake/Modules/CopyMSVCBins.cmake b/cmake/Modules/CopyMSVCBins.cmake index 390a49edf51643..5c7a9d6c8613ed 100644 --- a/cmake/Modules/CopyMSVCBins.cmake +++ b/cmake/Modules/CopyMSVCBins.cmake @@ -209,6 +209,18 @@ file(GLOB QT_IMAGEFORMATS_BIN_FILES "${QtCore_PLUGIN_DIR}/imageformats/qsvg.dll" file(GLOB QT_ICU_BIN_FILES "${QtCore_BIN_DIR}/icu*.dll") +file( + GLOB + CARLA_BIN_FILES + "${CARLAUTILS_INCLUDE_DIR}/../../bin/carla-*.exe" + "${CARLAUTILS_INCLUDE_DIR}/../../bin/libcarla_utils.dll" + "${CARLAUTILS_INCLUDE_DIR}/../../bin${_bin_suffix}/carla-*.exe" + "${CARLAUTILS_INCLUDE_DIR}/../../bin${_bin_suffix}/libcarla_utils.dll" + "${CARLAUTILS_INCLUDE_DIR}/../bin/carla-*.exe" + "${CARLAUTILS_INCLUDE_DIR}/../bin/libcarla_utils.dll" + "${CARLAUTILS_INCLUDE_DIR}/../bin${_bin_suffix}/carla-*.exe" + "${CARLAUTILS_INCLUDE_DIR}/../bin${_bin_suffix}/libcarla_utils.dll") + set(ALL_BASE_BIN_FILES ${FFMPEG_BIN_FILES} ${X264_BIN_FILES} @@ -219,6 +231,7 @@ set(ALL_BASE_BIN_FILES ${LIBFDK_BIN_FILES} ${FREETYPE_BIN_FILES} ${RNNOISE_BIN_FILES} + ${CARLA_BIN_FILES} ${QT_ICU_BIN_FILES}) set(ALL_REL_BIN_FILES ${QT_BIN_FILES}) @@ -272,6 +285,7 @@ obs_status(STATUS "curl files: ${CURL_BIN_FILES}") obs_status(STATUS "lua files: ${LUA_BIN_FILES}") obs_status(STATUS "ssl files: ${SSL_BIN_FILES}") obs_status(STATUS "zlib files: ${ZLIB_BIN_FILES}") +obs_status(STATUS "carla files: ${CARLA_BIN_FILES}") obs_status(STATUS "Qt Debug files: ${QT_DEBUG_BIN_FILES}") obs_status(STATUS "Qt Debug Platform files: ${QT_DEBUG_PLAT_BIN_FILES}") obs_status(STATUS "Qt Debug Styles files: ${QT_DEBUG_STYLES_BIN_FILES}") From 2591a4156ab5c0554494184d09e325b027cbfea9 Mon Sep 17 00:00:00 2001 From: falkTX Date: Fri, 29 Sep 2023 11:23:57 +0200 Subject: [PATCH 4/8] Cleanup Signed-off-by: falkTX --- buildspec.json | 2 +- cmake/finders/FindCarlaUtils.cmake | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/buildspec.json b/buildspec.json index 4967b3c1631253..90ebc76d643e70 100644 --- a/buildspec.json +++ b/buildspec.json @@ -17,7 +17,7 @@ "label": "Pre-Built Qt6", "hashes": { "macos-universal": "a5762f7ee377858907b9106fa26be095a9bbd0a51c4eacb940827420ae1683a0", - "windows-x64": "7fbe00b068b72f61c58d6291352f6cf37353dd09e4d2cc19efef1a48233dfc1b" + "windows-x64": "5ea5c280229ffd99f4ef44f513e7c34dbd051a83e55cc3f88201b4eee7da5345" }, "debugSymbols": { "windows-x64": "5011605a96411629f6e489d2f42d1a30099628d8a92775ef0cf4644f3b92a4b0" diff --git a/cmake/finders/FindCarlaUtils.cmake b/cmake/finders/FindCarlaUtils.cmake index 566aca066adba5..c4667b11da3034 100644 --- a/cmake/finders/FindCarlaUtils.cmake +++ b/cmake/finders/FindCarlaUtils.cmake @@ -36,7 +36,7 @@ if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin" AND NOT PC_CarlaUtils_FOUND) set(CarlaUtils_USE_MACOS_FRAMEWORK TRUE) else() message("DEBUG: NOT using carla-utils macos framework | ${PC_CarlaUtils_FOUND} | ${PC_CarlaUtils_LDFLAGS}") - # set(CarlaUtils_USE_MACOS_FRAMEWORK FALSE) + set(CarlaUtils_USE_MACOS_FRAMEWORK FALSE) endif() find_library( @@ -102,10 +102,11 @@ if(CarlaUtils_FOUND) set(CarlaUtils_LIBRARIES ${CarlaUtils_LIBRARY}) if(NOT TARGET carla::utils) - if(${CarlaUtils_USE_MACOS_FRAMEWORK}) + if(CarlaUtils_USE_MACOS_FRAMEWORK) add_library(carla::utils INTERFACE IMPORTED GLOBAL) set_target_properties(carla::utils PROPERTIES IMPORTED_LOCATION "${CarlaUtils_LIBRARIES}") - set_target_properties(carla::utils PROPERTIES INTERFACE_LINK_LIBRARIES $) + set_target_properties(carla::utils PROPERTIES INTERFACE_LINK_LIBRARIES + $) elseif(IS_ABSOLUTE "${CarlaUtils_LIBRARIES}") add_library(carla::utils UNKNOWN IMPORTED GLOBAL) set_target_properties(carla::utils PROPERTIES IMPORTED_LOCATION "${CarlaUtils_LIBRARIES}") @@ -116,8 +117,6 @@ if(CarlaUtils_FOUND) if(PC_CarlaUtils_FOUND) set_target_properties(carla::utils PROPERTIES INTERFACE_LINK_OPTIONS ${PC_CarlaUtils_LDFLAGS}) - # elseif(${CarlaUtils_USE_MACOS_FRAMEWORK}) - # set_target_properties(carla::utils PROPERTIES INTERFACE_LINK_LIBRARIES $) endif() set_target_properties(carla::utils PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${CarlaUtils_INCLUDE_DIRS}") From 959708505be965e6b5dc9daa72a12fe3526f3424 Mon Sep 17 00:00:00 2001 From: falkTX Date: Fri, 29 Sep 2023 12:11:41 +0200 Subject: [PATCH 5/8] Use old-style comments, cleanup Signed-off-by: falkTX --- build-aux/modules/90-carla.json | 4 +- plugins/carla/carla-bridge-wrapper.cpp | 26 +-- plugins/carla/carla-bridge.cpp | 288 ++++++++++++++----------- plugins/carla/carla-bridge.hpp | 77 +++---- plugins/carla/carla-patchbay-wrapper.c | 34 +-- plugins/carla/carla-wrapper.h | 10 +- plugins/carla/carla.c | 56 +++-- plugins/carla/common.c | 16 +- plugins/carla/common.h | 6 +- plugins/carla/pluginlistdialog.cpp | 179 +++++++-------- plugins/carla/pluginlistdialog.hpp | 17 +- plugins/carla/pluginrefreshdialog.hpp | 17 +- plugins/carla/qtutils.cpp | 23 +- plugins/carla/qtutils.h | 38 +--- 14 files changed, 358 insertions(+), 433 deletions(-) diff --git a/build-aux/modules/90-carla.json b/build-aux/modules/90-carla.json index 33af40260dbc60..10ade0b06b0e5d 100644 --- a/build-aux/modules/90-carla.json +++ b/build-aux/modules/90-carla.json @@ -5,8 +5,8 @@ "subdir": "cmake", "config-opts": [ "-DCMAKE_BUILD_TYPE=Release", - "-DCARLA_USE_JACK:BOOL=OFF", - "-DCARLA_USE_OSC:BOOL=OFF" + "-DCARLA_USE_JACK=OFF", + "-DCARLA_USE_OSC=OFF" ], "cleanup": [ "/include", diff --git a/plugins/carla/carla-bridge-wrapper.cpp b/plugins/carla/carla-bridge-wrapper.cpp index 0a7d91dec25ba4..42b3cc5d14648c 100644 --- a/plugins/carla/carla-bridge-wrapper.cpp +++ b/plugins/carla/carla-bridge-wrapper.cpp @@ -4,7 +4,7 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -// needed for strcasestr +/* needed for strcasestr */ #if !defined(_GNU_SOURCE) && !defined(_WIN32) #define _GNU_SOURCE #endif @@ -26,15 +26,14 @@ #define CARLA_2_6_FEATURES #endif -// ---------------------------------------------------------------------------- -// private data methods +/* private data methods */ struct carla_priv : carla_bridge_callback { obs_source_t *source = nullptr; uint32_t bufferSize = 0; double sampleRate = 0; - // update properties when timeout is reached, 0 means do nothing + /* update properties when timeout is reached, 0 means do nothing */ uint64_t update_request = 0; carla_bridge bridge; @@ -61,8 +60,7 @@ struct carla_priv : carla_bridge_callback { } }; -// ---------------------------------------------------------------------------- -// carla + obs integration methods +/* carla + obs integration methods */ struct carla_priv *carla_priv_create(obs_source_t *source, enum buffer_size_mode bufsize, @@ -86,8 +84,6 @@ void carla_priv_destroy(struct carla_priv *priv) delete priv; } -// ---------------------------------------------------------------------------- - void carla_priv_activate(struct carla_priv *priv) { priv->bridge.activate(); @@ -110,8 +106,6 @@ void carla_priv_idle(struct carla_priv *priv) handle_update_request(priv->source, &priv->update_request); } -// ---------------------------------------------------------------------------- - void carla_priv_save(struct carla_priv *priv, obs_data_t *settings) { priv->bridge.save_and_wait(); @@ -198,7 +192,7 @@ void carla_priv_load(struct carla_priv *priv, obs_data_t *settings) const PluginType ptype = getPluginTypeFromString(obs_data_get_string(settings, "ptype")); - // abort early if both of these are null, likely from an empty config + /* abort early if both of these are null, likely from an empty config */ if (btype == BINARY_NONE && ptype == PLUGIN_NONE) return; @@ -268,8 +262,6 @@ void carla_priv_load(struct carla_priv *priv, obs_data_t *settings) priv->bridge.activate(); } -// ---------------------------------------------------------------------------- - uint32_t carla_priv_get_num_channels(struct carla_priv *priv) { return std::max(priv->bridge.info.numAudioIns, @@ -282,8 +274,6 @@ void carla_priv_set_buffer_size(struct carla_priv *priv, priv->bridge.set_buffer_size(bufsize_mode_to_frames(bufsize)); } -// ---------------------------------------------------------------------------- - static bool carla_post_load_callback(struct carla_priv *priv, obs_properties_t *props) { @@ -309,7 +299,7 @@ static bool carla_priv_load_file_callback(obs_properties_t *props, return false; #ifndef _WIN32 - // truncate plug.vst3/Contents//plug.so -> plug.vst3 + /* truncate plug.vst3/Contents//plug.so -> plug.vst3 */ if (char *const vst3str = strcasestr(filename, ".vst3/Contents/")) vst3str[5] = '\0'; #endif @@ -398,7 +388,7 @@ static bool carla_priv_reload_callback(obs_properties_t *props, if (priv->bridge.info.btype == BINARY_NONE) return false; - // cache relevant information for later + /* cache relevant information for later */ const BinaryType btype = priv->bridge.info.btype; const PluginType ptype = priv->bridge.info.ptype; const int64_t uniqueId = priv->bridge.info.uniqueId; @@ -573,5 +563,3 @@ void carla_priv_readd_properties(struct carla_priv *priv, obs_data_release(settings); } - -// ---------------------------------------------------------------------------- diff --git a/plugins/carla/carla-bridge.cpp b/plugins/carla/carla-bridge.cpp index 3d336ec76cf14b..d2310a6535f766 100644 --- a/plugins/carla/carla-bridge.cpp +++ b/plugins/carla/carla-bridge.cpp @@ -26,9 +26,7 @@ #include "qtutils.h" #if defined(__APPLE__) && defined(__aarch64__) -// ---------------------------------------------------------------------------- -// check the header of a plugin binary to see if it matches mach 64bit + intel - +/* check the header of a plugin binary to see if it matches mach 64bit + intel */ static bool isIntel64BitPlugin(const char *const pluginBundle) { const char *const pluginBinary = findBinaryInBundle(pluginBundle); @@ -50,9 +48,7 @@ static bool isIntel64BitPlugin(const char *const pluginBundle) } #endif -// ---------------------------------------------------------------------------- -// utility class for reading and deleting incoming bridge text in RAII fashion - +/* utility class for reading and deleting incoming bridge text in RAII fashion */ struct BridgeTextReader { char *text = nullptr; @@ -82,15 +78,14 @@ struct BridgeTextReader { CARLA_DECLARE_NON_COPYABLE(BridgeTextReader) }; -// ---------------------------------------------------------------------------- -// custom bridge process implementation +/* custom bridge process implementation */ BridgeProcess::BridgeProcess(const char *const shmIds) { - // move object to the correct/expected thread + /* move object to the correct/expected thread */ moveToThread(qApp->thread()); - // setup environment for client side + /* setup environment for client side */ QProcessEnvironment env(QProcessEnvironment::systemEnvironment()); env.insert("ENGINE_BRIDGE_SHM_IDS", shmIds); setProcessEnvironment(env); @@ -98,13 +93,13 @@ BridgeProcess::BridgeProcess(const char *const shmIds) void BridgeProcess::start() { - // pass-through all bridge output + /* pass-through all bridge output */ setInputChannelMode(QProcess::ForwardedInputChannel); setProcessChannelMode(QProcess::ForwardedChannels); QProcess::start(QIODevice::Unbuffered | QIODevice::ReadOnly); } -// NOTE: process instance cannot be used after this! +/* NOTE: process instance cannot be used after this! */ void BridgeProcess::stop() { if (state() != QProcess::NotRunning) { @@ -121,14 +116,14 @@ void BridgeProcess::stop() deleteLater(); } -// ---------------------------------------------------------------------------- +/* carla bridge implementation */ bool carla_bridge::init(uint32_t maxBufferSize, double sampleRate) { - // add entropy to rand calls, used for finding unused paths + /* add entropy to rand calls, used for finding unused paths */ std::srand(static_cast(os_gettime_ns() / 1000000)); - // initialize the several communication channels + /* initialize the several communication channels */ if (!audiopool.initializeServer()) { blog(LOG_WARNING, "[carla] Failed to initialize shared memory audio pool"); @@ -153,25 +148,25 @@ bool carla_bridge::init(uint32_t maxBufferSize, double sampleRate) goto fail4; } - // resize audiopool data to be as large as needed + /* resize audiopool data to be as large as needed */ audiopool.resize(maxBufferSize, MAX_AV_PLANES, MAX_AV_PLANES); - // clear realtime data + /* clear realtime data */ rtClientCtrl.data->procFlags = 0; carla_zeroStruct(rtClientCtrl.data->timeInfo); carla_zeroBytes(rtClientCtrl.data->midiOut, kBridgeRtClientDataMidiOutSize); - // clear ringbuffers + /* clear ringbuffers */ rtClientCtrl.clearData(); nonRtClientCtrl.clearData(); nonRtServerCtrl.clearData(); - // first ever message is bridge API version + /* first ever message is bridge API version */ nonRtClientCtrl.writeOpcode(kPluginBridgeNonRtClientVersion); nonRtClientCtrl.writeUInt(CARLA_PLUGIN_BRIDGE_API_VERSION_CURRENT); - // then expected size for each data channel + /* then expected size for each data channel */ nonRtClientCtrl.writeUInt( static_cast(sizeof(BridgeRtClientData))); nonRtClientCtrl.writeUInt( @@ -179,19 +174,19 @@ bool carla_bridge::init(uint32_t maxBufferSize, double sampleRate) nonRtClientCtrl.writeUInt( static_cast(sizeof(BridgeNonRtServerData))); - // and finally the initial buffer size and sample rate + /* and finally the initial buffer size and sample rate */ nonRtClientCtrl.writeOpcode(kPluginBridgeNonRtClientInitialSetup); nonRtClientCtrl.writeUInt(maxBufferSize); nonRtClientCtrl.writeDouble(sampleRate); nonRtClientCtrl.commitWrite(); - // report audiopool size to client side + /* report audiopool size to client side */ rtClientCtrl.writeOpcode(kPluginBridgeRtClientSetAudioPool); rtClientCtrl.writeULong(static_cast(audiopool.dataSize)); rtClientCtrl.commitWrite(); - // FIXME + /* FIXME maybe not needed */ rtClientCtrl.writeOpcode(kPluginBridgeRtClientSetBufferSize); rtClientCtrl.writeUInt(maxBufferSize); rtClientCtrl.commitWrite(); @@ -215,17 +210,17 @@ bool carla_bridge::init(uint32_t maxBufferSize, double sampleRate) void carla_bridge::cleanup(const bool clearPluginData) { - // signal to stop processing audio + /* signal to stop processing audio */ const bool wasActivated = activated; ready = activated = false; - // stop bridge process + /* stop bridge process */ if (childprocess != nullptr) { - // make `childprocess` null first + /* make `childprocess` null first */ BridgeProcess *proc = childprocess; childprocess = nullptr; - // if process is running, ask nicely for it to close + /* if process is running, ask nicely for it to close */ if (proc->state() != QProcess::NotRunning) { { const CarlaMutexLocker cml( @@ -248,7 +243,7 @@ void carla_bridge::cleanup(const bool clearPluginData) if (!timedErr && !timedOut) wait("stopping", 3000); } else { - // log warning in case plugin process crashed + /* log warning in case plugin process crashed */ if (proc->exitStatus() == QProcess::CrashExit) { blog(LOG_WARNING, "[carla] bridge crashed"); @@ -260,17 +255,17 @@ void carla_bridge::cleanup(const bool clearPluginData) } } - // let Qt do the final cleanup on the main thread + /* let Qt do the final cleanup on the main thread */ QMetaObject::invokeMethod(proc, "stop"); } - // cleanup shared memory bits + /* cleanup shared memory bits */ nonRtServerCtrl.clear(); nonRtClientCtrl.clear(); rtClientCtrl.clear(); audiopool.clear(); - // clear cached plugin data if requested + /* clear cached plugin data if requested */ if (clearPluginData) { info.clear(); chunk.clear(); @@ -282,13 +277,13 @@ bool carla_bridge::start(const BinaryType btype, const PluginType ptype, const char *label, const char *filename, const int64_t uniqueId) { - // make sure we are trying to load something valid + /* make sure we are trying to load something valid */ if (btype == BINARY_NONE || ptype == PLUGIN_NONE) { setLastError("Invalid plugin state"); return false; } - // find path to bridge binary + /* find path to bridge binary */ QString bridgeBinary(QString::fromUtf8(get_carla_bin_path())); if (btype == BINARY_NATIVE) { @@ -320,7 +315,7 @@ bool carla_bridge::start(const BinaryType btype, const PluginType ptype, return false; } - // create string of shared memory ids to pass into the bridge process + /* create string of shared memory ids to pass into the bridge process */ char shmIdsStr[6 * 4 + 1] = {}; size_t len = audiopool.filename.length(); @@ -339,20 +334,21 @@ bool carla_bridge::start(const BinaryType btype, const PluginType ptype, CARLA_SAFE_ASSERT_RETURN(len > 6, false); std::strncpy(shmIdsStr + 18, &nonRtServerCtrl.filename[len - 6], 6); - // create bridge process and setup arguments + /* create bridge process and setup arguments */ BridgeProcess *proc = new BridgeProcess(shmIdsStr); QStringList arguments; #if defined(__APPLE__) && defined(__aarch64__) - // see if this binary needs special help (x86_64 plugins under arm64 systems) + /* see if this binary needs special help (x86_64 plugins under arm64 systems) */ switch (ptype) { case PLUGIN_VST2: case PLUGIN_VST3: case PLUGIN_CLAP: if (isIntel64BitPlugin(filename)) { - // TODO we need to hook into qprocess for: - // posix_spawnattr_setbinpref_np + CPU_TYPE_X86_64 + /* TODO we need to hook into qprocess for: + * posix_spawnattr_setbinpref_np + CPU_TYPE_X86_64 + */ arguments.append("-arch"); arguments.append("x86_64"); arguments.append(bridgeBinary); @@ -363,31 +359,31 @@ bool carla_bridge::start(const BinaryType btype, const PluginType ptype, } #endif - // do not use null strings for label and filename + /* do not use null strings for label and filename */ if (label == nullptr || label[0] == '\0') label = "(none)"; if (filename == nullptr || filename[0] == '\0') filename = "(none)"; - // arg 1: plugin type + /* arg 1: plugin type */ arguments.append(QString::fromUtf8(getPluginTypeAsString(ptype))); - // arg 2: filename + /* arg 2: filename */ arguments.append(QString::fromUtf8(filename)); - // arg 3: label + /* arg 3: label */ arguments.append(QString::fromUtf8(label)); - // arg 4: uniqueId + /* arg 4: uniqueId */ arguments.append(QString::number(uniqueId)); proc->setProgram(bridgeBinary); proc->setArguments(arguments); - // start process on main thread + /* start process on main thread */ QMetaObject::invokeMethod(proc, "start"); - // check if it started correctly + /* check if it started correctly */ const bool started = proc->waitForStarted(5000); if (!started) { @@ -396,19 +392,20 @@ bool carla_bridge::start(const BinaryType btype, const PluginType ptype, return false; } - // wait for plugin process to start talking to us + /* wait for plugin process to start talking to us */ ready = false; timedErr = false; timedOut = false; const uint64_t start_time = os_gettime_ns(); - // NOTE: we cannot rely on `proc->state() == QProcess::Running` here - // as Qt only updates QProcess state on main thread + /* NOTE: we cannot rely on `proc->state() == QProcess::Running` here + * as Qt only updates QProcess state on main thread + */ while (proc != nullptr && !ready && !timedErr) { os_sleep_ms(5); - // timeout after 5s + /* timeout after 5s */ if (os_gettime_ns() - start_time > 5 * 1000000000ULL) break; @@ -423,10 +420,10 @@ bool carla_bridge::start(const BinaryType btype, const PluginType ptype, return false; } - // refuse to load plugin with incompatible IO + /* refuse to load plugin with incompatible IO */ if (info.hasCV || info.numAudioIns > MAX_AV_PLANES || info.numAudioOuts > MAX_AV_PLANES) { - // tell bridge process to quit + /* tell bridge process to quit */ nonRtClientCtrl.writeOpcode(kPluginBridgeNonRtClientQuit); nonRtClientCtrl.commitWrite(); rtClientCtrl.writeOpcode(kPluginBridgeRtClientQuit); @@ -434,13 +431,13 @@ bool carla_bridge::start(const BinaryType btype, const PluginType ptype, wait("stopping", 3000); QMetaObject::invokeMethod(proc, "stop"); - // cleanup shared memory bits + /* cleanup shared memory bits */ nonRtServerCtrl.clear(); nonRtClientCtrl.clear(); rtClientCtrl.clear(); audiopool.clear(); - // also clear cached info + /* also clear cached info */ info.clear(); chunk.clear(); clear_custom_data(); @@ -452,14 +449,14 @@ bool carla_bridge::start(const BinaryType btype, const PluginType ptype, return false; } - // cache relevant information for later + /* cache relevant information for later */ info.btype = btype; info.ptype = ptype; info.filename = filename; info.label = label; info.uniqueId = uniqueId; - // finally assign childprocess and set active + /* finally assign childprocess and set active */ childprocess = proc; return true; @@ -523,8 +520,6 @@ bool carla_bridge::wait(const char *const action, const uint msecs) return false; } -// ---------------------------------------------------------------------------- - void carla_bridge::set_value(uint index, float value) { CARLA_SAFE_ASSERT_UINT2_RETURN(index < paramCount, index, paramCount, ); @@ -642,13 +637,13 @@ void carla_bridge::reload() CARLA_SAFE_EXCEPTION("reload - waitForClient"); } - // wait for plugin process to start talking back to us + /* wait for plugin process to start talking back to us */ const uint64_t start_time = os_gettime_ns(); while (childprocess != nullptr && !ready) { os_sleep_ms(5); - // timeout after 1s + /* timeout after 1s */ if (os_gettime_ns() - start_time > 1000000000ULL) break; @@ -801,7 +796,7 @@ void carla_bridge::add_custom_data(const char *const type, CARLA_SAFE_ASSERT_RETURN(key != nullptr && key[0] != '\0', ); CARLA_SAFE_ASSERT_RETURN(value != nullptr, ); - // Check if we already have this key + /* Check if we already have this key */ bool found = false; for (CustomData &cdata : customData) { if (std::strcmp(cdata.key, key) == 0) { @@ -812,7 +807,7 @@ void carla_bridge::add_custom_data(const char *const type, } } - // Otherwise store it + /* Otherwise store it */ if (!found) { CustomData cdata = {}; cdata.type = bstrdup(type); @@ -943,31 +938,32 @@ void carla_bridge::save_and_wait() { const CarlaMutexLocker cml(nonRtClientCtrl.mutex); - // deactivate bridge client-side ping check - // some plugins block during save, preventing regular ping timings + /* deactivate bridge client-side ping check + * some plugins block during save, preventing regular ping timings + */ nonRtClientCtrl.writeOpcode(kPluginBridgeNonRtClientPingOnOff); nonRtClientCtrl.writeBool(false); nonRtClientCtrl.commitWrite(); - // tell plugin bridge to save and report any pending data + /* tell plugin bridge to save and report any pending data */ nonRtClientCtrl.writeOpcode( kPluginBridgeNonRtClientPrepareForSave); nonRtClientCtrl.commitWrite(); } - // wait for "saved" reply + /* wait for "saved" reply */ const uint64_t start_time = os_gettime_ns(); while (is_running() && !saved) { os_sleep_ms(5); - // timeout after 10s + /* timeout after 10s */ if (os_gettime_ns() - start_time > 10 * 1000000000ULL) break; readMessages(); - // deactivate plugin if we timeout during save + /* deactivate plugin if we timeout during save */ if (timedOut && activated) { activated = false; @@ -982,7 +978,7 @@ void carla_bridge::save_and_wait() if (is_running()) { const CarlaMutexLocker cml(nonRtClientCtrl.mutex); - // reactivate ping check + /* reactivate ping check */ nonRtClientCtrl.writeOpcode(kPluginBridgeNonRtClientPingOnOff); nonRtClientCtrl.writeBool(true); nonRtClientCtrl.commitWrite(); @@ -1015,21 +1011,19 @@ const char *carla_bridge::get_last_error() const noexcept return lastError; } -// ---------------------------------------------------------------------------- - void carla_bridge::readMessages() { while (nonRtServerCtrl.isDataAvailableForReading()) { const PluginBridgeNonRtServerOpcode opcode = nonRtServerCtrl.readOpcode(); - // #ifdef DEBUG + /* #ifdef DEBUG */ if (opcode != kPluginBridgeNonRtServerPong && opcode != kPluginBridgeNonRtServerParameterValue2) { blog(LOG_DEBUG, "[carla] got opcode: %s", PluginBridgeNonRtServerOpcode2str(opcode)); } - // #endif + /* #endif */ switch (opcode) { case kPluginBridgeNonRtServerNull: @@ -1039,19 +1033,22 @@ void carla_bridge::readMessages() pendingPing = false; break; - // uint/version + /* uint/version */ case kPluginBridgeNonRtServerVersion: clientBridgeVersion = nonRtServerCtrl.readUInt(); break; - // uint/category, uint/hints, uint/optionsAvailable, uint/optionsEnabled, long/uniqueId + /* uint/category + * uint/hints + * uint/optionsAvailable + * uint/optionsEnabled + * long/uniqueId + */ case kPluginBridgeNonRtServerPluginInfo1: { - // const uint32_t category = - nonRtServerCtrl.readUInt(); + nonRtServerCtrl.readUInt(); /* category */ info.hints = nonRtServerCtrl.readUInt() | PLUGIN_IS_BRIDGE; - // const uint32_t optionAv = - nonRtServerCtrl.readUInt(); + nonRtServerCtrl.readUInt(); /* optionsAvailable */ info.options = nonRtServerCtrl.readUInt(); const int64_t uniqueId = nonRtServerCtrl.readLong(); @@ -1062,45 +1059,55 @@ void carla_bridge::readMessages() } } break; - // uint/size, str[] (realName), uint/size, str[] (label), uint/size, str[] (maker), uint/size, str[] (copyright) + /* uint/size, str[] (realName) + * uint/size, str[] (label) + * uint/size, str[] (maker) + * uint/size, str[] (copyright) + */ case kPluginBridgeNonRtServerPluginInfo2: { - // realName + /* realName */ const BridgeTextReader name(nonRtServerCtrl); info.name = name.text; - // label + /* label */ if (const uint32_t size = nonRtServerCtrl.readUInt()) nonRtServerCtrl.skipRead(size); - // maker + /* maker */ if (const uint32_t size = nonRtServerCtrl.readUInt()) nonRtServerCtrl.skipRead(size); - // copyright + /* copyright */ if (const uint32_t size = nonRtServerCtrl.readUInt()) nonRtServerCtrl.skipRead(size); } break; - // uint/ins, uint/outs + /* uint/ins + * uint/outs + */ case kPluginBridgeNonRtServerAudioCount: info.numAudioIns = nonRtServerCtrl.readUInt(); info.numAudioOuts = nonRtServerCtrl.readUInt(); break; - // uint/ins, uint/outs + /* uint/ins + * uint/outs + */ case kPluginBridgeNonRtServerMidiCount: nonRtServerCtrl.readUInt(); nonRtServerCtrl.readUInt(); break; - // uint/ins, uint/outs + /* uint/ins + * uint/outs + */ case kPluginBridgeNonRtServerCvCount: { const uint32_t cvIns = nonRtServerCtrl.readUInt(); const uint32_t cvOuts = nonRtServerCtrl.readUInt(); info.hasCV = cvIns + cvOuts != 0; } break; - // uint/count + /* uint/count */ case kPluginBridgeNonRtServerParameterCount: { paramCount = nonRtServerCtrl.readUInt(); @@ -1112,28 +1119,36 @@ void carla_bridge::readMessages() paramDetails = nullptr; } break; - // uint/count + /* uint/count */ case kPluginBridgeNonRtServerProgramCount: nonRtServerCtrl.readUInt(); break; - // uint/count + /* uint/count */ case kPluginBridgeNonRtServerMidiProgramCount: nonRtServerCtrl.readUInt(); break; - // byte/type, uint/index, uint/size, str[] (name) + /* byte/type + * uint/index + * uint/size, str[] (name) + */ case kPluginBridgeNonRtServerPortName: { nonRtServerCtrl.readByte(); nonRtServerCtrl.readUInt(); - // name + /* name */ if (const uint32_t size = nonRtServerCtrl.readUInt()) nonRtServerCtrl.skipRead(size); } break; - // uint/index, int/rindex, uint/type, uint/hints, short/cc + /* uint/index + * int/rindex + * uint/type + * uint/hints + * short/cc + */ case kPluginBridgeNonRtServerParameterData1: { const uint32_t index = nonRtServerCtrl.readUInt(); nonRtServerCtrl.readInt(); @@ -1155,17 +1170,21 @@ void carla_bridge::readMessages() paramDetails[index].hints = hints; } break; - // uint/index, uint/size, str[] (name), uint/size, str[] (unit) + /* uint/index + * uint/size, str[] (name) + * uint/size, str[] (symbol) + * uint/size, str[] (unit) + */ case kPluginBridgeNonRtServerParameterData2: { const uint32_t index = nonRtServerCtrl.readUInt(); - // name + /* name */ const BridgeTextReader name(nonRtServerCtrl); - // symbol + /* symbol */ const BridgeTextReader symbol(nonRtServerCtrl); - // unit + /* unit */ const BridgeTextReader unit(nonRtServerCtrl); CARLA_SAFE_ASSERT_UINT2_BREAK(index < paramCount, index, @@ -1178,7 +1197,14 @@ void carla_bridge::readMessages() } } break; - // uint/index, float/def, float/min, float/max, float/step, float/stepSmall, float/stepLarge + /* uint/index + * float/def + * float/min + * float/max + * float/step + * float/stepSmall + * float/stepLarge + */ case kPluginBridgeNonRtServerParameterRanges: { const uint32_t index = nonRtServerCtrl.readUInt(); const float def = nonRtServerCtrl.readFloat(); @@ -1203,7 +1229,9 @@ void carla_bridge::readMessages() } } break; - // uint/index, float/value + /* uint/index + * float/value + */ case kPluginBridgeNonRtServerParameterValue: { const uint32_t index = nonRtServerCtrl.readUInt(); const float value = nonRtServerCtrl.readFloat(); @@ -1218,7 +1246,7 @@ void carla_bridge::readMessages() paramDetails[index].value = fixedValue; if (callback != nullptr) { - // skip parameters that we do not show + /* skip parameters that we do not show */ if ((paramDetails[index].hints & PARAMETER_IS_ENABLED) == 0) break; @@ -1230,7 +1258,9 @@ void carla_bridge::readMessages() } } break; - // uint/index, float/value + /* uint/index + * float/value + */ case kPluginBridgeNonRtServerParameterValue2: { const uint32_t index = nonRtServerCtrl.readUInt(); const float value = nonRtServerCtrl.readFloat(); @@ -1243,13 +1273,17 @@ void carla_bridge::readMessages() } } break; - // uint/index, bool/touch + /* uint/index + * bool/touch + */ case kPluginBridgeNonRtServerParameterTouch: nonRtServerCtrl.readUInt(); nonRtServerCtrl.readBool(); break; - // uint/index, float/value + /* uint/index + * float/value + */ case kPluginBridgeNonRtServerDefaultValue: { const uint32_t index = nonRtServerCtrl.readUInt(); const float value = nonRtServerCtrl.readFloat(); @@ -1258,17 +1292,19 @@ void carla_bridge::readMessages() paramDetails[index].def = value; } break; - // int/index + /* int/index */ case kPluginBridgeNonRtServerCurrentProgram: nonRtServerCtrl.readInt(); break; - // int/index + /* int/index */ case kPluginBridgeNonRtServerCurrentMidiProgram: nonRtServerCtrl.readInt(); break; - // uint/index, uint/size, str[] (name) + /* uint/index + * uint/size, str[] (name) + */ case kPluginBridgeNonRtServerProgramName: { nonRtServerCtrl.readUInt(); @@ -1276,32 +1312,39 @@ void carla_bridge::readMessages() nonRtServerCtrl.skipRead(size); } break; - // uint/index, uint/bank, uint/program, uint/size, str[] (name) + /* uint/index + * uint/bank + * uint/program + * uint/size, str[] (name) + */ case kPluginBridgeNonRtServerMidiProgramData: { nonRtServerCtrl.readUInt(); nonRtServerCtrl.readUInt(); nonRtServerCtrl.readUInt(); - // name + /* name */ if (const uint32_t size = nonRtServerCtrl.readUInt()) nonRtServerCtrl.skipRead(size); } break; - // uint/size, str[], uint/size, str[], uint/size, str[] + /* uint/size, str[] + * uint/size, str[] + * uint/size, str[] + */ case kPluginBridgeNonRtServerSetCustomData: { const uint32_t maxLocalValueLen = clientBridgeVersion >= 10 ? 4096 : 16384; - // type + /* type */ const BridgeTextReader type(nonRtServerCtrl); - // key + /* key */ const BridgeTextReader key(nonRtServerCtrl); - // value + /* value */ const uint32_t valueSize = nonRtServerCtrl.readUInt(); - // special case for big values + /* special case for big values */ if (valueSize > maxLocalValueLen) { const BridgeTextReader bigValueFilePath( nonRtServerCtrl, valueSize); @@ -1329,9 +1372,8 @@ void carla_bridge::readMessages() } break; - // uint/size, str[] (filename, base64 content) + /* uint/size, str[] (filename, base64 content) */ case kPluginBridgeNonRtServerSetChunkDataFile: { - // chunkFilePath const BridgeTextReader chunkFilePath(nonRtServerCtrl); QString realChunkFilePath( @@ -1347,12 +1389,14 @@ void carla_bridge::readMessages() } } break; - // uint/latency + /* uint/latency */ case kPluginBridgeNonRtServerSetLatency: nonRtServerCtrl.readUInt(); break; - // uint/index, uint/size, str[] (name) + /* uint/index + * uint/size, str[] (name) + */ case kPluginBridgeNonRtServerSetParameterText: { nonRtServerCtrl.readInt(); @@ -1368,12 +1412,14 @@ void carla_bridge::readMessages() saved = true; break; - // ulong/window-id + /* ulong/window-id */ case kPluginBridgeNonRtServerRespEmbedUI: nonRtServerCtrl.readULong(); break; - // uint/width, uint/height + /* uint/width + * uint/height + */ case kPluginBridgeNonRtServerResizeEmbedUI: nonRtServerCtrl.readUInt(); nonRtServerCtrl.readUInt(); @@ -1382,7 +1428,7 @@ void carla_bridge::readMessages() case kPluginBridgeNonRtServerUiClosed: break; - // uint/size, str[] + /* uint/size, str[] */ case kPluginBridgeNonRtServerError: { const BridgeTextReader error(nonRtServerCtrl); timedErr = true; @@ -1393,12 +1439,8 @@ void carla_bridge::readMessages() } } -// ---------------------------------------------------------------------------- - void carla_bridge::setLastError(const char *const error) { bfree(lastError); lastError = bstrdup(error); } - -// ---------------------------------------------------------------------------- diff --git a/plugins/carla/carla-bridge.hpp b/plugins/carla/carla-bridge.hpp index b354a2d57b1416..0bbf8714d8ac49 100644 --- a/plugins/carla/carla-bridge.hpp +++ b/plugins/carla/carla-bridge.hpp @@ -19,8 +19,7 @@ CARLA_BACKEND_USE_NAMESPACE -// ---------------------------------------------------------------------------- -// custom class for allowing QProcess usage outside the main thread +/* custom class for allowing QProcess usage outside the main thread */ class BridgeProcess : public QProcess { Q_OBJECT @@ -33,8 +32,7 @@ public Q_SLOTS: void stop(); }; -// ---------------------------------------------------------------------------- -// relevant information for an exposed plugin parameter +/* relevant information for an exposed plugin parameter */ struct carla_param_data { uint32_t hints = 0; @@ -48,8 +46,7 @@ struct carla_param_data { CarlaString unit; }; -// ---------------------------------------------------------------------------- -// information about the currently active plugin +/* information about the currently active plugin */ struct carla_bridge_info { BinaryType btype = BINARY_NONE; @@ -79,25 +76,23 @@ struct carla_bridge_info { } }; -// ---------------------------------------------------------------------------- -// bridge callbacks, triggered during carla_bridge::idle() +/* bridge callbacks, triggered during carla_bridge::idle() */ struct carla_bridge_callback { virtual ~carla_bridge_callback(){}; virtual void bridge_parameter_changed(uint index, float value) = 0; }; -// ---------------------------------------------------------------------------- -// bridge implementation +/* bridge implementation */ struct carla_bridge { carla_bridge_callback *callback = nullptr; - // cached parameter info + /* cached parameter info */ uint32_t paramCount = 0; carla_param_data *paramDetails = nullptr; - // cached plugin info + /* cached plugin info */ carla_bridge_info info; QByteArray chunk; std::vector customData; @@ -109,74 +104,74 @@ struct carla_bridge { bfree(lastError); } - // initialize bridge shared memory details + /* initialize bridge shared memory details */ bool init(uint32_t maxBufferSize, double sampleRate); - // stop bridge process and cleanup shared memory + /* stop bridge process and cleanup shared memory */ void cleanup(bool clearPluginData = true); - // start plugin bridge + /* start plugin bridge */ bool start(BinaryType btype, PluginType ptype, const char *label, const char *filename, int64_t uniqueId); - // check if plugin bridge process is running - // return status might be wrong when called outside the main thread + /* check if plugin bridge process is running + * return status might be wrong when called outside the main thread */ bool is_running() const; - // to be called at regular intervals, from the main thread - // returns false if bridge process is not running + /* to be called at regular intervals, from the main thread + * returns false if bridge process is not running */ bool idle(); - // wait on RT client, making sure it is still active - // returns true on success - // NOTE: plugin will be deactivated on next `idle()` if timed out + /* wait on RT client, making sure it is still active + * returns true on success + * NOTE: plugin will be deactivated on next `idle()` if timed out */ bool wait(const char *action, uint msecs); - // change a plugin parameter value + /* change a plugin parameter value */ void set_value(uint index, float value); - // show the plugin's custom UI + /* show the plugin's custom UI */ void show_ui(); - // [de]activate, a deactivated plugin does not process any audio + /* [de]activate, a deactivated plugin does not process any audio */ bool is_active() const noexcept; void activate(); void deactivate(); - // reactivate and reload plugin information + /* reactivate and reload plugin information */ void reload(); - // restore current state from known info, useful when bridge crashes + /* restore current state from known info, useful when bridge crashes */ void restore_state(); - // process plugin audio - // frames must be <= `maxBufferSize` as passed during `init` + /* process plugin audio + * frames must be <= `maxBufferSize` as passed during `init` */ void process(float *buffers[MAX_AV_PLANES], uint32_t frames); - // add or replace custom data (non-parameter plugin values) + /* add or replace custom data (non-parameter plugin values) */ void add_custom_data(const char *type, const char *key, const char *value, bool sendToPlugin = true); - // inform plugin that all custom data has been loaded - // required after loading plugin state + /* inform plugin that all custom data has been loaded + * required after loading plugin state */ void custom_data_loaded(); - // clear all custom data stored so far + /* clear all custom data stored so far void clear_custom_data(); - // load plugin state as base64 chunk - // NOTE: do not save parameter values for plugins using "chunks" + /* load plugin state as base64 chunk + * NOTE: do not save parameter values for plugins using "chunks" */ void load_chunk(const char *b64chunk); - // request plugin bridge to save and report back its internal state - // must be called just before saving plugin state + /* request plugin bridge to save and report back its internal state + * must be called just before saving plugin state */ void save_and_wait(); - // change the maximum expected buffer size - // plugin is temporarily deactivated during the change + /* change the maximum expected buffer size + * plugin is temporarily deactivated during the change */ void set_buffer_size(uint32_t maxBufferSize); - // get last known error, e.g. reason for last bridge start to fail + /* get last known error, e.g. reason for last bridge start to fail */ const char *get_last_error() const noexcept; private: @@ -200,5 +195,3 @@ struct carla_bridge { void readMessages(); void setLastError(const char *error); }; - -// ---------------------------------------------------------------------------- diff --git a/plugins/carla/carla-patchbay-wrapper.c b/plugins/carla/carla-patchbay-wrapper.c index d3f2a6f1851ebe..d74a36c7ae2cf7 100644 --- a/plugins/carla/carla-patchbay-wrapper.c +++ b/plugins/carla/carla-patchbay-wrapper.c @@ -12,11 +12,10 @@ #include "CarlaNativePlugin.h" -// If this changes we need to adapt Carla side for matching port count +/* If this changes we need to adapt Carla side for matching port count */ _Static_assert(MAX_AV_PLANES == 8, "expected 8 IO"); -// ---------------------------------------------------------------------------- -// helper methods +/* helper methods */ struct carla_main_thread_param_change { const NativePluginDescriptor *descriptor; @@ -33,8 +32,7 @@ static void carla_main_thread_param_change(void *data) bfree(data); } -// ---------------------------------------------------------------------------- -// private data methods +/* private data methods */ struct carla_param_data { uint32_t hints; @@ -50,19 +48,18 @@ struct carla_priv { NativeHostDescriptor host; NativeTimeInfo timeInfo; - // cached parameter info + /* cached parameter info */ uint32_t paramCount; struct carla_param_data *paramDetails; - // update properties when timeout is reached, 0 means do nothing + /* update properties when timeout is reached, 0 means do nothing */ uint64_t update_request; - // keep track of active state + /* keep track of active state */ volatile bool activated; }; -// ---------------------------------------------------------------------------- -// carla host methods +/* carla host methods */ static uint32_t host_get_buffer_size(NativeHostHandle handle) { @@ -104,7 +101,7 @@ static void host_ui_parameter_changed(NativeHostHandle handle, uint32_t index, if (index >= priv->paramCount) return; - // skip parameters that we do not show + /* skip parameters that we do not show */ const uint32_t hints = priv->paramDetails[index].hints; if ((hints & NATIVE_PARAMETER_IS_ENABLED) == 0) return; @@ -203,8 +200,7 @@ static intptr_t host_dispatcher(NativeHostHandle handle, return 0; } -// ---------------------------------------------------------------------------- -// carla + obs integration methods +/* carla + obs integration methods */ struct carla_priv *carla_priv_create(obs_source_t *source, enum buffer_size_mode bufsize, @@ -225,7 +221,7 @@ struct carla_priv *carla_priv_create(obs_source_t *source, priv->descriptor = descriptor; { - // resource dir swaps .../lib/carla for .../share/carla/resources + /* resource dir swaps .../lib/carla for .../share/carla/resources */ const char *const binpath = get_carla_bin_path(); const size_t binlen = strlen(binpath); char *const respath = bmalloc(binlen + 13); @@ -279,8 +275,6 @@ void carla_priv_destroy(struct carla_priv *priv) bfree(priv); } -// ---------------------------------------------------------------------------- - void carla_priv_activate(struct carla_priv *priv) { priv->descriptor->activate(priv->handle); @@ -323,8 +317,6 @@ void carla_priv_load(struct carla_priv *priv, obs_data_t *settings) priv->descriptor->set_state(priv->handle, state); } -// ---------------------------------------------------------------------------- - uint32_t carla_priv_get_num_channels(struct carla_priv *priv) { UNUSED_PARAMETER(priv); @@ -349,8 +341,6 @@ void carla_priv_set_buffer_size(struct carla_priv *priv, carla_priv_activate(priv); } -// ---------------------------------------------------------------------------- - static bool carla_priv_param_changed(void *data, obs_properties_t *props, obs_property_t *property, obs_data_t *settings) @@ -400,7 +390,7 @@ static bool carla_priv_param_changed(void *data, obs_properties_t *props, priv->descriptor->set_parameter_value(priv->handle, pindex, value); - // UI param change notification needs to happen on main thread + /* UI param change notification needs to happen on main thread */ struct carla_main_thread_param_change mchange = { .descriptor = priv->descriptor, .handle = priv->handle, @@ -513,5 +503,3 @@ void carla_priv_readd_properties(struct carla_priv *priv, obs_data_release(settings); } - -// ---------------------------------------------------------------------------- diff --git a/plugins/carla/carla-wrapper.h b/plugins/carla/carla-wrapper.h index f7dd37c9aba433..c7fc677033db6f 100644 --- a/plugins/carla/carla-wrapper.h +++ b/plugins/carla/carla-wrapper.h @@ -8,7 +8,7 @@ #include -// maximum buffer used, can be smaller +/* maximum buffer used, can be smaller */ #define MAX_AUDIO_BUFFER_SIZE 512 enum buffer_size_mode { @@ -19,8 +19,7 @@ enum buffer_size_mode { buffer_size_buffered_max = buffer_size_buffered_512 }; -// ---------------------------------------------------------------------------- -// helper methods +/* helper methods */ static inline uint32_t bufsize_mode_to_frames(enum buffer_size_mode bufsize) { @@ -34,8 +33,7 @@ static inline uint32_t bufsize_mode_to_frames(enum buffer_size_mode bufsize) } } -// ---------------------------------------------------------------------------- -// carla + obs integration methods +/* carla + obs integration methods */ #ifdef __cplusplus extern "C" { @@ -69,5 +67,3 @@ void carla_priv_readd_properties(struct carla_priv *carla, #ifdef __cplusplus } #endif - -// ---------------------------------------------------------------------------- diff --git a/plugins/carla/carla.c b/plugins/carla/carla.c index f02470721d689f..7f510d12598bfd 100644 --- a/plugins/carla/carla.c +++ b/plugins/carla/carla.c @@ -4,7 +4,7 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -// for audio generator thread +/* for audio generator thread */ #include #include @@ -21,37 +21,34 @@ #error CARLA_MODULE_NAME undefined #endif -// -------------------------------------------------------------------------------------------------------------------- - struct carla_data { - // carla host details, intentionally kept private so we can easily swap internals + /* carla host details, intentionally kept private so we can easily swap internals */ struct carla_priv *priv; - // current OBS config + /* current OBS config */ bool activated; uint32_t sample_rate; obs_source_t *source; - // filter related options + /* filter related options */ size_t channels; - // audio generator thread + /* audio generator thread */ bool audiogen_enabled; volatile bool audiogen_running; pthread_t audiogen_thread; - // internal buffering + /* internal buffering */ float *buffers[MAX_AV_PLANES]; uint16_t buffer_head; uint16_t buffer_tail; enum buffer_size_mode buffer_size_mode; - // dummy buffer for unused audio channels + /* dummy buffer for unused audio channels */ float *dummybuffer; }; -// -------------------------------------------------------------------------------------------------------------------- -// private methods +/* private methods */ static enum speaker_layout carla_obs_channels_to_speakers(const size_t channels) { @@ -68,10 +65,10 @@ static enum speaker_layout carla_obs_channels_to_speakers(const size_t channels) return SPEAKERS_4POINT1; case 6: return SPEAKERS_5POINT1; - // FIXME missing case for 7 channels + /* FIXME missing case for 7 channels */ case 8: return SPEAKERS_7POINT1; - // use stereo as fallback + /* use stereo as fallback */ default: return SPEAKERS_STEREO; } @@ -124,8 +121,7 @@ static void carla_obs_idle_callback(void *data, float unused) carla_priv_idle(carla->priv); } -// -------------------------------------------------------------------------------------------------------------------- -// obs plugin methods +/* obs plugin methods */ static void carla_obs_deactivate(void *data); @@ -163,7 +159,7 @@ static void *carla_obs_create(obs_data_t *settings, obs_source_t *source, if (carla->dummybuffer == NULL) goto fail2; - // prefer no-latency mode for filter, lowest latency for generator + /* prefer no-latency mode for filter, lowest latency for generator */ const enum buffer_size_mode bufsize = isFilter ? buffer_size_direct : buffer_size_buffered_128; @@ -181,7 +177,7 @@ static void *carla_obs_create(obs_data_t *settings, obs_source_t *source, carla->buffer_tail = UINT16_MAX; carla->buffer_size_mode = bufsize; - // audio generator, aka input source + /* audio generator, aka input source */ carla->audiogen_enabled = !isFilter; obs_add_tick_callback(carla_obs_idle_callback, carla); @@ -254,14 +250,14 @@ static bool carla_obs_bufsize_callback(void *data, obs_properties_t *props, if (carla->buffer_size_mode == bufsize) return false; - // deactivate first, to stop audio from processing + /* deactivate first, to stop audio from processing */ carla_priv_deactivate(carla->priv); - // safely change to new buffer size + /* safely change to new buffer size */ carla->buffer_size_mode = bufsize; carla_priv_set_buffer_size(carla->priv, bufsize); - // activate again + /* activate again */ carla_priv_activate(carla->priv); return false; @@ -357,7 +353,7 @@ static void carla_obs_filter_audio_direct(struct carla_data *carla, uint32_t frames = audio->frames; float *obsbuffers[MAX_AV_PLANES]; - // process in blocks up to MAX_AUDIO_BUFFER_SIZE + /* process in blocks up to MAX_AUDIO_BUFFER_SIZE */ for (uint32_t i = 0; frames != 0;) { const uint32_t stepframes = frames >= MAX_AUDIO_BUFFER_SIZE ? MAX_AUDIO_BUFFER_SIZE @@ -385,25 +381,25 @@ static void carla_obs_filter_audio_buffered(struct carla_data *carla, const size_t channels = carla->channels; const uint32_t frames = audio->frames; - // cast audio buffers to correct type + /* cast audio buffers to correct type */ float *obsbuffers[MAX_AV_PLANES]; for (uint8_t c = 0; c < MAX_AV_PLANES; ++c) obsbuffers[c] = audio->data[c] ? (float *)audio->data[c] : carla->dummybuffer; - // preload some variables before looping section + /* preload some variables before looping section */ uint16_t buffer_head = carla->buffer_head; uint16_t buffer_tail = carla->buffer_tail; for (uint32_t i = 0, h, t; i < frames; ++i) { - // OBS -> plugin internal buffering + /* OBS -> plugin internal buffering */ h = buffer_head++; for (uint8_t c = 0; c < channels; ++c) carla->buffers[c][h] = obsbuffers[c][i]; - // when we reach the target buffer size, do audio processing + /* when we reach the target buffer size, do audio processing */ if (buffer_head == buffer_size) { buffer_head = 0; carla_priv_process_audio(carla->priv, carla->buffers, @@ -411,17 +407,17 @@ static void carla_obs_filter_audio_buffered(struct carla_data *carla, memset(carla->dummybuffer, 0, sizeof(float) * buffer_size); - // we can now begin to copy back the buffer into OBS + /* we can now begin to copy back the buffer into OBS */ if (buffer_tail == UINT16_MAX) buffer_tail = 0; } if (buffer_tail == UINT16_MAX) { - // buffering still taking place, skip until first audio cycle + /* buffering still taking place, skip until first audio cycle */ for (uint8_t c = 0; c < channels; ++c) obsbuffers[c][i] = 0.f; } else { - // plugin -> OBS buffer copy + /* plugin -> OBS buffer copy */ t = buffer_tail++; for (uint8_t c = 0; c < channels; ++c) @@ -467,8 +463,6 @@ static void carla_obs_load(void *data, obs_data_t *settings) carla_priv_load(carla->priv, settings); } -// -------------------------------------------------------------------------------------------------------------------- - OBS_DECLARE_MODULE() OBS_MODULE_USE_DEFAULT_LOCALE("carla", "en-US") OBS_MODULE_AUTHOR("Filipe Coelho") @@ -526,5 +520,3 @@ bool obs_module_load(void) return true; } - -// -------------------------------------------------------------------------------------------------------------------- diff --git a/plugins/carla/common.c b/plugins/carla/common.c index ee029643778bb0..9919e32ae0d7b4 100644 --- a/plugins/carla/common.c +++ b/plugins/carla/common.c @@ -5,7 +5,7 @@ */ #if !(defined(__APPLE__) || defined(_WIN32)) -// needed for libdl stuff and strcasestr +/* needed for libdl stuff and strcasestr */ #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif @@ -25,8 +25,6 @@ #include "common.h" -// ---------------------------------------------------------------------------- - static char *carla_bin_path = NULL; const char *get_carla_bin_path(void) @@ -55,7 +53,7 @@ const char *get_carla_bin_path(void) free(binpath); #if !(defined(__APPLE__) || defined(_WIN32)) - // check path of this OBS plugin as fallback + /* check path of this OBS plugin as fallback */ Dl_info info; dladdr(get_carla_bin_path, &info); binpath = realpath(info.dli_fname, NULL); @@ -63,7 +61,7 @@ const char *get_carla_bin_path(void) if (binpath == NULL) return NULL; - // truncate to last separator + /* truncate to last separator */ char *lastsep = strrchr(binpath, '/'); if (lastsep == NULL) goto free; @@ -77,7 +75,7 @@ const char *get_carla_bin_path(void) free: free(binpath); -#endif // !(__APPLE__ || _WIN32) +#endif return carla_bin_path; } @@ -127,13 +125,13 @@ void handle_update_request(obs_source_t *source, uint64_t *update_req) const uint64_t now = os_gettime_ns(); - // request in the future? + /* request in the future? */ if (now < old_update_req) { *update_req = now; return; } - if (now - old_update_req >= 100000000ULL) // 100ms + if (now - old_update_req >= 100000000ULL) /* 100ms */ { *update_req = 0; signal_handler_signal(obs_source_get_signal_handler(source), @@ -146,5 +144,3 @@ void obs_module_unload(void) bfree(carla_bin_path); carla_bin_path = NULL; } - -// ---------------------------------------------------------------------------- diff --git a/plugins/carla/common.h b/plugins/carla/common.h index 42659702fee16a..48a9e502ef667d 100644 --- a/plugins/carla/common.h +++ b/plugins/carla/common.h @@ -16,7 +16,7 @@ 'p', '0', '0', '0', '\0' \ } -// property names +/* property names */ #define PROP_LOAD_FILE "load-file" #define PROP_SELECT_PLUGIN "select-plugin" #define PROP_RELOAD_PLUGIN "reload" @@ -26,8 +26,6 @@ #define PROP_CHUNK "chunk" #define PROP_CUSTOM_DATA "customdata" -// ---------------------------------------------------------------------------- - #ifdef __cplusplus extern "C" { #endif @@ -43,5 +41,3 @@ void handle_update_request(obs_source_t *source, uint64_t *update_req); #ifdef __cplusplus } #endif - -// ---------------------------------------------------------------------------- diff --git a/plugins/carla/pluginlistdialog.cpp b/plugins/carla/pluginlistdialog.cpp index 2e94486b8281b5..c300bddebefbe8 100644 --- a/plugins/carla/pluginlistdialog.cpp +++ b/plugins/carla/pluginlistdialog.cpp @@ -19,8 +19,7 @@ CARLA_BACKEND_USE_NAMESPACE -// ---------------------------------------------------------------------------- -// check if the plugin IO makes sense for OBS +/* check if the plugin IO makes sense for OBS */ template static bool isSupportedIO(const T &info) { @@ -29,8 +28,7 @@ template static bool isSupportedIO(const T &info) info.audioOuts <= MAX_AV_PLANES; } -// ---------------------------------------------------------------------------- -// getenv with a fallback value if unset +/* getenv with a fallback value if unset */ static inline const char *getEnvWithFallback(const char *const env, const char *const fallback) @@ -41,8 +39,7 @@ static inline const char *getEnvWithFallback(const char *const env, return fallback; } -// ---------------------------------------------------------------------------- -// Plugin paths (from env vars first, then default locations) +/* Plugin paths (from env vars first, then default locations) */ struct PluginPaths { QUtf8String ladspa; @@ -54,7 +51,7 @@ struct PluginPaths { PluginPaths() { - // get common env vars first + /* get common env vars first */ const QString HOME = QDir::toNativeSeparators(QDir::homePath()); #if defined(Q_OS_WINDOWS) @@ -65,7 +62,7 @@ struct PluginPaths { const char *const envCOMMONPROGRAMFILES = std::getenv("COMMONPROGRAMFILES"); - // small integrity tests + /* small integrity tests */ if (envAPPDATA == nullptr) { qFatal("APPDATA variable not set, cannot continue"); abort(); @@ -90,11 +87,11 @@ struct PluginPaths { "XDG_CONFIG_HOME", (HOME + "/.config").toUtf8())); #endif - // now we set paths, listing format path spec if available + /* now we set paths, listing format path spec if available */ if (const char *const envLADSPA = std::getenv("LADSPA_PATH")) { ladspa = envLADSPA; } else { - // no official spec for LADSPA, use most common paths + /* no official spec for LADSPA, use most common paths */ #if defined(Q_OS_WINDOWS) ladspa = APPDATA + "\\LADSPA"; ladspa += ";" + PROGRAMFILES + "\\LADSPA"; @@ -111,8 +108,9 @@ struct PluginPaths { if (const char *const envLV2 = std::getenv("LV2_PATH")) { lv2 = envLV2; } else { - // use path spec as defined in: - // https://lv2plug.in/pages/filesystem-hierarchy-standard.html + /* use path spec as defined in: + * https://lv2plug.in/pages/filesystem-hierarchy-standard.html + */ #if defined(Q_OS_WINDOWS) lv2 = APPDATA + "\\LV2"; lv2 += ";" + COMMONPROGRAMFILES + "\\LV2"; @@ -130,19 +128,21 @@ struct PluginPaths { vst2 = envVST2; } else { #if defined(Q_OS_WINDOWS) - // use path spec as defined in: - // https://helpcenter.steinberg.de/hc/en-us/articles/115000177084 + /* use path spec as defined in: + * https://helpcenter.steinberg.de/hc/en-us/articles/115000177084 + */ vst2 = PROGRAMFILES + "\\VSTPlugins"; vst2 += ";" + PROGRAMFILES + "\\Steinberg\\VSTPlugins"; vst2 += ";" + COMMONPROGRAMFILES + "\\VST2"; vst2 += ";" + COMMONPROGRAMFILES + "\\Steinberg\\VST2"; #elif defined(Q_OS_DARWIN) - // use path spec as defined in: - // https://helpcenter.steinberg.de/hc/en-us/articles/115000171310 + /* use path spec as defined in: + * https://helpcenter.steinberg.de/hc/en-us/articles/115000171310 + */ vst2 = HOME + "/Library/Audio/Plug-Ins/VST"; vst2 += ":/Library/Audio/Plug-Ins/VST"; #else - // no official spec for VST2 on non-win/mac, use most common paths + /* no official spec for VST2 on non-win/mac, use most common paths */ vst2 = HOME + "/.vst"; vst2 += ":" + HOME + "/.lxvst"; vst2 += ":/usr/local/lib/vst"; @@ -155,8 +155,9 @@ struct PluginPaths { if (const char *const envVST3 = std::getenv("VST3_PATH")) { vst3 = envVST3; } else { - // use path spec as defined in: - // https://steinbergmedia.github.io/vst3_dev_portal/pages/Technical+Documentation/Locations+Format/Plugin+Locations.html + /* use path spec as defined in: + * https://steinbergmedia.github.io/vst3_dev_portal/pages/Technical+Documentation/Locations+Format/Plugin+Locations.html + */ #if defined(Q_OS_WINDOWS) vst3 = LOCALAPPDATA + "\\Programs\\Common\\VST3"; vst3 += ";" + COMMONPROGRAMFILES + "\\VST3"; @@ -173,8 +174,9 @@ struct PluginPaths { if (const char *const envCLAP = std::getenv("CLAP_PATH")) { clap = envCLAP; } else { - // use path spec as defined in: - // https://github.com/free-audio/clap/blob/main/include/clap/entry.h + /* use path spec as defined in: + * https://github.com/free-audio/clap/blob/main/include/clap/entry.h + */ #if defined(Q_OS_WINDOWS) clap = LOCALAPPDATA + "\\Programs\\Common\\CLAP"; clap += ";" + COMMONPROGRAMFILES + "\\CLAP"; @@ -191,7 +193,7 @@ struct PluginPaths { if (const char *const envJSFX = std::getenv("JSFX_PATH")) { jsfx = envJSFX; } else { - // there is no path spec, use REAPER's user data directory + /* there is no path spec, use REAPER's user data directory */ #if defined(Q_OS_WINDOWS) jsfx = APPDATA + "\\REAPER\\Effects"; #elif defined(Q_OS_DARWIN) @@ -204,10 +206,9 @@ struct PluginPaths { } }; -// ---------------------------------------------------------------------------- -// Qt-compatible plugin info (convert to and from QVariant) +/* Qt-compatible plugin info (convert to and from QVariant) */ -// base details, nicely packed and POD-only so we can directly use as binary +/* base details, nicely packed and POD-only so we can directly use as binary */ struct PluginInfoHeader { uint16_t build; uint16_t type; @@ -223,7 +224,7 @@ struct PluginInfoHeader { uint16_t parameterOuts; }; -// full details, now with non-POD types +/* full details, now with non-POD types */ struct PluginInfo : PluginInfoHeader { QString category; QString filename; @@ -232,17 +233,17 @@ struct PluginInfo : PluginInfoHeader { QString maker; }; -// convert PluginInfo to Qt types +/* convert PluginInfo to Qt types */ static QVariant asByteArray(const PluginInfo &info) { QByteArray qdata; - // start with the POD data, stored as-is + /* start with the POD data, stored as-is */ qdata.append( static_cast(static_cast(&info)), sizeof(PluginInfoHeader)); - // then all the strings, with a null terminating byte + /* then all the strings, with a null terminating byte */ { const QByteArray qcategory(info.category.toUtf8()); qdata += qcategory.constData(); @@ -281,16 +282,16 @@ static QVariant asVariant(const PluginInfo &info) return QVariant(asByteArray(info)); } -// convert Qt types to PluginInfo +/* convert Qt types to PluginInfo */ static PluginInfo asPluginInfo(const QByteArray &qdata) { - // make sure data is big enough to fit POD data + 5 strings + /* make sure data is big enough to fit POD data + 5 strings */ CARLA_SAFE_ASSERT_RETURN(static_cast(qdata.size()) >= sizeof(PluginInfoHeader) + sizeof(char) * 5, {}); - // read POD data first + /* read POD data first */ const PluginInfoHeader *const data = static_cast( static_cast(qdata.constData())); @@ -312,7 +313,7 @@ static PluginInfo asPluginInfo(const QByteArray &qdata) {}, {}}; - // then all the strings, keeping the same order as in `asVariant` + /* then all the strings, keeping the same order as in `asVariant` */ const char *sdata = static_cast(static_cast(data + 1)); @@ -360,7 +361,7 @@ static QList asPluginInfoList(const QVariant &var) } #ifndef CARLA_2_6_FEATURES -// convert cached plugin stuff to PluginInfo +/* convert cached plugin stuff to PluginInfo */ static PluginInfo asPluginInfo(const CarlaCachedPluginInfo *const desc, const PluginType ptype) { @@ -395,16 +396,15 @@ static PluginInfo asPluginInfo(const CarlaCachedPluginInfo *const desc, } #endif -// ---------------------------------------------------------------------------- -// Qt-compatible plugin favorite (convert to and from QVariant) +/* Qt-compatible plugin favorite (convert to and from QVariant) */ -// base details, nicely packed and POD-only so we can directly use as binary +/* base details, nicely packed and POD-only so we can directly use as binary */ struct PluginFavoriteHeader { uint16_t type; uint64_t uniqueId; }; -// full details, now with non-POD types +/* full details, now with non-POD types */ struct PluginFavorite : PluginFavoriteHeader { QString filename; QString label; @@ -416,16 +416,16 @@ struct PluginFavorite : PluginFavoriteHeader { } }; -// convert PluginFavorite to Qt types +/* convert PluginFavorite to Qt types */ static QByteArray asByteArray(const PluginFavorite &fav) { QByteArray qdata; - // start with the POD data, stored as-is + /* start with the POD data, stored as-is */ qdata.append(static_cast(static_cast(&fav)), sizeof(PluginFavoriteHeader)); - // then all the strings, with a null terminating byte + /* then all the strings, with a null terminating byte */ { const QByteArray qfilename(fav.filename.toUtf8()); qdata += qfilename.constData(); @@ -451,22 +451,22 @@ static QVariant asVariant(const QList &favlist) return QVariant(qdata); } -// convert Qt types to PluginInfo +/* convert Qt types to PluginInfo */ static PluginFavorite asPluginFavorite(const QByteArray &qdata) { - // make sure data is big enough to fit POD data + 3 strings + /* make sure data is big enough to fit POD data + 3 strings */ CARLA_SAFE_ASSERT_RETURN(static_cast(qdata.size()) >= sizeof(PluginFavoriteHeader) + sizeof(char) * 3, {}); - // read POD data first + /* read POD data first */ const PluginFavoriteHeader *const data = static_cast( static_cast(qdata.constData())); PluginFavorite fav = {data->type, data->uniqueId, {}, {}}; - // then all the strings, keeping the same order as in `asVariant` + /* then all the strings, keeping the same order as in `asVariant` */ const char *sdata = static_cast(static_cast(data + 1)); @@ -498,7 +498,7 @@ static QList asPluginFavoriteList(const QVariant &var) return favlist; } -// create PluginFavorite from PluginInfo data +/* create PluginFavorite from PluginInfo data */ static PluginFavorite asPluginFavorite(const PluginInfo &info) { return PluginFavorite{info.type, info.uniqueId, info.filename, @@ -506,9 +506,6 @@ static PluginFavorite asPluginFavorite(const PluginInfo &info) } #ifdef CARLA_2_6_FEATURES -// ---------------------------------------------------------------------------- -// discovery callbacks - static void discoveryCallback(void *const ptr, const CarlaPluginDiscoveryInfo *const info, const char *const sha1sum) @@ -525,9 +522,7 @@ static bool checkCacheCallback(void *const ptr, const char *const filename, return static_cast(ptr)->checkPluginCache(filename, sha1sum); } -#endif // CARLA_2_6_FEATURES - -// ---------------------------------------------------------------------------- +#endif struct PluginListDialog::PrivateData { int lastTableWidgetIndex = 0; @@ -609,20 +604,17 @@ struct PluginListDialog::PrivateData { } plugins; }; -// ---------------------------------------------------------------------------- - PluginListDialog::PluginListDialog(QWidget *const parent) : QDialog(parent), p(new PrivateData) { ui.setupUi(this); - // -------------------------------------------------------------------- - // Set-up GUI + /* Set-up GUI */ ui.b_load->setEnabled(false); - // do not resize info frame so much + /* do not resize info frame so much */ const QLayout *const infoLayout = ui.frame_info->layout(); const QMargins infoMargins = infoLayout->contentsMargins(); ui.frame_info->setMinimumWidth( @@ -638,10 +630,10 @@ PluginListDialog::PluginListDialog(QWidget *const parent) ui.ch_clap->hide(); #endif - // start with no plugin selected + /* start with no plugin selected */ checkPlugin(-1); - // custom action that listens for Ctrl+F shortcut + /* custom action that listens for Ctrl+F shortcut */ addAction(ui.act_focus_search); setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); @@ -649,13 +641,11 @@ PluginListDialog::PluginListDialog(QWidget *const parent) setWindowModality(Qt::WindowModal); #endif - // -------------------------------------------------------------------- - // Load settings + /* Load settings */ loadSettings(); - // -------------------------------------------------------------------- - // Set-up Icons + /* Set-up Icons */ ui.b_clear_filters->setProperty("themeID", "clearIconSmall"); ui.b_refresh->setProperty("themeID", "refreshIconSmall"); @@ -665,8 +655,7 @@ PluginListDialog::PluginListDialog(QWidget *const parent) hhi->setProperty("themeID", "starIconSmall"); */ - // -------------------------------------------------------------------- - // Set-up connections + /* Set-up connections */ QObject::connect(this, &QDialog::finished, this, &PluginListDialog::saveSettings); @@ -749,8 +738,7 @@ PluginListDialog::~PluginListDialog() delete p; } -// ---------------------------------------------------------------------------- -// public methods +/* public methods */ const PluginInfo &PluginListDialog::getSelectedPluginInfo() const { @@ -798,7 +786,7 @@ void PluginListDialog::addPluginInfo(const CarlaPluginDiscoveryInfo *const info, const QString qsha1sum(sha1sum); const QString key = QString("PluginCache/%1").arg(sha1sum); - // single sha1sum can contain >1 plugin + /* single sha1sum can contain >1 plugin */ QByteArray qdata; if (p->plugins.cache.contains(qsha1sum)) qdata = settings.valueByteArray(key); @@ -816,7 +804,7 @@ void PluginListDialog::addPluginInfo(const CarlaPluginDiscoveryInfo *const info, bool PluginListDialog::checkPluginCache(const char *const filename, const char *const sha1sum) { - // sha1sum is always valid for this call + /* sha1sum is always valid for this call */ const QString qsha1sum(sha1sum); if (filename != nullptr) @@ -830,7 +818,7 @@ bool PluginListDialog::checkPluginCache(const char *const filename, if (plist.isEmpty()) return p->discovery.ignoreCache || !p->discovery.checkInvalid; - // if filename does not match, abort (hash collision?) + /* if filename does not match, abort (hash collision?) */ if (filename == nullptr || plist.first().filename != filename) { p->plugins.cache.remove(qsha1sum); return false; @@ -845,8 +833,7 @@ bool PluginListDialog::checkPluginCache(const char *const filename, } #endif -// ---------------------------------------------------------------------------- -// protected methods +/* protected methods */ void PluginListDialog::done(const int r) { @@ -867,7 +854,7 @@ void PluginListDialog::showEvent(QShowEvent *const event) focusSearchFieldAndSelectAllText(); QDialog::showEvent(event); - // Set up initial discovery + /* Set up initial discovery */ if (p->discovery.firstInit) { p->discovery.firstInit = false; @@ -898,7 +885,7 @@ void PluginListDialog::timerEvent(QTimerEvent *const event) if (event->timerId() == p->timerId) { do { #ifdef CARLA_2_6_FEATURES - // discovery in progress, keep it going + /* discovery in progress, keep it going */ if (p->discovery.handle != nullptr) { if (!carla_plugin_discovery_idle( p->discovery.handle)) { @@ -909,7 +896,7 @@ void PluginListDialog::timerEvent(QTimerEvent *const event) break; } #endif - // start next discovery + /* start next discovery */ QUtf8String path; switch (p->discovery.ptype) { case PLUGIN_NONE: @@ -959,7 +946,7 @@ void PluginListDialog::timerEvent(QTimerEvent *const event) break; #endif default: - // discovery complete + /* discovery complete */ refreshPluginsStop(); } @@ -984,7 +971,7 @@ void PluginListDialog::timerEvent(QTimerEvent *const event) if (!info || !info->valid) continue; - // ignore plugins with non-compatible IO + /* ignore plugins with non-compatible IO */ if (isSupportedIO(*info)) p->plugins.add(asPluginInfo( info, @@ -998,13 +985,11 @@ void PluginListDialog::timerEvent(QTimerEvent *const event) QDialog::timerEvent(event); } -// ---------------------------------------------------------------------------- -// private methods +/* private methods */ void PluginListDialog::addPluginsToTable() { - // -------------------------------------------------------------------- - // sum plugins first, creating all needed rows in advance + /* sum plugins first, creating all needed rows in advance */ ui.tableWidget->setSortingEnabled(false); ui.tableWidget->clearContents(); @@ -1037,8 +1022,7 @@ void PluginListDialog::addPluginsToTable() .arg(QString::number(p->plugins.jsfx.size()))); #endif - // -------------------------------------------------------------------- - // now add all plugins to the table + /* now add all plugins to the table */ auto addPluginToTable = [=](const PluginInfo &info) { const int index = p->lastTableWidgetIndex++; @@ -1100,8 +1084,7 @@ void PluginListDialog::addPluginsToTable() p->lastTableWidgetIndex == ui.tableWidget->rowCount(), p->lastTableWidgetIndex, ui.tableWidget->rowCount()); - // -------------------------------------------------------------------- - // and reenable sorting + filtering + /* and reenable sorting + filtering */ ui.tableWidget->setSortingEnabled(true); @@ -1195,7 +1178,7 @@ void PluginListDialog::loadSettings() settings.valueByteArray("PluginListDialog/Favorites")); #ifdef CARLA_2_6_FEATURES - // load entire plugin cache + /* load entire plugin cache */ const QStringList keys = settings.allKeys(); for (const QUtf8String key : keys) { if (!key.startsWith("PluginCache/")) @@ -1212,8 +1195,7 @@ void PluginListDialog::loadSettings() #endif } -// ---------------------------------------------------------------------------- -// private slots +/* private slots */ void PluginListDialog::cellClicked(const int row, const int column) { @@ -1435,8 +1417,6 @@ void PluginListDialog::clearFilters() checkFilters(); } -// ---------------------------------------------------------------------------- - void PluginListDialog::checkPlugin(const int row) { if (row >= 0) { @@ -1492,8 +1472,6 @@ void PluginListDialog::checkPlugin(const int row) } } -// ---------------------------------------------------------------------------- - void PluginListDialog::refreshPlugins() { refreshPluginsStop(); @@ -1515,7 +1493,7 @@ void PluginListDialog::refreshPlugins() void PluginListDialog::refreshPluginsStart() { - // remove old plugins + /* remove old plugins */ p->plugins.internal.clear(); p->plugins.lv2.clear(); p->plugins.jsfx.clear(); @@ -1533,7 +1511,7 @@ void PluginListDialog::refreshPluginsStart() p->plugins.cache.clear(); #endif - // start discovery again + /* start discovery again */ p->discovery.ptype = PLUGIN_NONE; if (p->timerId == 0) @@ -1543,7 +1521,7 @@ void PluginListDialog::refreshPluginsStart() void PluginListDialog::refreshPluginsStop() { #ifdef CARLA_2_6_FEATURES - // stop previous discovery if still running + /* stop previous discovery if still running */ if (p->discovery.handle != nullptr) { carla_plugin_discovery_stop(p->discovery.handle); p->discovery.handle = nullptr; @@ -1570,8 +1548,6 @@ void PluginListDialog::refreshPluginsSkip() #endif } -// ---------------------------------------------------------------------------- - void PluginListDialog::saveSettings() { QSafeSettings settings; @@ -1633,12 +1609,11 @@ void PluginListDialog::saveSettings() asVariant(p->plugins.favorites)); } -// ---------------------------------------------------------------------------- - const PluginListDialogResults *carla_exec_plugin_list_dialog() { - // create and keep dialog around, as recreating the dialog means doing - // a rescan. Qt will delete it later together with the main window + /* create and keep dialog around, as recreating the dialog means doing + * a rescan. Qt will delete it later together with the main window + */ static PluginListDialog *const gui = new PluginListDialog(carla_qt_get_main_window()); @@ -1663,5 +1638,3 @@ const PluginListDialogResults *carla_exec_plugin_list_dialog() return nullptr; } - -// ---------------------------------------------------------------------------- diff --git a/plugins/carla/pluginlistdialog.hpp b/plugins/carla/pluginlistdialog.hpp index 7d6ba8983054a1..086f721e349776 100644 --- a/plugins/carla/pluginlistdialog.hpp +++ b/plugins/carla/pluginlistdialog.hpp @@ -18,8 +18,7 @@ class QSafeSettings; typedef struct _CarlaPluginDiscoveryInfo CarlaPluginDiscoveryInfo; struct PluginInfo; -// ---------------------------------------------------------------------------- -// Plugin List Dialog +/* Plugin List Dialog */ class PluginListDialog : public QDialog { enum TableIndex { @@ -40,8 +39,7 @@ class PluginListDialog : public QDialog { Ui_PluginListDialog ui; - // -------------------------------------------------------------------- - // public methods + /* public methods */ public: explicit PluginListDialog(QWidget *parent); @@ -54,23 +52,20 @@ class PluginListDialog : public QDialog { bool checkPluginCache(const char *filename, const char *sha1sum); #endif - // -------------------------------------------------------------------- - // protected methods + /* protected methods */ protected: void done(int) override; void showEvent(QShowEvent *) override; void timerEvent(QTimerEvent *) override; - // -------------------------------------------------------------------- - // private methods + /* private methods */ private: void addPluginsToTable(); void loadSettings(); - // -------------------------------------------------------------------- - // private slots + /* private slots */ private Q_SLOTS: void cellClicked(int row, int column); @@ -87,5 +82,3 @@ private Q_SLOTS: void refreshPluginsSkip(); void saveSettings(); }; - -// ---------------------------------------------------------------------------- diff --git a/plugins/carla/pluginrefreshdialog.hpp b/plugins/carla/pluginrefreshdialog.hpp index 961e2361c34617..4b810fad19d636 100644 --- a/plugins/carla/pluginrefreshdialog.hpp +++ b/plugins/carla/pluginrefreshdialog.hpp @@ -10,8 +10,7 @@ #include "qtutils.h" -// ---------------------------------------------------------------------------- -// Plugin Refresh Dialog +/* Plugin Refresh Dialog */ struct PluginRefreshDialog : QDialog, Ui_PluginRefreshDialog { explicit PluginRefreshDialog(QWidget *const parent) : QDialog(parent) @@ -27,8 +26,7 @@ struct PluginRefreshDialog : QDialog, Ui_PluginRefreshDialog { b_skip->setEnabled(false); ch_invalid->setEnabled(false); - // ------------------------------------------------------------ - // Load settings + /* Load settings */ { const QSafeSettings settings; @@ -46,20 +44,17 @@ struct PluginRefreshDialog : QDialog, Ui_PluginRefreshDialog { "PluginRefreshDialog/CheckInvalid", false)); } - // ------------------------------------------------------------ - // Set-up Icons + /* Set-up Icons */ b_start->setProperty("themeID", "playIcon"); - // ------------------------------------------------------------ - // Set-up connections + /* Set-up connections */ QObject::connect(this, &QDialog::finished, this, &PluginRefreshDialog::saveSettings); } - // -------------------------------------------------------------------- - // private slots + /* private slots */ private Q_SLOTS: void saveSettings() @@ -73,5 +68,3 @@ private Q_SLOTS: ch_invalid->isChecked()); } }; - -// ---------------------------------------------------------------------------- diff --git a/plugins/carla/qtutils.cpp b/plugins/carla/qtutils.cpp index 6c7cb250a776b4..ae3bbd124a25c8 100644 --- a/plugins/carla/qtutils.cpp +++ b/plugins/carla/qtutils.cpp @@ -14,8 +14,7 @@ #include "qtutils.h" -//----------------------------------------------------------------------------- -// open a qt file dialog +/* open a qt file dialog */ char *carla_qt_file_dialog(bool save, bool isDir, const char *title, const char *filter) @@ -38,8 +37,7 @@ char *carla_qt_file_dialog(bool save, bool isDir, const char *title, return ret.data(); } -//----------------------------------------------------------------------------- -// call a function on the main thread +/* call a function on the main thread */ void carla_qt_callback_on_main_thread(void (*callback)(void *param), void *param) @@ -61,8 +59,7 @@ void carla_qt_callback_on_main_thread(void (*callback)(void *param), Q_ARG(int, 0)); } -//----------------------------------------------------------------------------- -// get the top-level qt main window +/* get the top-level qt main window */ QMainWindow *carla_qt_get_main_window(void) { @@ -74,8 +71,7 @@ QMainWindow *carla_qt_get_main_window(void) return nullptr; } -//----------------------------------------------------------------------------- -// show an error dialog (on main thread and without blocking current scope) +/* show an error dialog (on main thread and without blocking current scope) */ static void carla_show_error_dialog_later(void *const param) { @@ -88,12 +84,13 @@ static void carla_show_error_dialog_later(void *const param) void carla_show_error_dialog(const char *const text1, const char *const text2) { - // there is no point showing incomplete error messages + /* there is no point showing incomplete error messages */ if (text1 == nullptr || text2 == nullptr) return; - // we cannot do Qt gui stuff outside the main thread - // do a little dance so we call ourselves later on the main thread + /* we cannot do Qt gui stuff outside the main thread + * do a little dance so we call ourselves later on the main thread + */ if (QThread::currentThread() != qApp->thread()) { char **const texts = static_cast(bmalloc(sizeof(char *) * 2)); @@ -111,7 +108,7 @@ void carla_show_error_dialog(const char *const text1, const char *const text2) QMetaObject::invokeMethod(box, "show", Qt::QueuedConnection); } -//----------------------------------------------------------------------------- +/* Safer QSettings class, which does not throw if type mismatches */ #if QT_VERSION >= 0x60000 static const auto q_meta_bool = QMetaType(QMetaType::Bool); @@ -154,5 +151,3 @@ QByteArray QSafeSettings::valueByteArray(const QString &key, return defaultValue; } - -//----------------------------------------------------------------------------- diff --git a/plugins/carla/qtutils.h b/plugins/carla/qtutils.h index 4120ce387e1b95..8e607f8436b3dc 100644 --- a/plugins/carla/qtutils.h +++ b/plugins/carla/qtutils.h @@ -6,8 +6,6 @@ #pragma once -//----------------------------------------------------------------------------- - #ifdef __cplusplus #include #include @@ -18,8 +16,6 @@ extern "C" { typedef struct QMainWindow QMainWindow; #endif -//----------------------------------------------------------------------------- - typedef struct { uint build; uint type; @@ -30,36 +26,24 @@ typedef struct { const PluginListDialogResults *carla_exec_plugin_list_dialog(); -//----------------------------------------------------------------------------- -// open a qt file dialog - +/* open a qt file dialog */ char *carla_qt_file_dialog(bool save, bool isDir, const char *title, const char *filter); -//----------------------------------------------------------------------------- -// call a function on the main thread - +/* call a function on the main thread */ void carla_qt_callback_on_main_thread(void (*callback)(void *param), void *param); -//----------------------------------------------------------------------------- -// get the top-level qt main window - +/* get the top-level qt main window */ QMainWindow *carla_qt_get_main_window(void); -//----------------------------------------------------------------------------- -// show an error dialog (on main thread and without blocking current scope) - +/* show an error dialog (on main thread and without blocking current scope) */ void carla_show_error_dialog(const char *text1, const char *text2); -//----------------------------------------------------------------------------- - #ifdef __cplusplus -} // extern "C" - -//----------------------------------------------------------------------------- -// Safer QSettings class, which does not throw if type mismatches +} /* extern "C" */ +/* Safer QSettings class, which does not throw if type mismatches */ class QSafeSettings : public QSettings { public: inline QSafeSettings() : QSettings("obs-studio", "obs") {} @@ -71,9 +55,7 @@ class QSafeSettings : public QSettings { QByteArray defaultValue = {}) const; }; -//----------------------------------------------------------------------------- -// Custom QString class with default utf-8 mode and a few extra methods - +/* Custom QString class with default utf-8 mode and a few extra methods */ class QUtf8String : public QString { public: explicit inline QUtf8String() : QString() {} @@ -107,9 +89,7 @@ class QUtf8String : public QString { #endif }; -//----------------------------------------------------------------------------- -// Custom QByteArray class with a few extra methods for Qt5 compat - +/* Custom QByteArray class with a few extra methods for Qt5 compat */ #if QT_VERSION < 0x60000 class QCompatByteArray : public QByteArray { public: @@ -132,4 +112,4 @@ class QCompatByteArray : public QByteArray { typedef QByteArray QCompatByteArray; #endif -#endif // __cplusplus +#endif /* __cplusplus */ From c1c1e4e5b899144bb71d212db6260bee5bc84936 Mon Sep 17 00:00:00 2001 From: falkTX Date: Fri, 29 Sep 2023 12:15:06 +0200 Subject: [PATCH 6/8] Remove old unused file as per request Signed-off-by: falkTX --- plugins/carla/cmake/macos/Info.plist.in | 28 ------------------------- 1 file changed, 28 deletions(-) delete mode 100644 plugins/carla/cmake/macos/Info.plist.in diff --git a/plugins/carla/cmake/macos/Info.plist.in b/plugins/carla/cmake/macos/Info.plist.in deleted file mode 100644 index c2d597444a48cd..00000000000000 --- a/plugins/carla/cmake/macos/Info.plist.in +++ /dev/null @@ -1,28 +0,0 @@ - - - - - CFBundleName - obs-carla - CFBundleIdentifier - com.obsproject.carla-bridge - CFBundleVersion - ${MACOSX_BUNDLE_BUNDLE_VERSION} - CFBundleShortVersionString - ${MACOSX_BUNDLE_SHORT_VERSION_STRING} - CFBundleInfoDictionaryVersion - 6.0 - CFBundleExecutable - carla-bridge - CFBundlePackageType - BNDL - CFBundleSupportedPlatforms - - MacOSX - - LSMinimumSystemVersion - ${CMAKE_OSX_DEPLOYMENT_TARGET} - NSHumanReadableCopyright - (c) 2023 Filipe Coelho - - From 0275c4af1ea53d7acb681de0e669c759b2ea6f28 Mon Sep 17 00:00:00 2001 From: falkTX Date: Fri, 29 Sep 2023 12:20:16 +0200 Subject: [PATCH 7/8] Do not use CARLA_OS_MAC macro Signed-off-by: falkTX --- plugins/carla/carla-bridge-wrapper.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/carla/carla-bridge-wrapper.cpp b/plugins/carla/carla-bridge-wrapper.cpp index 42b3cc5d14648c..34e9bcdef66ba4 100644 --- a/plugins/carla/carla-bridge-wrapper.cpp +++ b/plugins/carla/carla-bridge-wrapper.cpp @@ -311,7 +311,7 @@ static bool carla_priv_load_file_callback(obs_properties_t *props, const QFileInfo fileInfo(QString::fromUtf8(filename)); const QString extension(fileInfo.suffix()); -#if defined(CARLA_OS_MAC) +#if defined(__APPLE__) if (extension == "vst") ptype = PLUGIN_VST2; #else From c23ce8627deb03bbe0cee8f57757b9d052354ed1 Mon Sep 17 00:00:00 2001 From: falkTX Date: Fri, 29 Sep 2023 12:39:19 +0200 Subject: [PATCH 8/8] Fix a typo Signed-off-by: falkTX --- plugins/carla/carla-bridge.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/carla/carla-bridge.hpp b/plugins/carla/carla-bridge.hpp index 0bbf8714d8ac49..67c622bfe738a1 100644 --- a/plugins/carla/carla-bridge.hpp +++ b/plugins/carla/carla-bridge.hpp @@ -156,7 +156,7 @@ struct carla_bridge { * required after loading plugin state */ void custom_data_loaded(); - /* clear all custom data stored so far + /* clear all custom data stored so far */ void clear_custom_data(); /* load plugin state as base64 chunk