diff --git a/.gitmodules b/.gitmodules index 11089575a7ff9e..92f5cbc3c6c00e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,6 @@ [submodule "plugins/obs-websocket"] path = plugins/obs-websocket url = https://github.com/obsproject/obs-websocket.git +[submodule "plugins/carla/carla"] + path = plugins/carla/carla + url = https://github.com/falkTX/Carla.git diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index f928f772c564dc..3cf662a6cf335b 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 00000000000000..4487913a2ada72 --- /dev/null +++ b/plugins/carla/CMakeLists.txt @@ -0,0 +1,111 @@ +cmake_minimum_required(VERSION 3.16...3.25) + +legacy_check() + +option(ENABLE_CARLA "Enable building OBS with carla plugin host" ON) +if(NOT ENABLE_CARLA) + target_disable(carla) + return() +endif() + +# Find Qt +find_qt(COMPONENTS Core Widgets) + +# Use pkg-config to find optional deps +find_package(PkgConfig) + +# Find pthread via cmake +if(OS_WINDOWS) + set(carla_pthread_libs OBS::w32-pthreads) +else() + find_package(Threads REQUIRED) + set(carla_pthread_libs ${CMAKE_THREAD_LIBS_INIT}) +endif() + +# Optional: transient X11 window flags +if(PKGCONFIG_FOUND AND NOT (OS_MACOS OR OS_WINDOWS)) + pkg_check_modules(X11 "x11") +else() + set(X11_FOUND FALSE) +endif() + +# Use *.mm files under macOS, regular *.cpp everywhere else +if(OS_MACOS) + set(CARLA_OBJCPP_EXT "mm") +else() + set(CARLA_OBJCPP_EXT "cpp") +endif() + +# Import extra carla libs +include(cmake/jackbridge.cmake) +add_library(carla::jackbridge ALIAS carla-jackbridge) + +include(cmake/lilv.cmake) +add_library(carla::lilv ALIAS carla-lilv) + +include(cmake/rtmempool.cmake) +add_library(carla::rtmempool ALIAS carla-rtmempool) + +include(cmake/water.cmake) +add_library(carla::water ALIAS carla-water) + +# Setup binary tools +include(cmake/carla-discovery-native.cmake) +include(cmake/carla-bridge-native.cmake) + +# Setup carla-bridge target +add_library(carla-bridge MODULE) +add_library(OBS::carla-bridge ALIAS carla-bridge) + +target_compile_definitions( + carla-bridge + PRIVATE BUILDING_CARLA + BUILDING_CARLA_OBS + CARLA_BACKEND_NAMESPACE=CarlaBridgeOBS + CARLA_FRONTEND_NO_CACHED_PLUGIN_API + CARLA_MODULE_ID="carla-bridge" + CARLA_MODULE_NAME="Carla Bridge" + CARLA_PLUGIN_ONLY_BRIDGE + STATIC_PLUGIN_TARGET) + +target_include_directories( + carla-bridge + PRIVATE carla/source + carla/source/backend + carla/source/frontend + carla/source/frontend/utils + carla/source/includes + carla/source/modules + carla/source/utils) + +# TODO remove carla::water dependency from PluginDiscovery.cpp + +target_link_libraries(carla-bridge PRIVATE carla::jackbridge carla::lilv carla::water OBS::libobs Qt::Core Qt::Widgets) + +if(NOT (OS_MACOS OR OS_WINDOWS)) + target_link_options(carla-bridge PRIVATE -Wl,--no-undefined) +endif() + +target_sources( + carla-bridge + PRIVATE carla.c + carla-bridge.cpp + carla-bridge-wrapper.cpp + common.c + qtutils.cpp + carla/source/backend/utils/Information.cpp + carla/source/backend/utils/PluginDiscovery.cpp + carla/source/frontend/carla_frontend.cpp + carla/source/frontend/pluginlist/pluginlistdialog.cpp + carla/source/frontend/pluginlist/pluginlistrefreshdialog.cpp + carla/source/utils/CarlaBridgeUtils.cpp + carla/source/utils/CarlaMacUtils.${CARLA_OBJCPP_EXT} + carla/source/utils/CarlaPipeUtils.cpp) + +set_target_properties_obs( + carla-bridge + PROPERTIES AUTOMOC ON + AUTOUIC ON + AUTORCC ON + FOLDER plugins + PREFIX "") diff --git a/plugins/carla/carla b/plugins/carla/carla new file mode 160000 index 00000000000000..ad7def4bd0dbd9 --- /dev/null +++ b/plugins/carla/carla @@ -0,0 +1 @@ +Subproject commit ad7def4bd0dbd9c2470a72cae79089a0a17dad7f diff --git a/plugins/carla/carla-bridge-wrapper.cpp b/plugins/carla/carla-bridge-wrapper.cpp new file mode 100644 index 00000000000000..4a0209ac79d2b7 --- /dev/null +++ b/plugins/carla/carla-bridge-wrapper.cpp @@ -0,0 +1,561 @@ +/* + * Carla plugin for OBS + * Copyright (C) 2023 Filipe Coelho + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "carla-bridge.hpp" +#include "carla-wrapper.h" +#include "common.h" +#include "qtutils.h" +#include + +#include +#include + +#include "CarlaBackendUtils.hpp" +#include "CarlaBinaryUtils.hpp" +#include "CarlaFrontend.h" + +// generates a warning if this is defined as anything else +#define CARLA_API + +// ---------------------------------------------------------------------------- +// 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_source_t *source = priv->source; + 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 ? 1.f : 0.f); + 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; + + assert(priv->bufferSize != 0); + if (priv->bufferSize == 0) + goto fail1; + + return priv; + +fail1: + delete priv; + return nullptr; +} + +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) +{ + if (!priv->bridge.idle()) { + // bridge crashed! + // TODO something + } + + 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); + + 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 char *btype = obs_data_get_string(settings, "btype"); + const char *ptype = obs_data_get_string(settings, "ptype"); + const char *filename = obs_data_get_string(settings, "filename"); + const char *label = obs_data_get_string(settings, "label"); + int64_t uniqueId = 0; + + priv->bridge.cleanup(); + priv->bridge.init(priv->bufferSize, priv->sampleRate); + + if (!priv->bridge.start(getBinaryTypeFromString(btype), + getPluginTypeFromString(ptype), label, filename, + uniqueId)) { + // TODO show error message if bridge fails + 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); + } + } +} + +// ---------------------------------------------------------------------------- + +void carla_priv_set_buffer_size(struct carla_priv *priv, + enum buffer_size_mode bufsize) +{ + priv->bridge.set_buffer_size(bufsize_mode_to_frames(bufsize)); +} + +// ---------------------------------------------------------------------------- + +static bool carla_post_load_callback(struct carla_priv *priv, + obs_properties_t *props) +{ + obs_source_t *source = priv->source; + obs_data_t *settings = obs_source_get_settings(source); + remove_all_props(props, settings); + carla_priv_readd_properties(priv, props, true); + obs_data_release(settings); + return true; +} + +static bool carla_priv_load_file_callback(obs_properties_t *props, + obs_property_t *property, void *data) +{ + UNUSED_PARAMETER(property); + + struct carla_priv *priv = static_cast(data); + + const char *filename = carla_qt_file_dialog( + false, false, obs_module_text("Load File"), NULL); + + if (filename == NULL) + return false; + + BinaryType btype; + PluginType ptype; + + { + const QFileInfo fileInfo(QString::fromUtf8(filename)); + const QString extension(fileInfo.suffix()); + +#ifdef 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; + else if (extension == "clap") + ptype = PLUGIN_CLAP; + else + return false; + + btype = getBinaryTypeFromFile(filename); + } + + priv->bridge.cleanup(); + priv->bridge.init(priv->bufferSize, priv->sampleRate); + + // TODO show error message if bridge fails + if (priv->bridge.start(btype, ptype, "", filename, 0)) + priv->bridge.activate(); + + return carla_post_load_callback(priv, props); +} + +static bool carla_priv_select_plugin_callback(obs_properties_t *props, + obs_property_t *property, + void *data) +{ + UNUSED_PARAMETER(property); + + struct carla_priv *priv = static_cast(data); + + const PluginListDialogResults *plugin = + carla_frontend_createAndExecPluginListDialog( + carla_qt_get_main_window()); + + if (plugin == NULL) + return false; + + priv->bridge.cleanup(); + priv->bridge.init(priv->bufferSize, priv->sampleRate); + + // TODO show error message if bridge fails + if (priv->bridge.start(static_cast(plugin->build), + static_cast(plugin->type), + plugin->label, plugin->filename, + plugin->uniqueId)) + priv->bridge.activate(); + + return carla_post_load_callback(priv, props); +} + +static bool carla_priv_reload_callback(obs_properties_t *props, + obs_property_t *property, void *data) +{ + UNUSED_PARAMETER(property); + + struct carla_priv *priv = static_cast(data); + + if (priv->bridge.is_running()) { + priv->bridge.reload(); + return true; + } + + if (priv->bridge.info.btype == BINARY_NONE) + return false; + + // cache relevant information for later + const BinaryType btype = priv->bridge.info.btype; + const PluginType ptype = priv->bridge.info.ptype; + const int64_t uniqueId = priv->bridge.info.uniqueId; + char *const label = priv->bridge.info.label.releaseBufferPointer(); + char *const filename = + priv->bridge.info.filename.releaseBufferPointer(); + + priv->bridge.cleanup(false); + priv->bridge.init(priv->bufferSize, priv->sampleRate); + + if (priv->bridge.start(btype, ptype, label, filename, uniqueId)) { + priv->bridge.restore_state(); + priv->bridge.activate(); + } + + // TODO show error message if bridge fails + + std::free(label); + std::free(filename); + + return carla_post_load_callback(priv, props); +} + +static bool carla_priv_show_gui_callback(obs_properties_t *props, + obs_property_t *property, void *data) +{ + UNUSED_PARAMETER(props); + UNUSED_PARAMETER(property); + + struct carla_priv *priv = static_cast(data); + + priv->bridge.show_ui(); + + return false; +} + +static bool carla_priv_param_changed(void *data, obs_properties_t *props, + obs_property_t *property, + obs_data_t *settings) +{ + UNUSED_PARAMETER(props); + + struct carla_priv *priv = static_cast(data); + + const char *const pname = obs_property_name(property); + if (pname == NULL) + return false; + + const char *pname2 = pname + 1; + while (*pname2 == '0') + ++pname2; + + const int pindex = atoi(pname2); + + if (pindex < 0 || pindex >= (int)priv->bridge.paramCount) + return false; + + const uint index = static_cast(pindex); + + const float min = priv->bridge.paramDetails[index].min; + const float max = priv->bridge.paramDetails[index].max; + + float value; + switch (obs_property_get_type(property)) { + case OBS_PROPERTY_BOOL: + value = obs_data_get_bool(settings, pname) ? max : min; + break; + case OBS_PROPERTY_INT: + value = obs_data_get_int(settings, pname); + if (value < min) + value = min; + else if (value > max) + value = max; + break; + case OBS_PROPERTY_FLOAT: + value = obs_data_get_double(settings, pname); + if (value < min) + value = min; + else if (value > max) + value = max; + break; + default: + return false; + } + + priv->bridge.set_value(index, value); + + return false; +} + +void carla_priv_readd_properties(struct carla_priv *priv, + obs_properties_t *props, bool reset) +{ + if (!reset) { + obs_properties_add_button2(props, PROP_SELECT_PLUGIN, + obs_module_text("Select plugin..."), + carla_priv_select_plugin_callback, + priv); + + obs_properties_add_button2(props, PROP_LOAD_FILE, + obs_module_text("Load file..."), + carla_priv_load_file_callback, priv); + + 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); +} + +// ---------------------------------------------------------------------------- + +// these do nothing +extern "C" { +void carla_juce_init() {} +void carla_juce_idle() {} +void carla_juce_cleanup() {} +} diff --git a/plugins/carla/carla-bridge.cpp b/plugins/carla/carla-bridge.cpp new file mode 100644 index 00000000000000..91468e72bdc3a8 --- /dev/null +++ b/plugins/carla/carla-bridge.cpp @@ -0,0 +1,1379 @@ +/* + * Carla plugin for OBS + * Copyright (C) 2023 Filipe Coelho + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "carla-bridge.hpp" +#include "common.h" +#include "qtutils.h" + +#include "CarlaBackendUtils.hpp" +#include "CarlaBase64Utils.hpp" +#include "CarlaBinaryUtils.hpp" +#include "CarlaTimeUtils.hpp" + +#ifdef CARLA_OS_MAC +#include "CarlaMacUtils.hpp" +#endif + +#include + +#include +#include +#include +#include +#include + +#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_MODULE_ID "]" + " bridge refused to close, force kill now"); + kill(); + } else { + blog(LOG_DEBUG, "[" CARLA_MODULE_ID "]" + " bridge auto-closed successfully"); + } + } + + deleteLater(); +} + +// ---------------------------------------------------------------------------- + +bool carla_bridge::init(uint32_t maxBufferSize, double sampleRate) +{ + // add entropy to rand calls, used for finding unused paths + std::srand(static_cast(std::time(nullptr))); + + // initialize the several communication channels + if (!audiopool.initializeServer()) { + blog(LOG_WARNING, + "[" CARLA_MODULE_ID "]" + " Failed to initialize shared memory audio pool"); + return false; + } + + if (!rtClientCtrl.initializeServer()) { + blog(LOG_WARNING, "[" CARLA_MODULE_ID "]" + " Failed to initialize RT client control"); + goto fail1; + } + + if (!nonRtClientCtrl.initializeServer()) { + blog(LOG_WARNING, + "[" CARLA_MODULE_ID "]" + " Failed to initialize Non-RT client control"); + goto fail2; + } + + if (!nonRtServerCtrl.initializeServer()) { + blog(LOG_WARNING, + "[" CARLA_MODULE_ID "]" + " Failed to initialize Non-RT server control"); + goto fail3; + } + + // resize audiopool data to be as large as needed + audiopool.resize(maxBufferSize, MAX_AV_PLANES, MAX_AV_PLANES); + + // clear realtime data + rtClientCtrl.data->procFlags = 0; + carla_zeroStruct(rtClientCtrl.data->timeInfo); + carla_zeroBytes(rtClientCtrl.data->midiOut, + kBridgeRtClientDataMidiOutSize); + + // clear ringbuffers + rtClientCtrl.clearData(); + nonRtClientCtrl.clearData(); + nonRtServerCtrl.clearData(); + + // first ever message is bridge API version + nonRtClientCtrl.writeOpcode(kPluginBridgeNonRtClientVersion); + nonRtClientCtrl.writeUInt(CARLA_PLUGIN_BRIDGE_API_VERSION_CURRENT); + + // then expected size for each data channel + nonRtClientCtrl.writeUInt( + static_cast(sizeof(BridgeRtClientData))); + nonRtClientCtrl.writeUInt( + static_cast(sizeof(BridgeNonRtClientData))); + nonRtClientCtrl.writeUInt( + static_cast(sizeof(BridgeNonRtServerData))); + + // and finally the initial buffer size and sample rate + nonRtClientCtrl.writeOpcode(kPluginBridgeNonRtClientInitialSetup); + nonRtClientCtrl.writeUInt(maxBufferSize); + nonRtClientCtrl.writeDouble(sampleRate); + + nonRtClientCtrl.commitWrite(); + + // report audiopool size to client side + rtClientCtrl.writeOpcode(kPluginBridgeRtClientSetAudioPool); + rtClientCtrl.writeULong(static_cast(audiopool.dataSize)); + rtClientCtrl.commitWrite(); + + // FIXME + rtClientCtrl.writeOpcode(kPluginBridgeRtClientSetBufferSize); + rtClientCtrl.writeUInt(maxBufferSize); + rtClientCtrl.commitWrite(); + + bufferSize = maxBufferSize; + blog(LOG_DEBUG, "[" CARLA_MODULE_ID "] initialized with %u buffer size", + bufferSize); + + return true; + +fail3: + nonRtClientCtrl.clear(); + +fail2: + rtClientCtrl.clear(); + +fail1: + audiopool.clear(); + return false; +} + +void carla_bridge::cleanup(const bool clearPluginData) +{ + // signal to stop processing audio + ready = 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 (activated) { + activated = false; + + 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_MODULE_ID "]" + " carla_bridge::cleanup() - bridge crashed"); + } + } + + // 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 + CARLA_SAFE_ASSERT_RETURN(btype != BINARY_NONE, false); + CARLA_SAFE_ASSERT_RETURN(ptype != PLUGIN_NONE, 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()) { + blog(LOG_ERROR, + "[" CARLA_MODULE_ID "]" + " Cannot load plugin, the 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); + + blog(LOG_INFO, + "[" CARLA_MODULE_ID "]" + " Starting plugin bridge, command is:\n%s \"%s\" \"%s\" \"%s\" " P_INT64, + bridgeBinary.toUtf8().constData(), getPluginTypeAsString(ptype), + filename, label, uniqueId); + + // start process on main thread + QMetaObject::invokeMethod(proc, "start"); + + // check if it started correctly + const bool started = proc->waitForStarted(5000); + + if (!started) { + blog(LOG_INFO, "[" CARLA_MODULE_ID "] failed!"); + QMetaObject::invokeMethod(proc, "stop"); + return false; + } + + blog(LOG_INFO, "[" CARLA_MODULE_ID "] started ok!"); + + // wait for plugin process to start talking to us + ready = false; + timedErr = false; + timedOut = false; + + const uint64_t start_time = carla_gettime_ms(); + + // NOTE: we cannot rely on `proc->state() == QProcess::Running` here + // as Qt only updates QProcess state on main thread + while (proc != nullptr && !ready) { + carla_msleep(5); + + // timeout after 5s + if (carla_gettime_ms() - start_time > 5000) + break; + + readMessages(); + } + + if (!ready) { + blog(LOG_WARNING, + "[" CARLA_MODULE_ID "] failed to start plugin bridge"); + QMetaObject::invokeMethod(proc, "stop"); + return false; + } + + if (activated) { + nonRtClientCtrl.writeOpcode(kPluginBridgeNonRtClientActivate); + nonRtClientCtrl.commitWrite(); + } + + // cache relevant information for later + info.btype = btype; + info.ptype = ptype; + info.filename = filename; + info.label = label; + info.uniqueId = uniqueId; + + // finally assign childprocess + 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: + blog(LOG_INFO, + "[" CARLA_MODULE_ID "] bridge closed by itself!"); + 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(activated, false); + // CARLA_SAFE_ASSERT_RETURN(childprocess != nullptr, false); + CARLA_SAFE_ASSERT_RETURN(!timedErr, false); + CARLA_SAFE_ASSERT_RETURN(!timedOut, false); + + if (rtClientCtrl.waitForClient(msecs)) + return true; + + timedOut = true; + blog(LOG_WARNING, "[" CARLA_MODULE_ID "] 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(); + + 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() +{ + CARLA_SAFE_ASSERT_RETURN(!activated, ); + + activated = true; + + if (is_running()) { + { + const CarlaMutexLocker cml(nonRtClientCtrl.mutex); + + nonRtClientCtrl.writeOpcode( + kPluginBridgeNonRtClientActivate); + nonRtClientCtrl.commitWrite(); + } + + try { + wait("activate", 2000); + } + CARLA_SAFE_EXCEPTION("activate - waitForClient"); + } +} + +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("deactivate", 2000); + } + CARLA_SAFE_EXCEPTION("reload - waitForClient"); + } + + // wait for plugin process to start talking back to us + const uint64_t start_time = carla_gettime_ms(); + + while (childprocess != nullptr && !ready) { + carla_msleep(5); + + // timeout after 1s + if (carla_gettime_ms() - start_time > 1000) + break; + + readMessages(); + } +} + +void carla_bridge::restore_state() +{ + const uint32_t maxLocalValueLen = clientBridgeVersion >= 10 ? 4096 + : 16384; + + const CarlaMutexLocker cml(nonRtClientCtrl.mutex); + + for (CustomData &cdata : customData) { + const uint32_t typeLen = + static_cast(std::strlen(cdata.type)); + const uint32_t keyLen = + static_cast(std::strlen(cdata.key)); + const uint32_t valueLen = + static_cast(std::strlen(cdata.value)); + + nonRtClientCtrl.writeOpcode( + kPluginBridgeNonRtClientSetCustomData); + + nonRtClientCtrl.writeUInt(typeLen); + nonRtClientCtrl.writeCustomData(cdata.type, typeLen); + + nonRtClientCtrl.writeUInt(keyLen); + nonRtClientCtrl.writeCustomData(cdata.key, keyLen); + + nonRtClientCtrl.writeUInt(valueLen); + + if (valueLen > 0) { + if (valueLen > maxLocalValueLen) { + QString filePath(QDir::tempPath()); + + filePath += CARLA_OS_SEP_STR + ".CarlaCustomData_"; + filePath += audiopool.getFilenameSuffix(); + + QFile file(filePath); + if (file.open(QIODevice::WriteOnly) && + file.write(cdata.value) != + static_cast(valueLen)) { + const uint32_t ulength = + static_cast( + filePath.length()); + + nonRtClientCtrl.writeUInt(ulength); + nonRtClientCtrl.writeCustomData( + filePath.toUtf8().constData(), + ulength); + } else { + nonRtClientCtrl.writeUInt(0); + } + } else { + nonRtClientCtrl.writeCustomData(cdata.value, + valueLen); + } + } + + nonRtClientCtrl.commitWrite(); + + nonRtClientCtrl.waitIfDataIsReachingLimit(); + } + + if (info.ptype != PLUGIN_LV2) { + nonRtClientCtrl.writeOpcode( + kPluginBridgeNonRtClientRestoreLV2State); + nonRtClientCtrl.commitWrite(); + } + + if (info.options & PLUGIN_OPTION_USE_CHUNKS) { + QString filePath(QDir::tempPath()); + + filePath += CARLA_OS_SEP_STR ".CarlaChunk_"; + filePath += audiopool.getFilenameSuffix(); + + QFile file(filePath); + if (file.open(QIODevice::WriteOnly) && + file.write(CarlaString::asBase64(chunk.data(), chunk.size()) + .buffer()) != 0) { + file.close(); + + const uint32_t ulength = + static_cast(filePath.length()); + + nonRtClientCtrl.writeOpcode( + kPluginBridgeNonRtClientSetChunkDataFile); + nonRtClientCtrl.writeUInt(ulength); + nonRtClientCtrl.writeCustomData( + filePath.toUtf8().constData(), ulength); + nonRtClientCtrl.commitWrite(); + + nonRtClientCtrl.waitIfDataIsReachingLimit(); + } + } else { + for (uint32_t i = 0; i < paramCount; ++i) { + const carla_param_data ¶m(paramDetails[i]); + + nonRtClientCtrl.writeOpcode( + kPluginBridgeNonRtClientSetParameterValue); + nonRtClientCtrl.writeUInt(i); + nonRtClientCtrl.writeFloat(param.value); + nonRtClientCtrl.commitWrite(); + + nonRtClientCtrl.writeOpcode( + kPluginBridgeNonRtClientUiParameterChange); + nonRtClientCtrl.writeUInt(i); + nonRtClientCtrl.writeFloat(param.value); + nonRtClientCtrl.commitWrite(); + + nonRtClientCtrl.waitIfDataIsReachingLimit(); + } + } +} + +void carla_bridge::process(float *buffers[MAX_AV_PLANES], const uint32_t frames) +{ + if (!ready || !activated) + return; + + rtClientCtrl.data->timeInfo.usecs = carla_gettime_us(); + + for (uint32_t c = 0; c < MAX_AV_PLANES; ++c) + carla_copyFloats(audiopool.data + (c * bufferSize), buffers[c], + frames); + + { + rtClientCtrl.writeOpcode(kPluginBridgeRtClientProcess); + rtClientCtrl.writeUInt(frames); + rtClientCtrl.commitWrite(); + } + + if (wait("process", 1000)) { + for (uint32_t c = 0; c < MAX_AV_PLANES; ++c) + carla_copyFloats( + buffers[c], + audiopool.data + + ((c + info.numAudioIns) * bufferSize), + frames); + } +} + +void carla_bridge::add_custom_data(const char *const type, + const char *const key, + const char *const value, + const bool sendToPlugin) +{ + CARLA_SAFE_ASSERT_RETURN(type != nullptr && type[0] != '\0', ); + CARLA_SAFE_ASSERT_RETURN(key != nullptr && key[0] != '\0', ); + CARLA_SAFE_ASSERT_RETURN(value != nullptr, ); + + // Check if we already have this key + bool found = false; + for (CustomData &cdata : customData) { + if (std::strcmp(cdata.key, key) == 0) { + bfree(const_cast(cdata.value)); + cdata.value = bstrdup(value); + found = true; + break; + } + } + + // Otherwise store it + if (!found) { + CustomData cdata = {}; + cdata.type = bstrdup(type); + cdata.key = bstrdup(key); + cdata.value = bstrdup(value); + customData.push_back(cdata); + } + + if (sendToPlugin) { + const uint32_t maxLocalValueLen = + clientBridgeVersion >= 10 ? 4096 : 16384; + + const uint32_t typeLen = + static_cast(std::strlen(type)); + const uint32_t keyLen = static_cast(std::strlen(key)); + const uint32_t valueLen = + static_cast(std::strlen(value)); + + const CarlaMutexLocker cml(nonRtClientCtrl.mutex); + + if (valueLen > maxLocalValueLen) + nonRtClientCtrl.waitIfDataIsReachingLimit(); + + nonRtClientCtrl.writeOpcode( + kPluginBridgeNonRtClientSetCustomData); + + nonRtClientCtrl.writeUInt(typeLen); + nonRtClientCtrl.writeCustomData(type, typeLen); + + nonRtClientCtrl.writeUInt(keyLen); + nonRtClientCtrl.writeCustomData(key, keyLen); + + nonRtClientCtrl.writeUInt(valueLen); + + if (valueLen > 0) { + if (valueLen > maxLocalValueLen) { + QString filePath(QDir::tempPath()); + + filePath += CARLA_OS_SEP_STR + ".CarlaCustomData_"; + filePath += audiopool.getFilenameSuffix(); + + QFile file(filePath); + if (file.open(QIODevice::WriteOnly) && + file.write(value) != + static_cast(valueLen)) { + const uint32_t ulength = + static_cast( + filePath.length()); + + nonRtClientCtrl.writeUInt(ulength); + nonRtClientCtrl.writeCustomData( + filePath.toUtf8().constData(), + ulength); + } else { + nonRtClientCtrl.writeUInt(0); + } + } else { + nonRtClientCtrl.writeCustomData(value, + valueLen); + } + } + + nonRtClientCtrl.commitWrite(); + + nonRtClientCtrl.waitIfDataIsReachingLimit(); + } +} + +void carla_bridge::custom_data_loaded() +{ + if (info.ptype != PLUGIN_LV2) + return; + + const CarlaMutexLocker cml(nonRtClientCtrl.mutex); + + nonRtClientCtrl.writeOpcode(kPluginBridgeNonRtClientRestoreLV2State); + nonRtClientCtrl.commitWrite(); +} + +void carla_bridge::clear_custom_data() +{ + for (CustomData &cdata : customData) { + bfree(const_cast(cdata.type)); + bfree(const_cast(cdata.key)); + bfree(const_cast(cdata.value)); + } + customData.clear(); +} + +void carla_bridge::load_chunk(const char *b64chunk) +{ + chunk = QByteArray::fromBase64(b64chunk); + + QString filePath(QDir::tempPath()); + + filePath += CARLA_OS_SEP_STR ".CarlaChunk_"; + filePath += audiopool.getFilenameSuffix(); + + QFile file(filePath); + if (file.open(QIODevice::WriteOnly) && file.write(b64chunk) != 0) { + file.close(); + + const uint32_t ulength = + static_cast(filePath.length()); + + const CarlaMutexLocker cml(nonRtClientCtrl.mutex); + + nonRtClientCtrl.writeOpcode( + kPluginBridgeNonRtClientSetChunkDataFile); + nonRtClientCtrl.writeUInt(ulength); + nonRtClientCtrl.writeCustomData(filePath.toUtf8().constData(), + ulength); + nonRtClientCtrl.commitWrite(); + + nonRtClientCtrl.waitIfDataIsReachingLimit(); + } +} + +void carla_bridge::save_and_wait() +{ + if (!is_running()) + return; + + saved = false; + + { + 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 = carla_gettime_ms(); + + while (is_running() && !saved) { + carla_msleep(5); + + // timeout after 10s + if (carla_gettime_ms() - start_time > 10000) + break; + + readMessages(); + + // deactivate plugin if we timeout during save + if (timedOut && activated) { + activated = false; + const CarlaMutexLocker cml(nonRtClientCtrl.mutex); + + nonRtClientCtrl.writeOpcode( + kPluginBridgeNonRtClientDeactivate); + nonRtClientCtrl.commitWrite(); + } + } + + if (is_running()) { + const CarlaMutexLocker cml(nonRtClientCtrl.mutex); + + // reactivate ping check + nonRtClientCtrl.writeOpcode(kPluginBridgeNonRtClientPingOnOff); + nonRtClientCtrl.writeBool(true); + nonRtClientCtrl.commitWrite(); + } +} + +void carla_bridge::set_buffer_size(const uint32_t maxBufferSize) +{ + if (bufferSize == maxBufferSize) + return; + + bufferSize = maxBufferSize; + + if (is_running()) { + audiopool.resize(maxBufferSize, MAX_AV_PLANES, MAX_AV_PLANES); + + rtClientCtrl.writeOpcode(kPluginBridgeRtClientSetAudioPool); + rtClientCtrl.writeULong( + static_cast(audiopool.dataSize)); + rtClientCtrl.commitWrite(); + + rtClientCtrl.writeOpcode(kPluginBridgeRtClientSetBufferSize); + rtClientCtrl.writeUInt(maxBufferSize); + rtClientCtrl.commitWrite(); + } +} + +// ---------------------------------------------------------------------------- +void carla_bridge::readMessages() +{ + while (nonRtServerCtrl.isDataAvailableForReading()) { + const PluginBridgeNonRtServerOpcode opcode = + nonRtServerCtrl.readOpcode(); + + // #ifdef DEBUG + if (opcode != kPluginBridgeNonRtServerPong && + opcode != kPluginBridgeNonRtServerParameterValue2) { + blog(LOG_DEBUG, + "carla_bridge::readMessages() - 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 + if (const uint32_t size = nonRtServerCtrl.readUInt()) + nonRtServerCtrl.skipRead(size); + + // 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: + nonRtServerCtrl.readUInt(); + nonRtServerCtrl.readUInt(); + 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); + + /* + QMessageBox::critical(nullptr, + QString::fromUtf8("Plugin error"), + QString::fromUtf8(error.text)); + */ + } break; + } + } +} diff --git a/plugins/carla/carla-bridge.hpp b/plugins/carla/carla-bridge.hpp new file mode 100644 index 00000000000000..a7384626fd5a98 --- /dev/null +++ b/plugins/carla/carla-bridge.hpp @@ -0,0 +1,198 @@ +/* + * Carla plugin for OBS + * Copyright (C) 2023 Filipe Coelho + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#include "CarlaBackend.h" +#include "CarlaBridgeUtils.hpp" + +#include +#include +#include + +#include + +// generates warning if defined as anything else +#define CARLA_API + +// import macro from OBS, purposefully not including any other OBS APIs here +#define MAX_AV_PLANES 8 + +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; + uint32_t numAudioIns = 0; + uint32_t numAudioOuts = 0; + int64_t uniqueId = 0; + CarlaString filename; + CarlaString label; + + void clear() + { + btype = BINARY_NONE; + ptype = PLUGIN_NONE; + hints = 0; + options = PLUGIN_OPTIONS_NULL; + numAudioIns = numAudioOuts = 0; + uniqueId = 0; + label.clear(); + filename.clear(); + } +}; + +// ---------------------------------------------------------------------------- +// bridge callbacks, triggered during carla_bridge::idle() + +struct carla_bridge_callback { + virtual ~carla_bridge_callback(){}; + virtual void bridge_parameter_changed(uint index, float value) = 0; +}; + +// ---------------------------------------------------------------------------- +// bridge implementation + +struct carla_bridge { + carla_bridge_callback *callback = nullptr; + + // cached parameter info + uint32_t paramCount = 0; + carla_param_data *paramDetails = nullptr; + + // cached plugin info + carla_bridge_info info; + QByteArray chunk; + std::vector customData; + + ~carla_bridge() + { + delete[] paramDetails; + clear_custom_data(); + } + + // 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); + +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; + + BridgeAudioPool audiopool; + BridgeRtClientControl rtClientCtrl; + BridgeNonRtClientControl nonRtClientCtrl; + BridgeNonRtServerControl nonRtServerCtrl; + + BridgeProcess *childprocess = nullptr; + + void readMessages(); +}; + +// ---------------------------------------------------------------------------- diff --git a/plugins/carla/carla-wrapper.h b/plugins/carla/carla-wrapper.h new file mode 100644 index 00000000000000..fa180ebca35cad --- /dev/null +++ b/plugins/carla/carla-wrapper.h @@ -0,0 +1,71 @@ +/* + * Carla plugin for OBS + * Copyright (C) 2023 Filipe Coelho + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#include + +// maximum buffer used, can be smaller +#define MAX_AUDIO_BUFFER_SIZE 512 + +enum buffer_size_mode { + buffer_size_direct, + buffer_size_buffered_128, + buffer_size_buffered_256, + buffer_size_buffered_512, + buffer_size_buffered_max = buffer_size_buffered_512 +}; + +// ---------------------------------------------------------------------------- +// helper methods + +static inline uint32_t bufsize_mode_to_frames(enum buffer_size_mode bufsize) +{ + switch (bufsize) { + case buffer_size_buffered_128: + return 128; + case buffer_size_buffered_256: + return 256; + default: + return MAX_AUDIO_BUFFER_SIZE; + } +} + +// ---------------------------------------------------------------------------- +// carla + obs integration methods + +#ifdef __cplusplus +extern "C" { +#endif + +struct carla_priv; + +struct carla_priv *carla_priv_create(obs_source_t *source, + enum buffer_size_mode bufsize, + uint32_t srate); +void carla_priv_destroy(struct carla_priv *carla); + +void carla_priv_activate(struct carla_priv *carla); +void carla_priv_deactivate(struct carla_priv *carla); +void carla_priv_process_audio(struct carla_priv *carla, + float *buffers[MAX_AV_PLANES], uint32_t frames); + +void carla_priv_idle(struct carla_priv *carla); + +void carla_priv_save(struct carla_priv *carla, obs_data_t *settings); +void carla_priv_load(struct carla_priv *carla, obs_data_t *settings); + +void carla_priv_set_buffer_size(struct carla_priv *carla, + enum buffer_size_mode bufsize); + +void carla_priv_readd_properties(struct carla_priv *carla, + obs_properties_t *props, bool reset); + +#ifdef __cplusplus +} +#endif + +// ---------------------------------------------------------------------------- diff --git a/plugins/carla/carla.c b/plugins/carla/carla.c new file mode 100644 index 00000000000000..65a16e0929fc15 --- /dev/null +++ b/plugins/carla/carla.c @@ -0,0 +1,487 @@ +/* + * Carla plugin for OBS + * Copyright (C) 2023 Filipe Coelho + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include +#include + +#ifndef CARLA_MODULE_ID +#error CARLA_MODULE_ID undefined +#endif + +#ifndef CARLA_MODULE_NAME +#error CARLA_MODULE_NAME undefined +#endif + +#include "carla-wrapper.h" +#include "common.h" + +// for audio generator thread +#include + +// -------------------------------------------------------------------------------------------------------------------- + +struct carla_data { + // carla host details, intentionally kept private so we can easily swap internals + struct carla_priv *priv; + + // current OBS config + bool activated; + size_t channels; + uint32_t sample_rate; + obs_source_t *source; + + // 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 void *carla_obs_audio_gen_thread(void *data) +{ + struct carla_data *carla = data; + + struct obs_source_audio out = { + .speakers = SPEAKERS_STEREO, + .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; + 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_activate(void *data); +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 " (Input)"); +} + +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 (channels == 0 || sample_rate == 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; + + struct carla_priv *priv = + carla_priv_create(source, buffer_size_direct, 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 = buffer_size_direct; + + // 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; + + blog(LOG_INFO, "[" CARLA_MODULE_ID "] changing buffer size to %s", + value); + + // 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); + + 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]; + + for (uint32_t i = 0; i < frames;) { + 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); + + 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/carla-bridge-native.cmake b/plugins/carla/cmake/carla-bridge-native.cmake new file mode 100644 index 00000000000000..c90ba9fcd0d74a --- /dev/null +++ b/plugins/carla/cmake/carla-bridge-native.cmake @@ -0,0 +1,56 @@ +add_executable(carla-bridge-native) +mark_as_advanced(carla-bridge-native) + +target_compile_definitions( + carla-bridge-native + PRIVATE BUILDING_CARLA BUILD_BRIDGE BUILD_BRIDGE_ALTERNATIVE_ARCH CARLA_BACKEND_NAMESPACE=CarlaOBS + CARLA_LIB_EXT="${CMAKE_SHARED_LIBRARY_SUFFIX}" $<$:HAVE_X11>) + +target_compile_options(carla-bridge-native PRIVATE $<$:/wd4244 /wd4267 /wd4273> + $<$>:-Wno-error -Werror=vla> ${X11_CFLAGS}) + +target_include_directories( + carla-bridge-native + PRIVATE carla/source + carla/source/backend + carla/source/backend/engine + carla/source/backend/plugin + carla/source/includes + carla/source/modules + carla/source/utils + ${X11_INCLUDE_DIRS}) + +target_link_directories(carla-bridge-native PRIVATE ${X11_LIBRARY_DIRS}) + +target_link_libraries(carla-bridge-native PRIVATE carla::jackbridge carla::lilv carla::rtmempool carla::water + ${X11_LIBRARIES}) + +target_sources( + carla-bridge-native + PRIVATE carla/source/bridges-plugin/CarlaBridgePlugin.${CARLA_OBJCPP_EXT} + carla/source/backend/CarlaStandalone.${CARLA_OBJCPP_EXT} + carla/source/backend/engine/CarlaEngine.cpp + carla/source/backend/engine/CarlaEngineBridge.cpp + carla/source/backend/engine/CarlaEngineClient.cpp + carla/source/backend/engine/CarlaEngineData.cpp + carla/source/backend/engine/CarlaEngineInternal.cpp + carla/source/backend/engine/CarlaEnginePorts.cpp + carla/source/backend/engine/CarlaEngineRunner.cpp + carla/source/backend/plugin/CarlaPlugin.cpp + carla/source/backend/plugin/CarlaPluginBridge.cpp + carla/source/backend/plugin/CarlaPluginJuce.cpp + carla/source/backend/plugin/CarlaPluginInternal.cpp + carla/source/backend/plugin/CarlaPluginAU.cpp + carla/source/backend/plugin/CarlaPluginCLAP.${CARLA_OBJCPP_EXT} + carla/source/backend/plugin/CarlaPluginLADSPADSSI.cpp + carla/source/backend/plugin/CarlaPluginLV2.cpp + carla/source/backend/plugin/CarlaPluginVST2.${CARLA_OBJCPP_EXT} + carla/source/backend/plugin/CarlaPluginVST3.${CARLA_OBJCPP_EXT}) + +set_target_properties(carla-bridge-native PROPERTIES FOLDER plugins OSX_ARCHITECTURES "x86_64;arm64") + +if(OS_MACOS) + set_target_properties_obs(carla-bridge-native) +else() + setup_plugin_target(carla-bridge-native) +endif() diff --git a/plugins/carla/cmake/carla-discovery-native.cmake b/plugins/carla/cmake/carla-discovery-native.cmake new file mode 100644 index 00000000000000..a2c5a63de99283 --- /dev/null +++ b/plugins/carla/cmake/carla-discovery-native.cmake @@ -0,0 +1,35 @@ +add_executable(carla-discovery-native) +mark_as_advanced(carla-discovery-native) + +set(carla_discovery_extra_libs ${carla_pthread_libs}) + +if(OS_MACOS) + find_library(APPKIT AppKit) + mark_as_advanced(APPKIT) + set(carla_discovery_extra_libs ${carla_discovery_extra_libs} ${APPKIT}) +elseif(OS_WINDOWS) + set(carla_discovery_extra_libs ${carla_discovery_extra_libs} ole32 winmm) +elseif(NOT OS_FREEBSD) + set(carla_discovery_extra_libs ${carla_discovery_extra_libs} dl) +endif() + +target_compile_definitions(carla-discovery-native PRIVATE BUILDING_CARLA CARLA_BACKEND_NAMESPACE=CarlaOBS) + +target_compile_options(carla-discovery-native PRIVATE $<$:/wd4244 /wd4267 /wd4273> + $<$>:-Wno-error -Werror=vla>) + +target_include_directories(carla-discovery-native PRIVATE carla/source/backend carla/source/includes + carla/source/modules carla/source/utils) + +target_link_libraries(carla-discovery-native PRIVATE carla::lilv ${carla_discovery_extra_libs}) + +target_sources(carla-discovery-native PRIVATE carla/source/discovery/carla-discovery.${CARLA_OBJCPP_EXT} + carla/source/modules/water/water.files.${CARLA_OBJCPP_EXT}) + +set_target_properties(carla-discovery-native PROPERTIES FOLDER plugins OSX_ARCHITECTURES "x86_64;arm64") + +if(OS_MACOS) + set_target_properties_obs(carla-discovery-native) +else() + setup_plugin_target(carla-discovery-native) +endif() diff --git a/plugins/carla/cmake/jackbridge.cmake b/plugins/carla/cmake/jackbridge.cmake new file mode 100644 index 00000000000000..6be1544a95ccd2 --- /dev/null +++ b/plugins/carla/cmake/jackbridge.cmake @@ -0,0 +1,23 @@ +# base config +set(carla_jackbridge_basedir carla/source/jackbridge) +set(carla_jackbridge_extra_libs ${carla_pthread_libs}) + +if(NOT + (OS_FREEBSD + OR OS_MACOS + OR OS_WINDOWS)) + set(carla_jackbridge_extra_libs ${carla_jackbridge_extra_libs} dl rt) +endif() + +# static lib +add_library(carla-jackbridge STATIC) +mark_as_advanced(carla-jackbridge) + +set_target_properties(carla-jackbridge PROPERTIES OSX_ARCHITECTURES "x86_64;arm64" POSITION_INDEPENDENT_CODE ON) + +target_include_directories(carla-jackbridge PRIVATE carla/source/includes carla/source/utils) + +target_link_libraries(carla-jackbridge PUBLIC ${carla_jackbridge_extra_libs}) + +target_sources(carla-jackbridge PRIVATE ${carla_jackbridge_basedir}/JackBridge1.cpp + ${carla_jackbridge_basedir}/JackBridge2.cpp) diff --git a/plugins/carla/cmake/legacy.cmake b/plugins/carla/cmake/legacy.cmake new file mode 100644 index 00000000000000..5388b20a8502a1 --- /dev/null +++ b/plugins/carla/cmake/legacy.cmake @@ -0,0 +1,116 @@ +project(carla) + +option(ENABLE_CARLA "Enable building OBS with carla plugin host" ON) +if(NOT ENABLE_CARLA) + message(STATUS "OBS: DISABLED carla") + return() +endif() + +# Submodule deps check +if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/carla/source/utils/CarlaUtils.hpp) + obs_status(FATAL_ERROR "carla submodule deps not available.") +endif() + +# Find Qt +find_qt(COMPONENTS Core Widgets) + +# Use pkg-config to find optional deps +find_package(PkgConfig) + +# Find pthread via cmake +if(OS_WINDOWS) + set(carla_pthread_libs OBS::w32-pthreads) +else() + find_package(Threads REQUIRED) + set(carla_pthread_libs ${CMAKE_THREAD_LIBS_INIT}) +endif() + +# Optional: X11 support on freedesktop systems +if(PKGCONFIG_FOUND AND NOT (OS_MACOS OR OS_WINDOWS)) + pkg_check_modules(X11 "x11") +else() + set(X11_FOUND FALSE) +endif() + +# Use *.mm files under macOS, regular *.cpp everywhere else +if(OS_MACOS) + set(CARLA_OBJCPP_EXT "mm") +else() + set(CARLA_OBJCPP_EXT "cpp") +endif() + +# Import extra carla libs +include(cmake/jackbridge.cmake) +add_library(carla::jackbridge ALIAS carla-jackbridge) + +include(cmake/lilv.cmake) +add_library(carla::lilv ALIAS carla-lilv) + +include(cmake/rtmempool.cmake) +add_library(carla::rtmempool ALIAS carla-rtmempool) + +include(cmake/water.cmake) +add_library(carla::water ALIAS carla-water) + +# Setup binary tools +include(cmake/carla-discovery-native.cmake) +include(cmake/carla-bridge-native.cmake) + +# Setup carla-bridge target +add_library(carla-bridge MODULE) +add_library(OBS::carla-bridge ALIAS carla-bridge) + +target_compile_definitions( + carla-bridge + PRIVATE BUILDING_CARLA + BUILDING_CARLA_OBS + CARLA_BACKEND_NAMESPACE=CarlaBridgeOBS + CARLA_FRONTEND_NO_CACHED_PLUGIN_API + CARLA_MODULE_ID="carla-bridge" + CARLA_MODULE_NAME="Carla Bridge" + CARLA_PLUGIN_ONLY_BRIDGE + STATIC_PLUGIN_TARGET) + +target_include_directories( + carla-bridge + PRIVATE carla/source + carla/source/backend + carla/source/frontend + carla/source/frontend/utils + carla/source/includes + carla/source/modules + carla/source/utils) + +# TODO remove carla::water dependency from PluginDiscovery.cpp + +target_link_libraries(carla-bridge PRIVATE carla::jackbridge carla::lilv carla::water OBS::libobs Qt::Core Qt::Widgets) + +if(NOT (OS_MACOS OR OS_WINDOWS)) + target_link_options(carla-bridge PRIVATE -Wl,--no-undefined) +endif() + +target_sources( + carla-bridge + PRIVATE carla.c + carla-bridge.cpp + carla-bridge-wrapper.cpp + common.c + qtutils.cpp + carla/source/backend/utils/Information.cpp + carla/source/backend/utils/PluginDiscovery.cpp + carla/source/frontend/carla_frontend.cpp + carla/source/frontend/pluginlist/pluginlistdialog.cpp + carla/source/frontend/pluginlist/pluginlistrefreshdialog.cpp + carla/source/utils/CarlaBridgeUtils.cpp + carla/source/utils/CarlaMacUtils.${CARLA_OBJCPP_EXT} + carla/source/utils/CarlaPipeUtils.cpp) + +set_target_properties( + carla-bridge + PROPERTIES AUTOMOC ON + AUTOUIC ON + AUTORCC ON + FOLDER plugins + PREFIX "") + +setup_plugin_target(carla-bridge) diff --git a/plugins/carla/cmake/lilv.cmake b/plugins/carla/cmake/lilv.cmake new file mode 100644 index 00000000000000..8feaa84fae65ba --- /dev/null +++ b/plugins/carla/cmake/lilv.cmake @@ -0,0 +1,91 @@ +# base config +if(MSVC) + set(carla_lilv_compile_options /wd4005 /wd4090 /wd4133 /wd4244 /wd4267 /wd4273) +elseif(CLANG) + set(carla_lilv_compile_options -Wno-error -Wno-deprecated-declarations -Wno-implicit-fallthrough + -Wno-incompatible-pointer-types-discards-qualifiers -Wno-unused-parameter) +else() + set(carla_lilv_compile_options + -Wno-error + -Wno-deprecated-declarations + -Wno-discarded-qualifiers + -Wno-format-overflow + -Wno-implicit-fallthrough + -Wno-maybe-uninitialized + -Wno-unused-parameter) +endif() + +set(carla_lilv_basedir carla/source/modules/lilv) + +set(carla_lilv_include_directories carla/source/includes ${carla_lilv_basedir}/config) + +if(NOT + (OS_FREEBSD + OR OS_MACOS + OR OS_WINDOWS)) + set(carla_lilv_extra_libs dl m rt) +endif() + +# serd +add_library(carla-lilv_serd STATIC) + +set_target_properties(carla-lilv_serd PROPERTIES OSX_ARCHITECTURES "x86_64;arm64" POSITION_INDEPENDENT_CODE ON) + +target_compile_options(carla-lilv_serd PRIVATE ${carla_lilv_compile_options}) + +target_include_directories(carla-lilv_serd PRIVATE ${carla_lilv_include_directories} ${carla_lilv_basedir}/serd-0.24.0) + +target_sources(carla-lilv_serd PRIVATE ${carla_lilv_basedir}/serd.c) + +# sord +add_library(carla-lilv_sord STATIC) + +set_target_properties(carla-lilv_sord PROPERTIES OSX_ARCHITECTURES "x86_64;arm64" POSITION_INDEPENDENT_CODE ON) + +target_compile_options( + carla-lilv_sord + PRIVATE ${carla_lilv_compile_options} + # workaround compiler bug, see https://gcc.gnu.org/bugzilla/show_bug.cgi?id=109585 + $<$>:-fno-strict-aliasing>) + +target_include_directories(carla-lilv_sord PRIVATE ${carla_lilv_include_directories} ${carla_lilv_basedir}/sord-0.16.0 + ${carla_lilv_basedir}/sord-0.16.0/src) + +target_link_libraries(carla-lilv_sord PRIVATE carla-lilv_serd) + +target_sources(carla-lilv_sord PRIVATE ${carla_lilv_basedir}/sord.c) + +# sratom +add_library(carla-lilv_sratom STATIC) + +set_target_properties(carla-lilv_sratom PROPERTIES OSX_ARCHITECTURES "x86_64;arm64" POSITION_INDEPENDENT_CODE ON) + +target_compile_options(carla-lilv_sratom PRIVATE ${carla_lilv_compile_options}) + +target_include_directories(carla-lilv_sratom PRIVATE ${carla_lilv_include_directories} + ${carla_lilv_basedir}/sratom-0.6.0) + +target_link_libraries(carla-lilv_sratom PRIVATE carla-lilv_serd) + +target_sources(carla-lilv_sratom PRIVATE ${carla_lilv_basedir}/sratom.c) + +# lilv +add_library(carla-lilv_lilv STATIC) + +set_target_properties(carla-lilv_lilv PROPERTIES OSX_ARCHITECTURES "x86_64;arm64" POSITION_INDEPENDENT_CODE ON) + +target_compile_options(carla-lilv_lilv PRIVATE ${carla_lilv_compile_options}) + +target_include_directories(carla-lilv_lilv PRIVATE ${carla_lilv_include_directories} ${carla_lilv_basedir}/lilv-0.24.0 + ${carla_lilv_basedir}/lilv-0.24.0/src) + +target_link_libraries(carla-lilv_lilv PRIVATE carla-lilv_serd carla-lilv_sord carla-lilv_sratom) + +target_sources(carla-lilv_lilv PRIVATE ${carla_lilv_basedir}/lilv.c) + +# combined target +add_library(carla-lilv INTERFACE) +mark_as_advanced(carla-lilv) + +target_link_libraries(carla-lilv INTERFACE carla-lilv_serd carla-lilv_sord carla-lilv_sratom carla-lilv_lilv + ${carla_lilv_extra_libs}) diff --git a/plugins/carla/cmake/macos/Info.plist.in b/plugins/carla/cmake/macos/Info.plist.in new file mode 100644 index 00000000000000..c2d597444a48cd --- /dev/null +++ b/plugins/carla/cmake/macos/Info.plist.in @@ -0,0 +1,28 @@ + + + + + CFBundleName + obs-carla + CFBundleIdentifier + com.obsproject.carla-bridge + CFBundleVersion + ${MACOSX_BUNDLE_BUNDLE_VERSION} + CFBundleShortVersionString + ${MACOSX_BUNDLE_SHORT_VERSION_STRING} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleExecutable + carla-bridge + CFBundlePackageType + BNDL + CFBundleSupportedPlatforms + + MacOSX + + LSMinimumSystemVersion + ${CMAKE_OSX_DEPLOYMENT_TARGET} + NSHumanReadableCopyright + (c) 2023 Filipe Coelho + + diff --git a/plugins/carla/cmake/rtmempool.cmake b/plugins/carla/cmake/rtmempool.cmake new file mode 100644 index 00000000000000..c74b7d96e2e480 --- /dev/null +++ b/plugins/carla/cmake/rtmempool.cmake @@ -0,0 +1,15 @@ +# base config +set(carla_rtmempool_basedir carla/source/modules/rtmempool) +set(carla_rtmempool_extra_libs ${carla_pthread_libs}) + +# static lib +add_library(carla-rtmempool STATIC) +mark_as_advanced(carla-rtmempool) + +set_target_properties(carla-rtmempool PROPERTIES OSX_ARCHITECTURES "x86_64;arm64" POSITION_INDEPENDENT_CODE ON) + +target_include_directories(carla-rtmempool PRIVATE carla/source/includes) + +target_link_libraries(carla-rtmempool PUBLIC ${carla_rtmempool_extra_libs}) + +target_sources(carla-rtmempool PRIVATE ${carla_rtmempool_basedir}/rtmempool.c) diff --git a/plugins/carla/cmake/water.cmake b/plugins/carla/cmake/water.cmake new file mode 100644 index 00000000000000..6413afd726f1bf --- /dev/null +++ b/plugins/carla/cmake/water.cmake @@ -0,0 +1,48 @@ +# base config +set(carla_water_basedir carla/source/modules/water) +set(carla_water_extra_libs ${carla_pthread_libs}) + +if(OS_MACOS) + find_library(APPKIT AppKit) + mark_as_advanced(APPKIT) + set(carla_water_extra_libs ${carla_jackbridge_extra_libs} ${APPKIT}) +elseif(OS_WINDOWS) + set(carla_water_extra_libs + ${carla_jackbridge_extra_libs} + "uuid" + "wsock32" + "wininet" + "version" + "ole32" + "ws2_32" + "oleaut32" + "imm32" + "comdlg32" + "shlwapi" + "rpcrt4" + "winmm") +elseif(NOT OS_FREEBSD) + set(carla_water_extra_libs ${carla_jackbridge_extra_libs} dl rt) +endif() + +# static lib +add_library(carla-water STATIC) +mark_as_advanced(carla-water) + +set_target_properties(carla-water PROPERTIES OSX_ARCHITECTURES "x86_64;arm64") + +if(NOT OS_WINDOWS) + set_property(TARGET carla-water PROPERTY POSITION_INDEPENDENT_CODE ON) +endif() + +if(MSVC) + target_compile_options(carla-water PRIVATE /wd4267) +else() + target_compile_options(carla-water PRIVATE -Wno-error=deprecated-copy) +endif() + +target_include_directories(carla-water PRIVATE carla/source/includes carla/source/utils) + +target_link_libraries(carla-water PUBLIC ${carla_water_extra_libs}) + +target_sources(carla-water PRIVATE ${carla_water_basedir}/water.obs.${CARLA_OBJCPP_EXT}) diff --git a/plugins/carla/common.c b/plugins/carla/common.c new file mode 100644 index 00000000000000..55e34dadca66f5 --- /dev/null +++ b/plugins/carla/common.c @@ -0,0 +1,185 @@ +/* + * Carla plugin for OBS + * Copyright (C) 2023 Filipe Coelho + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +// needed for libdl stuff +#if !defined(_GNU_SOURCE) && !defined(_WIN32) +#define _GNU_SOURCE +#endif + +#include "common.h" + +#include +#include + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#include +#endif + +#ifdef __APPLE__ +// we assume running as bundle on macOS +bool is_in_bundle() +{ + return true; +} +#endif + +// ---------------------------------------------------------------------------- + +static char *module_path = NULL; + +#ifdef _WIN32 +static HINSTANCE module_handle = NULL; + +BOOL WINAPI DllMain(HINSTANCE hinst_dll, DWORD reason, LPVOID reserved) +{ + switch (reason) { + case DLL_PROCESS_ATTACH: + module_handle = hinst_dll; +#ifdef PTW32_STATIC_LIB + pthread_win32_process_attach_np(); +#endif + break; + case DLL_PROCESS_DETACH: +#ifdef PTW32_STATIC_LIB + pthread_win32_process_detach_np(); +#endif + break; + case DLL_THREAD_ATTACH: +#ifdef PTW32_STATIC_LIB + pthread_win32_thread_attach_np(); +#endif + break; + case DLL_THREAD_DETACH: +#ifdef PTW32_STATIC_LIB + pthread_win32_thread_detach_np(); +#endif + break; + } + + UNUSED_PARAMETER(reserved); + return true; +} +#endif + +const char *get_carla_bin_path(void) +{ + if (module_path != NULL) + return module_path; + + char *mpath; +#ifdef _WIN32 + wchar_t path_utf16[MAX_PATH]; + GetModuleFileNameW(module_handle, path_utf16, MAX_PATH); + os_wcs_to_utf8_ptr(path_utf16, 0, &mpath); +#else + Dl_info info; + dladdr(get_carla_bin_path, &info); + mpath = realpath(info.dli_fname, NULL); +#endif + + if (mpath == NULL) + goto fail; + + // truncate to last separator + char *lastsep = strrchr(mpath, '/'); + if (lastsep == NULL) + goto free; + *lastsep = '\0'; + +#ifdef __APPLE__ + // if running as app bundle, use its binary dir + if (is_in_bundle()) { + char *appbundlesep = strstr( + mpath, "/PlugIns/carla-bridge.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); + +fail: + 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_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_erase(settings, pname); + obs_data_unset_default_value(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_t *sighandler = + obs_source_get_signal_handler(source); + signal_handler_signal(sighandler, "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 00000000000000..42659702fee16a --- /dev/null +++ b/plugins/carla/common.h @@ -0,0 +1,47 @@ +/* + * Carla plugin for OBS + * Copyright (C) 2023 Filipe Coelho + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#include + +#define MAX_PARAMS 100 + +#define PARAM_NAME_SIZE 5 +#define PARAM_NAME_INIT \ + { \ + 'p', '0', '0', '0', '\0' \ + } + +// property names +#define PROP_LOAD_FILE "load-file" +#define PROP_SELECT_PLUGIN "select-plugin" +#define PROP_RELOAD_PLUGIN "reload" +#define PROP_BUFFER_SIZE "buffer-size" +#define PROP_SHOW_GUI "show-gui" + +#define PROP_CHUNK "chunk" +#define PROP_CUSTOM_DATA "customdata" + +// ---------------------------------------------------------------------------- + +#ifdef __cplusplus +extern "C" { +#endif + +const char *get_carla_bin_path(void); + +void param_index_to_name(uint32_t index, char name[PARAM_NAME_SIZE]); +void remove_all_props(obs_properties_t *props, obs_data_t *settings); + +void postpone_update_request(uint64_t *update_req); +void handle_update_request(obs_source_t *source, uint64_t *update_req); + +#ifdef __cplusplus +} +#endif + +// ---------------------------------------------------------------------------- diff --git a/plugins/carla/qtutils.cpp b/plugins/carla/qtutils.cpp new file mode 100644 index 00000000000000..bf2472b6134b57 --- /dev/null +++ b/plugins/carla/qtutils.cpp @@ -0,0 +1,41 @@ +/* + * Carla plugin for OBS + * Copyright (C) 2023 Filipe Coelho + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "qtutils.h" + +#include +#include + +const 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.constData(); +} + +QMainWindow *carla_qt_get_main_window(void) +{ + for (QWidget *w : QApplication::topLevelWidgets()) { + if (QMainWindow *mw = qobject_cast(w)) + return mw; + } + + return nullptr; +} diff --git a/plugins/carla/qtutils.h b/plugins/carla/qtutils.h new file mode 100644 index 00000000000000..a51ca7a3371304 --- /dev/null +++ b/plugins/carla/qtutils.h @@ -0,0 +1,25 @@ +/* + * Carla plugin for OBS + * Copyright (C) 2023 Filipe Coelho + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#ifdef __cplusplus +#include +#include +extern "C" { +#else +#include +typedef struct QMainWindow QMainWindow; +#endif + +const char *carla_qt_file_dialog(bool save, bool isDir, const char *title, + const char *filter); + +QMainWindow *carla_qt_get_main_window(void); + +#ifdef __cplusplus +} +#endif