diff --git a/sources/apps/obs-studio/1000_carla-plugin.patch b/sources/apps/obs-studio/1000_carla-plugin.patch new file mode 100644 index 0000000..08c1b4d --- /dev/null +++ b/sources/apps/obs-studio/1000_carla-plugin.patch @@ -0,0 +1,6949 @@ +diff --git a/cmake/Modules/FindCarlaUtils.cmake b/cmake/Modules/FindCarlaUtils.cmake +new file mode 100644 +index 000000000..1925a542e +--- /dev/null ++++ b/cmake/Modules/FindCarlaUtils.cmake +@@ -0,0 +1,118 @@ ++# Once done these will be defined: ++# ++# CARLAUTILS_FOUND CARLAUTILS_INCLUDE_DIRS CARLAUTILS_LIBRARIES ++ ++# QUIET ++find_package(PkgConfig) ++if(PKG_CONFIG_FOUND) ++ pkg_check_modules(_CARLAUTILS carla-utils) ++endif() ++ ++if(CMAKE_SIZEOF_VOID_P EQUAL 8) ++ set(_lib_suffix 64) ++else() ++ set(_lib_suffix 32) ++endif() ++ ++find_path( ++ CARLAUTILS_INCLUDE_DIR ++ NAMES utils/CarlaBridgeUtils.hpp ++ HINTS ENV CARLAUTILS_PATH ${CARLAUTILS_PATH} ${CMAKE_SOURCE_DIR}/${CARLAUTILS_PATH} ${_CARLAUTILS_INCLUDE_DIRS} ++ PATHS /usr/include/carla /usr/local/include/carla /opt/local/include/carla /sw/include/carla ++ PATH_SUFFIXES carla) ++ ++find_library( ++ CARLAUTILS_LIBRARY ++ NAMES carla_utils libcarla_utils ++ HINTS ENV CARLAUTILS_PATH ${CARLAUTILS_PATH} ${CMAKE_SOURCE_DIR}/${CARLAUTILS_PATH} ${_CARLAUTILS_LIBRARY_DIRS} ++ PATHS /usr/lib/carla /usr/local/lib/carla /opt/local/lib/carla /sw/lib/carla ++ PATH_SUFFIXES ++ lib${_lib_suffix}/carla ++ lib/carla ++ libs${_lib_suffix}/carla ++ libs/carla ++ bin${_lib_suffix} ++ bin ++ ../lib${_lib_suffix}/carla ++ ../lib/carla ++ ../libs${_lib_suffix}/carla ++ ../libs/carla ++ ../bin${_lib_suffix} ++ ../bin) ++ ++# $<$:.exe> ++ ++find_program( ++ CARLAUTILS_BRIDGE_NATIVE ++ NAMES carla-bridge-native ++ HINTS ENV CARLAUTILS_PATH ${CARLAUTILS_PATH} ${CMAKE_SOURCE_DIR}/${CARLAUTILS_PATH} ${_CARLAUTILS_LIBRARY_DIRS} ++ PATHS /usr/lib/carla /usr/local/lib/carla /opt/local/lib/carla /sw/lib/carla ++ PATH_SUFFIXES ++ lib${_lib_suffix}/carla ++ lib/carla ++ libs${_lib_suffix}/carla ++ libs/carla ++ bin${_lib_suffix} ++ bin ++ ../lib${_lib_suffix}/carla ++ ../lib/carla ++ ../libs${_lib_suffix}/carla ++ ../libs/carla ++ ../bin${_lib_suffix} ++ ../bin) ++ ++find_program( ++ CARLAUTILS_DISCOVERY_NATIVE ++ NAMES carla-discovery-native ++ HINTS ENV CARLAUTILS_PATH ${CARLAUTILS_PATH} ${CMAKE_SOURCE_DIR}/${CARLAUTILS_PATH} ${_CARLAUTILS_LIBRARY_DIRS} ++ PATHS /usr/lib/carla /usr/local/lib/carla /opt/local/lib/carla /sw/lib/carla ++ PATH_SUFFIXES ++ lib${_lib_suffix}/carla ++ lib/carla ++ libs${_lib_suffix}/carla ++ libs/carla ++ bin${_lib_suffix} ++ bin ++ ../lib${_lib_suffix}/carla ++ ../lib/carla ++ ../libs${_lib_suffix}/carla ++ ../libs/carla ++ ../bin${_lib_suffix} ++ ../bin) ++ ++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() ++ ++ set_target_properties(carla::utils PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${CARLAUTILS_INCLUDE_DIRS}") ++ 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/plugins/CMakeLists.txt b/plugins/CMakeLists.txt +index f928f772c..3cf662a6c 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() +@@ -191,3 +192,4 @@ add_subdirectory(obs-transitions) + add_subdirectory(rtmp-services) + add_subdirectory(text-freetype2) + add_subdirectory(aja) ++add_subdirectory(carla) +diff --git a/plugins/carla/CMakeLists.txt b/plugins/carla/CMakeLists.txt +new file mode 100644 +index 000000000..fe819b74f +--- /dev/null ++++ b/plugins/carla/CMakeLists.txt +@@ -0,0 +1,100 @@ ++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 ++if(APPLE) ++ # even though cmake finds the framework, it refuses to work with it. let's force it ++ find_library(CARLAUTILS_FRAMEWORK NAMES carla-utils) ++ get_filename_component(CARLAUTILS_LIBRARY_DIR "${CARLAUTILS_FRAMEWORK}/.." ABSOLUTE) ++ message("Found carla-utils: ${CARLAUTILS_LIBRARY_DIR} ${CARLAUTILS_FRAMEWORK}") ++ add_library(carla::utils INTERFACE IMPORTED) ++ set_target_properties( ++ carla::utils ++ PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${CARLAUTILS_FRAMEWORK}/Headers" ++ INTERFACE_LINK_OPTIONS "-F${CARLAUTILS_LIBRARY_DIR}" ++ INTERFACE_LINK_LIBRARIES $) ++else() ++ 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() ++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) ++ ++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 000000000..0a7d91dec +--- /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 000000000..5f20b7c01 +--- /dev/null ++++ b/plugins/carla/carla-bridge.cpp +@@ -0,0 +1,1405 @@ ++/* ++ * Carla plugin for OBS ++ * Copyright (C) 2023 Filipe Coelho ++ * SPDX-License-Identifier: GPL-2.0-or-later ++ */ ++ ++#include ++#include ++#include ++ ++#ifdef CARLA_OS_MAC ++#include ++#endif ++ ++#include ++#include ++#include ++#include ++#include ++ ++#include ++ ++#include "carla-bridge.hpp" ++ ++#include "common.h" ++#include "qtutils.h" ++ ++#if defined(CARLA_OS_MAC) && 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(CARLA_OS_MAC) && 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 000000000..b354a2d57 +--- /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 000000000..d3f2a6f18 +--- /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 000000000..f7dd37c9a +--- /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 000000000..f02470721 +--- /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 000000000..c2d597444 +--- /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 000000000..36470deb5 +--- /dev/null ++++ b/plugins/carla/common.c +@@ -0,0 +1,156 @@ ++/* ++ * Carla plugin for OBS ++ * Copyright (C) 2023 Filipe Coelho ++ * SPDX-License-Identifier: GPL-2.0-or-later ++ */ ++ ++#ifndef _WIN32 ++// needed for libdl stuff and strcasestr ++#ifndef _GNU_SOURCE ++#define _GNU_SOURCE ++#endif ++#include ++#include ++#include ++#endif ++ ++#include ++ ++#include ++#include ++ ++#include "common.h" ++ ++// ---------------------------------------------------------------------------- ++ ++static char *module_path = NULL; ++ ++const char *get_carla_bin_path(void) ++{ ++ if (module_path != NULL) ++ return module_path; ++ ++ char *mpath; ++ ++ // check path of linked carla-utils library first ++ const char *const utilspath = carla_get_library_folder(); ++ const size_t utilslen = strlen(utilspath); ++ ++ mpath = bmalloc(utilslen + 28); ++ memcpy(mpath, utilspath, utilslen); ++ memcpy(mpath + utilslen, CARLA_OS_SEP_STR "carla-discovery-native", 24); ++#ifdef _WIN32 ++ memcpy(mpath + utilslen + 24, ".exe", 5); ++#endif ++ ++ if (os_file_exists(mpath)) { ++ mpath[utilslen] = '\0'; ++ module_path = mpath; ++ return module_path; ++ } ++ ++ free(mpath); ++ ++#ifndef _WIN32 ++ // check path of this OBS plugin as fallback ++ Dl_info info; ++ dladdr(get_carla_bin_path, &info); ++ mpath = realpath(info.dli_fname, NULL); ++ ++ if (mpath == NULL) ++ return NULL; ++ ++ // truncate to last separator ++ char *lastsep = strrchr(mpath, '/'); ++ if (lastsep == NULL) ++ goto free; ++ *lastsep = '\0'; ++ ++#ifdef __APPLE__ ++ // running as macOS app bundle, use its binary dir ++ char *appbundlesep = strcasestr(mpath, "/PlugIns/" CARLA_MODULE_ID ++ ".plugin/Contents/MacOS"); ++ if (appbundlesep == NULL) ++ goto free; ++ strcpy(appbundlesep, "/MacOS"); ++#endif ++ ++ if (os_file_exists(mpath)) { ++ module_path = bstrdup(mpath); ++ free(mpath); ++ return module_path; ++ } ++ ++free: ++ free(mpath); ++#endif // !_WIN32 ++ ++ return module_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(module_path); ++ module_path = NULL; ++} ++ ++// ---------------------------------------------------------------------------- +diff --git a/plugins/carla/common.h b/plugins/carla/common.h +new file mode 100644 +index 000000000..42659702f +--- /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 000000000..9bb292d9c +--- /dev/null ++++ b/plugins/carla/pluginlistdialog.cpp +@@ -0,0 +1,1660 @@ ++/* ++ * 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 ++ ++#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 ++ const QString HOME = QDir::toNativeSeparators(QDir::homePath()); ++ ++#if defined(CARLA_OS_WIN) ++ 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(CARLA_OS_MAC) ++ const QUtf8String CONFIG_HOME(getEnvWithFallback( ++ "XDG_CONFIG_HOME", (HOME + "/.config").toUtf8())); ++#endif ++ ++ // now set paths, listing format path spec if available ++ if (const char *const envLADSPA = std::getenv("LADSPA_PATH")) { ++ ladspa = envLADSPA; ++ } else { ++ // no official spec, use common paths ++#if defined(CARLA_OS_WIN) ++ ladspa = APPDATA + "\\LADSPA"; ++ ladspa += ";" + PROGRAMFILES + "\\LADSPA"; ++#elif defined(CARLA_OS_MAC) ++ 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 { ++ // https://lv2plug.in/pages/filesystem-hierarchy-standard.html ++#if defined(CARLA_OS_WIN) ++ lv2 = APPDATA + "\\LV2"; ++ lv2 += ";" + COMMONPROGRAMFILES + "\\LV2"; ++#elif defined(CARLA_OS_MAC) ++ 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(CARLA_OS_WIN) ++ // 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(CARLA_OS_MAC) ++ // 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, use 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 { ++ // https://steinbergmedia.github.io/vst3_dev_portal/pages/Technical+Documentation/Locations+Format/Plugin+Locations.html ++#if defined(CARLA_OS_WIN) ++ vst3 = LOCALAPPDATA + "\\Programs\\Common\\VST3"; ++ vst3 += ";" + COMMONPROGRAMFILES + "\\VST3"; ++#elif defined(CARLA_OS_MAC) ++ 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 { ++ // https://github.com/free-audio/clap/blob/main/include/clap/entry.h ++#if defined(CARLA_OS_WIN) ++ clap = LOCALAPPDATA + "\\Programs\\Common\\CLAP"; ++ clap += ";" + COMMONPROGRAMFILES + "\\CLAP"; ++#elif defined(CARLA_OS_MAC) ++ 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 { ++ // REAPER user data directory ++#if defined(CARLA_OS_WIN) ++ jsfx = APPDATA + "\\REAPER\\Effects"; ++#elif defined(CARLA_OS_MAC) ++ jsfx = HOME + ++ "/Library/Application Support/REAPER/Effects"; ++#else ++ jsfx = CONFIG_HOME + "/REAPER/Effects"; ++#endif ++ } ++ } ++}; ++ ++// ---------------------------------------------------------------------------- ++// Qt-compatible plugin info ++ ++// 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 ++ ++// 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(); ++ tool += CARLA_OS_SEP_STR "carla-discovery-native"; ++#ifdef CARLA_OS_WIN ++ tool += ".exe"; ++#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 CARLA_OS_MAC ++ 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::focusSearchFieldAndSelectAll); ++} ++ ++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) ++{ ++ focusSearchFieldAndSelectAll(); ++ 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(), ++ 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::focusSearchFieldAndSelectAll() ++{ ++ 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 CarlaString filename; ++ static CarlaString 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; ++ ret.label = label; ++ 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 000000000..a3768c99a +--- /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 focusSearchFieldAndSelectAll(); ++ 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 000000000..fd6579ff3 +--- /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 000000000..961e2361c +--- /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 000000000..a47bc2770 +--- /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 000000000..6c7cb250a +--- /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 000000000..4120ce387 +--- /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 diff --git a/sources/apps/obs-studio/obs-ppa-regen.sh b/sources/apps/obs-studio/obs-ppa-regen.sh index 26ee74c..ab53683 100755 --- a/sources/apps/obs-studio/obs-ppa-regen.sh +++ b/sources/apps/obs-studio/obs-ppa-regen.sh @@ -4,11 +4,11 @@ set -e PPA_URL="https://launchpad.net/~obsproject/+archive/ubuntu/obs-studio" SUFFIX="0obsproject1" -VERSION="29.1.2" +VERSION="29.1.3" DISTS=("focal" "jammy" "kinetic" "lunar") -RVER=5 +RVER=1 -wget -c "${PPA_URL}/+files/obs-studio_29.1.2.orig.tar.gz" +wget -c "${PPA_URL}/+files/obs-studio_${VERSION}.orig.tar.gz" for d in ${DISTS[@]}; do wget -c "${PPA_URL}/+sourcefiles/obs-studio/${VERSION}-${SUFFIX}~${d}/obs-studio_${VERSION}-${SUFFIX}~${d}.debian.tar.xz"