From 7321be60c6c523cc755b0979571a0a6a5e0c7085 Mon Sep 17 00:00:00 2001 From: falkTX Date: Thu, 11 May 2023 04:30:04 +0200 Subject: [PATCH] Add auto-scanning plugin API, with initial implementation Signed-off-by: falkTX --- source/backend/CarlaUtils.h | 185 ++++++++- source/backend/utils/Makefile | 1 + source/backend/utils/PluginDiscovery.cpp | 458 +++++++++++++++++++++++ source/plugin/carla-host-plugin.cpp | 1 + 4 files changed, 635 insertions(+), 10 deletions(-) create mode 100644 source/backend/utils/PluginDiscovery.cpp diff --git a/source/backend/CarlaUtils.h b/source/backend/CarlaUtils.h index 84afd346b..b6218c0ff 100644 --- a/source/backend/CarlaUtils.h +++ b/source/backend/CarlaUtils.h @@ -21,6 +21,7 @@ #include "CarlaBackend.h" #ifdef __cplusplus +using CARLA_BACKEND_NAMESPACE::BinaryType; using CARLA_BACKEND_NAMESPACE::PluginCategory; using CARLA_BACKEND_NAMESPACE::PluginType; #endif @@ -34,16 +35,6 @@ using CARLA_BACKEND_NAMESPACE::PluginType; * @{ */ -/*! - * TODO. - */ -typedef void* CarlaPipeClientHandle; - -/*! - * TODO. - */ -typedef void (*CarlaPipeCallbackFunc)(void* ptr, const char* msg); - /*! * Information about a cached plugin. * @see carla_get_cached_plugin_info() @@ -138,6 +129,170 @@ typedef struct _CarlaCachedPluginInfo { } CarlaCachedPluginInfo; +/* -------------------------------------------------------------------------------------------------------------------- + * plugin discovery */ + +typedef void* CarlaPluginDiscoveryHandle; + +/*! + * TODO. + */ +typedef struct _CarlaPluginDiscoveryMetadata { + /*! + * Plugin name. + */ + const char* name; + + /*! + * Plugin author/maker. + */ + const char* maker; + + /*! + * Plugin category. + */ + PluginCategory category; + + /*! + * Plugin hints. + * @see PluginHints + */ + uint hints; + +#ifdef __cplusplus + /*! + * C++ constructor. + */ + CARLA_API _CarlaPluginDiscoveryMetadata() noexcept; + CARLA_DECLARE_NON_COPYABLE(_CarlaPluginDiscoveryMetadata) +#endif + +} CarlaPluginDiscoveryMetadata; + +/*! + * TODO. + */ +typedef struct _CarlaPluginDiscoveryIO { + /*! + * Number of audio inputs. + */ + uint32_t audioIns; + + /*! + * Number of audio outputs. + */ + uint32_t audioOuts; + + /*! + * Number of CV inputs. + */ + uint32_t cvIns; + + /*! + * Number of CV outputs. + */ + uint32_t cvOuts; + + /*! + * Number of MIDI inputs. + */ + uint32_t midiIns; + + /*! + * Number of MIDI outputs. + */ + uint32_t midiOuts; + + /*! + * Number of input parameters. + */ + uint32_t parameterIns; + + /*! + * Number of output parameters. + */ + uint32_t parameterOuts; + +#ifdef __cplusplus + /*! + * C++ constructor. + */ + CARLA_API _CarlaPluginDiscoveryIO() noexcept; + CARLA_DECLARE_NON_COPYABLE(_CarlaPluginDiscoveryIO) +#endif + +} CarlaPluginDiscoveryIO; + +/*! + * TODO. + */ +typedef struct _CarlaPluginDiscoveryInfo { + /*! + * Binary type. + */ + BinaryType btype; + + /*! + * Plugin type. + */ + PluginType ptype; + + /*! + * Plugin filename. + */ + const char* filename; + + /*! + * Plugin label/URI/Id. + */ + const char* label; + + /*! + * Plugin unique Id. + */ + uint64_t uniqueId; + + /*! + * Extra information, not required for load plugins. + */ + CarlaPluginDiscoveryMetadata metadata; + + /*! + * Extra information, not required for load plugins. + */ + CarlaPluginDiscoveryIO io; + +#ifdef __cplusplus + /*! + * C++ constructor. + */ + CARLA_API _CarlaPluginDiscoveryInfo() noexcept; + CARLA_DECLARE_NON_COPYABLE(_CarlaPluginDiscoveryInfo) +#endif + +} CarlaPluginDiscoveryInfo; + +/*! + * TODO. + */ +typedef void (*CarlaPluginDiscoveryCallback)(void* ptr, const CarlaPluginDiscoveryInfo* info); + +/*! + */ +CARLA_PLUGIN_EXPORT CarlaPluginDiscoveryHandle carla_plugin_discovery_start(const char* discoveryTool, + PluginType ptype, + const char* pluginPath, + CarlaPluginDiscoveryCallback callback, + void* callbackPtr); + +/*! + */ +CARLA_PLUGIN_EXPORT bool carla_plugin_discovery_idle(CarlaPluginDiscoveryHandle handle); + +/*! + */ +CARLA_PLUGIN_EXPORT void carla_plugin_discovery_stop(CarlaPluginDiscoveryHandle handle); + /* -------------------------------------------------------------------------------------------------------------------- * cached plugins */ @@ -222,6 +377,16 @@ CARLA_PLUGIN_EXPORT void carla_juce_cleanup(void); /* -------------------------------------------------------------------------------------------------------------------- * pipes */ +/*! + * TODO. + */ +typedef void* CarlaPipeClientHandle; + +/*! + * TODO. + */ +typedef void (*CarlaPipeCallbackFunc)(void* ptr, const char* msg); + /*! * TODO. */ diff --git a/source/backend/utils/Makefile b/source/backend/utils/Makefile index 5cb2fc26b..d19b008c2 100644 --- a/source/backend/utils/Makefile +++ b/source/backend/utils/Makefile @@ -19,6 +19,7 @@ OBJS = \ $(OBJDIR)/Information.cpp.o \ $(OBJDIR)/JUCE.cpp.o \ $(OBJDIR)/PipeClient.cpp.o \ + $(OBJDIR)/PluginDiscovery.cpp.o \ $(OBJDIR)/System.cpp.o \ $(OBJDIR)/Windows.cpp.o diff --git a/source/backend/utils/PluginDiscovery.cpp b/source/backend/utils/PluginDiscovery.cpp new file mode 100644 index 000000000..97717e8aa --- /dev/null +++ b/source/backend/utils/PluginDiscovery.cpp @@ -0,0 +1,458 @@ +/* + * Carla Plugin Host + * Copyright (C) 2011-2023 Filipe Coelho + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * For a full copy of the GNU General Public License see the doc/GPL.txt file. + */ + +#include "CarlaUtils.h" + +#include "CarlaBackendUtils.hpp" +#include "CarlaJuceUtils.hpp" +#include "CarlaPipeUtils.hpp" + +#include "water/files/File.h" +#include "water/threads/ChildProcess.h" +#include "water/text/StringArray.h" + +namespace CB = CARLA_BACKEND_NAMESPACE; + +// -------------------------------------------------------------------------------------------------------------------- + +static const char* const gPluginsDiscoveryNullCharPtr = ""; + +_CarlaPluginDiscoveryMetadata::_CarlaPluginDiscoveryMetadata() noexcept + : name(gPluginsDiscoveryNullCharPtr), + maker(gPluginsDiscoveryNullCharPtr), + category(CB::PLUGIN_CATEGORY_NONE), + hints(0x0) {} + +_CarlaPluginDiscoveryIO::_CarlaPluginDiscoveryIO() noexcept + : audioIns(0), + audioOuts(0), + cvIns(0), + cvOuts(0), + midiIns(0), + midiOuts(0), + parameterIns(0), + parameterOuts(0) {} + +_CarlaPluginDiscoveryInfo::_CarlaPluginDiscoveryInfo() noexcept + : btype(CB::BINARY_NONE), + ptype(CB::PLUGIN_NONE), + filename(gPluginsDiscoveryNullCharPtr), + label(gPluginsDiscoveryNullCharPtr), + uniqueId(0), + metadata() {} + +// -------------------------------------------------------------------------------------------------------------------- + +class CarlaPluginDiscovery : private CarlaPipeServer +{ +public: + CarlaPluginDiscovery(const char* const discoveryTool, + const PluginType ptype, + const std::vector&& binaries, + const CarlaPluginDiscoveryCallback callback, + void* const callbackPtr) + : fPluginType(ptype), + fCallback(callback), + fCallbackPtr(callbackPtr), + fBinaryIndex(0), + fBinaryCount(binaries.size()), + fBinaries(binaries), + fDiscoveryTool(discoveryTool), + nextLabel(nullptr), + nextMaker(nullptr), + nextName(nullptr) + { + startPipeServer(discoveryTool, getPluginTypeAsString(fPluginType), fBinaries[0].getFullPathName().toRawUTF8()); + } + + CarlaPluginDiscovery(const char* const discoveryTool, + const PluginType ptype, + const CarlaPluginDiscoveryCallback callback, + void* const callbackPtr) + : fPluginType(ptype), + fCallback(callback), + fCallbackPtr(callbackPtr), + fBinaryIndex(0), + fBinaryCount(1), + nextLabel(nullptr), + nextMaker(nullptr), + nextName(nullptr) + { + startPipeServer(discoveryTool, getPluginTypeAsString(fPluginType), ":all"); + } + + ~CarlaPluginDiscovery() + { + std::free(nextLabel); + std::free(nextMaker); + std::free(nextName); + } + + // closePipeServer() + + bool idle() + { + if (isPipeRunning()) + { + idlePipe(); + return true; + } + + if (++fBinaryIndex == fBinaryCount) + return false; + + startPipeServer(fDiscoveryTool, + getPluginTypeAsString(fPluginType), + fBinaries[fBinaryIndex].getFullPathName().toRawUTF8()); + + return true; + } + +protected: + bool msgReceived(const char* const msg) noexcept + { + if (std::strcmp(msg, "warning") == 0 || std::strcmp(msg, "error") == 0) + { + const char* text = nullptr; + readNextLineAsString(text, false); + carla_stdout("discovery: %s", text); + return true; + } + + if (std::strcmp(msg, "init") == 0) + { + const char* _; + readNextLineAsString(_, false); + new (&nextInfo) _CarlaPluginDiscoveryInfo(); + return true; + } + + if (std::strcmp(msg, "end") == 0) + { + const char* _; + readNextLineAsString(_, false); + + if (nextInfo.label == nullptr) + nextInfo.label = gPluginsDiscoveryNullCharPtr; + + if (nextInfo.metadata.maker == nullptr) + nextInfo.metadata.maker = gPluginsDiscoveryNullCharPtr; + + if (nextInfo.metadata.name == nullptr) + nextInfo.metadata.name = gPluginsDiscoveryNullCharPtr; + + if (fDiscoveryTool.isEmpty()) + { + char* filename = nullptr; + + if (fPluginType == CB::PLUGIN_LV2) + { + do { + const char* const slash = std::strchr(nextLabel, CARLA_OS_SEP); + CARLA_SAFE_ASSERT_BREAK(slash != nullptr); + filename = strdup(nextLabel); + filename[slash - nextLabel] = '\0'; + nextInfo.filename = filename; + nextInfo.label = slash + 1; + } while (false); + } + + fCallback(fCallbackPtr, &nextInfo); + + std::free(filename); + } + else + { + const water::String filename(fBinaries[fBinaryIndex].getFullPathName()); + nextInfo.filename = filename.toRawUTF8(); + fCallback(fCallbackPtr, &nextInfo); + } + + std::free(nextLabel); + nextLabel = nullptr; + + std::free(nextMaker); + nextMaker = nullptr; + + std::free(nextName); + nextName = nullptr; + + return true; + } + + if (std::strcmp(msg, "build") == 0) + { + uint8_t btype = 0; + readNextLineAsByte(btype); + nextInfo.btype = static_cast(btype); + return true; + } + + if (std::strcmp(msg, "hints") == 0) + { + readNextLineAsUInt(nextInfo.metadata.hints); + return true; + } + + if (std::strcmp(msg, "category") == 0) + { + const char* category = nullptr; + readNextLineAsString(category, false); + nextInfo.metadata.category = getPluginCategoryFromString(category); + return true; + } + + if (std::strcmp(msg, "name") == 0) + { + nextInfo.metadata.name = nextName = readNextLineAsString(); + return true; + } + + if (std::strcmp(msg, "label") == 0) + { + nextInfo.label = nextLabel = readNextLineAsString(); + return true; + } + + if (std::strcmp(msg, "maker") == 0) + { + nextInfo.metadata.maker = nextMaker = readNextLineAsString(); + return true; + } + + if (std::strcmp(msg, "uniqueId") == 0) + { + readNextLineAsULong(nextInfo.uniqueId); + return true; + } + + if (std::strcmp(msg, "audio.ins") == 0) + { + readNextLineAsUInt(nextInfo.io.audioIns); + return true; + } + + if (std::strcmp(msg, "audio.outs") == 0) + { + readNextLineAsUInt(nextInfo.io.audioOuts); + return true; + } + + if (std::strcmp(msg, "cv.ins") == 0) + { + readNextLineAsUInt(nextInfo.io.cvIns); + return true; + } + + if (std::strcmp(msg, "cv.outs") == 0) + { + readNextLineAsUInt(nextInfo.io.cvOuts); + return true; + } + + if (std::strcmp(msg, "midi.ins") == 0) + { + readNextLineAsUInt(nextInfo.io.midiIns); + return true; + } + + if (std::strcmp(msg, "midi.outs") == 0) + { + readNextLineAsUInt(nextInfo.io.midiOuts); + return true; + } + + if (std::strcmp(msg, "parameters.ins") == 0) + { + readNextLineAsUInt(nextInfo.io.parameterIns); + return true; + } + + if (std::strcmp(msg, "parameters.outs") == 0) + { + readNextLineAsUInt(nextInfo.io.parameterOuts); + return true; + } + + if (std::strcmp(msg, "exiting") == 0) + { + stopPipeServer(1000); + return true; + } + + carla_stdout("discovery: unknown message '%s' received", msg); + return true; + } + +private: + const PluginType fPluginType; + const CarlaPluginDiscoveryCallback fCallback; + void* const fCallbackPtr; + + uint fBinaryIndex; + const uint fBinaryCount; + const std::vector fBinaries; + const CarlaString fDiscoveryTool; + + CarlaPluginDiscoveryInfo nextInfo; + char* nextLabel; + char* nextMaker; + char* nextName; + + CARLA_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(CarlaPluginDiscovery) +}; + +// -------------------------------------------------------------------------------------------------------------------- + +static std::vector findBinaries(const char* const pluginPath, const char* const wildcard) +{ + CARLA_SAFE_ASSERT_RETURN(pluginPath != nullptr, {}); + + if (pluginPath[0] == '\0') + return {}; + + using water::File; + using water::String; + using water::StringArray; + + const StringArray splitPaths(StringArray::fromTokens(pluginPath, CARLA_OS_SPLIT_STR, "")); + + if (splitPaths.size() == 0) + return {}; + + std::vector ret; + + for (String *it = splitPaths.begin(), *end = splitPaths.end(); it != end; ++it) + { + const File dir(*it); + std::vector results; + + if (dir.findChildFiles(results, File::findFiles|File::ignoreHiddenFiles, true, wildcard) > 0) + { + ret.reserve(ret.size() + results.size()); + ret.insert(ret.end(), results.begin(), results.end()); + } + } + + return ret; +} + +static std::vector findVST3s(const char* const pluginPath) +{ + CARLA_SAFE_ASSERT_RETURN(pluginPath != nullptr, {}); + + if (pluginPath[0] == '\0') + return {}; + + using water::File; + using water::String; + using water::StringArray; + + const StringArray splitPaths(StringArray::fromTokens(pluginPath, CARLA_OS_SPLIT_STR, "")); + + if (splitPaths.size() == 0) + return {}; + + std::vector ret; + + for (String *it = splitPaths.begin(), *end = splitPaths.end(); it != end; ++it) + { + const File dir(*it); + std::vector results; + + if (dir.findChildFiles(results, File::findDirectories|File::findFiles|File::ignoreHiddenFiles, true, "*.vst3") > 0) + { + ret.reserve(ret.size() + results.size()); + ret.insert(ret.end(), results.begin(), results.end()); + } + } + + return ret; +} + +CarlaPluginDiscoveryHandle carla_plugin_discovery_start(const char* const discoveryTool, + const PluginType ptype, + const char* const pluginPath, + const CarlaPluginDiscoveryCallback callback, + void* const callbackPtr) +{ + CARLA_SAFE_ASSERT_RETURN(discoveryTool != nullptr && discoveryTool[0] != '\0', nullptr); + CARLA_SAFE_ASSERT_RETURN(callback != nullptr, nullptr); + + const char* wildcard = nullptr; + + switch (ptype) + { + case CB::PLUGIN_NONE: + case CB::PLUGIN_JACK: + case CB::PLUGIN_TYPE_COUNT: + return nullptr; + + case CB::PLUGIN_SFZ: + case CB::PLUGIN_JSFX: + { + const CarlaScopedEnvVar csev("CARLA_DISCOVERY_PATH", pluginPath); + return new CarlaPluginDiscovery(discoveryTool, ptype, callback, callbackPtr); + } + + case CB::PLUGIN_INTERNAL: + case CB::PLUGIN_LV2: + case CB::PLUGIN_AU: + return new CarlaPluginDiscovery(discoveryTool, ptype, callback, callbackPtr); + + case CB::PLUGIN_LADSPA: + case CB::PLUGIN_DSSI: + case CB::PLUGIN_VST2: + wildcard = "*.so"; + break; + case CB::PLUGIN_VST3: + // handled separately + break; + case CB::PLUGIN_CLAP: + wildcard = "*.clap"; + break; + case CB::PLUGIN_DLS: + wildcard = "*.dls"; + break; + case CB::PLUGIN_GIG: + wildcard = "*.gig"; + break; + case CB::PLUGIN_SF2: + wildcard = "*.sf2"; + break; + } + + const std::vector binaries(ptype == CB::PLUGIN_VST3 ? findVST3s(pluginPath) + : findBinaries(pluginPath, wildcard)); + + if (binaries.size() == 0) + return nullptr; + + return new CarlaPluginDiscovery(discoveryTool, ptype, std::move(binaries), callback, callbackPtr); +} + +bool carla_plugin_discovery_idle(CarlaPluginDiscoveryHandle handle) +{ + return static_cast(handle)->idle(); +} + +void carla_plugin_discovery_stop(CarlaPluginDiscoveryHandle handle) +{ + delete static_cast(handle); +} + +// -------------------------------------------------------------------------------------------------------------------- diff --git a/source/plugin/carla-host-plugin.cpp b/source/plugin/carla-host-plugin.cpp index 5bc748582..c341ab7bc 100644 --- a/source/plugin/carla-host-plugin.cpp +++ b/source/plugin/carla-host-plugin.cpp @@ -28,6 +28,7 @@ #include "utils/Information.cpp" #include "utils/JUCE.cpp" #include "utils/PipeClient.cpp" +#include "utils/PluginDiscovery.cpp" #include "utils/System.cpp" #include "utils/Windows.cpp"