|
- 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)
- +
- +# $<$<PLATFORM_ID:Windows>:.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 $<LINK_LIBRARY:FRAMEWORK,carla-utils.framework>)
- +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 $<$<C_COMPILER_ID:GNU>: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 <falktx@falktx.com>
- + * SPDX-License-Identifier: GPL-2.0-or-later
- + */
- +
- +// needed for strcasestr
- +#if !defined(_GNU_SOURCE) && !defined(_WIN32)
- +#define _GNU_SOURCE
- +#endif
- +
- +#include <CarlaBackendUtils.hpp>
- +#include <CarlaBinaryUtils.hpp>
- +
- +#include <QtCore/QFileInfo>
- +#include <QtCore/QString>
- +
- +#include <util/platform.h>
- +
- +#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<long long>(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<int64_t>(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<struct carla_priv *>(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/<plat>/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<struct carla_priv *>(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<BinaryType>(plugin->build),
- + static_cast<PluginType>(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<struct carla_priv *>(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<struct carla_priv *>(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<struct carla_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->bridge.paramCount)
- + return false;
- +
- + const uint index = static_cast<uint>(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 <falktx@falktx.com>
- + * SPDX-License-Identifier: GPL-2.0-or-later
- + */
- +
- +#include <CarlaBackendUtils.hpp>
- +#include <CarlaBase64Utils.hpp>
- +#include <CarlaBinaryUtils.hpp>
- +
- +#ifdef CARLA_OS_MAC
- +#include <CarlaMacUtils.hpp>
- +#endif
- +
- +#include <QtCore/QCoreApplication>
- +#include <QtCore/QDir>
- +#include <QtCore/QFile>
- +#include <QtCore/QFileInfo>
- +#include <QtWidgets/QMessageBox>
- +
- +#include <util/platform.h>
- +
- +#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<uint>(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<uint32_t>(sizeof(BridgeRtClientData)));
- + nonRtClientCtrl.writeUInt(
- + static_cast<uint32_t>(sizeof(BridgeNonRtClientData)));
- + nonRtClientCtrl.writeUInt(
- + static_cast<uint32_t>(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<uint64_t>(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<uint32_t>(std::strlen(cdata.type));
- + const uint32_t keyLen =
- + static_cast<uint32_t>(std::strlen(cdata.key));
- + const uint32_t valueLen =
- + static_cast<uint32_t>(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<qint64>(valueLen)) {
- + const uint32_t ulength =
- + static_cast<uint32_t>(
- + 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<uint32_t>(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<char *>(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<uint32_t>(std::strlen(type));
- + const uint32_t keyLen = static_cast<uint32_t>(std::strlen(key));
- + const uint32_t valueLen =
- + static_cast<uint32_t>(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<qint64>(valueLen)) {
- + const uint32_t ulength =
- + static_cast<uint32_t>(
- + 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<char *>(cdata.type));
- + bfree(const_cast<char *>(cdata.key));
- + bfree(const_cast<char *>(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<uint32_t>(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<uint64_t>(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 <falktx@falktx.com>
- + * SPDX-License-Identifier: GPL-2.0-or-later
- + */
- +
- +#pragma once
- +
- +#include <CarlaBackend.h>
- +#include <CarlaBridgeUtils.hpp>
- +
- +#include <QtCore/QByteArray>
- +#include <QtCore/QProcess>
- +#include <QtCore/QString>
- +
- +#include <vector>
- +
- +#include <obs.h>
- +
- +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> 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 <falktx@falktx.com>
- + * SPDX-License-Identifier: GPL-2.0-or-later
- + */
- +
- +#include "carla-wrapper.h"
- +#include "common.h"
- +#include "qtutils.h"
- +
- +#include <util/platform.h>
- +
- +#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 <falktx@falktx.com>
- + * SPDX-License-Identifier: GPL-2.0-or-later
- + */
- +
- +#pragma once
- +
- +#include <obs-module.h>
- +
- +// 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 <falktx@falktx.com>
- + * SPDX-License-Identifier: GPL-2.0-or-later
- + */
- +
- +// for audio generator thread
- +#include <pthread.h>
- +
- +#include <obs-module.h>
- +#include <util/platform.h>
- +
- +#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 @@
- +<?xml version="1.0" encoding="UTF-8"?>
- +<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
- +<plist version="1.0">
- +<dict>
- + <key>CFBundleName</key>
- + <string>obs-carla</string>
- + <key>CFBundleIdentifier</key>
- + <string>com.obsproject.carla-bridge</string>
- + <key>CFBundleVersion</key>
- + <string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
- + <key>CFBundleShortVersionString</key>
- + <string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
- + <key>CFBundleInfoDictionaryVersion</key>
- + <string>6.0</string>
- + <key>CFBundleExecutable</key>
- + <string>carla-bridge</string>
- + <key>CFBundlePackageType</key>
- + <string>BNDL</string>
- + <key>CFBundleSupportedPlatforms</key>
- + <array>
- + <string>MacOSX</string>
- + </array>
- + <key>LSMinimumSystemVersion</key>
- + <string>${CMAKE_OSX_DEPLOYMENT_TARGET}</string>
- + <key>NSHumanReadableCopyright</key>
- + <string>(c) 2023 Filipe Coelho</string>
- +</dict>
- +</plist>
- 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 <falktx@falktx.com>
- + * SPDX-License-Identifier: GPL-2.0-or-later
- + */
- +
- +#ifndef _WIN32
- +// needed for libdl stuff and strcasestr
- +#ifndef _GNU_SOURCE
- +#define _GNU_SOURCE
- +#endif
- +#include <dlfcn.h>
- +#include <limits.h>
- +#include <stdlib.h>
- +#endif
- +
- +#include <CarlaUtils.h>
- +
- +#include <obs-module.h>
- +#include <util/platform.h>
- +
- +#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 <falktx@falktx.com>
- + * SPDX-License-Identifier: GPL-2.0-or-later
- + */
- +
- +#pragma once
- +
- +#include <obs-module.h>
- +
- +#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 <falktx@falktx.com>
- + * SPDX-License-Identifier: GPL-2.0-or-later
- + */
- +
- +#include <CarlaBackendUtils.hpp>
- +#include <CarlaString.hpp>
- +#include <CarlaUtils.h>
- +
- +#include <QtCore/QDir>
- +#include <QtCore/QFileInfo>
- +#include <QtCore/QPointer>
- +
- +#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<class T> 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<const char *>(static_cast<const void *>(&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<size_t>(qdata.size()) >=
- + sizeof(PluginInfoHeader) +
- + sizeof(char) * 5,
- + {});
- +
- + // read POD data first
- + const PluginInfoHeader *const data =
- + static_cast<const PluginInfoHeader *>(
- + static_cast<const void *>(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<const char *>(static_cast<const void *>(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<PluginInfo> asPluginInfoList(const QVariant &var)
- +{
- + QCompatByteArray qdata(var.toByteArray());
- +
- + QList<PluginInfo> 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<const char *>(static_cast<const void *>(&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<PluginFavorite> &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<size_t>(qdata.size()) >=
- + sizeof(PluginFavoriteHeader) +
- + sizeof(char) * 3,
- + {});
- +
- + // read POD data first
- + const PluginFavoriteHeader *const data =
- + static_cast<const PluginFavoriteHeader *>(
- + static_cast<const void *>(qdata.constData()));
- + PluginFavorite fav = {data->type, data->uniqueId, {}, {}};
- +
- + // then all the strings, keeping the same order as in `asVariant`
- + const char *sdata =
- + static_cast<const char *>(static_cast<const void *>(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<PluginFavorite> asPluginFavoriteList(const QVariant &var)
- +{
- + QCompatByteArray qdata(var.toByteArray());
- +
- + QList<PluginFavorite> 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<PluginListDialog *>(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<PluginListDialog *>(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<PluginRefreshDialog> 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<PluginInfo> internal;
- + std::vector<PluginInfo> lv2;
- + std::vector<PluginInfo> jsfx;
- +#ifdef CARLA_2_6_FEATURES
- + std::vector<PluginInfo> ladspa;
- + std::vector<PluginInfo> vst2;
- + std::vector<PluginInfo> vst3;
- + std::vector<PluginInfo> clap;
- + QMap<QString, QList<PluginInfo>> cache;
- +#endif
- + QList<PluginFavorite> 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<uint16_t>(info->btype),
- + static_cast<uint16_t>(info->ptype),
- + info->metadata.hints,
- + info->uniqueId,
- + static_cast<uint16_t>(info->io.audioIns),
- + static_cast<uint16_t>(info->io.audioOuts),
- + static_cast<uint16_t>(info->io.cvIns),
- + static_cast<uint16_t>(info->io.cvOuts),
- + static_cast<uint16_t>(info->io.midiIns),
- + static_cast<uint16_t>(info->io.midiOuts),
- + static_cast<uint16_t>(info->io.parameterIns),
- + static_cast<uint16_t>(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<PluginInfo> &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<PluginType>(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 <falktx@falktx.com>
- + * SPDX-License-Identifier: GPL-2.0-or-later
- + */
- +
- +#pragma once
- +
- +#include <CarlaDefines.h>
- +
- +#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 @@
- +<?xml version="1.0" encoding="UTF-8"?>
- +<ui version="4.0">
- + <class>PluginListDialog</class>
- + <widget class="QDialog" name="PluginListDialog">
- + <property name="geometry">
- + <rect>
- + <x>0</x>
- + <y>0</y>
- + <width>1100</width>
- + <height>738</height>
- + </rect>
- + </property>
- + <property name="windowTitle">
- + <string>Plugin List</string>
- + </property>
- + <layout class="QGridLayout" name="gridLayout_3">
- + <item row="2" column="0">
- + <spacer name="verticalSpacer_6">
- + <property name="orientation">
- + <enum>Qt::Vertical</enum>
- + </property>
- + <property name="sizeHint" stdset="0">
- + <size>
- + <width>20</width>
- + <height>40</height>
- + </size>
- + </property>
- + </spacer>
- + </item>
- + <item row="4" column="0" colspan="3">
- + <layout class="QHBoxLayout" name="horizontalLayout_2">
- + <item>
- + <widget class="QLabel" name="label">
- + <property name="text">
- + <string/>
- + </property>
- + <property name="textInteractionFlags">
- + <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
- + </property>
- + </widget>
- + </item>
- + <item>
- + <spacer name="horizontalSpacer">
- + <property name="orientation">
- + <enum>Qt::Horizontal</enum>
- + </property>
- + <property name="sizeHint" stdset="0">
- + <size>
- + <width>40</width>
- + <height>20</height>
- + </size>
- + </property>
- + </spacer>
- + </item>
- + <item>
- + <widget class="QPushButton" name="b_load">
- + <property name="text">
- + <string>&Load Plugin</string>
- + </property>
- + </widget>
- + </item>
- + <item>
- + <widget class="QPushButton" name="b_cancel">
- + <property name="text">
- + <string>Cancel</string>
- + </property>
- + </widget>
- + </item>
- + </layout>
- + </item>
- + <item row="0" column="0" colspan="3">
- + <layout class="QHBoxLayout" name="horizontalLayout">
- + <item>
- + <widget class="QLineEdit" name="lineEdit">
- + <property name="clearButtonEnabled">
- + <bool>true</bool>
- + </property>
- + </widget>
- + </item>
- + <item>
- + <widget class="QPushButton" name="b_refresh">
- + <property name="text">
- + <string>Refresh</string>
- + </property>
- + </widget>
- + </item>
- + <item>
- + <widget class="QPushButton" name="b_clear_filters">
- + <property name="text">
- + <string>Reset filters</string>
- + </property>
- + </widget>
- + </item>
- + </layout>
- + </item>
- + <item row="1" column="1" rowspan="3">
- + <widget class="QTableWidget" name="tableWidget">
- + <property name="sizePolicy">
- + <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
- + <horstretch>1</horstretch>
- + <verstretch>0</verstretch>
- + </sizepolicy>
- + </property>
- + <property name="editTriggers">
- + <set>QAbstractItemView::NoEditTriggers</set>
- + </property>
- + <property name="showDropIndicator" stdset="0">
- + <bool>false</bool>
- + </property>
- + <property name="dragDropOverwriteMode">
- + <bool>false</bool>
- + </property>
- + <property name="alternatingRowColors">
- + <bool>true</bool>
- + </property>
- + <property name="selectionMode">
- + <enum>QAbstractItemView::SingleSelection</enum>
- + </property>
- + <property name="selectionBehavior">
- + <enum>QAbstractItemView::SelectRows</enum>
- + </property>
- + <property name="showGrid">
- + <bool>false</bool>
- + </property>
- + <property name="gridStyle">
- + <enum>Qt::NoPen</enum>
- + </property>
- + <property name="sortingEnabled">
- + <bool>true</bool>
- + </property>
- + <property name="wordWrap">
- + <bool>false</bool>
- + </property>
- + <attribute name="horizontalHeaderMinimumSectionSize">
- + <number>24</number>
- + </attribute>
- + <attribute name="horizontalHeaderStretchLastSection">
- + <bool>true</bool>
- + </attribute>
- + <attribute name="verticalHeaderVisible">
- + <bool>false</bool>
- + </attribute>
- + <attribute name="verticalHeaderMinimumSectionSize">
- + <number>12</number>
- + </attribute>
- + <attribute name="verticalHeaderDefaultSectionSize">
- + <number>22</number>
- + </attribute>
- + <column>
- + <property name="text">
- + <string notr="true"/>
- + </property>
- + <property name="toolTip">
- + <string notr="true"/>
- + </property>
- + <property name="whatsThis">
- + <string notr="true"/>
- + </property>
- + <property name="icon">
- + <iconset>
- + <normaloff>:/16x16/bookmarks.svgz</normaloff>:/16x16/bookmarks.svgz</iconset>
- + </property>
- + </column>
- + <column>
- + <property name="text">
- + <string>Name</string>
- + </property>
- + </column>
- + <column>
- + <property name="text">
- + <string>Label/Id/URI</string>
- + </property>
- + </column>
- + <column>
- + <property name="text">
- + <string>Maker</string>
- + </property>
- + </column>
- + <column>
- + <property name="text">
- + <string>Binary/Filename</string>
- + </property>
- + </column>
- + </widget>
- + </item>
- + <item row="1" column="0">
- + <widget class="QToolBox" name="toolBox">
- + <property name="currentIndex">
- + <number>0</number>
- + </property>
- + <widget class="QWidget" name="p_format">
- + <property name="geometry">
- + <rect>
- + <x>0</x>
- + <y>0</y>
- + <width>141</width>
- + <height>241</height>
- + </rect>
- + </property>
- + <attribute name="label">
- + <string>Format</string>
- + </attribute>
- + <layout class="QVBoxLayout" name="verticalLayout">
- + <item>
- + <widget class="QCheckBox" name="ch_internal">
- + <property name="text">
- + <string>Internal</string>
- + </property>
- + </widget>
- + </item>
- + <item>
- + <widget class="QCheckBox" name="ch_ladspa">
- + <property name="text">
- + <string>LADSPA</string>
- + </property>
- + </widget>
- + </item>
- + <item>
- + <widget class="QCheckBox" name="ch_lv2">
- + <property name="text">
- + <string>LV2</string>
- + </property>
- + </widget>
- + </item>
- + <item>
- + <widget class="QCheckBox" name="ch_vst">
- + <property name="text">
- + <string>VST2</string>
- + </property>
- + </widget>
- + </item>
- + <item>
- + <widget class="QCheckBox" name="ch_vst3">
- + <property name="text">
- + <string>VST3</string>
- + </property>
- + </widget>
- + </item>
- + <item>
- + <widget class="QCheckBox" name="ch_clap">
- + <property name="text">
- + <string>CLAP</string>
- + </property>
- + </widget>
- + </item>
- + <item>
- + <widget class="QCheckBox" name="ch_jsfx">
- + <property name="text">
- + <string>JSFX</string>
- + </property>
- + </widget>
- + </item>
- + <item>
- + <spacer name="verticalSpacer_5">
- + <property name="orientation">
- + <enum>Qt::Vertical</enum>
- + </property>
- + <property name="sizeHint" stdset="0">
- + <size>
- + <width>20</width>
- + <height>40</height>
- + </size>
- + </property>
- + </spacer>
- + </item>
- + </layout>
- + </widget>
- + <widget class="QWidget" name="p_type">
- + <property name="geometry">
- + <rect>
- + <x>0</x>
- + <y>0</y>
- + <width>141</width>
- + <height>168</height>
- + </rect>
- + </property>
- + <attribute name="label">
- + <string>Type</string>
- + </attribute>
- + <layout class="QVBoxLayout" name="verticalLayout_2">
- + <item>
- + <widget class="QCheckBox" name="ch_effects">
- + <property name="text">
- + <string>Effects</string>
- + </property>
- + </widget>
- + </item>
- + <item>
- + <widget class="QCheckBox" name="ch_instruments">
- + <property name="text">
- + <string>Instruments</string>
- + </property>
- + </widget>
- + </item>
- + <item>
- + <widget class="QCheckBox" name="ch_midi">
- + <property name="text">
- + <string>MIDI Plugins</string>
- + </property>
- + </widget>
- + </item>
- + <item>
- + <widget class="QCheckBox" name="ch_other">
- + <property name="text">
- + <string>Other/Misc</string>
- + </property>
- + </widget>
- + </item>
- + <item>
- + <spacer name="verticalSpacer_3">
- + <property name="orientation">
- + <enum>Qt::Vertical</enum>
- + </property>
- + <property name="sizeHint" stdset="0">
- + <size>
- + <width>20</width>
- + <height>40</height>
- + </size>
- + </property>
- + </spacer>
- + </item>
- + </layout>
- + </widget>
- + <widget class="QWidget" name="p_category">
- + <property name="geometry">
- + <rect>
- + <x>0</x>
- + <y>0</y>
- + <width>141</width>
- + <height>305</height>
- + </rect>
- + </property>
- + <attribute name="label">
- + <string>Category</string>
- + </attribute>
- + <layout class="QVBoxLayout" name="verticalLayout_4">
- + <item>
- + <widget class="QCheckBox" name="ch_cat_all">
- + <property name="text">
- + <string>All</string>
- + </property>
- + </widget>
- + </item>
- + <item>
- + <widget class="QCheckBox" name="ch_cat_delay">
- + <property name="text">
- + <string>Delay</string>
- + </property>
- + </widget>
- + </item>
- + <item>
- + <widget class="QCheckBox" name="ch_cat_distortion">
- + <property name="text">
- + <string>Distortion</string>
- + </property>
- + </widget>
- + </item>
- + <item>
- + <widget class="QCheckBox" name="ch_cat_dynamics">
- + <property name="text">
- + <string>Dynamics</string>
- + </property>
- + </widget>
- + </item>
- + <item>
- + <widget class="QCheckBox" name="ch_cat_eq">
- + <property name="text">
- + <string>EQ</string>
- + </property>
- + </widget>
- + </item>
- + <item>
- + <widget class="QCheckBox" name="ch_cat_filter">
- + <property name="text">
- + <string>Filter</string>
- + </property>
- + </widget>
- + </item>
- + <item>
- + <widget class="QCheckBox" name="ch_cat_modulator">
- + <property name="text">
- + <string>Modulator</string>
- + </property>
- + </widget>
- + </item>
- + <item>
- + <widget class="QCheckBox" name="ch_cat_synth">
- + <property name="text">
- + <string>Synth</string>
- + </property>
- + </widget>
- + </item>
- + <item>
- + <widget class="QCheckBox" name="ch_cat_utility">
- + <property name="text">
- + <string>Utility</string>
- + </property>
- + </widget>
- + </item>
- + <item>
- + <widget class="QCheckBox" name="ch_cat_other">
- + <property name="text">
- + <string>Other</string>
- + </property>
- + </widget>
- + </item>
- + <item>
- + <spacer name="verticalSpacer_7">
- + <property name="orientation">
- + <enum>Qt::Vertical</enum>
- + </property>
- + <property name="sizeHint" stdset="0">
- + <size>
- + <width>20</width>
- + <height>23</height>
- + </size>
- + </property>
- + </spacer>
- + </item>
- + </layout>
- + </widget>
- + </widget>
- + </item>
- + <item row="3" column="0">
- + <widget class="QFrame" name="frame_reqs">
- + <property name="sizePolicy">
- + <sizepolicy hsizetype="Minimum" vsizetype="Minimum">
- + <horstretch>0</horstretch>
- + <verstretch>0</verstretch>
- + </sizepolicy>
- + </property>
- + <layout class="QGridLayout" name="gridLayout">
- + <item row="2" column="1">
- + <widget class="QCheckBox" name="ch_stereo">
- + <property name="text">
- + <string>Stereo only</string>
- + </property>
- + </widget>
- + </item>
- + <item row="3" column="1">
- + <widget class="QCheckBox" name="ch_gui">
- + <property name="text">
- + <string>With Custom GUI </string>
- + </property>
- + </widget>
- + </item>
- + <item row="0" column="1">
- + <widget class="QLabel" name="l_reqs">
- + <property name="font">
- + <font>
- + <weight>75</weight>
- + <bold>true</bold>
- + </font>
- + </property>
- + <property name="text">
- + <string>Requirements</string>
- + </property>
- + </widget>
- + </item>
- + <item row="1" column="1">
- + <widget class="QCheckBox" name="ch_favorites">
- + <property name="text">
- + <string>Favorites only</string>
- + </property>
- + </widget>
- + </item>
- + </layout>
- + </widget>
- + </item>
- + <item row="1" column="2" rowspan="3">
- + <widget class="QFrame" name="frame_info">
- + <layout class="QGridLayout" name="gridLayout_2">
- + <item row="9" column="0">
- + <widget class="QLabel" name="label_14">
- + <property name="text">
- + <string>Parameter Ins:</string>
- + </property>
- + <property name="alignment">
- + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- + </property>
- + </widget>
- + </item>
- + <item row="3" column="0">
- + <widget class="QLabel" name="la_id">
- + <property name="text">
- + <string>UniqueID:</string>
- + </property>
- + <property name="alignment">
- + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- + </property>
- + </widget>
- + </item>
- + <item row="1" column="1">
- + <widget class="QLabel" name="l_format">
- + <property name="text">
- + <string>TextLabel</string>
- + </property>
- + </widget>
- + </item>
- + <item row="5" column="0">
- + <widget class="QLabel" name="label_8">
- + <property name="text">
- + <string>Audio Ins:</string>
- + </property>
- + <property name="alignment">
- + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- + </property>
- + </widget>
- + </item>
- + <item row="4" column="0" colspan="2">
- + <widget class="Line" name="line">
- + <property name="lineWidth">
- + <number>0</number>
- + </property>
- + <property name="midLineWidth">
- + <number>1</number>
- + </property>
- + <property name="orientation">
- + <enum>Qt::Horizontal</enum>
- + </property>
- + </widget>
- + </item>
- + <item row="6" column="1">
- + <widget class="QLabel" name="l_aouts">
- + <property name="text">
- + <string>TextLabel</string>
- + </property>
- + </widget>
- + </item>
- + <item row="10" column="0">
- + <widget class="QLabel" name="label_15">
- + <property name="text">
- + <string>Parameter Outs:</string>
- + </property>
- + <property name="alignment">
- + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- + </property>
- + </widget>
- + </item>
- + <item row="8" column="1">
- + <widget class="QLabel" name="l_mouts">
- + <property name="text">
- + <string>TextLabel</string>
- + </property>
- + </widget>
- + </item>
- + <item row="14" column="0">
- + <spacer name="verticalSpacer_2">
- + <property name="orientation">
- + <enum>Qt::Vertical</enum>
- + </property>
- + <property name="sizeHint" stdset="0">
- + <size>
- + <width>20</width>
- + <height>40</height>
- + </size>
- + </property>
- + </spacer>
- + </item>
- + <item row="9" column="1">
- + <widget class="QLabel" name="l_pins">
- + <property name="text">
- + <string>TextLabel</string>
- + </property>
- + </widget>
- + </item>
- + <item row="5" column="1">
- + <widget class="QLabel" name="l_ains">
- + <property name="text">
- + <string>TextLabel</string>
- + </property>
- + </widget>
- + </item>
- + <item row="7" column="1">
- + <widget class="QLabel" name="l_mins">
- + <property name="text">
- + <string>TextLabel</string>
- + </property>
- + </widget>
- + </item>
- + <item row="14" column="1">
- + <spacer name="verticalSpacer">
- + <property name="orientation">
- + <enum>Qt::Vertical</enum>
- + </property>
- + <property name="sizeHint" stdset="0">
- + <size>
- + <width>20</width>
- + <height>40</height>
- + </size>
- + </property>
- + </spacer>
- + </item>
- + <item row="7" column="0">
- + <widget class="QLabel" name="label_12">
- + <property name="text">
- + <string>MIDI Ins:</string>
- + </property>
- + <property name="alignment">
- + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- + </property>
- + </widget>
- + </item>
- + <item row="12" column="1">
- + <widget class="QLabel" name="l_gui">
- + <property name="text">
- + <string>TextLabel</string>
- + </property>
- + </widget>
- + </item>
- + <item row="13" column="0">
- + <widget class="QLabel" name="label_19">
- + <property name="text">
- + <string>Is Synth:</string>
- + </property>
- + <property name="alignment">
- + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- + </property>
- + </widget>
- + </item>
- + <item row="0" column="0" colspan="2">
- + <widget class="QLabel" name="label_3">
- + <property name="font">
- + <font>
- + <weight>75</weight>
- + <bold>true</bold>
- + </font>
- + </property>
- + <property name="text">
- + <string>Information</string>
- + </property>
- + <property name="alignment">
- + <set>Qt::AlignCenter</set>
- + </property>
- + </widget>
- + </item>
- + <item row="8" column="0">
- + <widget class="QLabel" name="label_13">
- + <property name="text">
- + <string>MIDI Outs:</string>
- + </property>
- + <property name="alignment">
- + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- + </property>
- + </widget>
- + </item>
- + <item row="11" column="0" colspan="2">
- + <widget class="Line" name="line_2">
- + <property name="lineWidth">
- + <number>0</number>
- + </property>
- + <property name="midLineWidth">
- + <number>1</number>
- + </property>
- + <property name="orientation">
- + <enum>Qt::Horizontal</enum>
- + </property>
- + </widget>
- + </item>
- + <item row="13" column="1">
- + <widget class="QLabel" name="l_synth">
- + <property name="text">
- + <string>TextLabel</string>
- + </property>
- + </widget>
- + </item>
- + <item row="3" column="1">
- + <widget class="QLabel" name="l_id">
- + <property name="text">
- + <string>TextLabel</string>
- + </property>
- + </widget>
- + </item>
- + <item row="6" column="0">
- + <widget class="QLabel" name="label_9">
- + <property name="text">
- + <string>Audio Outs:</string>
- + </property>
- + <property name="alignment">
- + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- + </property>
- + </widget>
- + </item>
- + <item row="12" column="0">
- + <widget class="QLabel" name="label_17">
- + <property name="text">
- + <string>Has Custom GUI:</string>
- + </property>
- + <property name="alignment">
- + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- + </property>
- + </widget>
- + </item>
- + <item row="2" column="0">
- + <widget class="QLabel" name="label_6">
- + <property name="text">
- + <string>Type:</string>
- + </property>
- + <property name="alignment">
- + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- + </property>
- + </widget>
- + </item>
- + <item row="1" column="0">
- + <widget class="QLabel" name="label_2">
- + <property name="text">
- + <string>Format:</string>
- + </property>
- + <property name="alignment">
- + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- + </property>
- + </widget>
- + </item>
- + <item row="10" column="1">
- + <widget class="QLabel" name="l_pouts">
- + <property name="text">
- + <string>TextLabel</string>
- + </property>
- + </widget>
- + </item>
- + <item row="2" column="1">
- + <widget class="QLabel" name="l_type">
- + <property name="text">
- + <string>TextLabel</string>
- + </property>
- + </widget>
- + </item>
- + </layout>
- + </widget>
- + </item>
- + </layout>
- + <action name="act_focus_search">
- + <property name="text">
- + <string>Focus Text Search</string>
- + </property>
- + <property name="shortcut">
- + <string>Ctrl+F</string>
- + </property>
- + </action>
- + </widget>
- + <tabstops>
- + <tabstop>lineEdit</tabstop>
- + <tabstop>tableWidget</tabstop>
- + <tabstop>b_load</tabstop>
- + <tabstop>b_cancel</tabstop>
- + <tabstop>b_refresh</tabstop>
- + <tabstop>b_clear_filters</tabstop>
- + <tabstop>ch_internal</tabstop>
- + <tabstop>ch_ladspa</tabstop>
- + <tabstop>ch_lv2</tabstop>
- + <tabstop>ch_vst</tabstop>
- + <tabstop>ch_vst3</tabstop>
- + <tabstop>ch_clap</tabstop>
- + <tabstop>ch_effects</tabstop>
- + <tabstop>ch_instruments</tabstop>
- + <tabstop>ch_midi</tabstop>
- + <tabstop>ch_other</tabstop>
- + <tabstop>ch_stereo</tabstop>
- + <tabstop>ch_gui</tabstop>
- + <tabstop>frame_reqs</tabstop>
- + <tabstop>frame_info</tabstop>
- + </tabstops>
- + <resources/>
- + <connections/>
- +</ui>
- 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 <falktx@falktx.com>
- + * 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 @@
- +<?xml version="1.0" encoding="UTF-8"?>
- +<ui version="4.0">
- + <class>PluginRefreshDialog</class>
- + <widget class="QDialog" name="PluginRefreshDialog">
- + <property name="geometry">
- + <rect>
- + <x>0</x>
- + <y>0</y>
- + <width>873</width>
- + <height>179</height>
- + </rect>
- + </property>
- + <property name="windowTitle">
- + <string>Plugin Refresh</string>
- + </property>
- + <layout class="QVBoxLayout" name="verticalLayout_5">
- + <item>
- + <layout class="QHBoxLayout" name="horizontalLayout_3">
- + <item>
- + <spacer name="horizontalSpacer">
- + <property name="orientation">
- + <enum>Qt::Horizontal</enum>
- + </property>
- + <property name="sizeType">
- + <enum>QSizePolicy::Preferred</enum>
- + </property>
- + <property name="sizeHint" stdset="0">
- + <size>
- + <width>30</width>
- + <height>20</height>
- + </size>
- + </property>
- + </spacer>
- + </item>
- + <item>
- + <widget class="QGroupBox" name="group">
- + <property name="title">
- + <string>Searching for:</string>
- + </property>
- + <property name="alignment">
- + <set>Qt::AlignCenter</set>
- + </property>
- + <layout class="QHBoxLayout" name="horizontalLayout_2">
- + <item>
- + <layout class="QVBoxLayout" name="verticalLayout">
- + <item>
- + <widget class="QRadioButton" name="ch_all">
- + <property name="text">
- + <string>All plugins, ignoring cache</string>
- + </property>
- + </widget>
- + </item>
- + <item>
- + <widget class="QRadioButton" name="ch_updated">
- + <property name="text">
- + <string>Updated plugins only</string>
- + </property>
- + </widget>
- + </item>
- + <item>
- + <widget class="QCheckBox" name="ch_invalid">
- + <property name="text">
- + <string>Check previously invalid plugins</string>
- + </property>
- + </widget>
- + </item>
- + </layout>
- + </item>
- + </layout>
- + </widget>
- + </item>
- + <item>
- + <spacer name="horizontalSpacer_3">
- + <property name="orientation">
- + <enum>Qt::Horizontal</enum>
- + </property>
- + <property name="sizeType">
- + <enum>QSizePolicy::Preferred</enum>
- + </property>
- + <property name="sizeHint" stdset="0">
- + <size>
- + <width>20</width>
- + <height>20</height>
- + </size>
- + </property>
- + </spacer>
- + </item>
- + </layout>
- + </item>
- + <item>
- + <spacer name="verticalSpacer">
- + <property name="orientation">
- + <enum>Qt::Vertical</enum>
- + </property>
- + <property name="sizeHint" stdset="0">
- + <size>
- + <width>20</width>
- + <height>6</height>
- + </size>
- + </property>
- + </spacer>
- + </item>
- + <item>
- + <layout class="QHBoxLayout" name="horizontalLayout">
- + <item>
- + <widget class="QProgressBar" name="progressBar">
- + <property name="sizePolicy">
- + <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
- + <horstretch>0</horstretch>
- + <verstretch>0</verstretch>
- + </sizepolicy>
- + </property>
- + <property name="maximum">
- + <number>100</number>
- + </property>
- + <property name="value">
- + <number>0</number>
- + </property>
- + <property name="format">
- + <string>Press 'Scan' to begin the search</string>
- + </property>
- + </widget>
- + </item>
- + <item>
- + <widget class="QPushButton" name="b_start">
- + <property name="text">
- + <string>Scan</string>
- + </property>
- + </widget>
- + </item>
- + <item>
- + <widget class="QPushButton" name="b_skip">
- + <property name="text">
- + <string>>> Skip</string>
- + </property>
- + </widget>
- + </item>
- + <item>
- + <widget class="QPushButton" name="b_close">
- + <property name="text">
- + <string>Close</string>
- + </property>
- + </widget>
- + </item>
- + </layout>
- + </item>
- + </layout>
- + </widget>
- + <connections>
- + <connection>
- + <sender>b_close</sender>
- + <signal>clicked()</signal>
- + <receiver>PluginRefreshDialog</receiver>
- + <slot>close()</slot>
- + <hints>
- + <hint type="sourcelabel">
- + <x>426</x>
- + <y>231</y>
- + </hint>
- + <hint type="destinationlabel">
- + <x>236</x>
- + <y>125</y>
- + </hint>
- + </hints>
- + </connection>
- + <connection>
- + <sender>ch_updated</sender>
- + <signal>toggled(bool)</signal>
- + <receiver>ch_invalid</receiver>
- + <slot>setEnabled(bool)</slot>
- + <hints>
- + <hint type="sourcelabel">
- + <x>436</x>
- + <y>78</y>
- + </hint>
- + <hint type="destinationlabel">
- + <x>436</x>
- + <y>105</y>
- + </hint>
- + </hints>
- + </connection>
- + </connections>
- +</ui>
- 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 <falktx@falktx.com>
- + * SPDX-License-Identifier: GPL-2.0-or-later
- + */
- +
- +#include <QtCore/QTimer>
- +#include <QtCore/QThread>
- +#include <QtWidgets/QApplication>
- +#include <QtWidgets/QFileDialog>
- +#include <QtWidgets/QMessageBox>
- +
- +#include <obs.h>
- +
- +#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<QMainWindow *>(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<char **>(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<char **>(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 <falktx@falktx.com>
- + * SPDX-License-Identifier: GPL-2.0-or-later
- + */
- +
- +#pragma once
- +
- +//-----------------------------------------------------------------------------
- +
- +#ifdef __cplusplus
- +#include <cstdint>
- +#include <QtCore/QSettings>
- +#include <QtWidgets/QMainWindow>
- +extern "C" {
- +#else
- +#include <stdint.h>
- +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
|