diff --git a/.travis/linux..install.sh b/.travis/linux..install.sh index fd2b79d948e..d56645603a1 100755 --- a/.travis/linux..install.sh +++ b/.travis/linux..install.sh @@ -13,8 +13,11 @@ SWH_PACKAGES="perl libxml2-utils libxml-perl liblist-moreutils-perl" # VST dependencies VST_PACKAGES="wine-dev qt59x11extras qtbase5-private-dev libxcb-util0-dev libxcb-keysyms1-dev" +# LV2 dependencies; libsuil-dev is not required +LV2_PACKAGES="lv2-dev liblilv-dev" + # Help with unmet dependencies -PACKAGES="$PACKAGES $SWH_PACKAGES $VST_PACKAGES libjack-jackd2-0" +PACKAGES="$PACKAGES $SWH_PACKAGES $VST_PACKAGES $LV2_PACKAGES libjack-jackd2-0" # shellcheck disable=SC2086 sudo apt-get install -y $PACKAGES diff --git a/CMakeLists.txt b/CMakeLists.txt index 8d039e53acd..36e3a699b2f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,6 +58,8 @@ OPTION(WANT_CARLA "Include Carla plugin" ON) OPTION(WANT_CMT "Include Computer Music Toolkit LADSPA plugins" ON) OPTION(WANT_JACK "Include JACK (Jack Audio Connection Kit) support" ON) OPTION(WANT_WEAKJACK "Loosely link JACK libraries" ON) +OPTION(WANT_LV2 "Include Lv2 plugins" ON) +OPTION(WANT_SUIL "Include SUIL for LV2 plugin UIs" ON) OPTION(WANT_MP3LAME "Include MP3/Lame support" ON) OPTION(WANT_OGGVORBIS "Include OGG/Vorbis support" ON) OPTION(WANT_PULSEAUDIO "Include PulseAudio support" ON) @@ -182,6 +184,39 @@ IF(NOT SNDFILE_VERSION VERSION_LESS 1.0.26) SET(LMMS_HAVE_SF_COMPLEVEL TRUE) ENDIF() +IF(WANT_LV2) + IF(PKG_CONFIG_FOUND) + PKG_CHECK_MODULES(LV2 lv2) + PKG_CHECK_MODULES(LILV lilv-0) + IF(LV2_FOUND AND LILV_FOUND) + SET(LMMS_HAVE_LV2 TRUE) + SET(STATUS_LV2 "OK") + ELSE() + SET(STATUS_LV2 "not found, install it or set PKG_CONFIG_PATH appropriately") + ENDIF() + ELSE() + SET(STATUS_LV2 "not found, requires pkg-config") + ENDIF() +ELSE(WANT_LV2) + SET(STATUS_LV2 "not built as requested") +ENDIF(WANT_LV2) + +IF(WANT_SUIL) + IF(PKG_CONFIG_FOUND) + PKG_CHECK_MODULES(SUIL suil-0) + IF(SUIL_FOUND) + SET(LMMS_HAVE_SUIL TRUE) + SET(STATUS_SUIL "OK") + ELSE() + SET(STATUS_SUIL "not found, install it or set PKG_CONFIG_PATH appropriately") + ENDIF() + ELSE() + SET(STATUS_SUIL "not found, requires pkg-config") + ENDIF() +ELSE(WANT_SUIL) + SET(STATUS_SUIL "not built as requested") +ENDIF(WANT_SUIL) + IF(WANT_CALF) SET(LMMS_HAVE_CALF TRUE) SET(STATUS_CALF "OK") @@ -686,6 +721,8 @@ MESSAGE( MESSAGE( "Optional plugins\n" "----------------\n" +"* Lv2 plugins : ${STATUS_LV2}\n" +"* SUIL for plugin UIs : ${STATUS_SUIL}\n" "* ZynAddSubFX instrument : ${STATUS_ZYN}\n" "* Carla Patchbay & Rack : ${STATUS_CARLA}\n" "* SoundFont2 player : ${STATUS_FLUIDSYNTH}\n" diff --git a/cmake/modules/PluginList.cmake b/cmake/modules/PluginList.cmake index 2d853038873..a2871bf99ec 100644 --- a/cmake/modules/PluginList.cmake +++ b/cmake/modules/PluginList.cmake @@ -39,6 +39,8 @@ SET(LMMS_PLUGIN_LIST HydrogenImport ladspa_browser LadspaEffect + Lv2Effect + Lv2Instrument lb302 MidiImport MidiExport diff --git a/include/Engine.h b/include/Engine.h index f4ff72fb2fb..8f988a1250e 100644 --- a/include/Engine.h +++ b/include/Engine.h @@ -30,6 +30,7 @@ #include +#include "lmmsconfig.h" #include "lmms_export.h" #include "lmms_basics.h" @@ -87,6 +88,13 @@ class LMMS_EXPORT LmmsCore : public QObject return s_projectJournal; } +#ifdef LMMS_HAVE_LV2 + static class Lv2Manager * getLv2Manager() + { + return s_lv2Manager; + } +#endif + static Ladspa2LMMS * getLADSPAManager() { return s_ladspaManager; @@ -143,6 +151,9 @@ class LMMS_EXPORT LmmsCore : public QObject static ProjectJournal * s_projectJournal; static DummyTrackContainer * s_dummyTC; +#ifdef LMMS_HAVE_LV2 + static class Lv2Manager* s_lv2Manager; +#endif static Ladspa2LMMS * s_ladspaManager; static void* s_dndPluginKey; diff --git a/include/Lv2Basics.h b/include/Lv2Basics.h new file mode 100644 index 00000000000..0003f83e814 --- /dev/null +++ b/include/Lv2Basics.h @@ -0,0 +1,67 @@ +/* + * Lv2Basics.h - basic Lv2 utils + * + * Copyright (c) 2018-2020 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * 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 (at your option) 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. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + + +#ifndef LV2BASICS_H +#define LV2BASICS_H + + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_LV2 + +#include +#include +#include +#include + +struct LilvNodeDeleter +{ + void operator()(LilvNode* n) { lilv_node_free(n); } +}; + +struct LilvNodesDeleter +{ + void operator()(LilvNodes* n) { lilv_nodes_free(n); } +}; + +using AutoLilvNode = std::unique_ptr; +using AutoLilvNodes = std::unique_ptr; + +/** + Return QString from a plugin's node, everything will be freed automatically + @param plug The plugin where the node is + @param getFunc The function to return the node from the plugin +*/ +QString qStringFromPluginNode(const LilvPlugin* plug, + LilvNode * (*getFunc)(const LilvPlugin*)); + +//! Return port name as QString, everything will be freed automatically +QString qStringFromPortName(const LilvPlugin* plug, const LilvPort* port); + +//! Return port name as std::string, everything will be freed automatically +std::string stdStringFromPortName(const LilvPlugin* plug, const LilvPort* port); + +#endif // LMMS_HAVE_LV2 +#endif // LV2BASICS_H diff --git a/include/Lv2ControlBase.h b/include/Lv2ControlBase.h new file mode 100644 index 00000000000..9f1b54992cd --- /dev/null +++ b/include/Lv2ControlBase.h @@ -0,0 +1,146 @@ +/* + * Lv2ControlBase.h - Lv2 control base class + * + * Copyright (c) 2018-2020 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * 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 (at your option) 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. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LV2_CONTROL_BASE_H +#define LV2_CONTROL_BASE_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_LV2 + +#include + +#include "DataFile.h" +#include "LinkedModelGroups.h" +#include "Plugin.h" + +class Lv2Proc; +class PluginIssue; + +/** + Common base class for Lv2 plugins + + This class contains a vector of Lv2Proc, usually 1 (for stereo plugins) or + 2 (for mono plugins). Most of the logic is done there, this class primarily + forwards work to the Lv2Proc and collects the results. + + This class provides everything Lv2 plugins have in common. It's not + named Lv2Plugin, because + * it does not inherit Instrument + * the Plugin subclass Effect does not inherit this class + + This class would usually be a Model subclass. However, Qt doesn't allow + this: + * inhertiting only from Model will cause diamond inheritance for QObject, + which will cause errors with Q_OBJECT + * making this a direct subclass of Instrument resp. EffectControls would + require CRTP, which would make this class a template class, which would + conflict with Q_OBJECT + + The consequence is that this class can neither inherit QObject or Model, nor + Instrument or EffectControls, which means in fact: + * this class contains no signals or slots, but it offers stubs for slots + that shall be called by child classes + * this class can not override virtuals of Instrument or EffectControls, so + it will offer functions that must be called by virtuals in its child class +*/ +class Lv2ControlBase : public LinkedModelGroups +{ +public: + static Plugin::PluginTypes check(const LilvPlugin* m_plugin, + std::vector &issues, bool printIssues = false); + + const LilvPlugin* getPlugin() const { return m_plugin; } + + Lv2Proc *control(std::size_t idx) { return m_procs[idx].get(); } + const Lv2Proc *control(std::size_t idx) const { return m_procs[idx].get(); } + + bool hasGui() const { return m_hasGUI; } + void setHasGui(bool val) { m_hasGUI = val; } + +protected: + /* + ctor/dtor + */ + //! @param that the class inheriting this class and inheriting Model; + //! this is the same pointer as this, but a different type + //! @param uri the Lv2 URI telling this class what plugin to construct + Lv2ControlBase(class Model *that, const QString& uri); + ~Lv2ControlBase() override; + //! Must be checked after ctor or reload + bool isValid() const { return m_valid; } + + /* + overrides + */ + LinkedModelGroup* getGroup(std::size_t idx) override; + const LinkedModelGroup* getGroup(std::size_t idx) const override; + + /* + utils for the run thread + */ + //! Copy values from all connected models into the respective ports + void copyModelsFromLmms(); + //! Copy buffer passed by LMMS into our ports + void copyBuffersFromLmms(const sampleFrame *buf, fpp_t frames); + //! Copy our ports into buffers passed by LMMS + void copyBuffersToLmms(sampleFrame *buf, fpp_t frames) const; + //! Run the Lv2 plugin instance for @param frames frames + void run(fpp_t frames); + + /* + load/save, must be called from virtuals + */ + void saveSettings(QDomDocument &doc, QDomElement &that); + void loadSettings(const QDomElement &that); + void loadFile(const QString &file); + //! TODO: not implemented + void reloadPlugin(); + + /* + more functions that must be called from virtuals + */ + std::size_t controlCount() const; + QString nodeName() const { return "lv2controls"; } + +private: + //! Return the DataFile settings type + virtual DataFile::Types settingsType() = 0; + //! Inform the plugin about a file name change + virtual void setNameFromFile(const QString &fname) = 0; + + //! Independent processors + //! If this is a mono effect, the vector will have size 2 in order to + //! fulfill LMMS' requirement of having stereo input and output + std::vector> m_procs; + + bool m_valid = true; + bool m_hasGUI = false; + unsigned m_channelsPerProc; + + const LilvPlugin* m_plugin; +}; + +#endif // LMMS_HAVE_LV2 +#endif // LV2_CONTROL_BASE_H diff --git a/include/Lv2Manager.h b/include/Lv2Manager.h new file mode 100644 index 00000000000..8437715619e --- /dev/null +++ b/include/Lv2Manager.h @@ -0,0 +1,126 @@ +/* + * Lv2Manager.h - Implementation of Lv2Manager class + * + * Copyright (c) 2018-2020 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * 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 (at your option) 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. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LV2MANAGER_H +#define LV2MANAGER_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_LV2 + +#include +#include + +#include "Lv2Basics.h" +#include "Plugin.h" + + +/* + all Lv2 classes in relation (use our "4 spaces per tab rule" to view): + + explanation: + "x = {y z}" means class "x" consists of classes "y" and "z" + (and probably other classes not mentioned) + "x = {y*}" means class "x" references/uses class "y" + + core: + Lv2Proc = {LilvInstance} + Lv2ControlBase = {Lv2Proc, Lv2Proc... (2 for mono, 1 for stereo)} + Lv2Manager = {LilvPlugin*, LilvPlugin* ...} + (creates Lv2ControlBase, Lv2ControlBase...) + + Lv2FxControls = {Lv2ControlBase} + Lv2Effect = {Effect + Lv2FxControls} + (takes Lv2SubPluginFeatures in ctor) + Lv2Instrument = {Instrument + Lv2ControlBase} + (takes Lv2SubPluginFeatures in ctor) + + gui: + Lv2ViewProc = {Lv2Proc*} + Lv2ViewBase = {Lv2ViewProc, Lv2ViewProc... + (2 for mono, 1 for stereo)} + Lv2FxControlDialog = {EffectControlDialog + Lv2ViewBase} + Lv2InsView = {InstrumentView + Lv2ViewBase} + + Lv2SubPluginFeatures: + Lv2SubPluginFeatures = {Lv2Manager*} + Lv2Effect::Descriptor = {Lv2SubPluginFeatures} + Lv2Instrument::Descriptor = {Lv2SubPluginFeatures} +*/ + + +//! Class to keep track of all LV2 plugins +class Lv2Manager +{ +public: + void initPlugins(); + + Lv2Manager(); + ~Lv2Manager(); + + + AutoLilvNode uri(const char* uriStr); + + //! Class representing info for one plugin + struct Lv2Info + { + public: + //! use only for std::map internals + Lv2Info() : m_plugin(nullptr) {} + //! ctor used inside Lv2Manager + Lv2Info(const LilvPlugin* plug, Plugin::PluginTypes type, bool valid) : + m_plugin(plug), m_type(type), m_valid(valid) {} + Lv2Info(Lv2Info&& other) = default; + Lv2Info& operator=(Lv2Info&& other) = default; + + const LilvPlugin* plugin() const { return m_plugin; } + Plugin::PluginTypes type() const { return m_type; } + bool isValid() const { return m_valid; } + + private: + const LilvPlugin* m_plugin; + Plugin::PluginTypes m_type; + bool m_valid = false; + }; + + //! Return descriptor with URI @p uri or nullptr if none exists + const LilvPlugin *getPlugin(const std::string &uri); + //! Return descriptor with URI @p uri or nullptr if none exists + const LilvPlugin *getPlugin(const QString& uri); + + using Lv2InfoMap = std::map; + using Iterator = Lv2InfoMap::iterator; + Iterator begin() { return m_lv2InfoMap.begin(); } + Iterator end() { return m_lv2InfoMap.end(); } + +private: + bool m_debug; //!< if set, debug output will be printed + LilvWorld* m_world; + Lv2InfoMap m_lv2InfoMap; + bool isSubclassOf(const LilvPluginClass *clvss, const char *uriStr); +}; + +#endif // LMMS_HAVE_LV2 + +#endif // LV2MANAGER_H diff --git a/include/Lv2Ports.h b/include/Lv2Ports.h new file mode 100644 index 00000000000..c9e6c16c855 --- /dev/null +++ b/include/Lv2Ports.h @@ -0,0 +1,237 @@ +/* + * Lv2Ports.h - Lv2 port classes definitions + * + * Copyright (c) 2019-2020 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * 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 (at your option) 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. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LV2PORTS_H +#define LV2PORTS_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_LV2 + +#include +#include +#include + +#include "lmms_basics.h" +#include "PluginIssue.h" + +struct ConnectPortVisitor; + +namespace Lv2Ports { + +/* + port structs +*/ +enum class Flow { + Unknown, + Input, + Output +}; + +enum class Type { + Unknown, + Control, + Audio, + Event, //!< TODO: unused, describe + Cv //!< TODO: unused, describe +}; + +//! Port visualization +//! @note All Lv2 audio ports are float, this is only the visualisation +enum class Vis { + None, + Integer, + Enumeration, + Toggled +}; + +const char* toStr(Lv2Ports::Flow pf); +const char* toStr(Lv2Ports::Type pt); +const char* toStr(Lv2Ports::Vis pv); + +struct ControlPortBase; +struct Control; +struct Audio; +struct Cv; +struct Unknown; + +struct ConstVisitor +{ + virtual void visit(const Lv2Ports::ControlPortBase& ) {} + virtual void visit(const Lv2Ports::Control& ) {} + virtual void visit(const Lv2Ports::Audio& ) {} + virtual void visit(const Lv2Ports::Cv& ) {} + virtual void visit(const Lv2Ports::Unknown& ) {} + + virtual ~ConstVisitor(); +}; + +struct Visitor +{ + virtual void visit(Lv2Ports::ControlPortBase& ) {} + virtual void visit(Lv2Ports::Control& ) {} + virtual void visit(Lv2Ports::Audio& ) {} + virtual void visit(Lv2Ports::Cv& ) {} + virtual void visit(Lv2Ports::Unknown& ) {} + + virtual ~Visitor(); +}; + +struct Meta +{ + Type m_type = Type::Unknown; + Flow m_flow = Flow::Unknown; + Vis m_vis = Vis::None; + + float m_def = .0f, m_min = .0f, m_max = .0f; + bool m_optional = false; + bool m_used = true; + + std::vector get(const LilvPlugin* plugin, std::size_t portNum); +}; + +struct PortBase : public Meta +{ + const LilvPort* m_port = nullptr; + const LilvPlugin* m_plugin = nullptr; + + virtual void accept(Visitor& v) = 0; + virtual void accept(ConstVisitor& v) const = 0; + + QString name() const; + QString uri() const; + + virtual ~PortBase(); +}; + +template +struct VisitablePort : public Base +{ + void accept(Visitor& v) override { v.visit(static_cast(*this)); } + void accept(ConstVisitor& v) const override { v.visit(static_cast(*this)); } +}; + +struct ControlPortBase : public VisitablePort +{ + //! LMMS models + //! Always up-to-date, except during runs + std::unique_ptr m_connectedModel; + + //! Enumerate float values + //! lv2 defines scale points as + //! "single float Points (for control inputs)" + std::vector m_scalePointMap; +}; + +struct Control : public VisitablePort +{ + //! Data location which Lv2 plugins see + //! Model values are being copied here every run + //! Between runs, this data is not up-to-date + float m_val; +}; + +struct Cv : public VisitablePort +{ + //! Data location which Lv2 plugins see + //! Model values are being copied here every run + //! Between runs, this data is not up-to-date + std::vector m_buffer; +}; + +struct Audio : public VisitablePort +{ + Audio(std::size_t bufferSize, bool isSidechain, bool isOptional); + + //! Copy buffer passed by LMMS into our ports + //! @param channel channel index into each sample frame + void copyBuffersFromCore(const sampleFrame *lmmsBuf, + unsigned channel, fpp_t frames); + //! Add buffer passed by LMMS into our ports, and halve the result + //! @param channel channel index into each sample frame + void averageWithBuffersFromCore(const sampleFrame *lmmsBuf, + unsigned channel, fpp_t frames); + //! Copy our ports into buffers passed by LMMS + //! @param channel channel index into each sample frame + void copyBuffersToCore(sampleFrame *lmmsBuf, + unsigned channel, fpp_t frames) const; + + bool isSideChain() const { return m_sidechain; } + bool isOptional() const { return m_optional; } + bool mustBeUsed() const { return !isSideChain() && !isOptional(); } + std::size_t bufferSize() const { return m_buffer.size(); } + +private: + //! the buffer where Lv2 reads/writes the data from/to + std::vector m_buffer; + bool m_sidechain; + bool m_optional; + + // the only case when data of m_buffer may be referenced: + friend struct ::ConnectPortVisitor; +}; + +struct Unknown : public VisitablePort +{ +}; + +/* + port casts +*/ +template +struct DCastVisitor : public Visitor +{ + Target* m_result = nullptr; + void visit(Target& tar) { m_result = &tar; } +}; + +template +struct ConstDCastVisitor : public ConstVisitor +{ + const Target* m_result = nullptr; + void visit(const Target& tar) { m_result = &tar; } +}; + +//! If you don't want to use a whole visitor, you can use dcast +template +Target* dcast(PortBase* base) +{ + DCastVisitor vis; + base->accept(vis); + return vis.m_result; +} + +//! const overload +template +const Target* dcast(const PortBase* base) +{ + ConstDCastVisitor vis; + base->accept(vis); + return vis.m_result; +} + +} // namespace Lv2Ports + +#endif // LMMS_HAVE_LV2 +#endif // LV2PORTS_H diff --git a/include/Lv2Proc.h b/include/Lv2Proc.h new file mode 100644 index 00000000000..fb1f3466634 --- /dev/null +++ b/include/Lv2Proc.h @@ -0,0 +1,183 @@ +/* + * Lv2Proc.h - Lv2 processor class + * + * Copyright (c) 2019-2020 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * 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 (at your option) 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. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LV2PROC_H +#define LV2PROC_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_LV2 + +#include +#include +#include + +#include "Lv2Basics.h" +#include "LinkedModelGroups.h" +#include "Plugin.h" +#include "PluginIssue.h" + + +// forward declare port structs/enums +namespace Lv2Ports +{ + struct Audio; + struct PortBase; + + enum class Type; + enum class Flow; + enum class Vis; +} + + +//! Class representing one Lv2 processor, i.e. one Lv2 handle +//! For Mono effects, 1 Lv2ControlBase references 2 Lv2Proc +class Lv2Proc : public LinkedModelGroup +{ +public: + static Plugin::PluginTypes check(const LilvPlugin* plugin, + std::vector &issues, bool printIssues = false); + + /* + ctor/dtor + */ + Lv2Proc(const LilvPlugin* plugin, Model *parent); + ~Lv2Proc() override; + //! Must be checked after ctor or reload + bool isValid() const { return m_valid; } + + /* + port access + */ + struct StereoPortRef + { + //! mono port or left port in case of stereo + Lv2Ports::Audio* m_left = nullptr; + //! unused, or right port in case of stereo + Lv2Ports::Audio* m_right = nullptr; + }; + + StereoPortRef& inPorts() { return m_inPorts; } + const StereoPortRef& inPorts() const { return m_inPorts; } + StereoPortRef& outPorts() { return m_outPorts; } + const StereoPortRef& outPorts() const { return m_outPorts; } + template + void foreach_port(const Functor& ftor) + { + for (std::unique_ptr& port : m_ports) + { + ftor(port.get()); + } + } + template + void foreach_port(const Functor& ftor) const + { + for (const std::unique_ptr& port : m_ports) + { + ftor(port.get()); + } + } + + //! Debug function to print ports to stdout + void dumpPorts(); + + /* + utils for the run thread + */ + //! Copy values from all connected models into the respective ports + void copyModelsFromCore(); + /** + * Copy buffer passed by the core into our ports + * @param buf buffer of sample frames, each sample frame is something like + * a `float[ * ]` array. + * @param firstChan The offset for @p buf where we have to read our + * first channel. + * This marks the first sample in each sample frame where we read from. + * If we are the 2nd of 2 mono procs, this can be greater than 0. + * @param num Number of channels we must read from @param buf (starting at + * @p offset) + */ + void copyBuffersFromCore(const sampleFrame *buf, + unsigned firstChan, unsigned num, fpp_t frames); + /** + * Copy our ports into buffers passed by the core + * @param buf buffer of sample frames, each sample frame is something like + * a `float[ * ]` array. + * @param firstChan The offset for @p buf where we have to write our + * first channel. + * This marks the first sample in each sample frame where we write to. + * If we are the 2nd of 2 mono procs, this can be greater than 0. + * @param num Number of channels we must write to @param buf (starting at + * @p offset) + */ + void copyBuffersToCore(sampleFrame *buf, unsigned firstChan, unsigned num, + fpp_t frames) const; + //! Run the Lv2 plugin instance for @param frames frames + void run(fpp_t frames); + + /* + misc + */ + class AutomatableModel *modelAtPort(const QString &uri); // unused currently + std::size_t controlCount() const { return LinkedModelGroup::modelNum(); } + +protected: + /* + load and save + */ + //! Create ports and instance, connect ports, activate plugin + void initPlugin(); + //! Deactivate instance + void shutdownPlugin(); + +private: + bool m_valid = true; + + const LilvPlugin* m_plugin; + LilvInstance* m_instance; + + std::vector> m_ports; + StereoPortRef m_inPorts, m_outPorts; + + //! models for the controls, sorted by port symbols + std::map m_connectedModels; + + //! load a file in the plugin, but don't do anything in LMMS + void loadFileInternal(const QString &file); + //! allocate m_ports, fill all with metadata, and assign meaning of ports + void createPorts(); + //! fill m_ports[portNum] with metadata + void createPort(std::size_t portNum); + //! connect m_ports[portNum] with Lv2 + void connectPort(std::size_t num); + + void dumpPort(std::size_t num); + + static bool portIsSideChain(const LilvPlugin* plugin, const LilvPort *port); + static bool portIsOptional(const LilvPlugin* plugin, const LilvPort *port); + static AutoLilvNode uri(const char* uriStr); +}; + +#endif // LMMS_HAVE_LV2 +#endif // LV2PROC_H diff --git a/include/Lv2SubPluginFeatures.h b/include/Lv2SubPluginFeatures.h new file mode 100644 index 00000000000..33c29c3ef26 --- /dev/null +++ b/include/Lv2SubPluginFeatures.h @@ -0,0 +1,61 @@ +/* + * Lv2SubPluginFeatures.h - derivation from + * Plugin::Descriptor::SubPluginFeatures for + * hosting LV2 plugins + * + * Copyright (c) 2018-2020 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * 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 (at your option) 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. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LV2_SUBPLUGIN_FEATURES_H +#define LV2_SUBPLUGIN_FEATURES_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_LV2 + +#include + +#include "Plugin.h" + +class Lv2SubPluginFeatures : public Plugin::Descriptor::SubPluginFeatures +{ +private: + static const LilvPlugin *getPlugin(const Key &k); + static QString pluginName(const LilvPlugin *plug); + +public: + Lv2SubPluginFeatures(Plugin::PluginTypes type); + + void fillDescriptionWidget( + QWidget *parent, const Key *k) const override; + + QString additionalFileExtensions(const Key &k) const override; + QString displayName(const Key &k) const override; + QString description(const Key &k) const override; + const PixmapLoader *logo(const Key &k) const override; + + void listSubPluginKeys( + const Plugin::Descriptor *desc, KeyList &kl) const override; +}; + +#endif // LMMS_HAVE_LV2 + +#endif diff --git a/include/Lv2ViewBase.h b/include/Lv2ViewBase.h new file mode 100644 index 00000000000..980e1f7efcf --- /dev/null +++ b/include/Lv2ViewBase.h @@ -0,0 +1,97 @@ +/* + * Lv2ViewBase.h - base class for Lv2 plugin views + * + * Copyright (c) 2018-2020 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * 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 (at your option) 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. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LV2VIEWBASE_H +#define LV2VIEWBASE_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_LV2 + + +#include +#include + +#include "LinkedModelGroupViews.h" +#include "Lv2Basics.h" + +class Lv2Proc; +class Lv2ControlBase; + + +//! View for one processor, Lv2ViewBase contains 2 of those for mono plugins +class Lv2ViewProc : public LinkedModelGroupView +{ +public: + //! @param colNum numbers of columns for the controls + Lv2ViewProc(QWidget *parent, Lv2Proc *ctrlBase, int colNum); + ~Lv2ViewProc(); + +private: + static AutoLilvNode uri(const char *uriStr); +}; + + +//! Base class for view for one Lv2 plugin +class Lv2ViewBase : public LinkedModelGroupsView +{ +protected: + //! @param pluginWidget A child class which inherits QWidget + Lv2ViewBase(class QWidget *pluginWidget, Lv2ControlBase *ctrlBase); + ~Lv2ViewBase(); + + // these widgets must be connected by child widgets + class QPushButton *m_reloadPluginButton = nullptr; + class QPushButton *m_toggleUIButton = nullptr; + class QPushButton *m_helpButton = nullptr; + + void toggleUI(); + void toggleHelp(bool visible); + + // to be called by child virtuals + //! Reconnect models if model changed + void modelChanged(Lv2ControlBase* ctrlBase); + +private: + enum Rows + { + ButtonRow, + ProcRow, + LinkChannelsRow + }; + + static AutoLilvNode uri(const char *uriStr); + LinkedModelGroupView* getGroupView() override { return m_procView; } + + Lv2ViewProc* m_procView; + + //! Numbers of controls per row; must be multiple of 2 for mono effects + const int m_colNum = 6; + class QMdiSubWindow* m_helpWindow = nullptr; + class LedCheckBox *m_multiChannelLink; +}; + + +#endif // LMMS_HAVE_LV2 +#endif // LV2VIEWBASE_H diff --git a/plugins/LadspaEffect/LadspaControls.h b/plugins/LadspaEffect/LadspaControls.h index ccb96dd7484..013b914d45d 100644 --- a/plugins/LadspaEffect/LadspaControls.h +++ b/plugins/LadspaEffect/LadspaControls.h @@ -71,6 +71,7 @@ protected slots: ch_cnt_t m_controlCount; bool m_noLink; BoolModel m_stereoLinkModel; + //! control vector for each processor QVector m_controls; diff --git a/plugins/Lv2Effect/CMakeLists.txt b/plugins/Lv2Effect/CMakeLists.txt new file mode 100644 index 00000000000..915751797d1 --- /dev/null +++ b/plugins/Lv2Effect/CMakeLists.txt @@ -0,0 +1,9 @@ +IF(LMMS_HAVE_LV2) + INCLUDE_DIRECTORIES(${LV2_INCLUDE_DIRS}) + INCLUDE_DIRECTORIES(${LILV_INCLUDE_DIRS}) + INCLUDE_DIRECTORIES(${SUIL_INCLUDE_DIRS}) + INCLUDE(BuildPlugin) + BUILD_PLUGIN(lv2effect Lv2Effect.cpp Lv2FxControls.cpp Lv2FxControlDialog.cpp Lv2Effect.h Lv2FxControls.h Lv2FxControlDialog.h + MOCFILES Lv2Effect.h Lv2FxControls.h Lv2FxControlDialog.h + EMBEDDED_RESOURCES logo.png) +ENDIF(LMMS_HAVE_LV2) diff --git a/plugins/Lv2Effect/Lv2Effect.cpp b/plugins/Lv2Effect/Lv2Effect.cpp new file mode 100644 index 00000000000..2ee5ceabbe5 --- /dev/null +++ b/plugins/Lv2Effect/Lv2Effect.cpp @@ -0,0 +1,111 @@ +/* + * Lv2Effect.cpp - implementation of LV2 effect + * + * Copyright (c) 2018-2020 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * 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 (at your option) 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. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "Lv2Effect.h" + +#include +#include + +#include "Lv2SubPluginFeatures.h" + +#include "embed.h" +#include "plugin_export.h" + + + + +Plugin::Descriptor PLUGIN_EXPORT lv2effect_plugin_descriptor = +{ + STRINGIFY(PLUGIN_NAME), + "LV2", + QT_TRANSLATE_NOOP("pluginBrowser", + "plugin for using arbitrary LV2-effects inside LMMS."), + "Johannes Lorenz ", + 0x0100, + Plugin::Effect, + new PluginPixmapLoader("logo"), + nullptr, + new Lv2SubPluginFeatures(Plugin::Effect) +}; + + + + +Lv2Effect::Lv2Effect(Model* parent, const Descriptor::SubPluginFeatures::Key *key) : + Effect(&lv2effect_plugin_descriptor, parent, key), + m_controls(this, key->attributes["uri"]), + m_tmpOutputSmps(Engine::mixer()->framesPerPeriod()) +{ +} + + + + +bool Lv2Effect::processAudioBuffer(sampleFrame *buf, const fpp_t frames) +{ + if (!isEnabled() || !isRunning()) { return false; } + Q_ASSERT(frames <= static_cast(m_tmpOutputSmps.size())); + + m_controls.copyBuffersFromLmms(buf, frames); + m_controls.copyModelsFromLmms(); + +// m_pluginMutex.lock(); + m_controls.run(frames); +// m_pluginMutex.unlock(); + + m_controls.copyBuffersToLmms(m_tmpOutputSmps.data(), frames); + + double outSum = .0; + bool corrupt = wetLevel() < 0; // #3261 - if w < 0, bash w := 0, d := 1 + const float d = corrupt ? 1 : dryLevel(); + const float w = corrupt ? 0 : wetLevel(); + for(fpp_t f = 0; f < frames; ++f) + { + buf[f][0] = d * buf[f][0] + w * m_tmpOutputSmps[f][0]; + buf[f][1] = d * buf[f][1] + w * m_tmpOutputSmps[f][1]; + double l = static_cast(buf[f][0]); + double r = static_cast(buf[f][1]); + outSum += l*l + r*r; + } + checkGate(outSum / frames); + + return isRunning(); +} + + + + +extern "C" +{ + +// necessary for getting instance out of shared lib +PLUGIN_EXPORT Plugin *lmms_plugin_main(Model *_parent, void *_data) +{ + using KeyType = Plugin::Descriptor::SubPluginFeatures::Key; + Lv2Effect* eff = new Lv2Effect(_parent, static_cast(_data)); + if (!eff->isValid()) { delete eff; eff = nullptr; } + return eff; +} + +} diff --git a/plugins/Lv2Effect/Lv2Effect.h b/plugins/Lv2Effect/Lv2Effect.h new file mode 100644 index 00000000000..77792d428a1 --- /dev/null +++ b/plugins/Lv2Effect/Lv2Effect.h @@ -0,0 +1,54 @@ +/* + * Lv2Effect.h - implementation of LV2 effect + * + * Copyright (c) 2018-2020 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * 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 (at your option) 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. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LV2_EFFECT_H +#define LV2_EFFECT_H + +#include "Effect.h" +#include "Lv2FxControls.h" + +class Lv2Effect : public Effect +{ + Q_OBJECT + +public: + /* + initialization + */ + Lv2Effect(Model* parent, const Descriptor::SubPluginFeatures::Key* _key); + //! Must be checked after ctor or reload + bool isValid() const { return m_controls.isValid(); } + + bool processAudioBuffer( sampleFrame* buf, const fpp_t frames ) override; + EffectControls* controls() override { return &m_controls; } + + Lv2FxControls* lv2Controls() { return &m_controls; } + const Lv2FxControls* lv2Controls() const { return &m_controls; } + +private: + Lv2FxControls m_controls; + std::vector m_tmpOutputSmps; +}; + +#endif // LMMS_HAVE_LV2 diff --git a/plugins/Lv2Effect/Lv2FxControlDialog.cpp b/plugins/Lv2Effect/Lv2FxControlDialog.cpp new file mode 100644 index 00000000000..9a77171d6df --- /dev/null +++ b/plugins/Lv2Effect/Lv2FxControlDialog.cpp @@ -0,0 +1,72 @@ +/* + * Lv2FxControlDialog.cpp - Lv2FxControlDialog implementation + * + * Copyright (c) 2018-2020 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * 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 (at your option) 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. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "Lv2FxControlDialog.h" + +#include +#include +#include + +#include "Lv2Effect.h" +#include "Lv2FxControls.h" + + +Lv2FxControlDialog::Lv2FxControlDialog(Lv2FxControls *controls) : + EffectControlDialog(controls), + Lv2ViewBase(this, controls) +{ + if (m_reloadPluginButton) { + connect(m_reloadPluginButton, &QPushButton::clicked, + this, [this](){ lv2Controls()->reloadPlugin(); }); + } + if (m_toggleUIButton) { + connect(m_toggleUIButton, &QPushButton::toggled, + this, [this](){ toggleUI(); }); + } + if (m_helpButton) { + connect(m_helpButton, &QPushButton::toggled, + this, [this](bool visible){ toggleHelp(visible); }); + } + // for Effects, modelChanged only goes to the top EffectView + // we need to call it manually + modelChanged(); +} + + + + +Lv2FxControls *Lv2FxControlDialog::lv2Controls() +{ + return static_cast(m_effectControls); +} + + + + +void Lv2FxControlDialog::modelChanged() +{ + Lv2ViewBase::modelChanged(lv2Controls()); +} + + diff --git a/plugins/Lv2Effect/Lv2FxControlDialog.h b/plugins/Lv2Effect/Lv2FxControlDialog.h new file mode 100644 index 00000000000..3abdb300d4d --- /dev/null +++ b/plugins/Lv2Effect/Lv2FxControlDialog.h @@ -0,0 +1,47 @@ +/* + * Lv2FxControlDialog.h - Lv2FxControlDialog implementation + * + * Copyright (c) 2018-2020 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * 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 (at your option) 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. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LV2_FX_CONTROL_DIALOG_H +#define LV2_FX_CONTROL_DIALOG_H + +#include "EffectControlDialog.h" +#include "Lv2ViewBase.h" + +class Lv2FxControls; + + +class Lv2FxControlDialog : public EffectControlDialog, public Lv2ViewBase +{ + Q_OBJECT + +public: + Lv2FxControlDialog(Lv2FxControls *controls); + +private: + Lv2FxControls *lv2Controls(); + void modelChanged() override; +}; + + +#endif diff --git a/plugins/Lv2Effect/Lv2FxControls.cpp b/plugins/Lv2Effect/Lv2FxControls.cpp new file mode 100644 index 00000000000..a5733fec670 --- /dev/null +++ b/plugins/Lv2Effect/Lv2FxControls.cpp @@ -0,0 +1,104 @@ +/* + * Lv2FxControls.cpp - Lv2FxControls implementation + * + * Copyright (c) 2018-2020 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * 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 (at your option) 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. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "Lv2FxControls.h" + +#include + +#include "Engine.h" +#include "Lv2Effect.h" +#include "Lv2FxControlDialog.h" +#include "Lv2Proc.h" + + + + +Lv2FxControls::Lv2FxControls(class Lv2Effect *effect, const QString& uri) : + EffectControls(effect), + Lv2ControlBase(this, uri) +{ + if (isValid()) + { + connect(Engine::mixer(), &Mixer::sampleRateChanged, + this, [this](){Lv2ControlBase::reloadPlugin();}); + } +} + + + + +void Lv2FxControls::saveSettings(QDomDocument &doc, QDomElement &that) +{ + Lv2ControlBase::saveSettings(doc, that); +} + + + + +void Lv2FxControls::loadSettings(const QDomElement &that) +{ + Lv2ControlBase::loadSettings(that); +} + + + + +int Lv2FxControls::controlCount() +{ + return static_cast(Lv2ControlBase::controlCount()); +} + + + + +EffectControlDialog *Lv2FxControls::createView() +{ + return new Lv2FxControlDialog(this); +} + + + + +void Lv2FxControls::changeControl() // TODO: what is that? +{ + // engine::getSong()->setModified(); +} + + + + +DataFile::Types Lv2FxControls::settingsType() +{ + return DataFile::EffectSettings; +} + + + + +void Lv2FxControls::setNameFromFile(const QString &name) +{ + effect()->setDisplayName(name); +} + + diff --git a/plugins/Lv2Effect/Lv2FxControls.h b/plugins/Lv2Effect/Lv2FxControls.h new file mode 100644 index 00000000000..e5449a4f7fe --- /dev/null +++ b/plugins/Lv2Effect/Lv2FxControls.h @@ -0,0 +1,61 @@ +/* + * Lv2FxControls.h - Lv2FxControls implementation + * + * Copyright (c) 2018-2020 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * 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 (at your option) 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. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LV2_FX_CONTROLS_H +#define LV2_FX_CONTROLS_H + +#include "EffectControls.h" +#include "Lv2ControlBase.h" + +class Lv2Effect; + + +class Lv2FxControls : public EffectControls, public Lv2ControlBase +{ + Q_OBJECT +public: + Lv2FxControls(Lv2Effect *effect, const QString &uri); + + void saveSettings(QDomDocument &_doc, QDomElement &_parent) override; + void loadSettings(const QDomElement &that) override; + inline QString nodeName() const override + { + return Lv2ControlBase::nodeName(); + } + + int controlCount() override; + EffectControlDialog *createView() override; + +private slots: + void changeControl(); + +private: + DataFile::Types settingsType() override; + void setNameFromFile(const QString &name) override; + + friend class Lv2FxControlDialog; + friend class Lv2Effect; +}; + +#endif diff --git a/plugins/Lv2Effect/logo.png b/plugins/Lv2Effect/logo.png new file mode 100644 index 00000000000..c423ccea458 Binary files /dev/null and b/plugins/Lv2Effect/logo.png differ diff --git a/plugins/Lv2Instrument/CMakeLists.txt b/plugins/Lv2Instrument/CMakeLists.txt new file mode 100644 index 00000000000..290bd84e82e --- /dev/null +++ b/plugins/Lv2Instrument/CMakeLists.txt @@ -0,0 +1,7 @@ +IF(LMMS_HAVE_LV2) + INCLUDE_DIRECTORIES(${LV2_INCLUDE_DIRS}) + INCLUDE_DIRECTORIES(${LILV_INCLUDE_DIRS}) + INCLUDE_DIRECTORIES(${SUIL_INCLUDE_DIRS}) + INCLUDE(BuildPlugin) + BUILD_PLUGIN(lv2instrument Lv2Instrument.cpp Lv2Instrument.h MOCFILES Lv2Instrument.h EMBEDDED_RESOURCES logo.png) +ENDIF(LMMS_HAVE_LV2) diff --git a/plugins/Lv2Instrument/Lv2Instrument.cpp b/plugins/Lv2Instrument/Lv2Instrument.cpp new file mode 100644 index 00000000000..974aaf416b4 --- /dev/null +++ b/plugins/Lv2Instrument/Lv2Instrument.cpp @@ -0,0 +1,303 @@ +/* + * Lv2Instrument.cpp - implementation of LV2 instrument + * + * Copyright (c) 2018-2020 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * 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 (at your option) 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. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "Lv2Instrument.h" + +#include +#include + +#include "Engine.h" +#include "InstrumentPlayHandle.h" +#include "InstrumentTrack.h" +#include "Lv2SubPluginFeatures.h" +#include "Mixer.h" +#include "StringPairDrag.h" + +#include "embed.h" +#include "plugin_export.h" + + + + +Plugin::Descriptor PLUGIN_EXPORT lv2instrument_plugin_descriptor = +{ + STRINGIFY(PLUGIN_NAME), + "LV2", + QT_TRANSLATE_NOOP("pluginBrowser", + "plugin for using arbitrary LV2 instruments inside LMMS."), + "Johannes Lorenz ", + 0x0100, + Plugin::Instrument, + new PluginPixmapLoader("logo"), + nullptr, + new Lv2SubPluginFeatures(Plugin::Instrument) +}; + + + + +/* + Lv2Instrument +*/ + + +Lv2Instrument::Lv2Instrument(InstrumentTrack *instrumentTrackArg, + Descriptor::SubPluginFeatures::Key *key) : + Instrument(instrumentTrackArg, &lv2instrument_plugin_descriptor, key), + Lv2ControlBase(this, key->attributes["uri"]) +{ + if (Lv2ControlBase::isValid()) + { +#ifdef LV2_INSTRUMENT_USE_MIDI + for (int i = 0; i < NumKeys; ++i) { m_runningNotes[i] = 0; } +#endif + connect(instrumentTrack()->pitchRangeModel(), SIGNAL(dataChanged()), + this, SLOT(updatePitchRange()), Qt::DirectConnection); + connect(Engine::mixer(), &Mixer::sampleRateChanged, + this, [this](){Lv2ControlBase::reloadPlugin();}); + + // now we need a play-handle which cares for calling play() + InstrumentPlayHandle *iph = + new InstrumentPlayHandle(this, instrumentTrackArg); + Engine::mixer()->addPlayHandle(iph); + } +} + + + + +Lv2Instrument::~Lv2Instrument() +{ + Engine::mixer()->removePlayHandlesOfTypes(instrumentTrack(), + PlayHandle::TypeNotePlayHandle | + PlayHandle::TypeInstrumentPlayHandle); +} + + + + +bool Lv2Instrument::isValid() const { return Lv2ControlBase::isValid(); } + + + + +void Lv2Instrument::saveSettings(QDomDocument &doc, QDomElement &that) +{ + Lv2ControlBase::saveSettings(doc, that); +} + + + + +void Lv2Instrument::loadSettings(const QDomElement &that) +{ + Lv2ControlBase::loadSettings(that); +} + + + + +void Lv2Instrument::loadFile(const QString &file) +{ + Lv2ControlBase::loadFile(file); +} + + + + +#ifdef LV2_INSTRUMENT_USE_MIDI +bool Lv2Instrument::handleMidiEvent( + const MidiEvent &event, const MidiTime &time, f_cnt_t offset) +{ + // this function can be called from GUI threads while the plugin is running, + // so this requires caching, e.g. in ringbuffers + (void)time; + (void)offset; + (void)event; + return true; +} +#endif + + + + +// not yet working +#ifndef LV2_INSTRUMENT_USE_MIDI +void Lv2Instrument::playNote(NotePlayHandle *nph, sampleFrame *) +{ +} +#endif + + + + +void Lv2Instrument::play(sampleFrame *buf) +{ + copyModelsFromLmms(); + + fpp_t fpp = Engine::mixer()->framesPerPeriod(); + + run(fpp); + + copyBuffersToLmms(buf, fpp); + + instrumentTrack()->processAudioBuffer(buf, fpp, nullptr); +} + + + + +PluginView *Lv2Instrument::instantiateView(QWidget *parent) +{ + return new Lv2InsView(this, parent); +} + + + + +void Lv2Instrument::updatePitchRange() +{ + qDebug() << "Lmms: Cannot update pitch range for lv2 plugin:" + "not implemented yet"; +} + + + + +QString Lv2Instrument::nodeName() const +{ + return Lv2ControlBase::nodeName(); +} + + + + +DataFile::Types Lv2Instrument::settingsType() +{ + return DataFile::InstrumentTrackSettings; +} + + + + +void Lv2Instrument::setNameFromFile(const QString &name) +{ + instrumentTrack()->setName(name); +} + + + + +/* + Lv2InsView +*/ + + +Lv2InsView::Lv2InsView(Lv2Instrument *_instrument, QWidget *_parent) : + InstrumentView(_instrument, _parent), + Lv2ViewBase(this, _instrument) +{ + setAutoFillBackground(true); + if (m_reloadPluginButton) { + connect(m_reloadPluginButton, &QPushButton::clicked, + this, [this](){ castModel()->reloadPlugin();} ); + } + if (m_toggleUIButton) { + connect(m_toggleUIButton, &QPushButton::toggled, + this, [this](){ toggleUI(); }); + } + if (m_helpButton) { + connect(m_helpButton, &QPushButton::toggled, + this, [this](bool visible){ toggleHelp(visible); }); + } +} + + + + +void Lv2InsView::dragEnterEvent(QDragEnterEvent *_dee) +{ + void (QDragEnterEvent::*reaction)(void) = &QDragEnterEvent::ignore; + + if (_dee->mimeData()->hasFormat(StringPairDrag::mimeType())) + { + const QString txt = + _dee->mimeData()->data(StringPairDrag::mimeType()); + if (txt.section(':', 0, 0) == "pluginpresetfile") { + reaction = &QDragEnterEvent::acceptProposedAction; + } + } + + (_dee->*reaction)(); +} + + + + +void Lv2InsView::dropEvent(QDropEvent *_de) +{ + const QString type = StringPairDrag::decodeKey(_de); + const QString value = StringPairDrag::decodeValue(_de); + if (type == "pluginpresetfile") + { + castModel()->loadFile(value); + _de->accept(); + return; + } + _de->ignore(); +} + + + + +void Lv2InsView::toggleUI() +{ +} + + + + +void Lv2InsView::modelChanged() +{ + Lv2ViewBase::modelChanged(castModel()); +} + + + + +extern "C" +{ + +// necessary for getting instance out of shared lib +PLUGIN_EXPORT Plugin *lmms_plugin_main(Model *_parent, void *_data) +{ + using KeyType = Plugin::Descriptor::SubPluginFeatures::Key; + Lv2Instrument* ins = new Lv2Instrument( + static_cast(_parent), + static_cast(_data )); + if (!ins->isValid()) { delete ins; ins = nullptr; } + return ins; +} + +} diff --git a/plugins/Lv2Instrument/Lv2Instrument.h b/plugins/Lv2Instrument/Lv2Instrument.h new file mode 100644 index 00000000000..6451d49cdcf --- /dev/null +++ b/plugins/Lv2Instrument/Lv2Instrument.h @@ -0,0 +1,123 @@ +/* + * Lv2Instrument.h - implementation of LV2 instrument + * + * Copyright (c) 2018-2020 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * 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 (at your option) 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. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LV2_INSTRUMENT_H +#define LV2_INSTRUMENT_H + +#include + +#include "Instrument.h" +#include "InstrumentView.h" +#include "Note.h" +#include "Lv2ControlBase.h" +#include "Lv2ViewBase.h" + +// whether to use MIDI vs playHandle +// currently only MIDI works +#define LV2_INSTRUMENT_USE_MIDI + +class QPushButton; + + +class Lv2Instrument : public Instrument, public Lv2ControlBase +{ + Q_OBJECT +public: + /* + initialization + */ + Lv2Instrument(InstrumentTrack *instrumentTrackArg, + Descriptor::SubPluginFeatures::Key* key); + ~Lv2Instrument() override; + //! Must be checked after ctor or reload + bool isValid() const; + + /* + load/save + */ + void saveSettings(QDomDocument &doc, QDomElement &that) override; + void loadSettings(const QDomElement &that) override; + void loadFile(const QString &file) override; + + /* + realtime funcs + */ + bool hasNoteInput() const override { return false; /* not supported yet */ } +#ifdef LV2_INSTRUMENT_USE_MIDI + bool handleMidiEvent(const MidiEvent &event, + const MidiTime &time = MidiTime(), f_cnt_t offset = 0) override; +#else + void playNote(NotePlayHandle *nph, sampleFrame *) override; +#endif + void play(sampleFrame *buf) override; + + /* + misc + */ + Flags flags() const override + { +#ifdef LV2_INSTRUMENT_USE_MIDI + return IsSingleStreamed | IsMidiBased; +#else + return IsSingleStreamed; +#endif + } + PluginView *instantiateView(QWidget *parent) override; + +private slots: + void updatePitchRange(); + +private: + QString nodeName() const override; + DataFile::Types settingsType() override; + void setNameFromFile(const QString &name) override; + +#ifdef LV2_INSTRUMENT_USE_MIDI + int m_runningNotes[NumKeys]; +#endif + + friend class Lv2InsView; +}; + + +class Lv2InsView : public InstrumentView, public Lv2ViewBase +{ + Q_OBJECT +public: + Lv2InsView(Lv2Instrument *_instrument, QWidget *_parent); + +protected: + void dragEnterEvent(QDragEnterEvent *_dee) override; + void dropEvent(QDropEvent *_de) override; + +private slots: + void reloadPlugin(); + void toggleUI(); + +private: + void modelChanged() override; +}; + + +#endif // LV2_INSTRUMENT_H diff --git a/plugins/Lv2Instrument/logo.png b/plugins/Lv2Instrument/logo.png new file mode 100644 index 00000000000..c423ccea458 Binary files /dev/null and b/plugins/Lv2Instrument/logo.png differ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 59710926d86..3ebec349e60 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -90,6 +90,17 @@ IF(NOT ("${LAME_INCLUDE_DIRS}" STREQUAL "")) INCLUDE_DIRECTORIES("${LAME_INCLUDE_DIRS}") ENDIF() +IF(NOT ("${LV2_INCLUDE_DIRS}" STREQUAL "")) + INCLUDE_DIRECTORIES(${LV2_INCLUDE_DIRS}) +ENDIF() + +IF(NOT ("${LILV_INCLUDE_DIRS}" STREQUAL "")) + INCLUDE_DIRECTORIES(${LILV_INCLUDE_DIRS}) +ENDIF() + +IF(NOT ("${SUIL_INCLUDE_DIRS}" STREQUAL "")) + INCLUDE_DIRECTORIES(${SUIL_INCLUDE_DIRS}) +ENDIF() LIST(APPEND LMMS_SRCS "${RINGBUFFER_DIR}/src/lib/ringbuffer.cpp") # Use libraries in non-standard directories (e.g., another version of Qt) @@ -167,6 +178,9 @@ SET(LMMS_REQUIRED_LIBS ${LMMS_REQUIRED_LIBS} ${JACK_LIBRARIES} ${OGGVORBIS_LIBRARIES} ${LAME_LIBRARIES} + ${LV2_LIBRARIES} + ${SUIL_LIBRARIES} + ${LILV_LIBRARIES} ${SAMPLERATE_LIBRARIES} ${SNDFILE_LIBRARIES} ${EXTRA_LIBRARIES} diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index f1c183e3f55..42d0f6784ae 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -90,6 +90,13 @@ set(LMMS_SRCS core/audio/AudioSampleRecorder.cpp core/audio/AudioSdl.cpp + core/lv2/Lv2Basics.cpp + core/lv2/Lv2ControlBase.cpp + core/lv2/Lv2Ports.cpp + core/lv2/Lv2Proc.cpp + core/lv2/Lv2Manager.cpp + core/lv2/Lv2SubPluginFeatures.cpp + core/midi/MidiAlsaRaw.cpp core/midi/MidiAlsaSeq.cpp core/midi/MidiClient.cpp diff --git a/src/core/DataFile.cpp b/src/core/DataFile.cpp index 129c9738b3e..44cb920d844 100644 --- a/src/core/DataFile.cpp +++ b/src/core/DataFile.cpp @@ -166,6 +166,9 @@ bool DataFile::validate( QString extension ) ( extension == "xiz" && ! pluginFactory->pluginSupportingExtension(extension).isNull()) || extension == "sf2" || extension == "sf3" || extension == "pat" || extension == "mid" || extension == "dll" +#ifdef LMMS_HAVE_LV2 + || extension == "lv2" +#endif ) ) { return true; diff --git a/src/core/Engine.cpp b/src/core/Engine.cpp index ce82310fa4c..81cd0acdb17 100644 --- a/src/core/Engine.cpp +++ b/src/core/Engine.cpp @@ -28,6 +28,7 @@ #include "ConfigManager.h" #include "FxMixer.h" #include "Ladspa2LMMS.h" +#include "Lv2Manager.h" #include "Mixer.h" #include "Plugin.h" #include "PresetPreviewPlayHandle.h" @@ -41,6 +42,9 @@ FxMixer * LmmsCore::s_fxMixer = NULL; BBTrackContainer * LmmsCore::s_bbTrackContainer = NULL; Song * LmmsCore::s_song = NULL; ProjectJournal * LmmsCore::s_projectJournal = NULL; +#ifdef LMMS_HAVE_LV2 +Lv2Manager * LmmsCore::s_lv2Manager = nullptr; +#endif Ladspa2LMMS * LmmsCore::s_ladspaManager = NULL; void* LmmsCore::s_dndPluginKey = nullptr; DummyTrackContainer * LmmsCore::s_dummyTC = NULL; @@ -63,6 +67,10 @@ void LmmsCore::init( bool renderOnly ) s_fxMixer = new FxMixer; s_bbTrackContainer = new BBTrackContainer; +#ifdef LMMS_HAVE_LV2 + s_lv2Manager = new Lv2Manager; + s_lv2Manager->initPlugins(); +#endif s_ladspaManager = new Ladspa2LMMS; s_projectJournal->setJournalling( true ); @@ -95,6 +103,9 @@ void LmmsCore::destroy() deleteHelper( &s_fxMixer ); deleteHelper( &s_mixer ); +#ifdef LMMS_HAVE_LV2 + deleteHelper( &s_lv2Manager ); +#endif deleteHelper( &s_ladspaManager ); //delete ConfigManager::inst(); diff --git a/src/core/lv2/Lv2Basics.cpp b/src/core/lv2/Lv2Basics.cpp new file mode 100644 index 00000000000..b6be53a2cf6 --- /dev/null +++ b/src/core/lv2/Lv2Basics.cpp @@ -0,0 +1,49 @@ +/* + * Lv2Basics.cpp - basic Lv2 functions + * + * Copyright (c) 2019-2020 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * 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 (at your option) 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. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "Lv2Basics.h" + +#ifdef LMMS_HAVE_LV2 + +QString qStringFromPluginNode(const LilvPlugin* plug, + LilvNode* (*getFunc)(const LilvPlugin*)) +{ + return QString::fromUtf8( + lilv_node_as_string(AutoLilvNode((*getFunc)(plug)).get())); +} + +QString qStringFromPortName(const LilvPlugin* plug, const LilvPort* port) +{ + return QString::fromUtf8( + lilv_node_as_string(AutoLilvNode(lilv_port_get_name(plug, port)).get())); +} + +std::string stdStringFromPortName(const LilvPlugin* plug, const LilvPort* port) +{ + return std::string( + lilv_node_as_string(AutoLilvNode(lilv_port_get_name(plug, port)).get())); +} + +#endif // LMMS_HAVE_LV2 + diff --git a/src/core/lv2/Lv2ControlBase.cpp b/src/core/lv2/Lv2ControlBase.cpp new file mode 100644 index 00000000000..3f50325e7d2 --- /dev/null +++ b/src/core/lv2/Lv2ControlBase.cpp @@ -0,0 +1,191 @@ +/* + * Lv2ControlBase.cpp - Lv2 control base class + * + * Copyright (c) 2018-2020 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * 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 (at your option) 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. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "Lv2ControlBase.h" + +#ifdef LMMS_HAVE_LV2 + +#include + +#include "Engine.h" +#include "Lv2Manager.h" +#include "Lv2Proc.h" +#include "stdshims.h" + + + + +Plugin::PluginTypes Lv2ControlBase::check(const LilvPlugin *plugin, + std::vector &issues, bool printIssues) +{ + // for some reason, all checks can be done by one processor... + return Lv2Proc::check(plugin, issues, printIssues); +} + + + + +Lv2ControlBase::Lv2ControlBase(Model* that, const QString &uri) : + m_plugin(Engine::getLv2Manager()->getPlugin(uri)) +{ + if (m_plugin) + { + int channelsLeft = DEFAULT_CHANNELS; // LMMS plugins are stereo + while (channelsLeft > 0) + { + std::unique_ptr newOne = make_unique(m_plugin, that); + if (newOne->isValid()) + { + channelsLeft -= std::max( + 1 + static_cast(newOne->inPorts().m_right), + 1 + static_cast(newOne->outPorts().m_right)); + Q_ASSERT(channelsLeft >= 0); + m_procs.push_back(std::move(newOne)); + } + else + { + qCritical() << "Failed instantiating LV2 processor"; + m_valid = false; + channelsLeft = 0; + } + } + if (m_valid) + { + m_channelsPerProc = DEFAULT_CHANNELS / m_procs.size(); + linkAllModels(); + } + } + else + { + qCritical() << "No Lv2 plugin found for URI" << uri; + m_valid = false; + } +} + + + + +Lv2ControlBase::~Lv2ControlBase() {} + + + + +LinkedModelGroup *Lv2ControlBase::getGroup(std::size_t idx) +{ + return (m_procs.size() > idx) ? m_procs[idx].get() : nullptr; +} + + + + +const LinkedModelGroup *Lv2ControlBase::getGroup(std::size_t idx) const +{ + return (m_procs.size() > idx) ? m_procs[idx].get() : nullptr; +} + + + + +void Lv2ControlBase::copyModelsFromLmms() { + for (auto& c : m_procs) { c->copyModelsFromCore(); } +} + + + + +void Lv2ControlBase::copyBuffersFromLmms(const sampleFrame *buf, fpp_t frames) { + unsigned firstChan = 0; // tell the procs which channels they shall read from + for (auto& c : m_procs) { + c->copyBuffersFromCore(buf, firstChan, m_channelsPerProc, frames); + firstChan += m_channelsPerProc; + } +} + + + + +void Lv2ControlBase::copyBuffersToLmms(sampleFrame *buf, fpp_t frames) const { + unsigned firstChan = 0; // tell the procs which channels they shall write to + for (const auto& c : m_procs) { + c->copyBuffersToCore(buf, firstChan, m_channelsPerProc, frames); + firstChan += m_channelsPerProc; + } +} + + + + +void Lv2ControlBase::run(fpp_t frames) { + for (auto& c : m_procs) { c->run(frames); } +} + + + + +void Lv2ControlBase::saveSettings(QDomDocument &doc, QDomElement &that) +{ + LinkedModelGroups::saveSettings(doc, that); + + // TODO: save state if supported by plugin +} + + + + +void Lv2ControlBase::loadSettings(const QDomElement &that) +{ + LinkedModelGroups::loadSettings(that); + + // TODO: load state if supported by plugin +} + + + + +void Lv2ControlBase::loadFile(const QString &file) +{ + (void)file; +} + + + + +void Lv2ControlBase::reloadPlugin() +{ + // TODO +} + + + + +std::size_t Lv2ControlBase::controlCount() const { + std::size_t res = 0; + for (const auto& c : m_procs) { res += c->controlCount(); } + return res; +} + + + + +#endif // LMMS_HAVE_LV2 diff --git a/src/core/lv2/Lv2Manager.cpp b/src/core/lv2/Lv2Manager.cpp new file mode 100644 index 00000000000..bce3bf372ca --- /dev/null +++ b/src/core/lv2/Lv2Manager.cpp @@ -0,0 +1,163 @@ +/* + * Lv2Manager.cpp - Implementation of Lv2Manager class + * + * Copyright (c) 2018-2020 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * 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 (at your option) 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. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "Lv2Manager.h" + +#ifdef LMMS_HAVE_LV2 + +#include +#include +#include +#include +#include +#include +#include + +#include "ConfigManager.h" +#include "Plugin.h" +#include "PluginFactory.h" +#include "Lv2ControlBase.h" +#include "PluginIssue.h" + + + + +Lv2Manager::Lv2Manager() +{ + const char* dbgStr = getenv("LMMS_LV2_DEBUG"); + m_debug = (dbgStr && *dbgStr); + + m_world = lilv_world_new(); + lilv_world_load_all(m_world); +} + + + + +Lv2Manager::~Lv2Manager() +{ + lilv_world_free(m_world); +} + + + + +AutoLilvNode Lv2Manager::uri(const char *uriStr) +{ + return AutoLilvNode(lilv_new_uri(m_world, uriStr)); +} + + + + +const LilvPlugin *Lv2Manager::getPlugin(const std::string &uri) +{ + auto itr = m_lv2InfoMap.find(uri); + return itr == m_lv2InfoMap.end() ? nullptr : itr->second.plugin(); +} + + + + +const LilvPlugin *Lv2Manager::getPlugin(const QString &uri) +{ + return getPlugin(uri.toStdString()); +} + + + + +void Lv2Manager::initPlugins() +{ + const LilvPlugins* plugins = lilv_world_get_all_plugins(m_world); + std::size_t pluginCount = 0, pluginsLoaded = 0; + QElapsedTimer timer; + timer.start(); + + LILV_FOREACH(plugins, itr, plugins) + { + const LilvPlugin* curPlug = lilv_plugins_get(plugins, itr); + + std::vector issues; + Plugin::PluginTypes type = Lv2ControlBase::check(curPlug, issues, m_debug); + Lv2Info info(curPlug, type, issues.empty()); + + m_lv2InfoMap[lilv_node_as_uri(lilv_plugin_get_uri(curPlug))] + = std::move(info); + if(issues.empty()) { ++pluginsLoaded; } + ++pluginCount; + } + + qDebug() << "Lv2 plugin SUMMARY:" + << pluginsLoaded << "of" << pluginCount << " loaded in" + << timer.elapsed() << "msecs."; + if(pluginsLoaded != pluginCount) + { + if (m_debug) + { + qDebug() << + "If you don't want to see all this debug output, please set\n" + " environment variable \"LMMS_LV2_DEBUG\" to empty or\n" + " do not set it."; + } + else + { + qDebug() << + "For details about not loaded plugins, please set\n" + " environment variable \"LMMS_LV2_DEBUG\" to nonempty."; + } + } +} + + + + +// unused + untested yet +bool Lv2Manager::isSubclassOf(const LilvPluginClass* clvss, const char* uriStr) +{ + const LilvPluginClasses* allClasses = lilv_world_get_plugin_classes(m_world); + const LilvPluginClass* root = lilv_world_get_plugin_class(m_world); + const LilvPluginClass* search = lilv_plugin_classes_get_by_uri(allClasses, + uri(uriStr).get()); + + auto clssEq = [](const LilvPluginClass* pc1, + const LilvPluginClass* pc2) -> bool + { + return lilv_node_equals( + lilv_plugin_class_get_uri(pc1), + lilv_plugin_class_get_uri(pc2)); + }; + bool isFound = false; + while (!(isFound = clssEq(clvss, search)) && !clssEq(clvss, root)) + { + clvss = lilv_plugin_classes_get_by_uri(allClasses, + lilv_plugin_class_get_parent_uri(clvss)); + } + return isFound; +} + + + + +#endif // LMMS_HAVE_LV2 diff --git a/src/core/lv2/Lv2Ports.cpp b/src/core/lv2/Lv2Ports.cpp new file mode 100644 index 00000000000..ae2d26d4971 --- /dev/null +++ b/src/core/lv2/Lv2Ports.cpp @@ -0,0 +1,256 @@ +/* + * Lv2Ports.cpp - Lv2 port classes implementation + * + * Copyright (c) 2019-2020 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * 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 (at your option) 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. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + + +#include "Lv2Ports.h" + +#ifdef LMMS_HAVE_LV2 + +#include "Engine.h" +#include "Lv2Basics.h" +#include "Lv2Manager.h" + +namespace Lv2Ports { + + + + +const char *toStr(Flow pf) +{ + switch(pf) + { + case Flow::Unknown: return "unknown"; + case Flow::Input: return "input"; + case Flow::Output: return "output"; + } + return ""; +} + + + + +const char *toStr(Type pt) +{ + switch(pt) + { + case Type::Unknown: return "unknown"; + case Type::Control: return "control"; + case Type::Audio: return "audio"; + case Type::Event: return "event"; + case Type::Cv: return "cv"; + } + return ""; +} + + + + +const char *toStr(Vis pv) +{ + switch(pv) + { + case Vis::Toggled: return "toggled"; + case Vis::Enumeration: return "enumeration"; + case Vis::Integer: return "integer"; + case Vis::None: return "none"; + } + return ""; +} + + + + +std::vector Meta::get(const LilvPlugin *plugin, + std::size_t portNum) +{ + std::vector portIssues; + auto issue = [&portIssues](PluginIssueType i, std::string msg = "") { + portIssues.emplace_back(i, std::move(msg)); }; + + Lv2Manager* man = Engine::getLv2Manager(); + + const LilvPort* lilvPort = lilv_plugin_get_port_by_index( + plugin, static_cast(portNum)); + + auto portFunc = [&plugin, &lilvPort, &man]( + bool (*fptr)(const LilvPlugin*, const LilvPort*, const LilvNode*), + const char* str) { + return fptr(plugin, lilvPort, man->uri(str).get()); + }; + + auto hasProperty = [&portFunc](const char* str) { + return portFunc(lilv_port_has_property, str); }; + auto isA = [&portFunc](const char* str) { + return portFunc(lilv_port_is_a, str); }; + + const std::string portName = stdStringFromPortName(plugin, lilvPort); + + m_optional = hasProperty(LV2_CORE__connectionOptional); + + m_vis = hasProperty(LV2_CORE__integer) + ? Vis::Integer // WARNING: this may still be changed below + : hasProperty(LV2_CORE__enumeration) + ? Vis::Enumeration + : hasProperty(LV2_CORE__toggled) + ? Vis::Toggled + : Vis::None; + + if (isA(LV2_CORE__InputPort)) { m_flow = Flow::Input; } + else if (isA(LV2_CORE__OutputPort)) { m_flow = Flow::Output; } + else { + m_flow = Flow::Unknown; + issue(unknownPortFlow, portName); + } + + m_def = .0f; m_min = .0f; m_max = .0f; + + if (isA(LV2_CORE__ControlPort)) + { + m_type = Type::Control; + + if (m_flow == Flow::Input) + { + bool isToggle = m_vis == Vis::Toggled; + + LilvNode * defN, * minN = nullptr, * maxN = nullptr; + lilv_port_get_range(plugin, lilvPort, &defN, + isToggle ? nullptr : &minN, + isToggle ? nullptr : &maxN); + AutoLilvNode def(defN), min(minN), max(maxN); + + auto takeRangeValue = [&](LilvNode* node, + float& storeHere, PluginIssueType it) + { + if (node) { storeHere = lilv_node_as_float(node); } + else { issue(it, portName); } + }; + + takeRangeValue(def.get(), m_def, portHasNoDef); + if (!isToggle) + { + takeRangeValue(min.get(), m_min, portHasNoMin); + takeRangeValue(max.get(), m_max, portHasNoMax); + + if (m_max - m_min > 15.0f) + { + // range too large for spinbox visualisation, use knobs + // e.g. 0...15 would be OK + m_vis = Vis::None; + } + } + } + } + else if (isA(LV2_CORE__AudioPort)) { m_type = Type::Audio; } + else if (isA(LV2_CORE__CVPort)) { + issue(badPortType, "cvPort"); + m_type = Type::Cv; + } else { + if (m_optional) { m_used = false; } + else { + issue(PluginIssueType::unknownPortType, portName); + m_type = Type::Unknown; + } + } + + return portIssues; +} + + + + +QString PortBase::name() const +{ + AutoLilvNode node(lilv_port_get_name(m_plugin, m_port)); + QString res = lilv_node_as_string(node.get()); + return res; +} + + + + +QString PortBase::uri() const +{ + return lilv_node_as_string(lilv_port_get_symbol(m_plugin, m_port)); +} + + + + +Audio::Audio(std::size_t bufferSize, bool isSidechain, bool isOptional) + : m_buffer(bufferSize), m_sidechain(isSidechain), m_optional(isOptional) +{ +} + + + + +void Audio::copyBuffersFromCore(const sampleFrame *lmmsBuf, + unsigned channel, fpp_t frames) +{ + for (std::size_t f = 0; f < static_cast(frames); ++f) + { + m_buffer[f] = lmmsBuf[f][channel]; + } +} + + + + +void Audio::averageWithBuffersFromCore(const sampleFrame *lmmsBuf, + unsigned channel, fpp_t frames) +{ + for (std::size_t f = 0; f < static_cast(frames); ++f) + { + m_buffer[f] = (m_buffer[f] + lmmsBuf[f][channel]) / 2.0f; + } +} + + + + +void Audio::copyBuffersToCore(sampleFrame *lmmsBuf, + unsigned channel, fpp_t frames) const +{ + for (std::size_t f = 0; f < static_cast(frames); ++f) + { + lmmsBuf[f][channel] = m_buffer[f]; + } +} + + + + +// make the compiler happy, give each class with virtuals +// a function (the destructor here) which is in a cpp file +PortBase::~PortBase() {} +ConstVisitor::~ConstVisitor() {} +Visitor::~Visitor() {} + + + + +} // namespace Lv2Ports + +#endif // LMMS_HAVE_LV2 + diff --git a/src/core/lv2/Lv2Proc.cpp b/src/core/lv2/Lv2Proc.cpp new file mode 100644 index 00000000000..2d77f3f9835 --- /dev/null +++ b/src/core/lv2/Lv2Proc.cpp @@ -0,0 +1,544 @@ +/* + * Lv2Proc.cpp - Lv2 processor class + * + * Copyright (c) 2019-2020 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * 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 (at your option) 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. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "Lv2Proc.h" + +#ifdef LMMS_HAVE_LV2 + +#include + +#include "AutomatableModel.h" +#include "ComboBoxModel.h" +#include "Engine.h" +#include "Lv2Manager.h" +#include "Lv2Ports.h" +#include "Mixer.h" + + + + +Plugin::PluginTypes Lv2Proc::check(const LilvPlugin *plugin, + std::vector& issues, bool printIssues) +{ + unsigned maxPorts = lilv_plugin_get_num_ports(plugin); + enum { inCount, outCount, maxCount }; + unsigned audioChannels[maxCount] = { 0, 0 }; // input and output count + + for (unsigned portNum = 0; portNum < maxPorts; ++portNum) + { + Lv2Ports::Meta meta; + // does all port checks: + std::vector tmp = meta.get(plugin, portNum); + std::move(tmp.begin(), tmp.end(), std::back_inserter(issues)); + + bool portMustBeUsed = + !portIsSideChain(plugin, + lilv_plugin_get_port_by_index(plugin, portNum)) && + !portIsOptional(plugin, + lilv_plugin_get_port_by_index(plugin, portNum)); + if (meta.m_type == Lv2Ports::Type::Audio && portMustBeUsed) + ++audioChannels[meta.m_flow == Lv2Ports::Flow::Output + ? outCount : inCount]; + } + + if (audioChannels[inCount] > 2) + issues.emplace_back(tooManyInputChannels, + std::to_string(audioChannels[inCount])); + if (audioChannels[outCount] == 0) + issues.emplace_back(noOutputChannel); + else if (audioChannels[outCount] > 2) + issues.emplace_back(tooManyOutputChannels, + std::to_string(audioChannels[outCount])); + + AutoLilvNodes reqFeats(lilv_plugin_get_required_features(plugin)); + LILV_FOREACH (nodes, itr, reqFeats.get()) + { + issues.emplace_back(featureNotSupported, + lilv_node_as_string(lilv_nodes_get(reqFeats.get(), itr))); + } + + if (printIssues && issues.size()) + { + qDebug() << "Lv2 plugin" + << qStringFromPluginNode(plugin, lilv_plugin_get_name) + << "(URI:" + << lilv_node_as_uri(lilv_plugin_get_uri(plugin)) + << ") can not be loaded:"; + for (const PluginIssue& iss : issues) { qDebug() << " - " << iss; } + } + + return (audioChannels[inCount] > 2 || audioChannels[outCount] > 2) + ? Plugin::Undefined + : (audioChannels[inCount] > 0) + ? Plugin::Effect + : Plugin::Instrument; +} + + + + +Lv2Proc::Lv2Proc(const LilvPlugin *plugin, Model* parent) : + LinkedModelGroup(parent), + m_plugin(plugin) +{ + initPlugin(); +} + + + + +Lv2Proc::~Lv2Proc() { shutdownPlugin(); } + + + + +void Lv2Proc::dumpPorts() +{ + std::size_t num = 0; + for (const std::unique_ptr& port: m_ports) + { + (void)port; + dumpPort(num++); + } +} + + + + +void Lv2Proc::copyModelsFromCore() +{ + struct FloatFromModelVisitor : public ConstModelVisitor + { + const std::vector* m_scalePointMap; // in + float m_res; // out + void visit(const FloatModel& m) override { m_res = m.value(); } + void visit(const IntModel& m) override { + m_res = static_cast(m.value()); } + void visit(const BoolModel& m) override { + m_res = static_cast(m.value()); } + void visit(const ComboBoxModel& m) override { + m_res = (*m_scalePointMap)[static_cast(m.value())]; } + }; + + struct Copy : public Lv2Ports::Visitor + { + void visit(Lv2Ports::Control& ctrl) override + { + if (ctrl.m_flow == Lv2Ports::Flow::Input) + { + FloatFromModelVisitor ffm; + ffm.m_scalePointMap = &ctrl.m_scalePointMap; + ctrl.m_connectedModel->accept(ffm); + ctrl.m_val = ffm.m_res; + } + } + void visit(Lv2Ports::Cv& cv) override + { + if (cv.m_flow == Lv2Ports::Flow::Input) + { + FloatFromModelVisitor ffm; + ffm.m_scalePointMap = &cv.m_scalePointMap; + cv.m_connectedModel->accept(ffm); + // dirty fix, needs better interpolation + std::fill(cv.m_buffer.begin(), cv.m_buffer.end(), ffm.m_res); + } + } + } copy; + + for (const std::unique_ptr& port : m_ports) { + port->accept(copy); } +} + + + + +void Lv2Proc::copyBuffersFromCore(const sampleFrame *buf, + unsigned firstChan, unsigned num, + fpp_t frames) +{ + inPorts().m_left->copyBuffersFromCore(buf, firstChan, frames); + if (num > 1) + { + // if the caller requests to take input from two channels, but we only + // have one input channel... take medium of left and right for + // mono input + // (this happens if we have two outputs and only one input) + if (inPorts().m_right) + { + inPorts().m_right->copyBuffersFromCore(buf, firstChan + 1, frames); + } + else + { + inPorts().m_left->averageWithBuffersFromCore(buf, firstChan + 1, frames); + } + } +} + + + + +void Lv2Proc::copyBuffersToCore(sampleFrame* buf, + unsigned firstChan, unsigned num, + fpp_t frames) const +{ + outPorts().m_left->copyBuffersToCore(buf, firstChan + 0, frames); + if (num > 1) + { + // if the caller requests to copy into two channels, but we only have + // one output channel, duplicate our output + // (this happens if we have two inputs and only one output) + Lv2Ports::Audio* ap = outPorts().m_right + ? outPorts().m_right : outPorts().m_left; + ap->copyBuffersToCore(buf, firstChan + 1, frames); + } +} + + + + +void Lv2Proc::run(fpp_t frames) +{ + lilv_instance_run(m_instance, static_cast(frames)); +} + + + + +AutomatableModel *Lv2Proc::modelAtPort(const QString &uri) +{ + // unused currently + AutomatableModel *mod; + auto itr = m_connectedModels.find(uri.toUtf8().data()); + if (itr != m_connectedModels.end()) { mod = itr->second; } + else { mod = nullptr; } + return mod; +} + + + + +void Lv2Proc::initPlugin() +{ + createPorts(); + + m_instance = lilv_plugin_instantiate(m_plugin, + Engine::mixer()->processingSampleRate(), + nullptr); + + if (m_instance) + { + for (std::size_t portNum = 0; portNum < m_ports.size(); ++portNum) + connectPort(portNum); + lilv_instance_activate(m_instance); + } + else + { + qCritical() << "Failed to create an instance of" + << qStringFromPluginNode(m_plugin, lilv_plugin_get_name) + << "(URI:" + << lilv_node_as_uri(lilv_plugin_get_uri(m_plugin)) + << ")"; + m_valid = false; + } +} + + + + +void Lv2Proc::shutdownPlugin() +{ + lilv_instance_deactivate(m_instance); + lilv_instance_free(m_instance); + m_instance = nullptr; +} + + + + +void Lv2Proc::loadFileInternal(const QString &file) +{ + (void)file; +} + + + + +void Lv2Proc::createPort(std::size_t portNum) +{ + Lv2Ports::Meta meta; + meta.get(m_plugin, portNum); + + const LilvPort* lilvPort = lilv_plugin_get_port_by_index(m_plugin, + static_cast(portNum)); + Lv2Ports::PortBase* port; + if (meta.m_type == Lv2Ports::Type::Control) + { + Lv2Ports::Control* ctrl = new Lv2Ports::Control; + if (meta.m_flow == Lv2Ports::Flow::Input) + { + AutoLilvNode node(lilv_port_get_name(m_plugin, lilvPort)); + QString dispName = lilv_node_as_string(node.get()); + switch (meta.m_vis) + { + case Lv2Ports::Vis::None: + { + // allow ~1000 steps + float stepSize = (meta.m_max - meta.m_min) / 1000.0f; + + // make multiples of 0.01 (or 0.1 for larger values) + float minStep = (stepSize >= 1.0f) ? 0.1f : 0.01f; + stepSize -= fmodf(stepSize, minStep); + stepSize = std::max(stepSize, minStep); + + ctrl->m_connectedModel.reset( + new FloatModel(meta.m_def, meta.m_min, meta.m_max, + stepSize, nullptr, dispName)); + break; + } + case Lv2Ports::Vis::Integer: + ctrl->m_connectedModel.reset( + new IntModel(static_cast(meta.m_def), + static_cast(meta.m_min), + static_cast(meta.m_max), + nullptr, dispName)); + break; + case Lv2Ports::Vis::Enumeration: + { + ComboBoxModel* comboModel + = new ComboBoxModel( + nullptr, dispName); + LilvScalePoints* sps = + lilv_port_get_scale_points(m_plugin, lilvPort); + LILV_FOREACH(scale_points, i, sps) + { + const LilvScalePoint* sp = lilv_scale_points_get(sps, i); + ctrl->m_scalePointMap.push_back(lilv_node_as_float( + lilv_scale_point_get_value(sp))); + comboModel->addItem( + lilv_node_as_string( + lilv_scale_point_get_label(sp))); + } + lilv_scale_points_free(sps); + ctrl->m_connectedModel.reset(comboModel); + break; + } + case Lv2Ports::Vis::Toggled: + ctrl->m_connectedModel.reset( + new BoolModel(static_cast(meta.m_def), + nullptr, dispName)); + break; + } + } + port = ctrl; + } + else if (meta.m_type == Lv2Ports::Type::Audio) + { + Lv2Ports::Audio* audio = + new Lv2Ports::Audio( + static_cast( + Engine::mixer()->framesPerPeriod()), + portIsSideChain(m_plugin, lilvPort), + portIsOptional(m_plugin, lilvPort) + ); + port = audio; + } else { + port = new Lv2Ports::Unknown; + } + + // `meta` is of class `Lv2Ports::Meta` and `port` is of a child class + // we can now assign the `Lv2Ports::Meta` part of meta to ports, leaving + // the additional members of `port` unchanged + *static_cast(port) = meta; + port->m_port = lilvPort; + port->m_plugin = m_plugin; + + m_ports[portNum].reset(port); +} + + + + +void Lv2Proc::createPorts() +{ + // register ports at the processor after creation, + // i.e. link their data or count them + struct RegisterPort : public Lv2Ports::Visitor + { + Lv2Proc* m_proc; + + void visit(Lv2Ports::Control& ctrl) override + { + if (ctrl.m_flow == Lv2Ports::Flow::Input) + { + AutomatableModel* amo = ctrl.m_connectedModel.get(); + m_proc->m_connectedModels.emplace( + lilv_node_as_string(lilv_port_get_symbol( + m_proc->m_plugin, ctrl.m_port)), + amo); + m_proc->addModel(amo, ctrl.uri()); + } + } + + void visit(Lv2Ports::Audio& audio) override + { + if (audio.mustBeUsed()) + { + StereoPortRef dummy; + StereoPortRef* portRef = &dummy; + switch (audio.m_flow) + { + case Lv2Ports::Flow::Input: + portRef = &m_proc->m_inPorts; + break; + case Lv2Ports::Flow::Output: + portRef = &m_proc->m_outPorts; + break; + case Lv2Ports::Flow::Unknown: + break; + } + // in Lv2, leftPort is defined to be the first port + if (!portRef->m_left) { portRef->m_left = &audio; } + else if (!portRef->m_right) { portRef->m_right = &audio; } + } + } + }; + + std::size_t maxPorts = lilv_plugin_get_num_ports(m_plugin); + m_ports.resize(maxPorts); + + for (std::size_t portNum = 0; portNum < maxPorts; ++portNum) + { + createPort(portNum); + RegisterPort registerPort; + registerPort.m_proc = this; + m_ports[portNum]->accept(registerPort); + } + + // initially assign model values to port values + copyModelsFromCore(); + + // debugging: + //dumpPorts(); +} + + + + +struct ConnectPortVisitor : public Lv2Ports::Visitor +{ + std::size_t m_num; + LilvInstance* m_instance; + void connectPort(void* location) { + lilv_instance_connect_port(m_instance, + static_cast(m_num), location); + } + void visit(Lv2Ports::Control& ctrl) override { connectPort(&ctrl.m_val); } + void visit(Lv2Ports::Audio& audio) override + { + connectPort((audio.mustBeUsed()) ? audio.m_buffer.data() : nullptr); + } + void visit(Lv2Ports::Unknown&) override { connectPort(nullptr); } + ~ConnectPortVisitor() override; +}; + +ConnectPortVisitor::~ConnectPortVisitor() {} + +void Lv2Proc::connectPort(std::size_t num) +{ + ConnectPortVisitor connect; + connect.m_num = num; + connect.m_instance = m_instance; + m_ports[num]->accept(connect); +} + + + + +void Lv2Proc::dumpPort(std::size_t num) +{ + struct DumpPortDetail : public Lv2Ports::ConstVisitor + { + void visit(const Lv2Ports::Control& ctrl) override { + qDebug() << " control port"; + // output ports may be uninitialized yet, only print inputs + if (ctrl.m_flow == Lv2Ports::Flow::Input) + { + qDebug() << " value:" << ctrl.m_val; + } + } + void visit(const Lv2Ports::Audio& audio) override { + qDebug() << (audio.isSideChain() ? " audio port (sidechain)" + : " audio port"); + qDebug() << " buffer size:" << audio.bufferSize(); + } + }; + + const Lv2Ports::PortBase& port = *m_ports[num]; + qDebug().nospace() << "port " << num << ":"; + qDebug() << " name:" + << qStringFromPortName(m_plugin, port.m_port); + qDebug() << " flow: " << Lv2Ports::toStr(port.m_flow); + qDebug() << " type: " << Lv2Ports::toStr(port.m_type); + qDebug() << " visualization: " << Lv2Ports::toStr(port.m_vis); + if (port.m_type == Lv2Ports::Type::Control || port.m_type == Lv2Ports::Type::Cv) + { + qDebug() << " default:" << port.m_def; + qDebug() << " min:" << port.m_min; + qDebug() << " max:" << port.m_max; + } + qDebug() << " optional: " << port.m_optional; + qDebug() << " => USED: " << port.m_used; + + DumpPortDetail dumper; + port.accept(dumper); +} + + + + +bool Lv2Proc::portIsSideChain(const LilvPlugin *plugin, const LilvPort *port) +{ + return lilv_port_has_property(plugin, port, + uri(LV2_CORE_PREFIX "isSidechain").get()); +} + + + + +bool Lv2Proc::portIsOptional(const LilvPlugin *plugin, const LilvPort *port) +{ + return lilv_port_has_property(plugin, port, + uri(LV2_CORE__connectionOptional).get()); +} + + + + +AutoLilvNode Lv2Proc::uri(const char *uriStr) +{ + return Engine::getLv2Manager()->uri(uriStr); +} + + +#endif // LMMS_HAVE_LV2 diff --git a/src/core/lv2/Lv2SubPluginFeatures.cpp b/src/core/lv2/Lv2SubPluginFeatures.cpp new file mode 100644 index 00000000000..3f86c5324e2 --- /dev/null +++ b/src/core/lv2/Lv2SubPluginFeatures.cpp @@ -0,0 +1,184 @@ +/* + * Lv2SubPluginFeatures.cpp - derivation from + * Plugin::Descriptor::SubPluginFeatures for + * hosting LV2 plugins + * + * Copyright (c) 2018-2020 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * 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 (at your option) 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. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "Lv2SubPluginFeatures.h" + +#ifdef LMMS_HAVE_LV2 + +#include +#include +#include + +#include "Engine.h" +#include "Lv2Basics.h" +#include "Lv2Manager.h" + + +const LilvPlugin *Lv2SubPluginFeatures::getPlugin( + const Plugin::Descriptor::SubPluginFeatures::Key &k) +{ + const LilvPlugin* result = Engine::getLv2Manager()-> + getPlugin(k.attributes["uri"]); + Q_ASSERT(result); + return result; +} + + + + +QString Lv2SubPluginFeatures::pluginName(const LilvPlugin *plug) +{ + return qStringFromPluginNode(plug, lilv_plugin_get_name); +} + + + + +Lv2SubPluginFeatures::Lv2SubPluginFeatures(Plugin::PluginTypes type) : + SubPluginFeatures(type) +{ +} + + + + +void Lv2SubPluginFeatures::fillDescriptionWidget(QWidget *parent, + const Key *k) const +{ + const LilvPlugin *plug = getPlugin(*k); + + QLabel *label = new QLabel(parent); + label->setText(QWidget::tr("Name: ") + pluginName(plug)); + + QLabel *label2 = new QLabel(parent); + label2->setText(QWidget::tr("URI: ") + + lilv_node_as_uri(lilv_plugin_get_uri(plug))); + + QWidget *maker = new QWidget(parent); + QHBoxLayout *l = new QHBoxLayout(maker); + l->setMargin(0); + l->setSpacing(0); + + QLabel *maker_label = new QLabel(maker); + maker_label->setText(QWidget::tr("Maker: ")); + maker_label->setAlignment(Qt::AlignTop); + + QLabel *maker_content = new QLabel(maker); + maker_content->setText( + qStringFromPluginNode(plug, lilv_plugin_get_author_name)); + maker_content->setWordWrap(true); + + l->addWidget(maker_label); + l->addWidget(maker_content, 1); + + QWidget *copyright = new QWidget(parent); + l = new QHBoxLayout(copyright); + l->setMargin(0); + l->setSpacing(0); + copyright->setMinimumWidth(parent->minimumWidth()); + + QLabel *copyright_label = new QLabel(copyright); + copyright_label->setText(QWidget::tr("Copyright: ")); + copyright_label->setAlignment(Qt::AlignTop); + + QLabel *copyright_content = new QLabel(copyright); + copyright_content->setText(""); + copyright_content->setWordWrap(true); + l->addWidget(copyright_label); + l->addWidget(copyright_content, 1); + + AutoLilvNodes extensions(lilv_plugin_get_extension_data(plug)); + (void)extensions; + // possibly TODO: version, project, plugin type, number of channels +} + + + + +QString Lv2SubPluginFeatures::additionalFileExtensions( + const Plugin::Descriptor::SubPluginFeatures::Key &k) const +{ + (void)k; + // lv2 only loads .lv2 files + // maybe add conversions later, e.g. for loading xmz + return QString(); +} + + + + +QString Lv2SubPluginFeatures::displayName( + const Plugin::Descriptor::SubPluginFeatures::Key &k) const +{ + return pluginName(getPlugin(k)); +} + + + + +QString Lv2SubPluginFeatures::description( + const Plugin::Descriptor::SubPluginFeatures::Key &k) const +{ + (void)k; + return QString::fromUtf8("description not implemented yet"); // TODO +} + + + + +const PixmapLoader *Lv2SubPluginFeatures::logo( + const Plugin::Descriptor::SubPluginFeatures::Key &k) const +{ + (void)k; // TODO + return nullptr; +} + + + + +void Lv2SubPluginFeatures::listSubPluginKeys(const Plugin::Descriptor *desc, + KeyList &kl) const +{ + Lv2Manager *lv2Mgr = Engine::getLv2Manager(); + for (const auto &uriInfoPair : *lv2Mgr) + { + if (uriInfoPair.second.type() == m_type && uriInfoPair.second.isValid()) + { + using KeyType = + Plugin::Descriptor::SubPluginFeatures::Key; + KeyType::AttributeMap atm; + atm["uri"] = QString::fromUtf8(uriInfoPair.first.c_str()); + const LilvPlugin* plug = uriInfoPair.second.plugin(); + + kl.push_back(KeyType(desc, pluginName(plug), atm)); + //qDebug() << "Found LV2 sub plugin key of type" << + // m_type << ":" << pr.first.c_str(); + } + } +} + +#endif // LMMS_HAVE_LV2 + diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 759be387290..81e588c6659 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -19,6 +19,7 @@ SET(LMMS_SRCS gui/LfoControllerDialog.cpp gui/LmmsPalette.cpp gui/LmmsStyle.cpp + gui/Lv2ViewBase.cpp gui/MainApplication.cpp gui/MainWindow.cpp gui/MidiSetupWidget.cpp diff --git a/src/gui/FileBrowser.cpp b/src/gui/FileBrowser.cpp index c976c39f689..b37bf03f207 100644 --- a/src/gui/FileBrowser.cpp +++ b/src/gui/FileBrowser.cpp @@ -457,7 +457,11 @@ void FileBrowserTreeWidget::mousePressEvent(QMouseEvent * me ) m_previewPlayHandle = s; delete tf; } - else if( ( f->extension ()== "xiz" || f->extension() == "sf2" || f->extension() == "sf3" || f->extension() == "gig" || f->extension() == "pat" ) && + else if ( ( f->extension ()== "xiz" || f->extension() == "sf2" || f->extension() == "sf3" || f->extension() == "gig" || f->extension() == "pat" +#ifdef LMMS_HAVE_LV2 + || f->extension() == "lv2" +#endif + ) && ! pluginFactory->pluginSupportingExtension(f->extension()).info.isNull() ) { m_previewPlayHandle = new PresetPreviewPlayHandle( f->fullName(), f->handling() == FileItem::LoadByPlugin ); @@ -1069,6 +1073,11 @@ void FileItem::determineFileType( void ) m_type = VstPluginFile; m_handling = LoadByPlugin; } + else if ( ext == "lv2" ) + { + m_type = PresetFile; + m_handling = LoadByPlugin; + } else { m_type = UnknownFile; diff --git a/src/gui/Lv2ViewBase.cpp b/src/gui/Lv2ViewBase.cpp new file mode 100644 index 00000000000..9b9217e48b2 --- /dev/null +++ b/src/gui/Lv2ViewBase.cpp @@ -0,0 +1,237 @@ +/* + * Lv2ViewBase.cpp - base class for Lv2 plugin views + * + * Copyright (c) 2018-2020 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * 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 (at your option) 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. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "Lv2ViewBase.h" + +#ifdef LMMS_HAVE_LV2 + +#include +#include +#include +#include +#include +#include + +#include "Controls.h" +#include "Engine.h" +#include "GuiApplication.h" +#include "embed.h" +#include "gui_templates.h" +#include "LedCheckbox.h" +#include "Lv2ControlBase.h" +#include "Lv2Manager.h" +#include "Lv2Proc.h" +#include "Lv2Ports.h" +#include "MainWindow.h" +#include "SubWindow.h" + + + + +Lv2ViewProc::Lv2ViewProc(QWidget* parent, Lv2Proc* ctrlBase, int colNum) : + LinkedModelGroupView (parent, ctrlBase, colNum) +{ + class SetupWidget : public Lv2Ports::ConstVisitor + { + public: + QWidget* m_par; // input + const LilvNode* m_commentUri; // input + Control* m_control = nullptr; // output + void visit(const Lv2Ports::Control& port) override + { + if (port.m_flow == Lv2Ports::Flow::Input) + { + using PortVis = Lv2Ports::Vis; + + switch (port.m_vis) + { + case PortVis::None: + m_control = new KnobControl(m_par); + break; + case PortVis::Integer: + m_control = new LcdControl((port.m_max <= 9.0f) ? 1 : 2, + m_par); + break; + case PortVis::Enumeration: + m_control = new ComboControl(m_par); + break; + case PortVis::Toggled: + m_control = new CheckControl(m_par); + break; + } + m_control->setText(port.name()); + + AutoLilvNodes props(lilv_port_get_value( + port.m_plugin, port.m_port, m_commentUri)); + LILV_FOREACH(nodes, itr, props.get()) + { + const LilvNode* nod = lilv_nodes_get(props.get(), itr); + m_control->topWidget()->setToolTip(lilv_node_as_string(nod)); + break; + } + } + } + }; + + AutoLilvNode commentUri = uri(LILV_NS_RDFS "comment"); + ctrlBase->foreach_port( + [this, &commentUri](const Lv2Ports::PortBase* port) + { + SetupWidget setup; + setup.m_par = this; + setup.m_commentUri = commentUri.get(); + port->accept(setup); + + if (setup.m_control) + { + addControl(setup.m_control, + lilv_node_as_string(lilv_port_get_symbol( + port->m_plugin, port->m_port)), + port->name().toUtf8().data(), + false); + } + }); +} + + + + +Lv2ViewProc::~Lv2ViewProc() {} + + + + +AutoLilvNode Lv2ViewProc::uri(const char *uriStr) +{ + return Engine::getLv2Manager()->uri(uriStr); +} + + + + +Lv2ViewBase::Lv2ViewBase(QWidget* meAsWidget, Lv2ControlBase *ctrlBase) +{ + QGridLayout* grid = new QGridLayout(meAsWidget); + + QHBoxLayout* btnBox = new QHBoxLayout(); + if (/* DISABLES CODE */ (false)) + { + m_reloadPluginButton = new QPushButton(QObject::tr("Reload Plugin"), + meAsWidget); + btnBox->addWidget(m_reloadPluginButton, 0); + } + + if (/* DISABLES CODE */ (false)) // TODO: check if the plugin has the UI extension + { + m_toggleUIButton = new QPushButton(QObject::tr("Show GUI"), + meAsWidget); + m_toggleUIButton->setCheckable(true); + m_toggleUIButton->setChecked(false); + m_toggleUIButton->setIcon(embed::getIconPixmap("zoom")); + m_toggleUIButton->setFont( + pointSize<8>(m_toggleUIButton->font())); + btnBox->addWidget(m_toggleUIButton, 0); + } + btnBox->addStretch(1); + + meAsWidget->setAcceptDrops(true); + + // note: the lifetime of C++ objects ends after the top expression in the + // expression syntax tree, so the AutoLilvNode gets freed after the function + // has been called + AutoLilvNodes props(lilv_plugin_get_value(ctrlBase->getPlugin(), + uri(LILV_NS_RDFS "comment").get())); + LILV_FOREACH(nodes, itr, props.get()) + { + const LilvNode* node = lilv_nodes_get(props.get(), itr); + QLabel* infoLabel = new QLabel(lilv_node_as_string(node)); + infoLabel->setWordWrap(true); + infoLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding); + + m_helpButton = new QPushButton(QObject::tr("Help")); + m_helpButton->setCheckable(true); + btnBox->addWidget(m_helpButton); + + m_helpWindow = gui->mainWindow()->addWindowedWidget(infoLabel); + m_helpWindow->setSizePolicy(QSizePolicy::Minimum, + QSizePolicy::Expanding); + m_helpWindow->setAttribute(Qt::WA_DeleteOnClose, false); + m_helpWindow->hide(); + + break; + } + + if (m_reloadPluginButton || m_toggleUIButton || m_helpButton) + { + grid->addLayout(btnBox, Rows::ButtonRow, 0, 1, m_colNum); + } + else { delete btnBox; } + + m_procView = new Lv2ViewProc(meAsWidget, ctrlBase->control(0), m_colNum); + grid->addWidget(m_procView, Rows::ProcRow, 0); +} + + + + +Lv2ViewBase::~Lv2ViewBase() { + // TODO: hide UI if required +} + + + + +void Lv2ViewBase::toggleHelp(bool visible) +{ + if (m_helpWindow) + { + if (visible) { m_helpWindow->show(); m_helpWindow->raise(); } + else { m_helpWindow->hide(); } + } +} + + + + +void Lv2ViewBase::modelChanged(Lv2ControlBase *ctrlBase) +{ + // reconnect models + if (m_toggleUIButton) + { + m_toggleUIButton->setChecked(ctrlBase->hasGui()); + } + + LinkedModelGroupsView::modelChanged(ctrlBase); +} + + + + +AutoLilvNode Lv2ViewBase::uri(const char *uriStr) +{ + return Engine::getLv2Manager()->uri(uriStr); +} + + +#endif // LMMS_HAVE_LV2 diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index bf8863ec616..2be265e047b 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -138,7 +138,7 @@ MainWindow::MainWindow() : sideBar->appendTab( new FileBrowser( confMgr->userPresetsDir() + "*" + confMgr->factoryPresetsDir(), - "*.xpf *.cs.xml *.xiz", + "*.xpf *.cs.xml *.xiz *.lv2", tr( "My Presets" ), embed::getIconPixmap( "preset_file" ).transformed( QTransform().rotate( 90 ) ), splitter , false, true ) ); diff --git a/src/lmmsconfig.h.in b/src/lmmsconfig.h.in index 3ea9d749c0c..86882d22e25 100644 --- a/src/lmmsconfig.h.in +++ b/src/lmmsconfig.h.in @@ -13,6 +13,8 @@ #cmakedefine LMMS_HAVE_FLUIDSYNTH #cmakedefine LMMS_HAVE_JACK #cmakedefine LMMS_HAVE_WEAKJACK +#cmakedefine LMMS_HAVE_LV2 +#cmakedefine LMMS_HAVE_SUIL #cmakedefine LMMS_HAVE_MP3LAME #cmakedefine LMMS_HAVE_OGGVORBIS #cmakedefine LMMS_HAVE_OSS