/* * Carla Plugin Host * Copyright (C) 2011-2022 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 "CarlaNative.h" #include "CarlaString.hpp" #include "CarlaBackendUtils.hpp" #include "CarlaLv2Utils.hpp" #include "CarlaJsfxUtils.hpp" #if defined(USING_JUCE) && defined(CARLA_OS_MAC) # include "AppConfig.h" # include "juce_audio_processors/juce_audio_processors.h" #endif #include "water/containers/Array.h" #include "water/files/File.h" namespace CB = CARLA_BACKEND_NAMESPACE; // ------------------------------------------------------------------------------------------------------------------- static const char* const gCachedPluginsNullCharPtr = ""; static bool isCachedPluginType(const CB::PluginType ptype) { switch (ptype) { case CB::PLUGIN_INTERNAL: case CB::PLUGIN_LV2: case CB::PLUGIN_AU: case CB::PLUGIN_SFZ: case CB::PLUGIN_JSFX: return true; default: return false; } } // ------------------------------------------------------------------------------------------------------------------- _CarlaCachedPluginInfo::_CarlaCachedPluginInfo() noexcept : valid(false), category(CB::PLUGIN_CATEGORY_NONE), hints(0x0), audioIns(0), audioOuts(0), cvIns(0), cvOuts(0), midiIns(0), midiOuts(0), parameterIns(0), parameterOuts(0), name(gCachedPluginsNullCharPtr), label(gCachedPluginsNullCharPtr), maker(gCachedPluginsNullCharPtr), copyright(gCachedPluginsNullCharPtr) {} // ------------------------------------------------------------------------------------------------------------------- static std::vector gSFZs; static void findSFZs(const char* const sfzPaths) { gSFZs.clear(); CARLA_SAFE_ASSERT_RETURN(sfzPaths != nullptr,); if (sfzPaths[0] == '\0') return; const water::StringArray splitPaths(water::StringArray::fromTokens(sfzPaths, CARLA_OS_SPLIT_STR, "")); for (water::String *it = splitPaths.begin(), *end = splitPaths.end(); it != end; ++it) { std::vector results; if (water::File(*it).findChildFiles(results, water::File::findFiles|water::File::ignoreHiddenFiles, true, "*.sfz") > 0) { gSFZs.reserve(gSFZs.size() + results.size()); gSFZs.insert(gSFZs.end(), results.begin(), results.end()); } } } // ------------------------------------------------------------------------------------------------------------------- static std::vector gJSFXs; static void findJSFXs(const char* const jsfxPaths) { gJSFXs.clear(); CARLA_SAFE_ASSERT_RETURN(jsfxPaths != nullptr,); if (jsfxPaths[0] == '\0') return; const water::StringArray splitPaths(water::StringArray::fromTokens(jsfxPaths, CARLA_OS_SPLIT_STR, "")); for (water::String *it = splitPaths.begin(), *end = splitPaths.end(); it != end; ++it) { std::vector results; const water::File path(*it); if (path.findChildFiles(results, water::File::findFiles|water::File::ignoreHiddenFiles, true, "*") > 0) { gJSFXs.reserve(gJSFXs.size() + results.size()); for (std::vector::iterator it2=results.begin(), end2=results.end(); it2 != end2; ++it2) { const water::File& file(*it2); const water::String fileExt = file.getFileExtension(); if (fileExt.isEmpty() || fileExt.equalsIgnoreCase(".jsfx")) gJSFXs.push_back(CB::CarlaJsfxUnit(path, file)); } } } } // ------------------------------------------------------------------------------------------------------------------- static const CarlaCachedPluginInfo* get_cached_plugin_internal(const NativePluginDescriptor& desc) { static CarlaCachedPluginInfo info; info.category = static_cast(desc.category); info.hints = 0x0; if (desc.hints & NATIVE_PLUGIN_IS_RTSAFE) info.hints |= CB::PLUGIN_IS_RTSAFE; if (desc.hints & NATIVE_PLUGIN_IS_SYNTH) info.hints |= CB::PLUGIN_IS_SYNTH; if (desc.hints & NATIVE_PLUGIN_HAS_UI) info.hints |= CB::PLUGIN_HAS_CUSTOM_UI; if (desc.hints & NATIVE_PLUGIN_HAS_INLINE_DISPLAY) info.hints |= CB::PLUGIN_HAS_INLINE_DISPLAY; if (desc.hints & NATIVE_PLUGIN_NEEDS_FIXED_BUFFERS) info.hints |= CB::PLUGIN_NEEDS_FIXED_BUFFERS; if (desc.hints & NATIVE_PLUGIN_NEEDS_UI_MAIN_THREAD) info.hints |= CB::PLUGIN_NEEDS_UI_MAIN_THREAD; if (desc.hints & NATIVE_PLUGIN_USES_MULTI_PROGS) info.hints |= CB::PLUGIN_USES_MULTI_PROGS; info.valid = true; info.audioIns = desc.audioIns; info.audioOuts = desc.audioOuts; info.cvIns = desc.cvIns; info.cvOuts = desc.cvOuts; info.midiIns = desc.midiIns; info.midiOuts = desc.midiOuts; info.parameterIns = desc.paramIns; info.parameterOuts = desc.paramOuts; info.name = desc.name; info.label = desc.label; info.maker = desc.maker; info.copyright = desc.copyright; return &info; } // ------------------------------------------------------------------------------------------------------------------- static const CarlaCachedPluginInfo* get_cached_plugin_lv2(Lv2WorldClass& lv2World, Lilv::Plugin& lilvPlugin) { static CarlaCachedPluginInfo info; info.valid = false; bool supported = true; // ---------------------------------------------------------------------------------------------------------------- // text data { static CarlaString suri, sname, smaker, slicense; suri.clear(); sname.clear(); smaker.clear(); slicense.clear(); suri = lilvPlugin.get_uri().as_uri(); if (char* const bundle = lilv_file_uri_parse(lilvPlugin.get_bundle_uri().as_uri(), nullptr)) { water::File fbundle(bundle); lilv_free(bundle); suri = (fbundle.getFileName() + CARLA_OS_SEP).toRawUTF8() + suri; } else { suri = CARLA_OS_SEP_STR + suri; } #if 0 // def HAVE_FLUIDSYNTH // If we have fluidsynth support built-in, loading these plugins will lead to issues if (suri == "urn:ardour:a-fluidsynth") return &info; if (suri == "http://calf.sourceforge.net/plugins/Fluidsynth") return &info; #endif if (LilvNode* const nameNode = lilv_plugin_get_name(lilvPlugin.me)) { if (const char* const name = lilv_node_as_string(nameNode)) sname = name; lilv_node_free(nameNode); } if (LilvNode* const authorNode = lilv_plugin_get_author_name(lilvPlugin.me)) { if (const char* const author = lilv_node_as_string(authorNode)) smaker = author; lilv_node_free(authorNode); } Lilv::Nodes licenseNodes(lilvPlugin.get_value(lv2World.doap_license)); if (licenseNodes.size() > 0) { if (LilvNode* const licenseNode = lilv_nodes_get_first(licenseNodes.me)) { if (const char* const license = lilv_node_as_string(licenseNode)) slicense = license; // lilv_node_free(licenseNode); } } lilv_nodes_free(const_cast(licenseNodes.me)); info.name = sname.buffer(); info.label = suri.buffer(); info.maker = smaker.buffer(); info.copyright = slicense.buffer(); } // ---------------------------------------------------------------------------------------------------------------- // features info.hints = 0x0; { Lilv::UIs lilvUIs(lilvPlugin.get_uis()); if (lilvUIs.size() > 0) info.hints |= CB::PLUGIN_HAS_CUSTOM_UI; #ifdef CARLA_OS_LINUX else if (lilvPlugin.get_modgui_resources_directory().as_uri() != nullptr) info.hints |= CB::PLUGIN_HAS_CUSTOM_UI; #endif lilv_nodes_free(const_cast(lilvUIs.me)); } { Lilv::Nodes lilvRequiredFeatureNodes(lilvPlugin.get_required_features()); LILV_FOREACH(nodes, it, lilvRequiredFeatureNodes) { Lilv::Node lilvFeatureNode(lilvRequiredFeatureNodes.get(it)); const char* const featureURI(lilvFeatureNode.as_uri()); CARLA_SAFE_ASSERT_CONTINUE(featureURI != nullptr); if (! is_lv2_feature_supported(featureURI)) { if (std::strcmp(featureURI, LV2_DATA_ACCESS_URI) == 0 || std::strcmp(featureURI, LV2_INSTANCE_ACCESS_URI) == 0) { // we give a warning about this below continue; } supported = false; carla_stderr("LV2 plugin '%s' requires unsupported feature '%s'", info.label, featureURI); } } lilv_nodes_free(const_cast(lilvRequiredFeatureNodes.me)); } { Lilv::Nodes lilvSupportedFeatureNodes(lilvPlugin.get_supported_features()); LILV_FOREACH(nodes, it, lilvSupportedFeatureNodes) { Lilv::Node lilvFeatureNode(lilvSupportedFeatureNodes.get(it)); const char* const featureURI(lilvFeatureNode.as_uri()); CARLA_SAFE_ASSERT_CONTINUE(featureURI != nullptr); /**/ if (std::strcmp(featureURI, LV2_CORE__hardRTCapable) == 0) { info.hints |= CB::PLUGIN_IS_RTSAFE; } else if (std::strcmp(featureURI, LV2_INLINEDISPLAY__queue_draw) == 0) { info.hints |= CB::PLUGIN_HAS_INLINE_DISPLAY; } else if (std::strcmp(featureURI, LV2_DATA_ACCESS_URI) == 0 || std::strcmp(featureURI, LV2_INSTANCE_ACCESS_URI) == 0) { carla_stderr("LV2 plugin '%s' DSP wants UI feature '%s', ignoring this", info.label, featureURI); } } lilv_nodes_free(const_cast(lilvSupportedFeatureNodes.me)); } // ---------------------------------------------------------------------------------------------------------------- // category info.category = CB::PLUGIN_CATEGORY_NONE; { Lilv::Nodes typeNodes(lilvPlugin.get_value(lv2World.rdf_type)); if (typeNodes.size() > 0) { if (typeNodes.contains(lv2World.class_allpass)) info.category = CB::PLUGIN_CATEGORY_FILTER; if (typeNodes.contains(lv2World.class_amplifier)) info.category = CB::PLUGIN_CATEGORY_DYNAMICS; if (typeNodes.contains(lv2World.class_analyzer)) info.category = CB::PLUGIN_CATEGORY_UTILITY; if (typeNodes.contains(lv2World.class_bandpass)) info.category = CB::PLUGIN_CATEGORY_FILTER; if (typeNodes.contains(lv2World.class_chorus)) info.category = CB::PLUGIN_CATEGORY_MODULATOR; if (typeNodes.contains(lv2World.class_comb)) info.category = CB::PLUGIN_CATEGORY_FILTER; if (typeNodes.contains(lv2World.class_compressor)) info.category = CB::PLUGIN_CATEGORY_DYNAMICS; if (typeNodes.contains(lv2World.class_constant)) info.category = CB::PLUGIN_CATEGORY_OTHER; if (typeNodes.contains(lv2World.class_converter)) info.category = CB::PLUGIN_CATEGORY_UTILITY; if (typeNodes.contains(lv2World.class_delay)) info.category = CB::PLUGIN_CATEGORY_DELAY; if (typeNodes.contains(lv2World.class_distortion)) info.category = CB::PLUGIN_CATEGORY_DISTORTION; if (typeNodes.contains(lv2World.class_dynamics)) info.category = CB::PLUGIN_CATEGORY_DYNAMICS; if (typeNodes.contains(lv2World.class_eq)) info.category = CB::PLUGIN_CATEGORY_EQ; if (typeNodes.contains(lv2World.class_envelope)) info.category = CB::PLUGIN_CATEGORY_DYNAMICS; if (typeNodes.contains(lv2World.class_expander)) info.category = CB::PLUGIN_CATEGORY_DYNAMICS; if (typeNodes.contains(lv2World.class_filter)) info.category = CB::PLUGIN_CATEGORY_FILTER; if (typeNodes.contains(lv2World.class_flanger)) info.category = CB::PLUGIN_CATEGORY_MODULATOR; if (typeNodes.contains(lv2World.class_function)) info.category = CB::PLUGIN_CATEGORY_UTILITY; if (typeNodes.contains(lv2World.class_gate)) info.category = CB::PLUGIN_CATEGORY_DYNAMICS; if (typeNodes.contains(lv2World.class_generator)) info.category = CB::PLUGIN_CATEGORY_OTHER; if (typeNodes.contains(lv2World.class_highpass)) info.category = CB::PLUGIN_CATEGORY_FILTER; if (typeNodes.contains(lv2World.class_limiter)) info.category = CB::PLUGIN_CATEGORY_DYNAMICS; if (typeNodes.contains(lv2World.class_lowpass)) info.category = CB::PLUGIN_CATEGORY_FILTER; if (typeNodes.contains(lv2World.class_mixer)) info.category = CB::PLUGIN_CATEGORY_UTILITY; if (typeNodes.contains(lv2World.class_modulator)) info.category = CB::PLUGIN_CATEGORY_MODULATOR; if (typeNodes.contains(lv2World.class_multiEQ)) info.category = CB::PLUGIN_CATEGORY_EQ; if (typeNodes.contains(lv2World.class_oscillator)) info.category = CB::PLUGIN_CATEGORY_OTHER; if (typeNodes.contains(lv2World.class_paraEQ)) info.category = CB::PLUGIN_CATEGORY_EQ; if (typeNodes.contains(lv2World.class_phaser)) info.category = CB::PLUGIN_CATEGORY_MODULATOR; if (typeNodes.contains(lv2World.class_pitch)) info.category = CB::PLUGIN_CATEGORY_OTHER; if (typeNodes.contains(lv2World.class_reverb)) info.category = CB::PLUGIN_CATEGORY_DELAY; if (typeNodes.contains(lv2World.class_simulator)) info.category = CB::PLUGIN_CATEGORY_OTHER; if (typeNodes.contains(lv2World.class_spatial)) info.category = CB::PLUGIN_CATEGORY_OTHER; if (typeNodes.contains(lv2World.class_spectral)) info.category = CB::PLUGIN_CATEGORY_OTHER; if (typeNodes.contains(lv2World.class_utility)) info.category = CB::PLUGIN_CATEGORY_UTILITY; if (typeNodes.contains(lv2World.class_waveshaper)) info.category = CB::PLUGIN_CATEGORY_DISTORTION; if (typeNodes.contains(lv2World.class_instrument)) { info.category = CB::PLUGIN_CATEGORY_SYNTH; info.hints |= CB::PLUGIN_IS_SYNTH; } } lilv_nodes_free(const_cast(typeNodes.me)); } // ---------------------------------------------------------------------------------------------------------------- // number data info.audioIns = 0; info.audioOuts = 0; info.cvIns = 0; info.cvOuts = 0; info.midiIns = 0; info.midiOuts = 0; info.parameterIns = 0; info.parameterOuts = 0; for (uint i=0, count=lilvPlugin.get_num_ports(); i(supportNodes.me)); } else if (lilvPort.is_a(lv2World.port_event)) { if (lilvPort.supports_event(lv2World.midi_event)) { if (isInput) ++(info.midiIns); else ++(info.midiOuts); } } else if (lilvPort.is_a(lv2World.port_midi)) { if (isInput) ++(info.midiIns); else ++(info.midiOuts); } else { const LilvNode* const symbolNode = lilvPort.get_symbol(); CARLA_SAFE_ASSERT_CONTINUE(symbolNode != nullptr && lilv_node_is_string(symbolNode)); const char* const symbol = lilv_node_as_string(symbolNode); CARLA_SAFE_ASSERT_CONTINUE(symbol != nullptr); supported = false; carla_stderr("LV2 plugin '%s' port '%s' is required but has unsupported type", info.label, symbol); } } if (supported) info.valid = true; return &info; } // ------------------------------------------------------------------------------------------------------------------- #if defined(USING_JUCE) && defined(CARLA_OS_MAC) static juce::StringArray gCachedAuPluginResults; static void findAUs() { if (gCachedAuPluginResults.size() != 0) return; juce::AudioUnitPluginFormat auFormat; gCachedAuPluginResults = auFormat.searchPathsForPlugins(juce::FileSearchPath(), false, false); } static const CarlaCachedPluginInfo* get_cached_plugin_au(const juce::String pluginId) { static CarlaCachedPluginInfo info; static CarlaString sname, slabel, smaker; info.valid = false; juce::AudioUnitPluginFormat auFormat; juce::OwnedArray results; auFormat.findAllTypesForFile(results, pluginId); CARLA_SAFE_ASSERT_RETURN(results.size() > 0, &info); CARLA_SAFE_ASSERT(results.size() == 1); juce::PluginDescription* const desc(results[0]); CARLA_SAFE_ASSERT_RETURN(desc != nullptr, &info); info.category = CB::getPluginCategoryFromName(desc->category.toRawUTF8()); info.hints = 0x0; info.valid = true; if (desc->isInstrument) info.hints |= CB::PLUGIN_IS_SYNTH; if (true) info.hints |= CB::PLUGIN_HAS_CUSTOM_UI; info.audioIns = static_cast(desc->numInputChannels); info.audioOuts = static_cast(desc->numOutputChannels); info.cvIns = 0; info.cvOuts = 0; info.midiIns = desc->isInstrument ? 1 : 0; info.midiOuts = 0; info.parameterIns = 0; info.parameterOuts = 0; sname = desc->name.toRawUTF8(); slabel = desc->fileOrIdentifier.toRawUTF8(); smaker = desc->manufacturerName.toRawUTF8(); info.name = sname; info.label = slabel; info.maker = smaker; info.copyright = gCachedPluginsNullCharPtr; return &info; } #endif // ------------------------------------------------------------------------------------------------------------------- static const CarlaCachedPluginInfo* get_cached_plugin_sfz(const water::File& file) { static CarlaCachedPluginInfo info; static CarlaString name, filename; name = file.getFileNameWithoutExtension().toRawUTF8(); name.replace('_',' '); filename = file.getFullPathName().toRawUTF8(); info.category = CB::PLUGIN_CATEGORY_SYNTH; info.hints = CB::PLUGIN_IS_SYNTH; // CB::PLUGIN_IS_RTSAFE info.valid = true; info.audioIns = 0; info.audioOuts = 2; info.cvIns = 0; info.cvOuts = 0; info.midiIns = 1; info.midiOuts = 0; info.parameterIns = 0; info.parameterOuts = 1; info.name = name.buffer(); info.label = filename.buffer(); info.maker = gCachedPluginsNullCharPtr; info.copyright = gCachedPluginsNullCharPtr; return &info; } // ------------------------------------------------------------------------------------------------------------------- static const CarlaCachedPluginInfo* get_cached_plugin_jsfx(const CB::CarlaJsfxUnit& unit) { static CarlaCachedPluginInfo info; ysfx_config_u config(ysfx_config_new()); const water::String rootPath = unit.getRootPath(); const water::String filePath = unit.getFilePath(); ysfx_register_builtin_audio_formats(config.get()); ysfx_set_import_root(config.get(), rootPath.toRawUTF8()); ysfx_guess_file_roots(config.get(), filePath.toRawUTF8()); ysfx_set_log_reporter(config.get(), &CB::CarlaJsfxLogging::logErrorsOnly); ysfx_u effect(ysfx_new(config.get())); if (! ysfx_load_file(effect.get(), filePath.toRawUTF8(), 0)) { info.valid = false; return &info; } // plugins with neither @block nor @sample are valid, but they are useless // also use this as a sanity check against misdetected files // since JSFX parsing is so permissive, it might accept lambda text files if (! ysfx_has_section(effect.get(), ysfx_section_block) && ! ysfx_has_section(effect.get(), ysfx_section_sample)) { info.valid = false; return &info; } static CarlaString name, label, maker; label = unit.getFileId().toRawUTF8(); name = ysfx_get_name(effect.get()); maker = ysfx_get_author(effect.get()); info.valid = true; info.category = CB::CarlaJsfxCategories::getFromEffect(effect.get()); info.audioIns = ysfx_get_num_inputs(effect.get()); info.audioOuts = ysfx_get_num_outputs(effect.get()); info.cvIns = 0; info.cvOuts = 0; info.midiIns = 1; info.midiOuts = 1; info.parameterIns = 0; info.parameterOuts = 0; for (uint32_t sliderIndex = 0; sliderIndex < ysfx_max_sliders; ++sliderIndex) { if (ysfx_slider_exists(effect.get(), sliderIndex)) ++info.parameterIns; } info.hints = 0; #if 0 // TODO(jsfx) when supporting custom graphics if (ysfx_has_section(effect.get(), ysfx_section_gfx)) info.hints |= CB::PLUGIN_HAS_CUSTOM_UI; #endif info.name = name.buffer(); info.label = label.buffer(); info.maker = maker.buffer(); info.copyright = gCachedPluginsNullCharPtr; return &info; } // ------------------------------------------------------------------------------------------------------------------- uint carla_get_cached_plugin_count(CB::PluginType ptype, const char* pluginPath) { CARLA_SAFE_ASSERT_RETURN(isCachedPluginType(ptype), 0); carla_debug("carla_get_cached_plugin_count(%i:%s, %s)", ptype, CB::PluginType2Str(ptype), pluginPath); switch (ptype) { case CB::PLUGIN_INTERNAL: { uint32_t count = 0; carla_get_native_plugins_data(&count); return count; } case CB::PLUGIN_LV2: { Lv2WorldClass& lv2World(Lv2WorldClass::getInstance()); lv2World.initIfNeeded(pluginPath); return lv2World.getPluginCount(); } #if defined(USING_JUCE) && defined(CARLA_OS_MAC) case CB::PLUGIN_AU: { findAUs(); return static_cast(gCachedAuPluginResults.size()); } #endif case CB::PLUGIN_SFZ: { findSFZs(pluginPath); return static_cast(gSFZs.size()); } case CB::PLUGIN_JSFX: { findJSFXs(pluginPath); return static_cast(gJSFXs.size()); } default: return 0; } } const CarlaCachedPluginInfo* carla_get_cached_plugin_info(CB::PluginType ptype, uint index) { carla_debug("carla_get_cached_plugin_info(%i:%s, %i)", ptype, CB::PluginType2Str(ptype), index); switch (ptype) { case CB::PLUGIN_INTERNAL: { uint32_t count = 0; const NativePluginDescriptor* const descs(carla_get_native_plugins_data(&count)); CARLA_SAFE_ASSERT_BREAK(index < count); CARLA_SAFE_ASSERT_BREAK(descs != nullptr); const NativePluginDescriptor& desc(descs[index]); return get_cached_plugin_internal(desc); } case CB::PLUGIN_LV2: { Lv2WorldClass& lv2World(Lv2WorldClass::getInstance()); const LilvPlugin* const cPlugin(lv2World.getPluginFromIndex(index)); CARLA_SAFE_ASSERT_BREAK(cPlugin != nullptr); Lilv::Plugin lilvPlugin(cPlugin); CARLA_SAFE_ASSERT_BREAK(lilvPlugin.get_uri().is_uri()); return get_cached_plugin_lv2(lv2World, lilvPlugin); } #if defined(USING_JUCE) && defined(CARLA_OS_MAC) case CB::PLUGIN_AU: { CARLA_SAFE_ASSERT_BREAK(index < static_cast(gCachedAuPluginResults.size())); return get_cached_plugin_au(gCachedAuPluginResults.strings.getUnchecked(static_cast(index))); } #endif case CB::PLUGIN_SFZ: { CARLA_SAFE_ASSERT_BREAK(index < gSFZs.size()); return get_cached_plugin_sfz(gSFZs[index]); } case CB::PLUGIN_JSFX: { CARLA_SAFE_ASSERT_BREAK(index < static_cast(gJSFXs.size())); return get_cached_plugin_jsfx(gJSFXs[index]); } default: break; } static CarlaCachedPluginInfo info; return &info; } // ------------------------------------------------------------------------------------------------------------------- #ifndef CARLA_PLUGIN_BUILD # include "../native-plugins/_data.cpp" #endif // -------------------------------------------------------------------------------------------------------------------