diff --git a/CMakeLists.txt b/CMakeLists.txt index a3fcf4050fe..b1c7700ecbf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,6 +60,7 @@ OPTION(WANT_CALF "Include CALF LADSPA plugins" ON) OPTION(WANT_CAPS "Include C* Audio Plugin Suite (LADSPA plugins)" ON) OPTION(WANT_CARLA "Include Carla plugin" ON) OPTION(WANT_CMT "Include Computer Music Toolkit LADSPA plugins" ON) +OPTION(WANT_SPA "Include SPA 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) @@ -276,6 +277,25 @@ ELSE(WANT_TAP) ENDIF(WANT_TAP) +IF(WANT_SPA) + IF(PKG_CONFIG_FOUND) + PKG_CHECK_MODULES(LIBSPA spa) + IF(${LIBSPA_FOUND}) + INCLUDE_DIRECTORIES(${LIBSPA_INCLUDE_DIRS}) + LINK_DIRECTORIES(${LIBSPA_LIBRARY_DIRS}) + SET(LMMS_HAVE_SPA TRUE) + SET(STATUS_SPA "OK") + ELSE() + SET(STATUS_SPA "not found, install it or set PKG_CONFIG_PATH appropriately") + ENDIF() + ELSE() + SET(STATUS_SPA "not found, requires pkg-config") + ENDIF() +ELSE(WANT_SPA) + SET(STATUS_SPA "not built as requested") +ENDIF(WANT_SPA) + + # check for CARLA IF(WANT_CARLA) PKG_CHECK_MODULES(CARLA carla-native-plugin) @@ -764,6 +784,7 @@ MESSAGE( "* CMT LADSPA plugins : ${STATUS_CMT}\n" "* TAP LADSPA plugins : ${STATUS_TAP}\n" "* SWH LADSPA plugins : ${STATUS_SWH}\n" +"* SPA plugin API : ${STATUS_SPA}\n" "* GIG player : ${STATUS_GIG}\n" ) diff --git a/cmake/modules/PluginList.cmake b/cmake/modules/PluginList.cmake index 151c5bd66a2..0c1ecb05c87 100644 --- a/cmake/modules/PluginList.cmake +++ b/cmake/modules/PluginList.cmake @@ -58,6 +58,8 @@ SET(LMMS_PLUGIN_LIST Sf2Player Sfxr Sid + SpaEffect + SpaInstrument SpectrumAnalyzer StereoEnhancer StereoMatrix diff --git a/include/Clipboard.h b/include/Clipboard.h index 1c2dcb647cd..819a7984367 100644 --- a/include/Clipboard.h +++ b/include/Clipboard.h @@ -27,6 +27,7 @@ #include #include +class QMimeData; class QMimeData; @@ -36,6 +37,7 @@ namespace lmms::Clipboard enum class MimeType { StringPair, + Osc, Default }; @@ -58,11 +60,11 @@ namespace lmms::Clipboard { case MimeType::StringPair: return "application/x-lmms-stringpair"; - break; + case MimeType::Osc: + return "application/x-osc-stringpair"; case MimeType::Default: default: return "application/x-lmms-clipboard"; - break; } } diff --git a/include/ConfigManager.h b/include/ConfigManager.h index 52e31850890..07549ebdbc4 100644 --- a/include/ConfigManager.h +++ b/include/ConfigManager.h @@ -205,6 +205,12 @@ class LMMS_EXPORT ConfigManager : public QObject return m_dataDir + TRACK_ICON_PATH; } + const QString & spaDir() const + { + return m_spaDir; + } + + const QString recoveryFile() const { return m_workingDir + "recover.mmp"; @@ -258,6 +264,7 @@ class LMMS_EXPORT ConfigManager : public QObject void setLADSPADir(const QString & ladspaDir); void setSF2Dir(const QString & sf2Dir); void setSF2File(const QString & sf2File); + void setSPADir(const QString & sd); void setSTKDir(const QString & stkDir); void setGIGDir(const QString & gigDir); void setThemeDir(const QString & themeDir); @@ -288,6 +295,7 @@ class LMMS_EXPORT ConfigManager : public QObject QString m_dataDir; QString m_vstDir; QString m_ladspaDir; + QString m_spaDir; QString m_sf2Dir; #ifdef LMMS_HAVE_FLUIDSYNTH QString m_sf2File; diff --git a/include/ControllerRackView.h b/include/ControllerRackView.h index a522071c822..68d115d0a11 100644 --- a/include/ControllerRackView.h +++ b/include/ControllerRackView.h @@ -71,6 +71,8 @@ public slots: protected: void closeEvent( QCloseEvent * _ce ) override; + virtual void dragEnterEvent( QDragEnterEvent *dee ); + virtual void dropEvent( QDropEvent * de ); private slots: void addController(); diff --git a/include/ControllerView.h b/include/ControllerView.h index 8b8db0674a0..1d86d662059 100644 --- a/include/ControllerView.h +++ b/include/ControllerView.h @@ -75,7 +75,8 @@ public slots: void contextMenuEvent( QContextMenuEvent * _me ) override; void modelChanged() override; void mouseDoubleClickEvent( QMouseEvent * event ) override; - + virtual void dragEnterEvent( QDragEnterEvent *dee ); + virtual void dropEvent( QDropEvent * de ); private: QMdiSubWindow * m_subWindow; diff --git a/include/Engine.h b/include/Engine.h index b63308cde35..e86895eb9ae 100644 --- a/include/Engine.h +++ b/include/Engine.h @@ -97,6 +97,15 @@ class LMMS_EXPORT Engine : public QObject return s_ladspaManager; } +#ifdef LMMS_HAVE_SPA + static class SpaManager * getSPAManager() + { + return s_spaManager; + } +#endif + + static void addPluginByPort(unsigned port, class Plugin* plug); + static float framesPerTick() { return s_framesPerTick; @@ -115,6 +124,8 @@ class LMMS_EXPORT Engine : public QObject return s_instanceOfMe; } + static class AutomatableModel* + getAutomatableModel(const QString &val, bool hasPort); static void setDndPluginKey(void* newKey); static void* pickDndPluginKey(); @@ -123,6 +134,9 @@ class LMMS_EXPORT Engine : public QObject private: + static class AutomatableModel* + getAutomatableModelAtPort(const QString& val, const QUrl& url); + // small helper function which sets the pointer to NULL before actually deleting // the object it refers to template @@ -146,6 +160,10 @@ class LMMS_EXPORT Engine : public QObject static class Lv2Manager* s_lv2Manager; #endif static Ladspa2LMMS* s_ladspaManager; +#ifdef LMMS_HAVE_SPA + static class SpaManager* s_spaManager; +#endif + static QMap s_pluginsByPort; static void* s_dndPluginKey; // even though most methods are static, an instance is needed for Qt slots/signals diff --git a/include/Plugin.h b/include/Plugin.h index da2deeaed5d..2396c29583a 100644 --- a/include/Plugin.h +++ b/include/Plugin.h @@ -297,6 +297,22 @@ class LMMS_EXPORT Plugin : public Model, public JournallingObject //! Create a view for the model gui::PluginView * createView( QWidget * parent ); + //! If the plugin offers to identify controls as strings (aka "ports", + //! like OSC does), this shall return the AutomatableModel for given + //! port or nullptr if there's none at that port + virtual class AutomatableModel *modelAtPort(const class QString &) + { + return nullptr; + } + + //! If the plugin has a network port where it can be reached, this + //! should return that port; if not, it should return 0 + virtual unsigned netPort(std::size_t channel) const + { + (void)channel; + return 0; + } + protected: //! Create a view for the model virtual gui::PluginView* instantiateView( QWidget * ) = 0; diff --git a/include/SetupDialog.h b/include/SetupDialog.h index 9ce6e043d8e..d1a373e6ac6 100644 --- a/include/SetupDialog.h +++ b/include/SetupDialog.h @@ -87,6 +87,7 @@ private slots: void toggleDisableBackup(bool enabled); void toggleOpenLastProject(bool enabled); void setLanguage(int lang); + void setSPADir(const QString& ld); // Performance settings widget. void setAutoSaveInterval(int time); @@ -116,6 +117,7 @@ private slots: void setVSTDir(const QString & vstDir); void openLADSPADir(); void setLADSPADir(const QString & ladspaDir); + void openSPADir(); void openSF2Dir(); void setSF2Dir(const QString & sf2Dir); void openSF2File(); @@ -192,6 +194,7 @@ private slots: QString m_workingDir; QString m_vstDir; QString m_ladspaDir; + QString m_spaDir; QString m_gigDir; QString m_sf2Dir; #ifdef LMMS_HAVE_FLUIDSYNTH @@ -204,6 +207,7 @@ private slots: QLineEdit * m_vstDirLineEdit; QLineEdit * m_themeDirLineEdit; QLineEdit * m_ladspaDirLineEdit; + QLineEdit * m_spaLineEdit; QLineEdit * m_gigDirLineEdit; QLineEdit * m_sf2DirLineEdit; #ifdef LMMS_HAVE_FLUIDSYNTH diff --git a/include/SpaControlBase.h b/include/SpaControlBase.h new file mode 100644 index 00000000000..846da17f4f8 --- /dev/null +++ b/include/SpaControlBase.h @@ -0,0 +1,121 @@ +/* + * SpaControlBase.h - implementation of SPA interface + * + * Copyright (c) 2018-2019 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 SPA_CONTROL_BASE_H +#define SPA_CONTROL_BASE_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_SPA + +//#include +#include +#include +#include + +// general LMMS includes +#include "DataFile.h" +#include "LinkedModelGroups.h" +#include "lmms_basics.h" + +// includes from the spa library +#include + +namespace lmms +{ + +namespace gui +{ + class SpaViewBase; +} + +class SpaProc; + +class SpaControlBase : public LinkedModelGroups +{ + friend class gui::SpaViewBase; +public: + SpaControlBase(Model *that, const QString &uniqueName, + DataFile::Types settingsType); + ~SpaControlBase() override; + + std::vector>& controls() { return m_procs; } + + void saveSettings(QDomDocument &doc, QDomElement &that); + void loadSettings(const QDomElement &that); + +// void writeOsc(const char *dest, const char *args, va_list va) {} +// void writeOsc(const char *dest, const char *args, ...) {} + + void loadFile(const QString &file, bool user); + + const spa::descriptor *m_spaDescriptor = nullptr; + bool hasUi() const; + void uiExtShow(bool doShow); + void copyModelsFromLmms(); + void copyBuffersFromLmms(const sampleFrame *buf, fpp_t frames); + void copyBuffersToLmms(sampleFrame *buf, fpp_t frames) const; + void run(unsigned frames); + + AutomatableModel *modelAtPort(const QString &dest); + void writeOscToAll(const char *dest, const char *args, va_list va); + void writeOscToAll(const char *dest, const char *args...); +protected: + void reloadPlugin() { /* TODO */ } + bool isValid() { return m_valid; } + +private: + bool m_valid = true; + + virtual void setNameFromFile(const QString &fname) = 0; + + Model* m_that; + +protected: + + LinkedModelGroup* getGroup(std::size_t idx) override; + const LinkedModelGroup* getGroup(std::size_t idx) const override; + +/* bool initPlugin() {} + void shutdownPlugin() {}*/ + + bool m_hasGUI = false; + bool m_loaded; + + QString nodeName() const { return "spacontrols"; } + + std::vector> m_procs; + + void handleMidiInputEvent(const class MidiEvent &event, + const class TimePos &time, f_cnt_t offset); + +private: + unsigned m_channelsPerProc; +}; + +} // namespace lmms + +#endif // LMMS_HAVE_SPA + +#endif // SPA_CONTROL_BASE_H diff --git a/include/SpaManager.h b/include/SpaManager.h new file mode 100644 index 00000000000..a4fae7f0b8d --- /dev/null +++ b/include/SpaManager.h @@ -0,0 +1,97 @@ +/* + * SpaManager.h - Implementation of SpaManager class + * + * Copyright (c) 2018-2019 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 SPAMANAGER_H +#define SPAMANAGER_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_SPA + +#include +#include + +#include "Plugin.h" + +class QLibrary; + +namespace lmms +{ + +//! Class to keep track of all SPA plugins +class SpaManager +{ + Plugin::PluginTypes computePluginType(spa::descriptor *desc); + +public: + struct SpaInfo + { + // only required when plugins shall not be loaded at startup + /*const*/ QString m_path; + QLibrary *m_lib = nullptr; + spa::descriptor *m_descriptor; + Plugin::PluginTypes m_type; + SpaInfo(const SpaInfo &) = delete; + SpaInfo() = default; + void cleanup(); + }; + + SpaManager(); + ~SpaManager(); + + //! returns a descriptor with @p uniqueName or nullptr if none exists + //! @param uniqueName The spa::unique_name of the plugin + spa::descriptor *getDescriptor(const std::string &uniqueName); + spa::descriptor *getDescriptor(const QString uniqueName); + + struct Iterator + { + std::map::iterator itr; + bool operator!=(const Iterator &other) + { + return itr != other.itr; + } + Iterator &operator++() + { + ++itr; + return *this; + } + std::pair &operator*() + { + return *itr; + } + }; + + Iterator begin() { return {m_spaInfoMap.begin()}; } + Iterator end() { return {m_spaInfoMap.end()}; } + +private: + std::map m_spaInfoMap; +}; + +} // namespace lmms + +#endif // LMMS_HAVE_SPA + +#endif // SPAMANAGER_H diff --git a/include/SpaOscModel.h b/include/SpaOscModel.h new file mode 100644 index 00000000000..22f03d62a94 --- /dev/null +++ b/include/SpaOscModel.h @@ -0,0 +1,81 @@ +/* + * SpaOscModel.h - AutomatableModel which forwards OSC events + * + * Copyright (c) 2018-2022 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 SPA_OSC_MODEL_H +#define SPA_OSC_MODEL_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_SPA + +#include "AutomatableModel.h" + +namespace lmms +{ + +template class SpaOscModel : public Base +{ +protected: + class SpaProc *m_plugRef; + QByteArray m_dest; + + using Base::Base; + void init(class SpaProc *plugRef, const QString dest) + { + m_plugRef = plugRef; + m_dest = dest.toUtf8(); + } +}; + +class BoolOscModel : public SpaOscModel +{ + Q_OBJECT +public: + void sendOsc(); + BoolOscModel(SpaProc *plugRef, const QString dest, bool val); +}; + +class IntOscModel : public SpaOscModel +{ + Q_OBJECT +public: + void sendOsc(); + IntOscModel(class SpaProc *plugRef, const QString dest, int min, + int max, int val); +}; + +class FloatOscModel : public SpaOscModel +{ + Q_OBJECT +public: + void sendOsc(); + FloatOscModel(class SpaProc *plugRef, const QString dest, + float min, float max, float step, float val); +}; + +} // namespace lmms + +#endif // LMMS_HAVE_SPA + +#endif // SPA_OSC_MODEL_H diff --git a/include/SpaProc.h b/include/SpaProc.h new file mode 100644 index 00000000000..5754b894e2a --- /dev/null +++ b/include/SpaProc.h @@ -0,0 +1,141 @@ +#ifndef SPAPROC_H +#define SPAPROC_H + +#include +#include +#include +#include + +#include "DataFile.h" +#include "LinkedModelGroups.h" +#include "lmms_basics.h" +#include "Note.h" +#include "../src/3rdparty/ringbuffer/include/ringbuffer/ringbuffer.h" + +namespace lmms { + +class AutomatableModel; + +class SpaProc : public LinkedModelGroup +{ + Q_OBJECT + friend class SpaViewBase; + +public: + SpaProc(Model *parent, const spa::descriptor* desc, + DataFile::Types settingsType); + ~SpaProc() override; + //! Check if ctor succeeded + bool isValid() const { return m_valid; } + + void writeOsc(const char *dest, const char *args, va_list va); + void writeOsc(const char *dest, const char *args, ...); + + + void loadFile(const QString &file, bool user); + + void run(unsigned frames); + + //! Return the net port, or 0 if there is currently none + unsigned netPort() const; + + const spa::descriptor *m_spaDescriptor = nullptr; + spa::plugin *m_plugin = nullptr; + + uint64_t m_loadTicket = 0, m_saveTicket = 0, m_restoreTicket = 0; + +protected: +// void reloadPlugin(); +public: + //! create or return an OSC model for port "dest" + //! RT safe if the model already exists + AutomatableModel *modelAtPort(const QString &dest); + + int m_audioInCount = 0, m_audioOutCount = 0; + + std::size_t controlCount() const { return LinkedModelGroup::modelNum(); } + + struct LmmsPorts + { + unsigned samplecount; + unsigned buffersize; + long samplerate; // TODO: use const? + std::vector m_lUnprocessed, m_rUnprocessed, m_lProcessed, + m_rProcessed; + // only for directly connected ports (not OSC) + struct TypedPorts + { + char m_type; + // storage, plugin references this + union + { + float m_f; + int m_i; + bool m_b; + } m_val; + // models where we copy the values from/to + union // TODO: use AutomatableModel? + { + class FloatModel *m_floatModel; + class IntModel *m_intModel; + class BoolModel *m_boolModel; + } m_connectedModel; + std::string m_id; + TypedPorts() = default; + TypedPorts(char type, std::string id) : m_type(type), m_id(id) {} + }; + + //! these are forwarded to the user in the LMMS-internal GUI + //! inited at plugin initialization time + //! right after initing, they are added to LinkedModelGroup class + std::vector m_userPorts; + LmmsPorts(int bufferSize); + std::unique_ptr rb; + } m_ports; + + void copyModelsToPorts(); + + void copyBuffersFromCore(const sampleFrame *buf, unsigned offset, unsigned num, fpp_t frames); + void copyBuffersToCore(sampleFrame *buf, unsigned offset, unsigned num, fpp_t frames) const; + + void uiExtShow(bool doShow); + void saveState(QDomDocument &doc, QDomElement &that); + void loadState(const QDomElement &that); + void reloadPlugin(); + + void handleMidiInputEvent(const class MidiEvent &event, + const class TimePos &time, f_cnt_t offset); + +private: + friend struct LmmsVisitor; + friend struct TypeChecker; + std::atomic_flag m_writeOscInUse; + bool m_valid = true; + const DataFile::Types m_settingsType; + + int m_runningNotes[NumKeys]; + + // MIDI + // many things here may be moved into the `Instrument` class + constexpr const static std::size_t m_maxMidiInputEvents = 1024; + //! spinlock for the MIDI ringbuffer (for MIDI events going to the plugin) + std::atomic_flag m_ringLock = ATOMIC_FLAG_INIT; + + //! MIDI ringbuffer (for MIDI events going to the plugin) + ringbuffer_t m_midiInputBuf; + //! MIDI ringbuffer reader + ringbuffer_reader_t m_midiInputReader; + + //! load a file into the plugin, but don't do anything in LMMS +// void loadFile(const QString &file); + +protected: +// QMutex m_pluginMutex; + + void initPlugin(); + void shutdownPlugin(); +}; + +} // namespace lmms + +#endif // SPAPROC_H diff --git a/include/SpaSubPluginFeatures.h b/include/SpaSubPluginFeatures.h new file mode 100644 index 00000000000..782bd9c5cd7 --- /dev/null +++ b/include/SpaSubPluginFeatures.h @@ -0,0 +1,65 @@ +/* + * SpaSubPluginFeatures.h - derivation from + * Plugin::Descriptor::SubPluginFeatures for + * hosting SPA plugins + * + * Copyright (c) 2018-2019 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 SPA_SUBPLUGIN_FEATURES_H +#define SPA_SUBPLUGIN_FEATURES_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_SPA + +#include + +#include "Plugin.h" + +namespace lmms +{ + +class SpaSubPluginFeatures : public Plugin::Descriptor::SubPluginFeatures +{ +private: + static spa::descriptor *spaDescriptor(const Key &k); + +public: + SpaSubPluginFeatures(Plugin::PluginTypes _type); + + virtual 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; +}; + +} // namespace lmms + +#endif // LMMS_HAVE_SPA + +#endif diff --git a/include/SpaViewBase.h b/include/SpaViewBase.h new file mode 100644 index 00000000000..64847cdc188 --- /dev/null +++ b/include/SpaViewBase.h @@ -0,0 +1,145 @@ +/* + * SpaViewBase.h - base class for SPA plugin views + * + * Copyright (c) 2018-2019 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 SPAVIEWBASE_H +#define SPAVIEWBASE_H + +#include +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_SPA + +#include "LinkedModelGroupViews.h" + +class AutomatableModel; +class QDropEvent; +class QGridLayout; +class QPushButton; +class QWidget; + +namespace lmms +{ +class AutomatableModel; +class SpaProc; +class SpaControlBase; +} + +namespace lmms::gui +{ + +class SpaViewProc : public LinkedModelGroupView +{ + Q_OBJECT +public: + SpaViewProc(QWidget *parent, SpaProc *proc, + std::size_t colNum); + // TODO: make those two private? + void dropEvent(QDropEvent *de) override; + void dragEnterEvent(QDragEnterEvent *dev) override; +private slots: + void modelAdded(lmms::AutomatableModel* mdl); + void modelRemoved(lmms::AutomatableModel *mdl); +private: + SpaProc* m_proc; +}; + +class SpaViewBase : LinkedModelGroupsView +{ + QGridLayout *m_grid; + const int m_firstModelRow = 1; // row 0 is for buttons + const int m_rowNum = 6; // just some guess for what might look good + + //QVector m_modelViews; + + SpaViewProc* m_procView; // TODO: unique_ptr + + LinkedModelGroupView *getGroupView() override; + +protected: + QPushButton *m_toggleUIButton = nullptr; + QPushButton *m_reloadPluginButton; + + // to be called by virtuals + void modelChanged(SpaControlBase* ctrlBase); + void connectSlots(const char* toggleUiSlot); + SpaViewBase(QWidget *meAsWidget, SpaControlBase* ctrlBase); + virtual ~SpaViewBase(); + void dropEvent(QDropEvent *de); + +private: + //! Numbers of controls per row; must be multiple of 2 for mono effects + const std::size_t m_colNum = 6; + + enum Rows + { + ButtonRow, + ProcRow, + LinkChannelsRow/*, + DropButtonRow*/ + }; + void dragEnterEvent(QDragEnterEvent *de); +}; + +#if 0 +class SpaFxControlDialog : public EffectControlDialog +{ + Q_OBJECT + + class SpaFxControls *spaControls(); + void modelChanged() override; + +public: + SpaFxControlDialog(class SpaFxControls *controls); + virtual ~SpaFxControlDialog() override {} + +private slots: + void toggleUI(); + void reloadPlugin(); +}; + + +class SpaInsView : public InstrumentView +{ + Q_OBJECT +public: + SpaInsView(Instrument *_instrument, QWidget *_parent); + virtual ~SpaInsView(); + +protected: + virtual void dragEnterEvent(QDragEnterEvent *_dee); + virtual void dropEvent(QDropEvent *_de); + + +private slots: + void toggleUI(); + void reloadPlugin(); +}; + +#endif + +} // namespace lmms::gui + +#endif // LMMS_HAVE_SPA + +#endif // SPAVIEWBASE_H diff --git a/plugins/SpaEffect/CMakeLists.txt b/plugins/SpaEffect/CMakeLists.txt new file mode 100644 index 00000000000..48a06632cc9 --- /dev/null +++ b/plugins/SpaEffect/CMakeLists.txt @@ -0,0 +1,4 @@ +IF(LMMS_HAVE_SPA) + INCLUDE(BuildPlugin) + BUILD_PLUGIN(spaeffect SpaEffect.cpp SpaFxControls.cpp SpaFxControlDialog.cpp SpaEffect.h SpaFxControls.h SpaFxControlDialog.h MOCFILES SpaEffect.h SpaFxControls.h SpaFxControlDialog.h EMBEDDED_RESOURCES logo.png) +ENDIF(LMMS_HAVE_SPA) diff --git a/plugins/SpaEffect/SpaEffect.cpp b/plugins/SpaEffect/SpaEffect.cpp new file mode 100644 index 00000000000..593b8d04613 --- /dev/null +++ b/plugins/SpaEffect/SpaEffect.cpp @@ -0,0 +1,112 @@ +/* + * SpaEffect.cpp - implementation of SPA effect + * + * Copyright (c) 2018-2019 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 "SpaEffect.h" + +#include "SpaProc.h" +#include "SpaSubPluginFeatures.h" +#include "embed.h" +#include "plugin_export.h" + +namespace lmms +{ + +extern "C" +{ + +Plugin::Descriptor PLUGIN_EXPORT spaeffect_plugin_descriptor = +{ + LMMS_STRINGIFY(PLUGIN_NAME), + "SPA", + QT_TRANSLATE_NOOP("SpaEffect", + "plugin for using arbitrary SPA-effects inside LMMS."), + "Johannes Lorenz ", + 0x0100, + Plugin::Effect, + new PluginPixmapLoader("logo"), + nullptr, + new SpaSubPluginFeatures(Plugin::Effect) +}; + +} + +SpaEffect::SpaEffect(Model* parent, const Descriptor::SubPluginFeatures::Key *key) : + Effect(&spaeffect_plugin_descriptor, parent, key), + m_controls(this, key->attributes["plugin"]) +{ +} + +SpaEffect::~SpaEffect() +{ +} + +bool SpaEffect::processAudioBuffer(sampleFrame *buf, const fpp_t frames) +{ + if(!isEnabled() || !isRunning()) + { + return false; + } + + SpaFxControls& ctrl = m_controls; + + ctrl.copyBuffersFromLmms(buf, frames); + ctrl.copyModelsFromLmms(); + +// m_pluginMutex.lock(); + ctrl.run(static_cast(frames)); +// m_pluginMutex.unlock(); + + ctrl.copyBuffersToLmms(buf, frames); + + // TODO: check gate + + return isRunning(); +} + +unsigned SpaEffect::netPort(std::size_t chan) const +{ + return spaControls()->m_procs[chan]->netPort(); +} + +AutomatableModel *SpaEffect::modelAtPort(const QString &dest) +{ + return spaControls()->modelAtPort(dest); +} + + + + +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; + return new SpaEffect(_parent, static_cast(_data)); +} + +} + +} // namespace lmms diff --git a/plugins/SpaEffect/SpaEffect.h b/plugins/SpaEffect/SpaEffect.h new file mode 100644 index 00000000000..e8c27fee794 --- /dev/null +++ b/plugins/SpaEffect/SpaEffect.h @@ -0,0 +1,64 @@ +/* + * SpaEffect.h - implementation of SPA effect + * + * Copyright (c) 2018-2019 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 SPA_EFFECT_H +#define SPA_EFFECT_H + +#include "Effect.h" +#include "SpaFxControls.h" + +class QString; + +namespace lmms +{ + +class AutomatableModel; + +class SpaEffect : public Effect +{ + Q_OBJECT + +public: + SpaEffect(Model* parent, const Descriptor::SubPluginFeatures::Key* _key); + ~SpaEffect() override; + + bool processAudioBuffer( sampleFrame* buf, const fpp_t frames ) override; + + EffectControls* controls() override { return &m_controls; } + + SpaFxControls* spaControls() { return &m_controls; } + const SpaFxControls* spaControls() const { return &m_controls; } + + unsigned netPort(std::size_t chan) const override; + class AutomatableModel* modelAtPort(const QString& dest) override; + +private: + SpaFxControls m_controls; + + friend class SpaFxControls; +}; + +} // namespace lmms + +#endif // LMMS_HAVE_SPA diff --git a/plugins/SpaEffect/SpaFxControlDialog.cpp b/plugins/SpaEffect/SpaFxControlDialog.cpp new file mode 100644 index 00000000000..89541ba2d10 --- /dev/null +++ b/plugins/SpaEffect/SpaFxControlDialog.cpp @@ -0,0 +1,89 @@ +/* + * SpaControlDialog.cpp - control dialog for amplifier effect + * + * Copyright (c) 2018-2019 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 "SpaFxControlDialog.h" + +#include + +#include "SpaFxControls.h" + +namespace lmms::gui +{ + +SpaFxControls *SpaFxControlDialog::spaControls() +{ + return static_cast(m_effectControls); +} + +SpaFxControlDialog::SpaFxControlDialog(SpaFxControls *controls) : + EffectControlDialog(controls), + SpaViewBase(this, controls) +{ + connect(m_reloadPluginButton, SIGNAL(toggled(bool)), + this, SLOT(reloadPlugin())); + if(m_toggleUIButton) + connect(m_toggleUIButton, SIGNAL(toggled(bool)), + this, SLOT(toggleUI())); + // for Effects, modelChanged only goes to the top EffectView + // we need to call it manually + modelChanged(); +} + +/* +// TODO: common UI class..., as this must be usable for instruments, too +SpaControlDialog::~SpaControlDialog() +{ + SpaEffect *model = castModel(); + + if (model && spaControls()->m_spaDescriptor->ui_ext() && +spaControls()->m_hasGUI) + { + qDebug() << "shutting down UI..."; + model->m_plugin->ui_ext_show(false); + } +} +*/ + +void SpaFxControlDialog::modelChanged() +{ + SpaViewBase::modelChanged(spaControls()); +} + +void SpaFxControlDialog::toggleUI() +{ +#if 0 + SpaEffect *model = castModel(); + if (model->m_spaDescriptor->ui_ext() && + model->m_hasGUI != m_toggleUIButton->isChecked()) + { + model->m_hasGUI = m_toggleUIButton->isChecked(); + model->m_plugin->ui_ext_show(model->m_hasGUI); + ControllerConnection::finalizeConnections(); + } +#endif +} + +void SpaFxControlDialog::reloadPlugin() { spaControls()->reloadPlugin(); } + +} // namespace lmms::gui diff --git a/plugins/SpaEffect/SpaFxControlDialog.h b/plugins/SpaEffect/SpaFxControlDialog.h new file mode 100644 index 00000000000..e921c1e8251 --- /dev/null +++ b/plugins/SpaEffect/SpaFxControlDialog.h @@ -0,0 +1,57 @@ +/* + * SpaControlDialog.h - control dialog for amplifier effect + * + * Copyright (c) 2018-2019 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 SPA_FX_CONTROL_DIALOG_H +#define SPA_FX_CONTROL_DIALOG_H + +#include "EffectControlDialog.h" +#include "SpaViewBase.h" + +namespace lmms +{ + class SpaFxControls; +} + +namespace lmms::gui +{ + +class SpaFxControlDialog : public EffectControlDialog, public SpaViewBase +{ + Q_OBJECT + + SpaFxControls *spaControls(); + void modelChanged() override; + +public: + SpaFxControlDialog(class SpaFxControls *controls); + virtual ~SpaFxControlDialog() override {} + +private slots: + void toggleUI(); + void reloadPlugin(); +}; + +} // namespace lmms::gui + +#endif diff --git a/plugins/SpaEffect/SpaFxControls.cpp b/plugins/SpaEffect/SpaFxControls.cpp new file mode 100644 index 00000000000..a726aa68083 --- /dev/null +++ b/plugins/SpaEffect/SpaFxControls.cpp @@ -0,0 +1,80 @@ +/* + * SpaControls.cpp - controls for amplifier effect + * + * Copyright (c) 2018-2019 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 "SpaFxControls.h" + +#include "Engine.h" +#include "SpaEffect.h" +#include "SpaFxControlDialog.h" +#include "SpaProc.h" + +namespace lmms +{ + +SpaFxControls::SpaFxControls(class SpaEffect *effect, const QString& uniqueName) : + EffectControls(effect), + SpaControlBase(static_cast(this), uniqueName, + DataFile::Type::EffectSettings), + m_effect(effect) +{ + if (isValid()) + { + connect(Engine::audioEngine(), SIGNAL(sampleRateChanged()), this, + SLOT(reloadPlugin())); + } +} + +void SpaFxControls::setNameFromFile(const QString &name) +{ + effect()->setDisplayName(name); +} + +void SpaFxControls::changeControl() // TODO: what is that? +{ + // engine::getSong()->setModified(); +} + +void SpaFxControls::saveSettings(QDomDocument &doc, QDomElement &that) +{ + SpaControlBase::saveSettings(doc, that); +} + +void SpaFxControls::loadSettings(const QDomElement &that) +{ + SpaControlBase::loadSettings(that); +} + +int SpaFxControls::controlCount() +{ + std::size_t res = 0; + for (const std::unique_ptr& c : m_procs) { res += c->controlCount(); } + return static_cast(res); +} + +gui::EffectControlDialog *SpaFxControls::createView() +{ + return new gui::SpaFxControlDialog(this); +} + +} // namespace lmms diff --git a/plugins/SpaEffect/SpaFxControls.h b/plugins/SpaEffect/SpaFxControls.h new file mode 100644 index 00000000000..45f51997d51 --- /dev/null +++ b/plugins/SpaEffect/SpaFxControls.h @@ -0,0 +1,76 @@ +/* + * SpaControls.h - controls for bassboosterx -effect + * + * Copyright (c) 2018-2019 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 SPA_FX_CONTROLS_H +#define SPA_FX_CONTROLS_H + +#include "EffectControls.h" +#include "SpaControlBase.h" + +class QDomElement; + +namespace lmms +{ + +namespace gui +{ + class SpaFxControlDialog; +} + +class SpaFxControls : public EffectControls, public SpaControlBase +{ + Q_OBJECT + + //DataFile::Types settingsType() override; + void setNameFromFile(const QString &name) override; + +public: + SpaFxControls(class SpaEffect *effect, const QString &uniqueName); + ~SpaFxControls() override {} + + void saveSettings(QDomDocument &_doc, QDomElement &_parent) override; + void loadSettings(const QDomElement &that) override; + inline QString nodeName() const override + { + return SpaControlBase::nodeName(); + } + + int controlCount() override; + + gui::EffectControlDialog *createView() override; + +private slots: + void changeControl(); + + void reloadPlugin() { SpaControlBase::reloadPlugin(); } + +private: + class SpaEffect *m_effect; + friend class gui::SpaFxControlDialog; + friend class SpaEffect; +}; + +} // namespace lmms + +#endif diff --git a/plugins/SpaEffect/logo.png b/plugins/SpaEffect/logo.png new file mode 100644 index 00000000000..24d9135ffff Binary files /dev/null and b/plugins/SpaEffect/logo.png differ diff --git a/plugins/SpaInstrument/CMakeLists.txt b/plugins/SpaInstrument/CMakeLists.txt new file mode 100644 index 00000000000..e6df35c777d --- /dev/null +++ b/plugins/SpaInstrument/CMakeLists.txt @@ -0,0 +1,4 @@ +IF(LMMS_HAVE_SPA) + INCLUDE(BuildPlugin) + BUILD_PLUGIN(spainstrument SpaInstrument.cpp SpaInstrument.h MOCFILES SpaInstrument.h EMBEDDED_RESOURCES logo.png) +ENDIF(LMMS_HAVE_SPA) diff --git a/plugins/SpaInstrument/SpaInstrument.cpp b/plugins/SpaInstrument/SpaInstrument.cpp new file mode 100644 index 00000000000..60c9ed9a6ba --- /dev/null +++ b/plugins/SpaInstrument/SpaInstrument.cpp @@ -0,0 +1,273 @@ +/* + * SpaInstrument.cpp - implementation of SPA instrument + * + * Copyright (c) 2018-2019 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 "SpaInstrument.h" + +#include +#include +#include +#include +#include +#include + +#include "AutomatableModel.h" +#include "ControllerConnection.h" +#include "InstrumentPlayHandle.h" +#include "InstrumentTrack.h" +#include "AudioEngine.h" +#include "SpaProc.h" // TODO: remove from here -> use proper class cascading +#include "SpaSubPluginFeatures.h" +#include "StringPairDrag.h" // DnD TODO: move to SpaViewBase? +#include "embed.h" +#include "TimePos.h" + +#include "plugin_export.h" + +namespace lmms +{ + +extern "C" +{ + +Plugin::Descriptor PLUGIN_EXPORT spainstrument_plugin_descriptor = +{ + LMMS_STRINGIFY(PLUGIN_NAME), + "SPA", + QT_TRANSLATE_NOOP("SpaInstrument", + "plugin for using arbitrary SPA instruments inside LMMS."), + "Johannes Lorenz ", + 0x0100, + Plugin::Instrument, + new PluginPixmapLoader("logo"), + nullptr, + new SpaSubPluginFeatures(Plugin::Instrument) +}; + +} + +/*DataFile::Types SpaInstrument::settingsType() +{ + return DataFile::InstrumentTrackSettings; +}*/ + +void SpaInstrument::setNameFromFile(const QString &name) +{ + instrumentTrack()->setName(name); +} + +SpaInstrument::SpaInstrument(InstrumentTrack *instrumentTrackArg, + Descriptor::SubPluginFeatures::Key *key) : + Instrument(instrumentTrackArg, &spainstrument_plugin_descriptor, key), + SpaControlBase(this, key->attributes["plugin"], + DataFile::Type::InstrumentTrackSettings) +{ + if (isValid()) + { + connect(instrumentTrack()->pitchRangeModel(), SIGNAL(dataChanged()), + this, SLOT(updatePitchRange())); + connect(Engine::audioEngine(), SIGNAL(sampleRateChanged()), + this, SLOT(reloadPlugin())); // TODO: refactor to SpaControlBase? + + // now we need a play-handle which cares for calling play() + InstrumentPlayHandle *iph = + new InstrumentPlayHandle(this, instrumentTrackArg); + Engine::audioEngine()->addPlayHandle(iph); + } +} + +SpaInstrument::~SpaInstrument() +{ + Engine::audioEngine()->removePlayHandlesOfTypes(instrumentTrack(), + PlayHandle::TypeNotePlayHandle | + PlayHandle::TypeInstrumentPlayHandle); +} + +void SpaInstrument::saveSettings(QDomDocument &doc, QDomElement &that) +{ + SpaControlBase::saveSettings(doc, that); +} + +void SpaInstrument::loadSettings(const QDomElement &that) +{ + SpaControlBase::loadSettings(that); +} + +// not yet working +#ifndef SPA_INSTRUMENT_USE_MIDI +void SpaInstrument::playNote(NotePlayHandle *nph, sampleFrame *) +{ + // no idea what that means + if (nph->isMasterNote() || (nph->hasParent() && nph->isReleased())) + { + return; + } + + const f_cnt_t tfp = nph->totalFramesPlayed(); + + const float LOG440 = 2.643452676f; + + int midiNote = (int)floor( + 12.0 * (log2(nph->unpitchedFrequency()) - LOG440) - 4.0); + + qDebug() << "midiNote: " << midiNote << ", r? " << nph->isReleased(); + // out of range? + if (midiNote <= 0 || midiNote >= 128) + { + return; + } + + if (tfp == 0) + { + const int baseVelocity = + instrumentTrack()->midiPort()->baseVelocity(); + m_plugin->send_osc("/noteOn", "iii", 0, midiNote, baseVelocity); + } + else if (nph->isReleased() && + !nph->instrumentTrack() + ->isSustainPedalPressed()) // note is released during + // this period + { + m_plugin->send_osc("/noteOff", "ii", 0, midiNote); + } + else if (nph->framesLeft() <= 0) + { + m_plugin->send_osc("/noteOff", "ii", 0, midiNote); + } +} +#endif + +void SpaInstrument::play(sampleFrame *buf) +{ + copyModelsFromLmms(); + + fpp_t fpp = Engine::audioEngine()->framesPerPeriod(); + + run(static_cast(fpp)); + + copyBuffersToLmms(buf, fpp); + + instrumentTrack()->processAudioBuffer(buf, fpp, nullptr); +} + +void SpaInstrument::updatePitchRange() +{ + qDebug() << "Lmms: Cannot update pitch range for spa plugin:" + "not implemented yet"; +} + +QString SpaInstrument::nodeName() const +{ + return SpaControlBase::nodeName(); +} + +#ifdef SPA_INSTRUMENT_USE_MIDI +bool SpaInstrument::handleMidiEvent( + const MidiEvent &event, const TimePos &time, f_cnt_t offset) +{ + // this function can be called from GUI threads while the plugin is running + // handleMidiInputEvent will use a thread-safe ringbuffer + handleMidiInputEvent(event, time, offset); + return true; +} +#endif + +gui::PluginView *SpaInstrument::instantiateView(QWidget *parent) +{ + return new gui::SpaInsView(this, parent); +} + +unsigned SpaInstrument::netPort(std::size_t chan) const +{ + return chan < m_procs.size() ? m_procs[chan]->netPort() : 0; +} + +AutomatableModel *SpaInstrument::modelAtPort(const QString &dest) +{ + return SpaControlBase::modelAtPort(dest); +} + +namespace gui { + +SpaInsView::SpaInsView(SpaInstrument *_instrument, QWidget *_parent) : + InstrumentView(_instrument, _parent), + SpaViewBase(this, _instrument) +{ + setAutoFillBackground(true); + connect(m_reloadPluginButton, SIGNAL(toggled(bool)), + this, SLOT(reloadPlugin())); + if(m_toggleUIButton) + connect(m_toggleUIButton, SIGNAL(toggled(bool)), + this, SLOT(toggleUI())); +} + +SpaInsView::~SpaInsView() +{ + SpaInstrument *model = castModel(); + if (model && model->m_spaDescriptor->ui_ext() && model->m_hasGUI) + { + qDebug() << "shutting down UI..."; + model->uiExtShow(false); + } +} + +void SpaInsView::modelChanged() +{ + SpaViewBase::modelChanged(castModel()); +} + +void SpaInsView::toggleUI() +{ + SpaInstrument *model = castModel(); + if (model->m_spaDescriptor->ui_ext() && + model->m_hasGUI != m_toggleUIButton->isChecked()) + { + model->m_hasGUI = m_toggleUIButton->isChecked(); + model->uiExtShow(model->m_hasGUI); + ControllerConnection::finalizeConnections(); + } +} + +void SpaInsView::reloadPlugin() +{ + castModel()->reloadPlugin(); +} + + + + +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; + return new SpaInstrument(static_cast(_parent), + static_cast(_data )); +} + +} + +} // namespace gui +} // namespace lmms diff --git a/plugins/SpaInstrument/SpaInstrument.h b/plugins/SpaInstrument/SpaInstrument.h new file mode 100644 index 00000000000..f5c98092971 --- /dev/null +++ b/plugins/SpaInstrument/SpaInstrument.h @@ -0,0 +1,118 @@ +/* + * SpaInstrument.h - implementation of SPA instrument + * + * Copyright (c) 2018-2019 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 SPA_INSTRUMENT_H +#define SPA_INSTRUMENT_H + +#include +#include + +#include "Instrument.h" +#include "InstrumentView.h" +#include "Note.h" +#include "SpaControlBase.h" +#include "SpaViewBase.h" + +class QPushButton; + +namespace lmms +{ + +namespace gui +{ + class SpaInsView; +} + +// whether to use MIDI vs playHandle +// currently only MIDI works +#define SPA_INSTRUMENT_USE_MIDI + +class SpaInstrument : public Instrument, public SpaControlBase +{ + Q_OBJECT + + void setNameFromFile(const QString &name) override; + +public: + SpaInstrument(InstrumentTrack *instrumentTrackArg, + Descriptor::SubPluginFeatures::Key* key); + ~SpaInstrument() override; + + void saveSettings(QDomDocument &doc, QDomElement &that) override; + void loadSettings(const QDomElement &that) override; + void loadFile(const QString &file) override { + SpaControlBase::loadFile(file, true); } + +#ifdef SPA_INSTRUMENT_USE_MIDI + bool handleMidiEvent(const MidiEvent &event, + const class TimePos &time = TimePos(), f_cnt_t offset = 0) override; +#else + void playNote(NotePlayHandle *nph, sampleFrame *) override; +#endif + void play(sampleFrame *buf) override; + + Flags flags() const override + { +#ifdef SPA_INSTRUMENT_USE_MIDI + return IsSingleStreamed | IsMidiBased; +#else + return IsSingleStreamed; +#endif + } + + gui::PluginView *instantiateView(QWidget *parent) override; + + unsigned netPort(std::size_t) const override; + class AutomatableModel* modelAtPort(const QString& dest) override; + +private slots: + void updatePitchRange(); + void reloadPlugin() { SpaControlBase::reloadPlugin(); } + +private: + friend class gui::SpaInsView; + QString nodeName() const override; +}; + +namespace gui { + +class SpaInsView : public InstrumentView, public SpaViewBase +{ + Q_OBJECT +public: + SpaInsView(SpaInstrument *_instrument, QWidget *_parent); + virtual ~SpaInsView() override; + +private: + void modelChanged() override; + +private slots: + void toggleUI(); + void reloadPlugin(); +}; + +} // namespace gui +} // namespace lmms + +#endif // SPA_INSTRUMENT_H diff --git a/plugins/SpaInstrument/logo.png b/plugins/SpaInstrument/logo.png new file mode 100644 index 00000000000..24d9135ffff Binary files /dev/null and b/plugins/SpaInstrument/logo.png differ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7f8a0dfcb8c..55ca87ffc18 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -62,6 +62,7 @@ INCLUDE_DIRECTORIES( ${SNDFILE_INCLUDE_DIRS} ${SNDIO_INCLUDE_DIRS} ${FFTW3F_INCLUDE_DIRS} + ${LIBSPA_INCLUDE_DIRS} ) IF(NOT LMMS_HAVE_SDL2 AND NOT ("${SDL_INCLUDE_DIR}" STREQUAL "")) @@ -111,6 +112,9 @@ IF(LMMS_BUILD_LINUX) ENDIF() SET(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) +# Note: LINK_DIRECTORIES only affects targets after its call +LINK_DIRECTORIES(${LIBSPA_LIBRARY_DIRS}) + ADD_LIBRARY(lmmsobjs OBJECT ${LMMS_SRCS} ${LMMS_INCLUDES} @@ -188,6 +192,7 @@ SET(LMMS_REQUIRED_LIBS ${LMMS_REQUIRED_LIBS} ${LILV_LIBRARIES} ${SAMPLERATE_LIBRARIES} ${SNDFILE_LIBRARIES} + ${LIBSPA_LIBRARIES} ${FFTW3F_LIBRARIES} ${EXTRA_LIBRARIES} rpmalloc diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index b8809ed78f3..4b33fdeda47 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -72,6 +72,11 @@ set(LMMS_SRCS core/Scale.cpp core/SerializingObject.cpp core/Song.cpp + core/SpaControlBase.cpp + core/SpaManager.cpp + core/SpaOscModel.cpp + core/SpaProc.cpp + core/SpaSubPluginFeatures.cpp core/TempoSyncKnobModel.cpp core/TimePos.cpp core/ToolPlugin.cpp diff --git a/src/core/Clipboard.cpp b/src/core/Clipboard.cpp index 6e1503b2f4e..2618a3b21b6 100644 --- a/src/core/Clipboard.cpp +++ b/src/core/Clipboard.cpp @@ -24,6 +24,7 @@ #include #include +#include #include #include "Clipboard.h" @@ -81,7 +82,8 @@ namespace lmms::Clipboard QString decodeKey( const QMimeData * mimeData ) { - return( QString::fromUtf8( mimeData->data( mimeType( MimeType::StringPair ) ) ).section( ':', 0, 0 ) ); + bool hasMt = (mimeData->hasFormat(mimeType(MimeType::StringPair))); + return( QString::fromUtf8( mimeData->data( mimeType( hasMt ? MimeType::StringPair : MimeType::Osc ) ) ).section( ':', 0, 0 ) ); } @@ -89,7 +91,8 @@ namespace lmms::Clipboard QString decodeValue( const QMimeData * mimeData ) { - return( QString::fromUtf8( mimeData->data( mimeType( MimeType::StringPair ) ) ).section( ':', 1, -1 ) ); + bool hasMt = (mimeData->hasFormat(mimeType(MimeType::StringPair))); + return( QString::fromUtf8( mimeData->data( mimeType( hasMt ? MimeType::StringPair : MimeType::Osc ) ) ).section( ':', 1, -1 ) ); } diff --git a/src/core/ConfigManager.cpp b/src/core/ConfigManager.cpp index 59987de97aa..ef7fccf779c 100644 --- a/src/core/ConfigManager.cpp +++ b/src/core/ConfigManager.cpp @@ -246,6 +246,14 @@ void ConfigManager::setLADSPADir(const QString & ladspaDir) +void ConfigManager::setSPADir(const QString &sd) +{ + m_spaDir = sd; +} + + + + void ConfigManager::setSTKDir(const QString & stkDir) { #ifdef LMMS_HAVE_STK diff --git a/src/core/DataFile.cpp b/src/core/DataFile.cpp index 1cbacc2fd80..cc2f18aaf2e 100644 --- a/src/core/DataFile.cpp +++ b/src/core/DataFile.cpp @@ -214,7 +214,8 @@ bool DataFile::validate( QString extension ) case Type::UnknownType: if (! ( extension == "mmp" || extension == "mpt" || extension == "mmpz" || extension == "xpf" || extension == "xml" || - ( extension == "xiz" && ! getPluginFactory()->pluginSupportingExtension(extension).isNull()) || + ( (extension == "xiz" || extension == "xmz") && + ! getPluginFactory()->pluginSupportingExtension(extension).isNull()) || extension == "sf2" || extension == "sf3" || extension == "pat" || extension == "mid" || extension == "dll" #ifdef LMMS_BUILD_LINUX @@ -1683,7 +1684,8 @@ void DataFile::upgrade_extendedNoteRange() instrument.attribute("name") == "vestige" || instrument.attribute("name") == "lv2instrument" || instrument.attribute("name") == "carlapatchbay" || - instrument.attribute("name") == "carlarack"; + instrument.attribute("name") == "carlarack" || + instrument.attribute("name") == "spainstrument"; }; if (!elementsByTagName("song").item(0).isNull()) diff --git a/src/core/Engine.cpp b/src/core/Engine.cpp index 6c810472179..6d090671839 100644 --- a/src/core/Engine.cpp +++ b/src/core/Engine.cpp @@ -23,6 +23,8 @@ */ +#include + #include "Engine.h" #include "AudioEngine.h" #include "ConfigManager.h" @@ -34,6 +36,7 @@ #include "PresetPreviewPlayHandle.h" #include "ProjectJournal.h" #include "Song.h" +#include "SpaManager.h" #include "BandLimitedWave.h" #include "Oscillator.h" @@ -50,7 +53,11 @@ ProjectJournal * Engine::s_projectJournal = nullptr; Lv2Manager * Engine::s_lv2Manager = nullptr; #endif Ladspa2LMMS * Engine::s_ladspaManager = nullptr; +#ifdef LMMS_HAVE_SPA +SpaManager * Engine::s_spaManager = nullptr; +#endif void* Engine::s_dndPluginKey = nullptr; +QMap Engine::s_pluginsByPort; @@ -77,7 +84,9 @@ void Engine::init( bool renderOnly ) s_lv2Manager->initPlugins(); #endif s_ladspaManager = new Ladspa2LMMS; - +#ifdef LMMS_HAVE_SPA + s_spaManager = new SpaManager; +#endif s_projectJournal->setJournalling( true ); emit engine->initProgress(tr("Opening audio and midi devices")); @@ -110,6 +119,9 @@ void Engine::destroy() deleteHelper( &s_lv2Manager ); #endif deleteHelper( &s_ladspaManager ); +#ifdef LMMS_HAVE_SPA + deleteHelper( &s_spaManager ); +#endif //delete ConfigManager::inst(); deleteHelper( &s_projectJournal ); @@ -152,6 +164,60 @@ void Engine::updateFramesPerTick() +// url: val as url +AutomatableModel *Engine::getAutomatableModelAtPort(const QString& val, + const QUrl& url) +{ + AutomatableModel* mod = nullptr; + + // qDebug() << val; + auto itr = s_pluginsByPort.find(static_cast(url.port())); + if(itr == s_pluginsByPort.end()) + { + puts( "DnD from a plugin which is not " + "in LMMS... ignoring"); + // TODO: MessageBox? + } + else + { + mod = itr.value()->modelAtPort(val); + } + + return mod; +} + + + + +AutomatableModel *Engine::getAutomatableModel(const QString& val, bool hasPort) +{ + AutomatableModel* mod = nullptr; + if(hasPort) + { + QUrl url(val); + if(!url.isValid()) + { + printf( "Could not find a port in %s => " + "can not make connection\n", + val.toUtf8().data()); + } + else + { + mod = getAutomatableModelAtPort(val, url); + } + } + else + { + mod = dynamic_cast( + projectJournal()-> + journallingObject( val.toInt() ) ); + } + return mod; +} + + + + void Engine::setDndPluginKey(void *newKey) { Q_ASSERT(static_cast(newKey)); @@ -169,6 +235,14 @@ void *Engine::pickDndPluginKey() +void Engine::addPluginByPort(unsigned port, Plugin *plug) +{ + s_pluginsByPort.insert(port, plug); +} + + + + Engine * Engine::s_instanceOfMe = nullptr; } // namespace lmms \ No newline at end of file diff --git a/src/core/Plugin.cpp b/src/core/Plugin.cpp index e71df15b89f..12fb38e5c1e 100644 --- a/src/core/Plugin.cpp +++ b/src/core/Plugin.cpp @@ -237,9 +237,16 @@ Plugin * Plugin::instantiate(const QString& pluginName, Model * parent, if ((instantiationHook = ( InstantiationHook ) pi.library->resolve( "lmms_plugin_main" ))) { inst = instantiationHook(parent, data); - if(!inst) { - inst = new DummyPlugin(); + if(inst) + { + unsigned port; + for (std::size_t chan = 0; + (port = inst->netPort(chan)); ++chan) + { + Engine::addPluginByPort(port, inst); + } } + else { inst = new DummyPlugin();} } else { diff --git a/src/core/SpaControlBase.cpp b/src/core/SpaControlBase.cpp new file mode 100644 index 00000000000..49cf83bae64 --- /dev/null +++ b/src/core/SpaControlBase.cpp @@ -0,0 +1,263 @@ +/* + * SpaControlBase.cpp - base class for spa instruments, effects, etc + * + * Copyright (c) 2018-2019 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 "AutomatableModel.h" +#include "SpaControlBase.h" + +#ifdef LMMS_HAVE_SPA + +#include +#include +#include +#include + +#include + +#include "Engine.h" +#include "SpaManager.h" +#include "SpaProc.h" + +namespace lmms +{ + +SpaControlBase::SpaControlBase(Model* that, const QString& uniqueName, + DataFile::Types settingsType) : + m_spaDescriptor(Engine::getSPAManager()->getDescriptor(uniqueName)), + m_that(that) +{ + if (m_spaDescriptor) + { + int channelsLeft = DEFAULT_CHANNELS; // LMMS plugins are stereo + while (channelsLeft > 0) + { + std::unique_ptr newOne( + new SpaProc(that, m_spaDescriptor, settingsType)); + if (newOne->isValid()) + { + channelsLeft -= std::max( + 1 + static_cast(newOne->m_audioInCount), + 1 + static_cast(newOne->m_audioOutCount)); + Q_ASSERT(channelsLeft >= 0); + m_procs.push_back(std::move(newOne)); + } + else + { + qCritical() << "Failed instantiating Spa processor"; + m_valid = false; + channelsLeft = 0; + } + } + if (m_valid) + { + m_channelsPerProc = DEFAULT_CHANNELS / m_procs.size(); + linkAllModels(); + } + } + else { + qCritical() << "No SPA descriptor found for URI" << uniqueName; + m_valid = false; + } + // TODO: error handling +} + +void SpaControlBase::saveSettings(QDomDocument &doc, QDomElement &that) +{ + if (m_procs.empty()) { /* don't even add a "states" node */ } + else + { + QDomElement states = doc.createElement("states"); + that.appendChild(states); + + char chanName[] = "state0"; + for (std::size_t chanIdx = 0; chanIdx < m_procs.size(); ++chanIdx) + { + chanName[5] = '0' + static_cast(chanIdx); + QDomElement channel = doc.createElement( + QString::fromUtf8(chanName)); + states.appendChild(channel); + m_procs[chanIdx]->saveState(doc, channel); + } + } + + LinkedModelGroups::saveSettings(doc, that); +} + +void SpaControlBase::loadSettings(const QDomElement &that) +{ + // first load state, then the ports ("settings") + // if we first load the ports, those ports might not exist in current state + + QDomElement states = that.firstChildElement("states"); + if (!states.isNull() && (!m_procs.empty())) + { + QDomElement lastChan; + char chanName[] = "state0"; + for (std::size_t chanIdx = 0; chanIdx < m_procs.size(); ++chanIdx) + { + chanName[5] = '0' + static_cast(chanIdx); + QDomElement chan = states.firstChildElement(chanName); + if (!chan.isNull()) { lastChan = chan; } + + m_procs[chanIdx]->loadState(lastChan); + } + } + + // this will load only initial models and ignore added models + LinkedModelGroups::loadSettings(that); + + bool slept = false; + QDomElement models = that.firstChildElement("models"); + for(QDomElement el = models.firstChildElement(); !el.isNull(); + el = el.nextSiblingElement()) + { + QString nodename = el.hasAttribute("nodename") ? el.attribute("nodename") + : el.nodeName(); + if(!m_procs[0]->containsModel(nodename)) + { + if (!slept) + { + QThread::sleep(3); + slept = true; + } + AutomatableModel* newModel = modelAtPort(nodename); // create model in all processes + newModel->loadSettings(models, nodename); + for(std::unique_ptr& proc : m_procs) + { + proc->addModel(newModel, nodename); + } + } + } +} + +SpaControlBase::~SpaControlBase() {} + +void SpaControlBase::loadFile(const QString &file, bool user) +{ + // for now, only support loading one proc into all proc (duplicating) + for(std::unique_ptr& proc : m_procs) + proc->loadFile(file, user); + setNameFromFile(QFileInfo(file).baseName().replace( + QRegExp("^[0-9]{4}-"), QString())); +} + +bool SpaControlBase::hasUi() const +{ + // do not support external UI for mono effects yet + // (how would that look??) + return m_procs.size() == 1 && m_spaDescriptor->ui_ext(); +} + +void SpaControlBase::uiExtShow(bool doShow) +{ + if(m_procs.size() == 1) + m_procs[0]->uiExtShow(doShow); +} + +void SpaControlBase::writeOscToAll( + const char *dest, const char *args, va_list va) +{ + for(std::unique_ptr& proc : m_procs) + proc->writeOsc(dest, args, va); +} + +void SpaControlBase::writeOscToAll(const char *dest, const char *args, ...) +{ + va_list va; + va_start(va, args); + writeOscToAll(dest, args, va); + va_end(va); +} + +LinkedModelGroup *SpaControlBase::getGroup(std::size_t idx) +{ + return (idx < m_procs.size()) ? m_procs[idx].get() : nullptr; +} + +const LinkedModelGroup *SpaControlBase::getGroup(std::size_t idx) const +{ + return (idx < m_procs.size()) ? m_procs[idx].get() : nullptr; +} + + + + +void SpaControlBase::copyModelsFromLmms() { + for (std::unique_ptr& c : m_procs) { c->copyModelsToPorts(); } +} + + + + +void SpaControlBase::copyBuffersFromLmms(const sampleFrame *buf, fpp_t frames) { + unsigned offset = 0; + for (std::unique_ptr& c : m_procs) { + c->copyBuffersFromCore(buf, offset, m_channelsPerProc, frames); + offset += m_channelsPerProc; + } +} + + + + +void SpaControlBase::copyBuffersToLmms(sampleFrame *buf, fpp_t frames) const { + unsigned offset = 0; + for (const std::unique_ptr& c : m_procs) { + c->copyBuffersToCore(buf, offset, m_channelsPerProc, frames); + offset += m_channelsPerProc; + } +} + + + + +void SpaControlBase::run(unsigned frames) { + for (std::unique_ptr& c : m_procs) { c->run(frames); } +} + +AutomatableModel *SpaControlBase::modelAtPort(const QString &dest) +{ + QUrl url(dest); + // create model at all ports, if it does not yet exist + for (std::unique_ptr& proc : m_procs) + { + proc->modelAtPort(url.path()); + } + linkAllModels(); // link the newly created models + // now return the right model + // always return the first proc's model, since this function is used for + // automation (all other proc's equivalent models are linked, and thus don't + // require automation) + return m_procs[0]->modelAtPort(url.path()); +} + +void SpaControlBase::handleMidiInputEvent(const MidiEvent &event, + const TimePos &time, f_cnt_t offset) +{ + for (auto& c : m_procs) { c->handleMidiInputEvent(event, time, offset); } +} + + +} // namespace lmms + +#endif // LMMS_HAVE_SPA diff --git a/src/core/SpaManager.cpp b/src/core/SpaManager.cpp new file mode 100644 index 00000000000..185f92338a5 --- /dev/null +++ b/src/core/SpaManager.cpp @@ -0,0 +1,226 @@ +/* + * SpaManager.cpp - Implementation of SpaManager class + * + * Copyright (c) 2018-2019 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 "SpaManager.h" + +#ifdef LMMS_HAVE_SPA + +#include +#include +#include +#include +#include + +#include "ConfigManager.h" +#include "Plugin.h" +#include "PluginFactory.h" + +namespace lmms +{ + +SpaManager::SpaManager() +{ + // TODO: make common func? (LADSPA, SPA, LV2, ...) + + // Make sure plugin search paths are set up + PluginFactory::setupSearchPaths(); + + // TODO: "LADSPA" + QStringList spaDirectories = + QString(getenv("SPA_PATH")).split(LADSPA_PATH_SEPERATOR); + spaDirectories += ConfigManager::inst()->spaDir().split(','); + + spaDirectories.push_back("plugins:spa"); + /* + // nothing official yet: + #ifndef LMMS_BUILD_WIN32 + spaDirectories.push_back( qApp->applicationDirPath() + '/' + + LIB_DIR + "spa" ); spaDirectories.push_back( "/usr/lib/spa" ); + spaDirectories.push_back( "/usr/lib64/spa" ); + spaDirectories.push_back( "/usr/local/lib/spa" ); + spaDirectories.push_back( "/usr/local/lib64/spa" ); + spaDirectories.push_back( "/Library/Audio/Plug-Ins/SPA" ); + #endif*/ + + auto addSinglePlugin = [this](const QString &absolutePath) { + SpaInfo info; + info.m_lib = new QLibrary(absolutePath); + spa::descriptor_loader_t spaDescriptorLoader; + + if (info.m_lib->load()) + { + spaDescriptorLoader = + reinterpret_cast( + info.m_lib->resolve( + spa::descriptor_name)); + + if (spaDescriptorLoader) + { + info.m_descriptor = (*spaDescriptorLoader)( + 0 /* = plugin number, TODO */); + if (info.m_descriptor) + { + info.m_type = computePluginType( + info.m_descriptor); + if (info.m_type != + Plugin::PluginTypes::Undefined) + { + qDebug() + << "SpaManager: Adding " + << spa::unique_name( + *info.m_descriptor) + .c_str(); + info.m_path = absolutePath; + m_spaInfoMap[spa::unique_name( + *info.m_descriptor)] = + std::move(info); + } + } + } + else + { + qWarning() << info.m_lib->errorString(); + } + } + else + { + qWarning() << info.m_lib->errorString(); + } + }; + + for (QStringList::iterator it = spaDirectories.begin(); + it != spaDirectories.end(); ++it) + { + QDir directory((*it)); + QFileInfoList list = directory.entryInfoList(); + for (QFileInfoList::iterator file = list.begin(); + file != list.end(); ++file) + { + const QFileInfo &f = *file; + if (f.isFile() && + f.fileName().right(3).toLower() == +#ifdef LMMS_BUILD_WIN32 + "dll" +#else + ".so" +#endif + ) + { + addSinglePlugin(f.absoluteFilePath()); + } + } + } +} + +SpaManager::~SpaManager() +{ + for (std::pair &pr : m_spaInfoMap) + { + pr.second.cleanup(); + } +} + +Plugin::PluginTypes SpaManager::computePluginType(spa::descriptor *desc) +{ + spa::plugin *plug = desc->instantiate(); + + struct TypeChecker final : public virtual spa::audio::visitor + { + std::size_t m_inCount = 0, m_outCount = 0; + void visit(spa::audio::in &) override { ++m_inCount; } + void visit(spa::audio::out &) override { ++m_outCount; } + void visit(spa::audio::stereo::in &) override { ++++m_inCount; } + void visit(spa::audio::stereo::out &) override + { + ++++m_outCount; + } + } tyc; + + const auto portNames = desc->port_names(); + for (const spa::simple_str &portname : portNames) + { + try + { + plug->port(portname.data()).accept(tyc); + } + catch (spa::port_not_found &) + { + return Plugin::PluginTypes::Undefined; + } + } + + delete plug; + + Plugin::PluginTypes res; + if (tyc.m_inCount > 2 || tyc.m_outCount > 2) + { + res = Plugin::PluginTypes::Undefined; + } // TODO: enable mono effects? + else if ((tyc.m_inCount == 2 && tyc.m_outCount == 2) + || (tyc.m_inCount == 1 && tyc.m_outCount == 1)) + { + res = Plugin::PluginTypes::Effect; + } + else if (tyc.m_inCount == 0 && (tyc.m_outCount == 2 || tyc.m_inCount == 1)) + { + res = Plugin::PluginTypes::Instrument; + } + else + { + res = Plugin::PluginTypes::Other; + } + + qDebug() << "Plugin type of " << spa::unique_name(*desc).c_str() << ":"; + qDebug() << (res == Plugin::PluginTypes::Undefined + ? " undefined" + : res == Plugin::PluginTypes::Effect + ? " effect" + : res == Plugin::PluginTypes::Instrument + ? " instrument" + : " other"); + + return res; +} + +spa::descriptor *SpaManager::getDescriptor(const std::string &uniqueName) +{ + auto itr = m_spaInfoMap.find(uniqueName); + return itr == m_spaInfoMap.end() ? nullptr : itr->second.m_descriptor; +} + +spa::descriptor *SpaManager::getDescriptor(const QString uniqueName) +{ + return getDescriptor(std::string(uniqueName.toUtf8())); +} + +void SpaManager::SpaInfo::cleanup() +{ + //m_descriptor->delete_self(); + delete m_descriptor; + delete m_lib; +} + +} // namespace lmms + +#endif // LMMS_HAVE_SPA diff --git a/src/core/SpaOscModel.cpp b/src/core/SpaOscModel.cpp new file mode 100644 index 00000000000..b2c6833497a --- /dev/null +++ b/src/core/SpaOscModel.cpp @@ -0,0 +1,79 @@ +/* + * SpaOscModel.cpp - AutomatableModel which forwards OSC events + * + * Copyright (c) 2018-2022 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 "SpaOscModel.h" + +#ifdef LMMS_HAVE_SPA + +#include + +#include "SpaProc.h" + +namespace lmms { + +// Send own value via OSC +// This is only allowed if the plugin can not run currently! + +void BoolOscModel::sendOsc() +{ + m_plugRef->writeOsc(m_dest.data(), value() ? "T" : "F"); +} +void IntOscModel::sendOsc() +{ + m_plugRef->writeOsc(m_dest.data(), "i", value()); +} +void FloatOscModel::sendOsc() +{ + m_plugRef->writeOsc(m_dest.data(), "f", value()); +} + +BoolOscModel::BoolOscModel(SpaProc *plugRef, const QString dest, bool val) : + SpaOscModel(val, nullptr, dest, false) +{ + qDebug() << "LMMS: receiving bool model: val = " << val; + init(plugRef, dest); +} + +IntOscModel::IntOscModel(SpaProc *plugRef, const QString dest, int min, + int max, int val) : + SpaOscModel(val, min, max, nullptr, dest, false) +{ + qDebug() << "LMMS: receiving int model: (val, min, max) = (" << val + << ", " << min << ", " << max << ")"; + init(plugRef, dest); +} + +FloatOscModel::FloatOscModel(SpaProc *plugRef, const QString dest, + float min, float max, float step, float val) : + // Ctor for FloatModel (see using clause in SpaOscModel) + SpaOscModel(val, min, max, step, nullptr, dest, false) +{ + qDebug() << "LMMS: receiving float model: (val, min, max, step) = (" << val + << ", " << min << ", " << max << ", " << step << ")"; + init(plugRef, dest); +} + +} // namespace lmms + +#endif // LMMS_HAVE_SPA diff --git a/src/core/SpaProc.cpp b/src/core/SpaProc.cpp new file mode 100644 index 00000000000..9646d1b3d54 --- /dev/null +++ b/src/core/SpaProc.cpp @@ -0,0 +1,738 @@ + + +#include "../../include/SpaProc.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "AutomatableModel.h" +#include "Engine.h" +#include "MidiEvent.h" +#include "AudioEngine.h" +#include "SpaOscModel.h" +#include "TimePos.h" + +namespace lmms { + +SpaProc::SpaProc(Model *parent, const spa::descriptor* desc, DataFile::Types settingsType) : + LinkedModelGroup(parent), + m_spaDescriptor(desc), + m_ports(Engine::audioEngine()->framesPerPeriod()), +// writeOscInUse(ATOMIC_FLAG_INIT), // not MSVC-compatible, workaround below + m_settingsType(settingsType), + m_midiInputBuf(m_maxMidiInputEvents), + m_midiInputReader(m_midiInputBuf) +{ + for (int i = 0; i < NumKeys; ++i) { + m_runningNotes[i] = 0; + } + + m_writeOscInUse.clear(); // workaround + initPlugin(); +} + +SpaProc::~SpaProc() { shutdownPlugin(); } + + + + +void SpaProc::saveState(QDomDocument &doc, QDomElement &that) +{ + if (m_spaDescriptor->save_has()) + { + QTemporaryFile tf; + if (tf.open()) + { + const QString fn = QDir::toNativeSeparators(tf.fileName()); +// m_pluginMutex.lock(); + m_plugin->save(fn.toUtf8().data(), ++m_saveTicket); +// m_pluginMutex.unlock(); + + while (!m_plugin->save_check(fn.toUtf8().data(), m_saveTicket)) { + QThread::msleep(1); + } + + QDomCDATASection cdata = doc.createCDATASection( + QString::fromUtf8(tf.readAll())); + that.appendChild(cdata); + } + tf.remove(); + } +} + +void SpaProc::loadState(const QDomElement &that) +{ + if (!that.hasChildNodes()) + { + return; + } + + for (QDomNode node = that.firstChild(); !node.isNull(); + node = node.nextSibling()) + { + QDomCDATASection cdata = node.toCDATASection(); + QDomElement elem; + // load internal state? + if (!cdata.isNull() && m_spaDescriptor->load_has()) + { + QTemporaryFile tf; + tf.setAutoRemove(false); + if (tf.open()) + { + tf.write(cdata.data().toUtf8()); + tf.flush(); + loadFile(tf.fileName(), false); + } + } + } +} + + +void SpaProc::loadFile(const QString &file, bool user) +{ + const QByteArray fn = file.toUtf8(); + if(!user || fn.endsWith(".xmz")) + { + // m_pluginMutex.lock(); + m_plugin->load(fn.data(), ++m_saveTicket); + while (!m_plugin->load_check(fn.data(), m_saveTicket)) { + QThread::msleep(1); + } + // m_pluginMutex.unlock(); + } + else + { + qDebug() << "Unsupported file type (only \".xmz\" works):" << file; + } +} + +void SpaProc::reloadPlugin() +{ + // refresh ports that are only read on restore + m_ports.samplerate = Engine::audioEngine()->processingSampleRate(); + int16_t fpp = Engine::audioEngine()->framesPerPeriod(); + assert(fpp >= 0); + m_ports.buffersize = static_cast(fpp); + + if (m_spaDescriptor->restore_has()) + { + // use the offered restore function +// m_pluginMutex.lock(); + m_plugin->restore(++m_restoreTicket); +// m_pluginMutex.unlock(); + + while (!m_plugin->restore_check(m_restoreTicket)) { + QThread::msleep(1); + } + } + else + { + // save state of current plugin instance + DataFile m(m_settingsType); + saveState(m, m.content()); + + shutdownPlugin(); + initPlugin(); // (will create a new instance) + + // and load the settings again + loadState(m.content()); + } +} + +// container for everything required to store MIDI events going to the plugin +struct MidiInputEvent +{ + MidiEvent ev; + TimePos time; + f_cnt_t offset; +}; + +// in case there will be a PR which removes this callback and instead adds a +// `ringbuffer_t` to `class Instrument`, this +// function (and the ringbuffer and its reader in `SpaProc`) will simply vanish +void SpaProc::handleMidiInputEvent(const MidiEvent &event, const TimePos &time, f_cnt_t offset) +{ + if(/*m_midiIn*/true) + { + // ringbuffer allows only one writer at a time + // however, this function can be called by multiple threads + // (different RT and non-RT!) at the same time + // for now, a spinlock looks like the most safe/easy compromise + + // source: https://en.cppreference.com/w/cpp/atomic/atomic_flag + while (m_ringLock.test_and_set(std::memory_order_acquire)) // acquire lock + ; // spin + + MidiInputEvent ev { event, time, offset }; + std::size_t written = m_midiInputBuf.write(&ev, 1); + if(written != 1) + { + qWarning("MIDI ringbuffer is too small! Discarding MIDI event."); + } + + m_ringLock.clear(std::memory_order_release); + } + else + { + qWarning() << "Warning: Caught MIDI event for an Lv2 instrument" + << "that can not hande MIDI... Ignoring"; + } +} + + + + +void SpaProc::copyModelsToPorts() +{ + // copy non-OSC models + for (LmmsPorts::TypedPorts &tp : m_ports.m_userPorts) + { + switch (tp.m_type) + { + case 'f': + tp.m_val.m_f = tp.m_connectedModel.m_floatModel->value(); + break; + case 'i': + tp.m_val.m_i = tp.m_connectedModel.m_intModel->value(); + break; + case 'b': + tp.m_val.m_b = tp.m_connectedModel.m_boolModel->value(); + break; + default: + assert(false); + } + } + + // copy remainder models, which are OSC models, into the ringbuffer ports + auto F = [](const std::string& , const ModelInfo& info) + { + BoolOscModel* boolMod; + IntOscModel* intMod; + FloatOscModel* floatMod; + + if((boolMod = qobject_cast( info.m_model ))) + { boolMod->sendOsc(); } + else if((intMod = qobject_cast( info.m_model ))) + { intMod->sendOsc(); } + else if((floatMod = qobject_cast( info.m_model ))) + { floatMod->sendOsc(); } + }; + + foreach_model(F); + + while(m_midiInputReader.read_space() > 0) + { + const MidiInputEvent ev = m_midiInputReader.read(1)[0]; + const MidiEvent& event = ev.ev; + switch (event.type()) + { + // the old zynaddsubfx plugin always uses channel 0 + case MidiNoteOn: + if (event.velocity() > 0) + { + if (event.key() <= 0 || event.key() >= 128) + { + break; + } + if (m_runningNotes[event.key()] > 0) + { + writeOsc("/noteOff", "ii", 0, event.key()); + } + ++m_runningNotes[event.key()]; + // m_pluginMutex.lock(); + writeOsc("/noteOn", "iii", 0, event.key(), event.velocity()); + // m_pluginMutex.unlock(); + } + break; + case MidiNoteOff: + if (event.key() > 0 && event.key() < 128) { + if (--m_runningNotes[event.key()] <= 0) + { + // m_pluginMutex.lock(); + writeOsc("/noteOff", "ii", 0, event.key()); + // m_pluginMutex.unlock(); + } + } + break; + /* case MidiPitchBend: + m_master->SetController( event.channel(), + C_pitchwheel, event.pitchBend()-8192 ); break; case + MidiControlChange: m_master->SetController( event.channel(), + midiIn.getcontroller( + event.controllerNumber() ), event.controllerValue() ); + break;*/ + default: + break; + } + } +} + +void SpaProc::shutdownPlugin() +{ + m_plugin->deactivate(); + + foreach_model([&](const std::string& name, LinkedModelGroup::ModelInfo& minf) + { + qDebug() << "deleting" << name.c_str() << minf.m_model->id(); + delete minf.m_model; + minf.m_model = nullptr; + }); + + //m_spaDescriptor->delete_plugin(m_plugin); + delete m_plugin; + m_plugin = nullptr; + + clearModels(); + // clear all port data (object is just raw memory after dtor call)... + m_ports.~LmmsPorts(); + // ... so we can reuse it - C++ is just awesome + new (&m_ports) LmmsPorts(Engine::audioEngine()->framesPerPeriod()); +} + +struct LmmsVisitor final : public virtual spa::audio::visitor +{ + SpaProc* proc; + SpaProc::LmmsPorts *m_ports; + const char *m_curName; + int m_audioInputs = 0; // out + int m_audioOutputs = 0; // out + using spa::audio::visitor::visit; // not sure if this is right, it fixes + // the -Woverloaded-virtual issues + + void visit(spa::audio::in &p) override + { + qDebug() << "in, c: " << +p.channel; + ++m_audioInputs; + p.set_ref((p.channel == spa::audio::stereo::left) + ? m_ports->m_lUnprocessed.data() + : m_ports->m_rUnprocessed.data()); + } + void visit(spa::audio::out &p) override + { + qDebug() << "out, c: %d\n" << +p.channel; + ++m_audioOutputs; + p.set_ref((p.channel == spa::audio::stereo::left) + ? m_ports->m_lProcessed.data() + : m_ports->m_rProcessed.data()); + } + void visit(spa::audio::stereo::in &p) override + { + qDebug() << "in, stereo"; + ++++m_audioInputs; + p.left = m_ports->m_lUnprocessed.data(); + p.right = m_ports->m_rUnprocessed.data(); + } + void visit(spa::audio::stereo::out &p) override + { + qDebug() << "out, stereo"; + ++++m_audioOutputs; + p.left = m_ports->m_lProcessed.data(); + p.right = m_ports->m_rProcessed.data(); + } + void visit(spa::audio::buffersize &p) override + { + qDebug() << "buffersize"; + p.set_ref(&m_ports->buffersize); + } + void visit(spa::audio::samplerate &p) override + { + qDebug() << "samplerate"; + p.set_ref(&m_ports->samplerate); + } + void visit(spa::audio::samplecount &p) override + { + qDebug() << "samplecount"; + p.set_ref(&m_ports->samplecount); + } + + template + void setupPort( + spa::audio::control_in &port, BaseType &portData, + ModelClass *&connectedModel, + const ModelCtorArgs &... modelCtorArgs) + { + portData = port.def; + port.set_ref(&portData); + connectedModel = new ModelClass(static_cast(port), + modelCtorArgs..., nullptr, + QString::fromUtf8(m_curName)); + proc->addModel(connectedModel, m_curName); + } + + // TODO: port_ref does not work yet (clang warnings), so we use + // control_in + void visit(spa::audio::control_in &p) override + { + qDebug() << "other control port (float)"; + m_ports->m_userPorts.emplace_back('f', m_curName); + SpaProc::LmmsPorts::TypedPorts &bck = m_ports->m_userPorts.back(); + setupPort(p, bck.m_val.m_f, bck.m_connectedModel.m_floatModel, + p.min, p.max, p.step); + } + void visit(spa::audio::control_in &p) override + { + qDebug() << "other control port (int)"; + m_ports->m_userPorts.emplace_back('i', m_curName); + SpaProc::LmmsPorts::TypedPorts &bck = m_ports->m_userPorts.back(); + setupPort(p, bck.m_val.m_i, bck.m_connectedModel.m_intModel, + p.min, p.max); + } + void visit(spa::audio::control_in &p) override + { + qDebug() << "other control port (bool)"; + m_ports->m_userPorts.emplace_back('b', m_curName); + SpaProc::LmmsPorts::TypedPorts &bck = + m_ports->m_userPorts.back(); + setupPort(p, bck.m_val.m_b, bck.m_connectedModel.m_boolModel); + } + + void visit(spa::audio::osc_ringbuffer_in &p) override + { + qDebug() << "ringbuffer input"; + if (m_ports->rb) { + throw std::runtime_error("can not handle 2 OSC ports"); + } + else + { + m_ports->rb.reset( + new spa::audio::osc_ringbuffer(p.get_size())); + p.connect(*m_ports->rb); + } + } + void visit(spa::port_ref_base &) override + { + qDebug() << "port of unknown type"; + } +}; + +void SpaProc::initPlugin() +{ +// m_pluginMutex.lock(); + if (!m_spaDescriptor) + { +// m_pluginMutex.unlock(); + m_valid = false; + } + else + { + try + { + spa::assert_versions_match(*m_spaDescriptor); + m_plugin = m_spaDescriptor->instantiate(); + // TODO: unite error handling in the ctor + } + catch (spa::version_mismatch &mismatch) + { + qCritical() + << "Version mismatch loading plugin: " + << mismatch.what(); + // TODO: make an operator<< + qCritical() + << "Got: " << mismatch.version.major() + << "." << mismatch.version.minor() + << "." << mismatch.version.patch() + << ", expect at least " + << mismatch.least_version.major() << "." + << mismatch.least_version.minor() << "." + << mismatch.least_version.patch(); + m_valid = false; + } +// m_pluginMutex.unlock(); + } + + m_ports.samplerate = Engine::audioEngine()->processingSampleRate(); + spa::simple_vec portNames = + m_spaDescriptor->port_names(); + for (const spa::simple_str &portname : portNames) + { + try + { + // qDebug() << "portname: " << portname.data(); + spa::port_ref_base &port_ref = m_plugin->port(portname.data()); + + LmmsVisitor v; + v.proc = this; + v.m_ports = &m_ports; + v.m_curName = portname.data(); + port_ref.accept(v); + m_audioInCount += v.m_audioInputs; + m_audioOutCount += v.m_audioOutputs; + } + catch (spa::port_not_found &e) + { + if (e.portname) { + qWarning() << "plugin specifies invalid port \"" + << e.portname + << "\", but does not provide it"; + } else { + qWarning() << "plugin specifies invalid port, " + "but does not provide it"; + } + m_valid = false; // TODO: free plugin, handle etc... + break; + } + } + + if(m_valid) + { + // all initial ports are already set, we can do + // initialization of buffers etc. + m_plugin->init(); + + m_plugin->activate(); + + // checks not yet implemented: + // spa::host_utils::check_ports(descriptor, plugin); + // plugin->test_more(); + } +} + +// TODO: maybe use event queue... +void SpaProc::writeOsc( + const char *dest, const char *args, va_list va) +{ + // spinlock on atomic; realtime safe as the write function is very fast + while (m_writeOscInUse.test_and_set(std::memory_order_acquire)) // acquire lock + ; // spin + m_ports.rb->write(dest, args, va); + m_writeOscInUse.clear(std::memory_order_release); +} + +void SpaProc::writeOsc(const char *dest, const char *args, ...) +{ + va_list va; + va_start(va, args); + writeOsc(dest, args, va); + va_end(va); +} + +void SpaProc::run(unsigned frames) +{ + m_ports.samplecount = static_cast(frames); + m_plugin->run(); + // ringbuffers can already be reset now: + if(m_ports.rb) + { + m_ports.rb->reset(); + } +} + +unsigned SpaProc::netPort() const { return m_plugin->net_port(); } + + +namespace detail { + +void copyBuffersFromCore(std::vector& portBuf, + const sampleFrame *lmmsBuf, + unsigned channel, fpp_t frames) +{ + for (std::size_t f = 0; f < static_cast(frames); ++f) + { + portBuf[f] = lmmsBuf[f][channel]; + } +} + + + + +void addBuffersFromCore(std::vector& portBuf, const sampleFrame *lmmsBuf, + unsigned channel, fpp_t frames) +{ + for (std::size_t f = 0; f < static_cast(frames); ++f) + { + portBuf[f] = (portBuf[f] + lmmsBuf[f][channel]) / 2.0f; + } +} + + + + +void copyBuffersToCore(const std::vector& portBuf, sampleFrame *lmmsBuf, + unsigned channel, fpp_t frames) +{ + for (std::size_t f = 0; f < static_cast(frames); ++f) + { + lmmsBuf[f][channel] = portBuf[f]; + } +} + +} // namespace detail + +void SpaProc::copyBuffersFromCore(const sampleFrame *buf, + unsigned offset, unsigned num, + fpp_t frames) +{ + detail::copyBuffersFromCore(m_ports.m_lUnprocessed, buf, offset, 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 (m_ports.m_rUnprocessed.size()) + detail::copyBuffersFromCore(m_ports.m_rUnprocessed, buf, offset + 1, frames); + else + detail::addBuffersFromCore(m_ports.m_lUnprocessed, buf, offset + 1, frames); + } +} + + + + +void SpaProc::copyBuffersToCore(sampleFrame* buf, + unsigned offset, unsigned num, + fpp_t frames) const +{ + detail::copyBuffersToCore(m_ports.m_lProcessed, buf, offset + 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) + const std::vector& portBuf = m_ports.m_rProcessed.size() + ? m_ports.m_rProcessed : m_ports.m_lProcessed; + detail::copyBuffersToCore(portBuf, buf, offset + 1, frames); + } +} + + + + +void SpaProc::uiExtShow(bool doShow) { m_plugin->ui_ext_show(doShow); } + + + + +struct SpaOscModelFactory : public spa::audio::visitor +{ + SpaProc *m_plugRef; + const QString m_dest; + + float calc_floating_step(float step, float min, float max) + { + float d = fabsf(max - min); + if(step == .0f) + { + step = (d >= 10.f) ? 0.01f + : (d >= 1.f) ? 0.001f + : 0.0001f; + } + return step; + } + +public: + AutomatableModel *m_res = nullptr; + + template + void make(MoreArgs... args) + { + m_res = new ModelType(m_plugRef, m_dest, args...); + } + + template using CtlIn = spa::audio::control_in; + virtual void visit(CtlIn &in) + { + make(in.min, in.max, + calc_floating_step(in.step, in.min, in.max), in.def ); + } + virtual void visit(CtlIn &in) + { + // LMMS has no double models, cast it all away + float + min = static_cast(in.min), + max = static_cast(in.max), + def = static_cast(in.def), + step = static_cast(in.step); + make(min, max, + calc_floating_step(step, min, max), def ); + } + virtual void visit(CtlIn &in) + { + make(in.min, in.max, in.def); + } + virtual void visit(CtlIn &in) + { + make(in.def); + } + + SpaOscModelFactory(SpaProc *ctrlBase, const QString &dest) : + m_plugRef(ctrlBase), m_dest(dest) + { + } +}; + +/* +trash button ---connection---> + 1. LinkedModelGroup::removeControl -> SpaProc::removeControl + ---> modelRemoved()---conn-> SpaViewProc::modelRemoved + -> LinkedModelGroupView::removeControl -> GUI stuff + ---> LinkedModelGroup::eraseModel() (erases pointer from map) + 2. delete model + ---> destroyed -> removeControl + ---> delete +*/ + +// this function is also responsible to create a model, e.g. in case of DnD +AutomatableModel *SpaProc::modelAtPort(const QString &dest) +{ + QUrl url(dest); + + AutomatableModel *mod; + if (containsModel(url.path())) + { + mod = getModel(url.path().toStdString()); + } + else + { + AutomatableModel *spaMod; + { + SpaOscModelFactory vis(this, url.path()); + spa::port_ref_base &base = + m_plugin->port(url.path().toUtf8().data()); + base.accept(vis); + spaMod = vis.m_res; + } + + if (spaMod) + { + // somehow, those two dictionaries look redundant: + addModel(mod = spaMod, url.path()); + } + else + { + qDebug() << "LMMS: Could not create model from " + << "OSC port (received port\"" << url.port() + << "\", path \"" << url.path() << "\")"; + mod = nullptr; + } + } + return mod; +} + + + + +template UnsignedType castToUnsigned(int val) +{ + return val >= 0 ? static_cast(val) : 0u; +} + + + + +SpaProc::LmmsPorts::LmmsPorts(int bufferSize) : + buffersize(castToUnsigned(bufferSize)), + m_lUnprocessed(castToUnsigned(bufferSize)), + m_rUnprocessed(castToUnsigned(bufferSize)), + m_lProcessed(castToUnsigned(bufferSize)), + m_rProcessed(castToUnsigned(bufferSize)) +{ +} + + +} // namespace lmms diff --git a/src/core/SpaSubPluginFeatures.cpp b/src/core/SpaSubPluginFeatures.cpp new file mode 100644 index 00000000000..d84e1b9c398 --- /dev/null +++ b/src/core/SpaSubPluginFeatures.cpp @@ -0,0 +1,191 @@ +/* + * SpaSubPluginFeatures.cpp - derivation from + * Plugin::Descriptor::SubPluginFeatures for + * hosting SPA plugins + * + * Copyright (c) 2006-2007 Danny McRae + * Copyright (c) 2006-2014 Tobias Doerffel + * + * 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 "SpaSubPluginFeatures.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "AudioDevice.h" +#include "ConfigManager.h" +#include "Engine.h" +#include "PluginFactory.h" +#include "SpaManager.h" +#include "embed.h" + +namespace lmms +{ + +spa::descriptor *SpaSubPluginFeatures::spaDescriptor( + const Plugin::Descriptor::SubPluginFeatures::Key &k) +{ + return Engine::getSPAManager()->getDescriptor(k.attributes["plugin"]); +} + +SpaSubPluginFeatures::SpaSubPluginFeatures(Plugin::PluginTypes _type) : + SubPluginFeatures(_type) +{ +} + +void SpaSubPluginFeatures::fillDescriptionWidget( + QWidget *_parent, const Key *k) const +{ + spa::descriptor *spaDes = spaDescriptor(*k); + + QLabel *label = new QLabel(_parent); + label->setText(QWidget::tr("Name: ") + spaDes->name()); + + QLabel *label2 = new QLabel(_parent); + label2->setText(spaDes->organizations() + ? QWidget::tr((strchr(spaDes->organizations(), ',') + ? "Organizations: " + : "Organization: ")) + + spaDes->organizations() + : QWidget::tr("Organization: ") + ""); + + 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(spaDes->authors()); + 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); + + using license_type = spa::descriptor::license_type; + QLabel *copyright_content = new QLabel(copyright); + copyright_content->setText(spaDes->license() == license_type::gpl_3_0 + ? "GPL v3.0" + : spaDes->license() == license_type::gpl_2_0 + ? "GPL v2.0" + : spaDes->license() == license_type::lgpl_3_0 + ? "LGPL v3.0" + : spaDes->license() == + license_type::lgpl_2_1 + ? "LGPL v2.1" + : ""); + copyright_content->setWordWrap(true); + l->addWidget(copyright_label); + l->addWidget(copyright_content, 1); + + QLabel *requiresRealTime = new QLabel(_parent); + requiresRealTime->setText(QWidget::tr("Requires Real Time: ") + + (spaDes->properties.realtime_dependency ? QWidget::tr("Yes") + : QWidget::tr("No"))); + + QLabel *realTimeCapable = new QLabel(_parent); + realTimeCapable->setText(QWidget::tr("Real Time Capable: ") + + (spaDes->properties.hard_rt_capable ? QWidget::tr("Yes") + : QWidget::tr("No"))); + + // possibly TODO: version, project, plugin type, number of channels +} + +QString SpaSubPluginFeatures::additionalFileExtensions( + const Plugin::Descriptor::SubPluginFeatures::Key &k) const +{ + return spaDescriptor(k)->save_formats(); +} + +QString SpaSubPluginFeatures::displayName( + const Plugin::Descriptor::SubPluginFeatures::Key &k) const +{ + return spaDescriptor(k)->name(); +} + +QString SpaSubPluginFeatures::description( + const Plugin::Descriptor::SubPluginFeatures::Key &k) const +{ + return spaDescriptor(k)->description_line(); +} + +const PixmapLoader *SpaSubPluginFeatures::logo( + const Plugin::Descriptor::SubPluginFeatures::Key &k) const +{ + spa::descriptor *spaDes = spaDescriptor(k); + + const char **xpm = nullptr; + QString xpmKey; + if(spaDes) + { + xpm = spaDes->xpm_load(); + QString uniqueName = spa::unique_name(*spaDes).c_str(); + xpmKey = "spa-plugin:" + uniqueName; + } + + return xpm ? new PixmapLoader(QString("xpm:" + xpmKey), xpm) + : new PixmapLoader("plugins"); +} + +void SpaSubPluginFeatures::listSubPluginKeys( + const Plugin::Descriptor *_desc, KeyList &_kl) const +{ + SpaManager *spaMgr = Engine::getSPAManager(); + for (const std::pair &pr : + *spaMgr) + { + if (pr.second.m_type == m_type) + { + using KeyType = + Plugin::Descriptor::SubPluginFeatures::Key; + KeyType::AttributeMap atm; + atm["file"] = pr.second.m_path; // TODO: remove path, + // remove so/dll + atm["plugin"] = QString::fromUtf8(pr.first.c_str()); + const spa::descriptor &spaDes = *pr.second.m_descriptor; + QString uniqueName = spa::unique_name(spaDes).c_str(); + + _kl.push_back(KeyType(_desc, spaDes.name(), atm)); + // qDebug() << "Found SPA sub plugin key of type" << + // m_type << ":" << _kl.back().name; + } + } +} + +} // namespace lmms diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 1000ba51d1c..3950b406772 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -37,6 +37,7 @@ SET(LMMS_SRCS gui/SendButtonIndicator.cpp gui/SideBar.cpp gui/SideBarWidget.cpp + gui/SpaViewBase.cpp gui/StringPairDrag.cpp gui/SubWindow.cpp gui/ToolPluginView.cpp diff --git a/src/gui/ControllerRackView.cpp b/src/gui/ControllerRackView.cpp index e9f23c7679b..8571d4aa1b1 100644 --- a/src/gui/ControllerRackView.cpp +++ b/src/gui/ControllerRackView.cpp @@ -31,12 +31,15 @@ #include "Song.h" #include "embed.h" +#include "Clipboard.h" +#include "ControllerConnection.h" #include "GuiApplication.h" #include "MainWindow.h" #include "ControllerRackView.h" #include "ControllerView.h" #include "LfoController.h" #include "SubWindow.h" +#include "StringPairDrag.h" namespace lmms::gui { @@ -88,6 +91,8 @@ ControllerRackView::ControllerRackView() : subWin->resize( 350, 200 ); subWin->setFixedWidth( 350 ); subWin->setMinimumHeight( 200 ); + + setAcceptDrops(true); } @@ -203,7 +208,7 @@ void ControllerRackView::addController() void ControllerRackView::closeEvent( QCloseEvent * _ce ) - { +{ if( parentWidget() ) { parentWidget()->hide(); @@ -213,7 +218,47 @@ void ControllerRackView::closeEvent( QCloseEvent * _ce ) hide(); } _ce->ignore(); - } +} + + + + +void ControllerRackView::dragEnterEvent( QDragEnterEvent *dee ) +{ + StringPairDrag::processDragEnterEvent( dee, "automatable_model" ); +} + + + + +void ControllerRackView::dropEvent( QDropEvent *de ) +{ + QString type = StringPairDrag::decodeKey( de ); + QString val = StringPairDrag::decodeValue( de ); + // qDebug() << "DROP: type/val:" << type << ", " << val; + + // dragging on the rack view - i.e. not on a controller, + // but away from any controller - shall remove the connection + if( type == "automatable_model" ) + { + AutomatableModel* mod = + Engine::getAutomatableModel( val, + !de->mimeData()->hasFormat(Clipboard::mimeType(Clipboard::MimeType::StringPair))); + + if (mod) + { + de->acceptProposedAction(); + + if( mod->controllerConnection() ) + { + delete mod->controllerConnection(); + mod->setControllerConnection( nullptr ); + } + + delete mod; + } + } +} } // namespace lmms::gui diff --git a/src/gui/ControllerView.cpp b/src/gui/ControllerView.cpp index 903c028621c..8c26b53044a 100644 --- a/src/gui/ControllerView.cpp +++ b/src/gui/ControllerView.cpp @@ -34,10 +34,13 @@ #include "ControllerView.h" #include "CaptionMenu.h" +#include "Clipboard.h" +#include "ControllerConnection.h" #include "ControllerDialog.h" #include "embed.h" #include "GuiApplication.h" #include "MainWindow.h" +#include "StringPairDrag.h" #include "SubWindow.h" namespace lmms::gui @@ -92,6 +95,7 @@ ControllerView::ControllerView( Controller * _model, QWidget * _parent ) : m_subWindow->hide(); setModel( _model ); + setAcceptDrops( true ); } @@ -158,6 +162,42 @@ void ControllerView::renameController() } + + +void ControllerView::dragEnterEvent( QDragEnterEvent * dee ) +{ + StringPairDrag::processDragEnterEvent( dee, "automatable_model" ); +} + + + + +void ControllerView::dropEvent(QDropEvent *de) +{ + QString type = StringPairDrag::decodeKey( de ); + QString val = StringPairDrag::decodeValue( de ); + + // drop on a controller shall connect the drag source to this controller + if( type == "automatable_model" ) + { + AutomatableModel * mod = Engine::getAutomatableModel( val, + !de->mimeData()->hasFormat(Clipboard::mimeType(Clipboard::MimeType::StringPair)) ); + + if(mod) + { + mod->setControllerConnection( new ControllerConnection( getController() ) ); + + // tell the source app that the drop did succeed + de->acceptProposedAction(); + } + } + + update(); +} + + + + void ControllerView::mouseDoubleClickEvent( QMouseEvent * event ) { renameController(); diff --git a/src/gui/FileBrowser.cpp b/src/gui/FileBrowser.cpp index 0a6f8a216b7..c0e91af9729 100644 --- a/src/gui/FileBrowser.cpp +++ b/src/gui/FileBrowser.cpp @@ -632,7 +632,7 @@ void FileBrowserTreeWidget::previewFileItem(FileItem* file) delete tf; } else if ( - (ext == "xiz" || ext == "sf2" || ext == "sf3" || + (ext == "xiz" || ext == "xmz" || ext == "sf2" || ext == "sf3" || ext == "gig" || ext == "pat") && !getPluginFactory()->pluginSupportingExtension(ext).isNull()) { @@ -1236,7 +1236,7 @@ void FileItem::determineFileType() m_type = PresetFile; m_handling = LoadAsPreset; } - else if( ext == "xiz" && ! getPluginFactory()->pluginSupportingExtension(ext).isNull() ) + else if( (ext == "xiz" || ext == "xmz" ) && ! getPluginFactory()->pluginSupportingExtension(ext).isNull() ) { m_type = PresetFile; m_handling = LoadByPlugin; diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index a77094b1948..8f9befa1dd6 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -149,7 +149,7 @@ MainWindow::MainWindow() : sideBar->appendTab( new FileBrowser( confMgr->userPresetsDir() + "*" + confMgr->factoryPresetsDir(), - "*.xpf *.cs.xml *.xiz *.lv2", + "*.xpf *.cs.xml *.xiz *.xmz *.lv2", tr( "My Presets" ), embed::getIconPixmap( "preset_file" ).transformed( QTransform().rotate( 90 ) ), splitter , false, true, diff --git a/src/gui/SpaViewBase.cpp b/src/gui/SpaViewBase.cpp new file mode 100644 index 00000000000..50f791ebacf --- /dev/null +++ b/src/gui/SpaViewBase.cpp @@ -0,0 +1,328 @@ +/* + * SpaViewBase.cpp - base class for SPA plugin views + * + * Copyright (c) 2018-2019 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 "SpaViewBase.h" + +#ifdef LMMS_HAVE_SPA + +#include +#include +#include +#include +#include + +#include "Controls.h" +#include "embed.h" +#include "Engine.h" +#include "gui_templates.h" +#include "SpaControlBase.h" +#include "SpaOscModel.h" +#include "SpaProc.h" +#include "StringPairDrag.h" +#include "Clipboard.h" +#include "LedCheckBox.h" + +namespace lmms::gui { + +SpaViewBase::SpaViewBase(QWidget* meAsWidget, SpaControlBase *ctrlBase) +{ + m_grid = new QGridLayout(meAsWidget); + + m_reloadPluginButton = new QPushButton(QObject::tr("Reload Plugin"), + meAsWidget); + m_grid->addWidget(m_reloadPluginButton, Rows::ButtonRow, 0, 1, 3); + + if (ctrlBase->hasUi()) + { + 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())); + m_toggleUIButton->setWhatsThis( + QObject::tr("Click here to show or hide the " + "graphical user interface (GUI)")); + m_grid->addWidget(m_toggleUIButton, Rows::ButtonRow, 3, 1, 3); + } + + m_procView = new SpaViewProc(meAsWidget, + ctrlBase->controls()[0].get(), + m_colNum); + m_grid->addWidget(m_procView, Rows::ProcRow, 0); +} + +SpaViewBase::~SpaViewBase() {} + +void SpaViewBase::modelChanged(SpaControlBase *ctrlBase) +{ + if(m_toggleUIButton) + { + m_toggleUIButton->setChecked(ctrlBase->m_hasGUI); + } + + // reconnect models + LinkedModelGroupsView::modelChanged(ctrlBase); +} + +// TODO: handle modelChanged + + +SpaViewProc::SpaViewProc(QWidget* parent, SpaProc *proc, + std::size_t colNum) : + LinkedModelGroupView(parent, proc, colNum), + m_proc(proc) +{ +#if 0 + class SetupWidget : public Lv2Ports::Visitor + { + public: + QWidget* m_par; // input + const AutoLilvNode* m_commentUri; // input + ControlBase* m_control = nullptr; // output + void visit(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()); + + LilvNodes* props = lilv_port_get_value( + port.m_plugin, port.m_port, m_commentUri->get()); + LILV_FOREACH(nodes, itr, props) + { + const LilvNode* nod = lilv_nodes_get(props, itr); + m_control->topWidget()->setToolTip(lilv_node_as_string(nod)); + break; + } + lilv_nodes_free(props); + } + } + }; + + AutoLilvNode commentUri = uri(LILV_NS_RDFS "comment"); + for (std::unique_ptr& port : ctrlBase->getPorts()) + { + SetupWidget setup; + setup.m_par = this; + setup.m_commentUri = &commentUri; + port->accept(setup); + + if (setup.m_control) { addControl(setup.m_control); } + } +#endif + Control* control = nullptr; + for (SpaProc::LmmsPorts::TypedPorts &ports : + proc->m_ports.m_userPorts) + { + //QWidget* wdg; + //AutomatableModelView* modelView; + switch (ports.m_type) + { + case 'f': + { + control = new KnobControl(this); + control->setText(ports.m_connectedModel.m_floatModel->displayName()); + // wdg = k; + //modelView = k; + break; + } + case 'i': + { + // TODO: check max + control = new LcdControl(2, this); + control->setText(ports.m_connectedModel.m_intModel->displayName()); + // wdg = l; + // modelView = l; + break; + } + case 'b': + { + control = new CheckControl(this); + control->setText(ports.m_connectedModel.m_boolModel->displayName()); + // wdg = l; + // modelView = l; + break; + } + default: + // wdg = nullptr; + // modelView = nullptr; + control = nullptr; + break; + } + + if (control) + { + // start in row one, add widgets cell by cell +// m_modelViews.push_back(modelView); + /*m_grid->addWidget( + wdg, + m_firstModelRow + wdgNum / m_rowNum, + wdgNum % m_rowNum); + ++wdgNum;*/ + addControl(control, ports.m_id, ports.m_id, false); + } + else + { + qCritical() << "this should never happen..."; + } + } + + QObject::connect(proc, SIGNAL(modelAdded(AutomatableModel*)), + this, SLOT(modelAdded(AutomatableModel*)), Qt::DirectConnection); + QObject::connect(proc, SIGNAL(modelRemoved(AutomatableModel*)), + this, SLOT(modelRemoved(AutomatableModel*)), Qt::DirectConnection); + + setAcceptDrops(true); +} + + + + +void SpaViewProc::modelAdded(AutomatableModel *mdl) +{ + class ModelAdder : public ModelVisitor + { + // this is duplicating stuff to the ctor routines + Control* m_control = nullptr; + QWidget* m_parent; + void visit(FloatModel& fm) override + { + m_control = new KnobControl(m_parent); + FloatOscModel& om = dynamic_cast(fm); + m_control->setText(om.displayName()); + } + void visit(IntModel& im) override + { + // TODO: check max + m_control = new LcdControl(2, m_parent); + IntOscModel& om = dynamic_cast(im); + m_control->setText(om.displayName()); + } + void visit(BoolModel& bm) override + { + m_control = new CheckControl(m_parent); + BoolOscModel& om = dynamic_cast(bm); + m_control->setText(om.displayName()); + } + public: + ModelAdder(QWidget* parent) : m_parent(parent) {} + Control* control() { return m_control; } + }; + + ModelAdder adder(this); + mdl->accept(adder); + addControl(adder.control(), mdl->objectName().toStdString(), + mdl->objectName().toStdString(), true); + adder.control()->setModel(mdl); +} + +void SpaViewProc::modelRemoved(AutomatableModel *mdl) +{ + removeControl(mdl->objectName()); +} + +LinkedModelGroupView *SpaViewBase::getGroupView() +{ + return m_procView; +} + +void SpaViewBase::dropEvent( QDropEvent *de ) +{ + if(m_procView) { m_procView->dropEvent(de); } +} + +void SpaViewBase::dragEnterEvent( QDragEnterEvent *de ) +{ + if(m_procView) { m_procView->dragEnterEvent(de); } +} + +void SpaViewProc::dragEnterEvent(QDragEnterEvent *dev) +{ + void (QDragEnterEvent::*reaction)(void) = &QDragEnterEvent::ignore; + + if (dev->mimeData()->hasFormat(Clipboard::mimeType(Clipboard::MimeType::StringPair))) + { + const QString txt = + dev->mimeData()->data(Clipboard::mimeType(Clipboard::MimeType::StringPair)); + if (txt.section(':', 0, 0) == "pluginpresetfile") { + reaction = &QDragEnterEvent::acceptProposedAction; + } + } + else if (dev->mimeData()->hasFormat(Clipboard::mimeType(Clipboard::MimeType::Osc))) + { + const QString txt = + dev->mimeData()->data(Clipboard::mimeType(Clipboard::MimeType::Osc)); + if (txt.section(':', 0, 0) == "automatable_model") { + reaction = &QDragEnterEvent::acceptProposedAction; + } + } + + (dev->*reaction)(); +} + +void SpaViewProc::dropEvent(QDropEvent *dev) +{ + const QString type = StringPairDrag::decodeKey(dev); + const QString value = StringPairDrag::decodeValue(dev); + if (type == "pluginpresetfile") + { + m_proc->loadFile(value, true); + dev->accept(); + } + else if( type == "automatable_model" ) + { + Engine::getAutomatableModel( value, + dev->mimeData()->hasFormat( "application/x-osc-stringpair" )); + // this will create the model if it does not exist yet + dev->accept(); + } + else + dev->ignore(); +} + +} // namespace lmms::gui + + +#endif // LMMS_HAVE_SPA + + diff --git a/src/gui/StringPairDrag.cpp b/src/gui/StringPairDrag.cpp index e9eef0b88fc..75095ccb8f5 100644 --- a/src/gui/StringPairDrag.cpp +++ b/src/gui/StringPairDrag.cpp @@ -25,6 +25,7 @@ */ +#include #include #include @@ -86,18 +87,35 @@ bool StringPairDrag::processDragEnterEvent( QDragEnterEvent * _dee, // For mimeType() and MimeType enum class using namespace Clipboard; - if( !_dee->mimeData()->hasFormat( mimeType( MimeType::StringPair ) ) ) + if( !_dee->mimeData()->hasFormat( mimeType( MimeType::StringPair ) ) && + !_dee->mimeData()->hasFormat( mimeType( MimeType::Osc ) ) ) { - return( false ); + qDebug() << "will reject: bad mimetype:" + << (_dee->mimeData()->formats().empty() + ? "none" + : _dee->mimeData()->formats().front().toUtf8().data()); + return false; } QString txt = _dee->mimeData()->data( mimeType( MimeType::StringPair ) ); if( _allowed_keys.split( ',' ).contains( txt.section( ':', 0, 0 ) ) ) { _dee->acceptProposedAction(); - return( true ); + return true; + } + else { + QString txtOsc = _dee->mimeData()->data( mimeType( MimeType::Osc ) ); + if( _allowed_keys.split( ',' ).contains( txtOsc.section( ':', 0, 0 ) ) ) + { + _dee->acceptProposedAction(); + qDebug() << "will accept OSC DnD"; + return true; + } + else + qDebug() << "will reject DnD: cannot drop" + << txt << "or" << txtOsc << "here"; } _dee->ignore(); - return( false ); + return false; } diff --git a/src/gui/clips/AutomationClipView.cpp b/src/gui/clips/AutomationClipView.cpp index 2bc29184ca8..b6a24b113fb 100644 --- a/src/gui/clips/AutomationClipView.cpp +++ b/src/gui/clips/AutomationClipView.cpp @@ -441,9 +441,9 @@ void AutomationClipView::dropEvent( QDropEvent * _de ) QString val = StringPairDrag::decodeValue( _de ); if( type == "automatable_model" ) { - AutomatableModel * mod = dynamic_cast( - Engine::projectJournal()-> - journallingObject( val.toInt() ) ); + AutomatableModel * mod = Engine::getAutomatableModel( val, + _de->mimeData()->hasFormat( "application/x-osc-stringpair") ); + if( mod != nullptr ) { bool added = m_clip->addObject( mod ); @@ -463,6 +463,8 @@ void AutomationClipView::dropEvent( QDropEvent * _de ) { getGUI()->automationEditor()->setCurrentClip( m_clip ); } + + _de->acceptProposedAction(); } else { diff --git a/src/gui/editors/AutomationEditor.cpp b/src/gui/editors/AutomationEditor.cpp index fba88643e16..2da84b2aa34 100644 --- a/src/gui/editors/AutomationEditor.cpp +++ b/src/gui/editors/AutomationEditor.cpp @@ -2036,9 +2036,9 @@ void AutomationEditorWindow::dropEvent( QDropEvent *_de ) QString val = StringPairDrag::decodeValue( _de ); if( type == "automatable_model" ) { - AutomatableModel * mod = dynamic_cast( - Engine::projectJournal()-> - journallingObject( val.toInt() ) ); + AutomatableModel * mod = Engine::getAutomatableModel( val, + _de->mimeData()->hasFormat( "application/x-osc-stringpair") ); + if (mod != nullptr) { bool added = m_editor->m_clip->addObject( mod ); @@ -2052,6 +2052,8 @@ void AutomationEditorWindow::dropEvent( QDropEvent *_de ) } setCurrentClip( m_editor->m_clip ); } + + _de->acceptProposedAction(); } update(); diff --git a/src/gui/instrument/InstrumentTrackWindow.cpp b/src/gui/instrument/InstrumentTrackWindow.cpp index cead80e7c8d..bb9b4916bfd 100644 --- a/src/gui/instrument/InstrumentTrackWindow.cpp +++ b/src/gui/instrument/InstrumentTrackWindow.cpp @@ -24,6 +24,7 @@ #include "InstrumentTrackWindow.h" +#include #include #include #include @@ -569,8 +570,76 @@ void InstrumentTrackWindow::dropEvent( QDropEvent* event ) if( type == "instrument" ) { + /* + This is a dirty fix to be able to update zyn + */ + + Instrument* oldIns = m_track->instrument(); + + Plugin::Descriptor::SubPluginFeatures::Key* dndKey = + static_cast(Engine::pickDndPluginKey()); + + bool convert = value == "spainstrument" && + dndKey && + (dndKey->displayName() == "ZynAddSubFX") && + !strcmp(oldIns->descriptor()->name, "zynaddsubfx"); + + DataFile savedSettings(DataFile::SongProject); + QDomDocument newDoc("ZynAddSubFX-data"); + const char* newNode = "ZASF"; + + if(convert) + { + qDebug() << "Trying to convert instrument"; + + oldIns->saveState(savedSettings, + savedSettings.content()); + QDomNode xmzNode = savedSettings + .namedItem("lmms-project") + .namedItem("song") + .namedItem("zynaddsubfx") + .namedItem("ZynAddSubFX-data"); + + if(!xmzNode.isNull()) + { + QDomNode zasf = newDoc.createElement(newNode); + newDoc.appendChild(zasf); + + QString str; + QTextStream stream(&str); + stream << "\n" + << "\n"; + xmzNode.save(stream, 1); + QDomCDATASection cdata = newDoc.createCDATASection(str); + zasf.appendChild(cdata); + + // if you want to import the controller models, + // too, you'd need to add model for each + // old one into "connected-models": + // + // + // + // + // + // The problem is that most MIDI events must be + // forwarded to *multiple* OSC ports at the same + // time + } + else { + qDebug() << "Failed to extract zyn data :-("; + qDebug() << "Saved settings: " + << savedSettings.toString(); + } + } m_track->loadInstrument( value, nullptr, true /* DnD */ ); + if(convert) + { + m_track->instrument()->restoreState( + newDoc.namedItem(newNode).toElement()); + } + Engine::getSong()->setModified(); event->accept(); diff --git a/src/gui/modals/SetupDialog.cpp b/src/gui/modals/SetupDialog.cpp index 9c15f5b534e..5b7978e63c9 100644 --- a/src/gui/modals/SetupDialog.cpp +++ b/src/gui/modals/SetupDialog.cpp @@ -149,6 +149,7 @@ SetupDialog::SetupDialog(ConfigTabs tab_to_open) : m_workingDir(QDir::toNativeSeparators(ConfigManager::inst()->workingDir())), m_vstDir(QDir::toNativeSeparators(ConfigManager::inst()->vstDir())), m_ladspaDir(QDir::toNativeSeparators(ConfigManager::inst()->ladspaDir())), + m_spaDir(QDir::toNativeSeparators(ConfigManager::inst()->spaDir())), m_gigDir(QDir::toNativeSeparators(ConfigManager::inst()->gigDir())), m_sf2Dir(QDir::toNativeSeparators(ConfigManager::inst()->sf2Dir())), #ifdef LMMS_HAVE_FLUIDSYNTH @@ -787,6 +788,10 @@ SetupDialog::SetupDialog(ConfigTabs tab_to_open) : SLOT(setLADSPADir(const QString&)), SLOT(openLADSPADir()), m_ladspaDirLineEdit, "add_folder"); + addPathEntry("SPA plugin directories", m_spaDir, + SLOT(setSPADir(const QString &)), + SLOT(openSPADir()), + m_spaLineEdit, "add_folder"); addPathEntry(tr("SF2 directory"), m_sf2Dir, SLOT(setSF2Dir(const QString&)), SLOT(openSF2Dir()), @@ -969,6 +974,7 @@ void SetupDialog::accept() ConfigManager::inst()->setWorkingDir(QDir::fromNativeSeparators(m_workingDir)); ConfigManager::inst()->setVSTDir(QDir::fromNativeSeparators(m_vstDir)); ConfigManager::inst()->setLADSPADir(QDir::fromNativeSeparators(m_ladspaDir)); + ConfigManager::inst()->setSPADir(QDir::fromNativeSeparators(m_spaDir)); ConfigManager::inst()->setSF2Dir(QDir::fromNativeSeparators(m_sf2Dir)); #ifdef LMMS_HAVE_FLUIDSYNTH ConfigManager::inst()->setSF2File(m_sf2File); @@ -1280,12 +1286,38 @@ void SetupDialog::openLADSPADir() } +void SetupDialog::openSPADir() +{ + QString newDir = FileDialog::getExistingDirectory( this, + tr( "Choose SPA plugin directory" ), + m_spaDir ); + if( ! newDir.isEmpty() ) + { + if( m_spaLineEdit->text() == "" ) + { + m_spaLineEdit->setText( newDir ); + } + else + { + m_spaLineEdit->setText( m_spaLineEdit->text() + "," + + newDir ); + } + } +} + + void SetupDialog::setLADSPADir(const QString & ladspaDir) { m_ladspaDir = ladspaDir; } +void SetupDialog::setSPADir(const QString &ld) +{ + m_spaDir = ld; +} + + void SetupDialog::openSF2Dir() { QString new_dir = FileDialog::getExistingDirectory(this, diff --git a/src/gui/tracks/AutomationTrackView.cpp b/src/gui/tracks/AutomationTrackView.cpp index 60f00679ba0..17e61b13ba7 100644 --- a/src/gui/tracks/AutomationTrackView.cpp +++ b/src/gui/tracks/AutomationTrackView.cpp @@ -62,9 +62,9 @@ void AutomationTrackView::dropEvent( QDropEvent * _de ) QString val = StringPairDrag::decodeValue( _de ); if( type == "automatable_model" ) { - AutomatableModel * mod = dynamic_cast( - Engine::projectJournal()-> - journallingObject( val.toInt() ) ); + AutomatableModel * mod = Engine::getAutomatableModel(val, + _de->mimeData()->hasFormat( "application/x-osc-stringpair")); + if( mod != nullptr ) { TimePos pos = TimePos( trackContainerView()-> @@ -84,6 +84,9 @@ void AutomationTrackView::dropEvent( QDropEvent * _de ) AutomationClip * autoClip = dynamic_cast( clip ); autoClip->addObject( mod ); } + + // tell the source app that the drop did succeed + _de->acceptProposedAction(); } update(); diff --git a/src/lmmsconfig.h.in b/src/lmmsconfig.h.in index d130d6fc255..cf37d3f6aeb 100644 --- a/src/lmmsconfig.h.in +++ b/src/lmmsconfig.h.in @@ -29,6 +29,7 @@ #cmakedefine LMMS_HAVE_PORTAUDIO #cmakedefine LMMS_HAVE_SOUNDIO #cmakedefine LMMS_HAVE_PULSEAUDIO +#cmakedefine LMMS_HAVE_SPA #cmakedefine LMMS_HAVE_SDL #cmakedefine LMMS_HAVE_SDL2 #cmakedefine LMMS_HAVE_STK diff --git a/src/tracks/InstrumentTrack.cpp b/src/tracks/InstrumentTrack.cpp index 000318d7b33..5fdeb3647ca 100644 --- a/src/tracks/InstrumentTrack.cpp +++ b/src/tracks/InstrumentTrack.cpp @@ -23,6 +23,8 @@ */ #include "InstrumentTrack.h" +#include +#include #include "AudioEngine.h" #include "AutomationClip.h" #include "ConfigManager.h"