diff --git a/.gitmodules b/.gitmodules index 58c4d8526d4..d97d1263f6b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -46,3 +46,9 @@ [submodule "src/3rdparty/hiir/hiir"] path = src/3rdparty/hiir/hiir url = https://github.com/LostRobotMusic/hiir +[submodule "src/3rdparty/clap/clap"] + path = src/3rdparty/clap/clap + url = https://github.com/free-audio/clap.git +[submodule "src/3rdparty/clap/clap-helpers"] + path = src/3rdparty/clap/clap-helpers + url = https://github.com/free-audio/clap-helpers.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 5bb4adc4aa8..b310db8f9c1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -75,6 +75,7 @@ OPTION(WANT_OSS "Include Open Sound System support" ON) 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_CLAP "Include CLAP plugins" ON) OPTION(WANT_CMT "Include Computer Music Toolkit LADSPA plugins" ON) OPTION(WANT_JACK "Include JACK (Jack Audio Connection Kit) support" ON) OPTION(WANT_WEAKJACK "Loosely link JACK libraries" ON) @@ -276,6 +277,13 @@ ELSE(WANT_SUIL) SET(STATUS_SUIL "not built as requested") ENDIF(WANT_SUIL) +IF(WANT_CLAP) + SET(LMMS_HAVE_CLAP TRUE) + SET(STATUS_CLAP "OK") +ELSE(WANT_CLAP) + SET(STATUS_CLAP "not built as requested") +ENDIF(WANT_CLAP) + IF(WANT_CALF) SET(LMMS_HAVE_CALF TRUE) SET(STATUS_CALF "OK") @@ -807,6 +815,7 @@ MESSAGE( "----------------\n" "* Lv2 plugins : ${STATUS_LV2}\n" "* SUIL for plugin UIs : ${STATUS_SUIL}\n" +"* CLAP plugins : ${STATUS_CLAP}\n" "* ZynAddSubFX instrument : ${STATUS_ZYN}\n" "* Carla Patchbay & Rack : ${STATUS_CARLA}\n" "* SoundFont2 player : ${STATUS_FLUIDSYNTH}\n" diff --git a/cmake/modules/PluginList.cmake b/cmake/modules/PluginList.cmake index 009679533ae..129f8b4e23a 100644 --- a/cmake/modules/PluginList.cmake +++ b/cmake/modules/PluginList.cmake @@ -30,6 +30,8 @@ SET(LMMS_PLUGIN_LIST CarlaBase CarlaPatchbay CarlaRack + ClapEffect + ClapInstrument Compressor CrossoverEQ Delay diff --git a/include/AutomatableModel.h b/include/AutomatableModel.h index 15285e17ab3..025c7a4779f 100644 --- a/include/AutomatableModel.h +++ b/include/AutomatableModel.h @@ -471,6 +471,7 @@ class LMMS_EXPORT FloatModel : public TypedAutomatableModel QString displayValue( const float val ) const override; } ; +// TODO: Add DoubleModel? class LMMS_EXPORT IntModel : public TypedAutomatableModel { diff --git a/include/ClapAudioPorts.h b/include/ClapAudioPorts.h new file mode 100644 index 00000000000..2710d6ae8c2 --- /dev/null +++ b/include/ClapAudioPorts.h @@ -0,0 +1,165 @@ +/* + * ClapAudioPorts.h - Implements CLAP audio ports extension + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 LMMS_CLAP_AUDIO_PORTS_H +#define LMMS_CLAP_AUDIO_PORTS_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_CLAP + +#include +#include +#include + +#include "ClapExtension.h" +#include "lmms_basics.h" +#include "lmms_export.h" +#include "PluginIssue.h" +#include "PluginPortConfig.h" +#include "SampleFrame.h" + +namespace lmms +{ + +//! RAII-enabled CLAP AudioBuffer +class ClapAudioBuffer +{ +public: + ClapAudioBuffer(std::uint32_t channels, fpp_t frames) + : m_channels{channels} + , m_frames{frames} + { + if (channels == 0) { return; } + m_data = new float*[m_channels](); + for (std::uint32_t channel = 0; channel < m_channels; ++channel) + { + m_data[channel] = new float[m_frames](); + } + } + + ClapAudioBuffer(const ClapAudioBuffer&) = delete; + auto operator=(const ClapAudioBuffer&) -> ClapAudioBuffer& = delete; + + ClapAudioBuffer(ClapAudioBuffer&& other) noexcept + : m_channels{std::exchange(other.m_channels, 0)} + , m_frames{std::exchange(other.m_frames, 0)} + , m_data{std::exchange(other.m_data, nullptr)} + { + } + + auto operator=(ClapAudioBuffer&& other) noexcept -> ClapAudioBuffer& + { + if (this != &other) + { + free(); + m_channels = std::exchange(other.m_channels, 0); + m_frames = std::exchange(other.m_frames, 0); + m_data = std::exchange(other.m_data, nullptr); + } + return *this; + } + + ~ClapAudioBuffer() { free(); } + + //! [channel][frame] + auto data() const -> float** { return m_data; } + +private: + + void free() noexcept + { + if (!m_data) { return; } + for (std::uint32_t channel = 0; channel < m_channels; ++channel) + { + if (m_data[channel]) { delete[] m_data[channel]; } + } + delete[] m_data; + } + + std::uint32_t m_channels; + fpp_t m_frames; + float** m_data = nullptr; +}; + +class LMMS_EXPORT ClapAudioPorts final + : public ClapExtension + , public PluginPortConfig +{ +public: + ClapAudioPorts(ClapInstance* parent); + ~ClapAudioPorts() override; + + auto init(clap_process& process) noexcept -> bool; + + auto extensionId() const -> std::string_view override { return CLAP_EXT_AUDIO_PORTS; } + + void copyBuffersFromCore(const SampleFrame* buffer, fpp_t frames); + void copyBuffersToCore(SampleFrame* buffer, fpp_t frames) const; + +private: + auto hostExtImpl() const -> const clap_host_audio_ports* override { return nullptr; } // not impl for host yet + auto checkSupported(const clap_plugin_audio_ports& ext) -> bool override; + + std::vector m_issues; // TODO: Remove? + + /** + * Process-related + */ + std::vector m_audioIn, m_audioOut; + clap_audio_buffer_t* m_audioInActive = nullptr; //!< Pointer to m_audioIn element used by LMMS + clap_audio_buffer_t* m_audioOutActive = nullptr; //!< Pointer to m_audioOut element used by LMMS + + std::vector m_audioInBuffers, m_audioOutBuffers; //!< [port][channel][frame] + + /** + * Ports + */ + enum class AudioPortType + { + Unsupported, + Mono, + Stereo + }; + + struct AudioPort + { + clap_audio_port_info info{}; + std::uint32_t index = 0; //!< Index on plugin side, not m_audioPorts*** + bool isInput = false; + PluginPortConfig::PortType type = PluginPortConfig::PortType::None; // None = unsupported + bool used = false; //!< In use by LMMS + }; + + std::vector m_audioPortsIn, m_audioPortsOut; + AudioPort* m_audioPortInActive = nullptr; //!< Pointer to m_audioPortsIn element used by LMMS + AudioPort* m_audioPortOutActive = nullptr; //!< Pointer to m_audioPortsOut element used by LMMS + +}; + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP + +#endif // LMMS_CLAP_AUDIO_PORTS_H diff --git a/include/ClapExtension.h b/include/ClapExtension.h new file mode 100644 index 00000000000..6f632127d9e --- /dev/null +++ b/include/ClapExtension.h @@ -0,0 +1,301 @@ +/* + * ClapExtension.h - Base class templates for implementing CLAP extensions + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 LMMS_CLAP_EXTENSION_H +#define LMMS_CLAP_EXTENSION_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_CLAP + +#include +#include + +#include "lmms_export.h" +#include "NoCopyNoMove.h" + +namespace lmms +{ + +class ClapInstance; +class ClapLog; + +namespace detail +{ + +class LMMS_EXPORT ClapExtensionHelper : public NoCopyNoMove +{ +public: + ClapExtensionHelper(ClapInstance* instance) + : m_instance{instance} + { + } + + virtual auto extensionId() const -> std::string_view = 0; + virtual auto extensionIdCompat() const -> std::string_view { return std::string_view{}; } + + void beginPluginInit() { m_inPluginInit = true; } + void endPluginInit() { m_inPluginInit = false; } + auto logger() const -> const ClapLog&; + + static auto fromHost(const clap_host* host) -> ClapInstance*; + +protected: + enum class State + { + Uninit, + PartialInit, + FullInit, + Unsupported + }; + + auto instance() const { return m_instance; } + auto instance() { return m_instance; } + auto host() const -> const clap_host*; + auto plugin() const -> const clap_plugin*; + + /** + * For additional initialization steps. + * - Only called if basic init was successful + * - By default, will not be called during plugin->init() + * unless `delayInit()` returns false + * - supported() == true during this call, + * and (if applicable) pluginExt() is non-null + */ + virtual auto initImpl() noexcept -> bool { return true; } + + /** + * For additional deinitialization steps. + */ + virtual void deinitImpl() noexcept {} + + /** + * Whether `initImpl()` is postponed until after plugin->init() + */ + virtual auto delayInit() const noexcept -> bool { return true; } + + //! Whether the plugin is calling into the host during plugin->init() + auto inPluginInit() const { return m_inPluginInit; } + +private: + ClapInstance* m_instance = nullptr; + bool m_inPluginInit = false; +}; + +} // namespace detail + + +/** + * Template for extensions with both a host and plugin side + */ +template +class LMMS_EXPORT ClapExtension : public detail::ClapExtensionHelper +{ +public: + using detail::ClapExtensionHelper::ClapExtensionHelper; + virtual ~ClapExtension() = default; + + auto init() -> bool + { + switch (m_state) + { + case State::Uninit: + { + auto ext = static_cast(plugin()->get_extension(plugin(), extensionId().data())); + if (!ext) + { + // Try using compatibility ID if it exists + if (const auto compatId = extensionIdCompat(); !compatId.empty()) + { + ext = static_cast(plugin()->get_extension(plugin(), compatId.data())); + } + } + + if (!ext || !checkSupported(*ext)) + { + m_pluginExt = nullptr; + m_state = State::Unsupported; + return false; + } + + m_pluginExt = ext; + m_state = State::PartialInit; + if ((!delayInit() || !inPluginInit()) && !initImpl()) + { + m_state = State::Unsupported; + return false; + } + + m_state = delayInit() ? State::PartialInit : State::FullInit; + + return true; + } + case State::PartialInit: + { + if (inPluginInit()) { return false; } + if (!initImpl()) + { + m_state = State::Unsupported; + return false; + } + m_state = State::FullInit; + return true; + } + case State::FullInit: return true; + case State::Unsupported: return false; + default: return false; + } + } + + void deinit() + { + deinitImpl(); + m_pluginExt = nullptr; + m_state = State::Uninit; + } + + /** + * Returns whether plugin implements required interface + * and passes any additional checks from initImpl(). + * Do not use before init(). + */ + auto supported() const + { + return m_state == State::PartialInit || m_state == State::FullInit; + } + + /** + * Returns pointer to host extension. + * If called during plugin.init(), will lazily initialize the extension. + */ + auto hostExt() -> const HostExt* + { + init(); + return hostExtImpl(); + } + + //! Non-null after init() is called if plugin implements the needed interface + auto pluginExt() const { return m_pluginExt; } + +protected: + virtual auto hostExtImpl() const -> const HostExt* = 0; + + /** + * Checks whether the plugin extension implements the required + * API methods for use within LMMS. May not be all the methods. + */ + virtual auto checkSupported(const PluginExt& ext) -> bool = 0; + +private: + //const HostExt* m_hostExt = nullptr; + const PluginExt* m_pluginExt = nullptr; + + using State = detail::ClapExtensionHelper::State; + State m_state = State::Uninit; +}; + + +/** + * Template for host-only extensions +*/ +template +class LMMS_EXPORT ClapExtension : public detail::ClapExtensionHelper +{ +public: + using detail::ClapExtensionHelper::ClapExtensionHelper; + virtual ~ClapExtension() = default; + + auto init() -> bool + { + switch (m_state) + { + case State::Uninit: + { + m_state = State::PartialInit; + if ((!delayInit() || !inPluginInit()) && !initImpl()) + { + m_state = State::Unsupported; + return false; + } + + m_state = delayInit() ? State::PartialInit : State::FullInit; + return true; + } + case State::PartialInit: + { + if (inPluginInit()) { return false; } + if (!initImpl()) + { + m_state = State::Unsupported; + return false; + } + m_state = State::FullInit; + return true; + } + case State::FullInit: return true; + case State::Unsupported: return false; + default: return false; + } + } + + void deinit() + { + deinitImpl(); + m_state = State::Uninit; + } + + /** + * Returns whether initImpl() was successful. + * Do not use before init(). + */ + auto supported() const + { + return m_state == State::PartialInit || m_state == State::FullInit; + } + + /** + * Returns pointer to host extension. + * If called during plugin.init(), will lazily initialize the extension. + */ + auto hostExt() -> const HostExt* + { + init(); + return hostExtImpl(); + } + +protected: + virtual auto hostExtImpl() const -> const HostExt* = 0; + +private: + //const HostExt* m_hostExt = nullptr; + + using State = detail::ClapExtensionHelper::State; + State m_state = State::Uninit; +}; + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP + +#endif // LMMS_CLAP_EXTENSION_H diff --git a/include/ClapFile.h b/include/ClapFile.h new file mode 100644 index 00000000000..a9b54697d0e --- /dev/null +++ b/include/ClapFile.h @@ -0,0 +1,116 @@ +/* + * ClapFile.h - Implementation of ClapFile class + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 LMMS_CLAP_FILE_H +#define LMMS_CLAP_FILE_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_CLAP + +#include +#include +#include +#include + +#include "ClapPluginInfo.h" +#include "ClapPresetDatabase.h" +#include "lmms_export.h" + +namespace lmms +{ + +//! Manages a .clap file, each of which may contain multiple plugins +class LMMS_EXPORT ClapFile +{ +public: + //! passkey idiom + class Access + { + public: + friend class ClapManager; + Access(Access&&) noexcept = default; + private: + Access() {} + Access(const Access&) = default; + }; + + explicit ClapFile(std::filesystem::path filename); + ~ClapFile(); + + ClapFile(const ClapFile&) = delete; + ClapFile(ClapFile&&) noexcept = default; + auto operator=(const ClapFile&) -> ClapFile& = delete; + auto operator=(ClapFile&&) noexcept -> ClapFile& = default; + + //! Loads the .clap file and scans for plugins + auto load() -> bool; + + auto filename() const -> auto& { return m_filename; } + auto factory() const { return m_factory; } + + //! Only includes plugins that successfully loaded; Some may be invalidated later + auto pluginInfo() const -> auto& { return m_pluginInfo; } + + //! Only includes plugins that successfully loaded; Some may be invalidated later + auto pluginInfo(Access) -> auto& { return m_pluginInfo; } + + //! Includes plugins that failed to load + auto pluginCount() const { return m_pluginCount; } + + auto presetDatabase() -> ClapPresetDatabase* { return m_presetDatabase.get(); } + +private: + void unload() noexcept; + + std::filesystem::path m_filename; + + std::unique_ptr m_library; + + struct EntryDeleter + { + void operator()(const clap_plugin_entry* p) const noexcept + { + p->deinit(); + } + }; + + std::unique_ptr m_entry; + + const clap_plugin_factory* m_factory = nullptr; + + //! Only includes info for plugins that successfully loaded + std::vector> m_pluginInfo; + + //! Includes plugins that failed to load + std::uint32_t m_pluginCount = 0; + + std::unique_ptr m_presetDatabase; +}; + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP + +#endif // LMMS_CLAP_FILE_H diff --git a/include/ClapGui.h b/include/ClapGui.h new file mode 100644 index 00000000000..72c68088be5 --- /dev/null +++ b/include/ClapGui.h @@ -0,0 +1,89 @@ +/* + * ClapGui.h - Implements CLAP gui extension + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 LMMS_CLAP_GUI_H +#define LMMS_CLAP_GUI_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_CLAP + +#include +#include + +#include "ClapExtension.h" +#include "WindowEmbed.h" +#include "lmms_basics.h" + +namespace lmms +{ + +class ClapGui : public ClapExtension +{ +public: + ClapGui(ClapInstance* instance); + ~ClapGui() { destroy(); } + + auto extensionId() const -> std::string_view override { return CLAP_EXT_GUI; } + + auto create() -> bool; + void destroy(); + + auto isFloating() const { return m_embedMethod == WindowEmbed::Method::Floating; } + + auto supportsEmbed() const { return m_supportsEmbed; } + auto supportsFloating() const { return m_supportsFloating; } + +private: + auto initImpl() noexcept -> bool override; + void deinitImpl() noexcept override; + auto hostExtImpl() const -> const clap_host_gui* override; + auto checkSupported(const clap_plugin_gui& ext) -> bool override; + + static auto windowSupported(const clap_plugin_gui& ext, bool floating) -> bool; + + /** + * clap_host_gui implementation + */ + static void clapResizeHintsChanged(const clap_host* host); + static auto clapRequestResize(const clap_host* host, std::uint32_t width, std::uint32_t height) -> bool; + static auto clapRequestShow(const clap_host* host) -> bool; + static auto clapRequestHide(const clap_host* host) -> bool; + static void clapRequestClosed(const clap_host* host, bool wasDestroyed); + + clap_window m_window{}; + + bool m_created = false; + bool m_visible = false; + + WindowEmbed::Method m_embedMethod = WindowEmbed::Method::Headless; + bool m_supportsEmbed = false; + bool m_supportsFloating = false; +}; + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP + +#endif // LMMS_CLAP_GUI_H diff --git a/include/ClapInstance.h b/include/ClapInstance.h new file mode 100644 index 00000000000..4f93e49b180 --- /dev/null +++ b/include/ClapInstance.h @@ -0,0 +1,280 @@ +/* + * ClapInstance.h - Implementation of ClapInstance class + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 LMMS_CLAP_INSTANCE_H +#define LMMS_CLAP_INSTANCE_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_CLAP + +#include +#include +#include +#include +#include + +#include "ClapAudioPorts.h" +#include "ClapGui.h" +#include "ClapLog.h" +#include "ClapNotePorts.h" +#include "ClapParams.h" +#include "ClapPluginInfo.h" +#include "ClapPresetLoader.h" +#include "ClapState.h" +#include "ClapThreadCheck.h" +#include "ClapTimerSupport.h" +#include "MidiEvent.h" +#include "Plugin.h" +#include "SerializingObject.h" +#include "TimePos.h" +#include "../src/3rdparty/ringbuffer/include/ringbuffer/ringbuffer.h" +#include "lmms_export.h" + +namespace lmms +{ + +/** + * ClapInstance is a CLAP instrument/effect processor which provides + * basic CLAP functionality plus support for multiple CLAP extensions. + */ +class LMMS_EXPORT ClapInstance final + : public QObject + , public SerializingObject +{ + Q_OBJECT; + +public: + + //! Passkey idiom + class Access + { + public: + friend class ClapInstance; + Access(Access&&) noexcept = default; + private: + Access() {} + Access(const Access&) = default; + }; + + //! Creates and starts a plugin, returning nullptr if an error occurred + static auto create(const std::string& pluginId, Model* parent) -> std::unique_ptr; + + ClapInstance() = delete; + ClapInstance(Access, const ClapPluginInfo& pluginInfo, Model* parent); + ~ClapInstance() override; + + ClapInstance(const ClapInstance&) = delete; + ClapInstance(ClapInstance&&) noexcept = delete; + auto operator=(const ClapInstance&) -> ClapInstance& = delete; + auto operator=(ClapInstance&&) noexcept -> ClapInstance& = delete; + + auto parent() const -> Model* { return dynamic_cast(QObject::parent()); } + + enum class PluginState + { + None, // Plugin hasn't been created yet or failed to load (NoneWithError) + Loaded, // Plugin has been created but not initialized + LoadedWithError, // Initialization failed - LMMS might not support this plugin yet + Inactive, // Plugin is initialized and inactive, only the main thread uses it + InactiveWithError, // Activation failed + ActiveAndSleeping, // The audio engine can call set_processing() + ActiveAndProcessing, + ActiveWithError, // The plugin did process but an irrecoverable error occurred + ActiveAndReadyToDeactivate // Plugin is unused by audio engine; can deactivate on main thread + }; + + /** + * LMMS audio thread + */ + + //! Copy values from the LMMS core (connected models, MIDI events, ...) into the respective ports + void copyModelsFromCore(); + + //! Bring values from all ports to the LMMS core + void copyModelsToCore(); + + //! Run the CLAP plugin instance for @param frames frames + void run(fpp_t frames); + + void handleMidiInputEvent(const MidiEvent& event, const TimePos& time, f_cnt_t offset); + + auto controlCount() const -> std::size_t; + auto hasNoteInput() const -> bool; + + + /** + * SerializingObject implementation + */ + static constexpr std::string_view ClapNodeName = "clapcontrols"; + void saveSettings(QDomDocument& doc, QDomElement& elem) override; + void loadSettings(const QDomElement& elem) override; + auto nodeName() const -> QString override { return ClapNodeName.data(); } + + /** + * Info + */ + + auto isValid() const -> bool; + auto info() const -> const ClapPluginInfo& { return m_pluginInfo; } + + /** + * Control + */ + + auto start() -> bool; //!< Loads, inits, and activates in that order + auto restart() -> bool; + void destroy(); + + auto load() -> bool; + auto unload() -> bool; + auto init() -> bool; + auto activate() -> bool; + auto deactivate() -> bool; + + void idle(); + + auto processBegin(std::uint32_t frames) -> bool; + void processNote(f_cnt_t offset, std::int8_t channel, std::int16_t key, std::uint8_t velocity, bool isOn); + void processKeyPressure(f_cnt_t offset, std::int8_t channel, std::int16_t key, std::uint8_t pressure); + auto process(std::uint32_t frames) -> bool; + auto processEnd(std::uint32_t frames) -> bool; + + auto isActive() const -> bool; + auto isProcessing() const -> bool; + auto isSleeping() const -> bool; + auto isErrorState() const -> bool; + + /** + * Extensions + */ + + auto host() const -> const clap_host* { return &m_host; }; + auto plugin() const -> const clap_plugin* { return m_plugin; } + + auto audioPorts() -> ClapAudioPorts& { return m_audioPorts; } + auto gui() -> ClapGui& { return m_gui; } + auto logger() -> ClapLog& { return m_log; } + auto notePorts() -> ClapNotePorts& { return m_notePorts; } + auto params() -> ClapParams& { return m_params; } + auto presetLoader() -> ClapPresetLoader& { return m_presetLoader; } + auto state() -> ClapState& { return m_state; } + auto timerSupport() -> ClapTimerSupport& { return m_timerSupport; } + +private: + /** + * State + */ + + void setPluginState(PluginState state); + auto isNextStateValid(PluginState next) const -> bool; + + template + auto isCurrentStateValid(Args... validStates) const -> bool + { + return ((m_pluginState == validStates) || ...); + } + + /** + * Host API implementation + */ + + static auto clapGetExtension(const clap_host* host, const char* extensionId) -> const void*; + static void clapRequestCallback(const clap_host* host); + static void clapRequestProcess(const clap_host* host); + static void clapRequestRestart(const clap_host* host); + + /** + * Latency extension + * TODO: Fully implement and move to separate class + */ + + static void clapLatencyChanged(const clap_host* host); + static constexpr const clap_host_latency s_clapLatency { + &clapLatencyChanged + }; + + /** + * Important data members + */ + + ClapPluginInfo m_pluginInfo; + PluginState m_pluginState = PluginState::None; + + clap_host m_host; + const clap_plugin* m_plugin = nullptr; + + /** + * Process-related + */ + + clap_process m_process{}; + clap::helpers::EventList m_evIn; // TODO: Find better way to handle param and note events + clap::helpers::EventList m_evOut; // TODO: Find better way to handle param and note events + + /** + * MIDI + */ + + // TODO: Many things here may be moved into the `Instrument` class + static constexpr std::size_t s_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; + + /** + * Scheduling + */ + + bool m_scheduleRestart = false; + bool m_scheduleDeactivate = false; + bool m_scheduleProcess = true; + bool m_scheduleMainThreadCallback = false; + + /** + * Extensions + */ + + ClapAudioPorts m_audioPorts{ this }; + ClapGui m_gui{ this }; + ClapLog m_log{ this }; + ClapNotePorts m_notePorts{ this }; + ClapParams m_params; + ClapPresetLoader m_presetLoader; + ClapState m_state{ this }; + ClapThreadCheck m_threadCheck{ this }; + ClapTimerSupport m_timerSupport{ this }; +}; + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP + +#endif // LMMS_CLAP_INSTANCE_H diff --git a/include/ClapLog.h b/include/ClapLog.h new file mode 100644 index 00000000000..df2b2cc1ec6 --- /dev/null +++ b/include/ClapLog.h @@ -0,0 +1,71 @@ +/* + * ClapLog.h - Implements CLAP log extension + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 LMMS_CLAP_LOG_H +#define LMMS_CLAP_LOG_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_CLAP + +#include + +#include "ClapExtension.h" +#include "lmms_export.h" + +namespace lmms +{ + +class LMMS_EXPORT ClapLog : public ClapExtension +{ +public: + using ClapExtension::ClapExtension; + + auto extensionId() const -> std::string_view override { return CLAP_EXT_LOG; } + + void log(clap_log_severity severity, std::string_view msg) const; + void log(clap_log_severity severity, const char* msg) const; + + //! Log without plugin information + static void globalLog(clap_log_severity severity, std::string_view msg); + + //! Log without any additional information + static void plainLog(clap_log_severity severity, std::string_view msg); + + static void plainLog(std::string_view msg); + +private: + auto hostExtImpl() const -> const clap_host_log* override; + + /** + * clap_host_log implementation + */ + static void clapLog(const clap_host* host, clap_log_severity severity, const char* msg); +}; + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP + +#endif // LMMS_CLAP_LOG_H diff --git a/include/ClapManager.h b/include/ClapManager.h new file mode 100644 index 00000000000..8846f9d16e7 --- /dev/null +++ b/include/ClapManager.h @@ -0,0 +1,110 @@ +/* + * ClapManager.h - Implementation of ClapManager class + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 LMMS_CLAP_MANAGER_H +#define LMMS_CLAP_MANAGER_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_CLAP + +#include +#include +#include +#include +#include +#include + +#include "ClapFile.h" +#include "NoCopyNoMove.h" +#include "lmms_export.h" + +namespace lmms +{ + +//! Manages loaded .clap files, plugin info, and plugin instances +class LMMS_EXPORT ClapManager : public NoCopyNoMove +{ +public: + ClapManager(); + ~ClapManager(); + + //! Allows access to loaded .clap files + auto files() const -> auto& { return m_files; } + + //! Returns a cached plugin info vector + auto pluginInfo() const -> auto& { return m_pluginInfo; } + + //! Returns a URI-to-PluginInfo map + auto uriInfoMap() const -> auto& { return m_uriInfoMap; } + + //! Return plugin info for plugin with the given uri or nullptr if none exists + auto pluginInfo(const std::string& uri) const -> const ClapPluginInfo*; + + //! Return preset database for plugin with the given uri or nullptr if none exists + auto presetDatabase(const std::string& uri) -> ClapPresetDatabase*; + + //! Called by Engine at LMMS startup + void initPlugins(); + + static auto debugging() { return s_debugging; } + +private: + //! For hashing since std::hash is not available until C++23's LWG issue 3657 for god knows why + struct PathHash + { + auto operator()(const std::filesystem::path& path) const noexcept -> std::size_t + { + return std::filesystem::hash_value(path); + } + }; + + using UniquePaths = std::unordered_set; + + //! Finds all CLAP search paths and populates m_searchPaths + void findSearchPaths(); + + //! Returns search paths found by prior call to findSearchPaths() + auto searchPaths() const -> auto& { return m_searchPaths; } + + //! Finds and loads all .clap files in the provided search paths @p searchPaths + void loadClapFiles(const UniquePaths& searchPaths); + + UniquePaths m_searchPaths; //!< Owns all CLAP search paths; Populated by findSearchPaths() + std::vector m_files; //!< Owns all loaded .clap files; Populated by loadClapFiles() + + // Non-owning plugin caches (for fast iteration/lookup) + + std::vector m_pluginInfo; //!< successfully loaded plugins + std::unordered_map m_uriInfoMap; //!< maps plugin URIs (IDs) to ClapPluginInfo + std::unordered_map m_uriFileIndexMap; //!< maps plugin URIs (IDs) to ClapFile index in `m_files` + + static inline bool s_debugging = false; //!< If LMMS_CLAP_DEBUG is set, debug output will be printed +}; + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP + +#endif // LMMS_CLAP_MANAGER_H diff --git a/include/ClapNotePorts.h b/include/ClapNotePorts.h new file mode 100644 index 00000000000..8faeac5b796 --- /dev/null +++ b/include/ClapNotePorts.h @@ -0,0 +1,70 @@ +/* + * ClapNotePorts.h - Implements CLAP note ports extension + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 LMMS_CLAP_NOTE_PORTS_H +#define LMMS_CLAP_NOTE_PORTS_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_CLAP + +#include + +#include "ClapExtension.h" + +namespace lmms +{ + +class ClapNotePorts final : public ClapExtension +{ +public: + using ClapExtension::ClapExtension; + + auto extensionId() const -> std::string_view override { return CLAP_EXT_NOTE_PORTS; } + + auto portIndex() const { return m_portIndex; } + auto dialect() const { return m_dialect; } + + auto hasInput() const -> bool { return m_dialect != 0; } + +private: + auto initImpl() noexcept -> bool override; + auto hostExtImpl() const -> const clap_host_note_ports* override; + auto checkSupported(const clap_plugin_note_ports& ext) -> bool override; + + /** + * clap_host_note_ports implementation + */ + static auto clapSupportedDialects(const clap_host* host) -> std::uint32_t; + static void clapRescan(const clap_host* host, std::uint32_t flags); + + std::uint16_t m_portIndex = 0; // Chosen plugin note port index (not the port id!) + std::uint32_t m_dialect = 0; // Chosen plugin input dialect (0 == no note input) +}; + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP + +#endif // LMMS_CLAP_NOTE_PORTS_H diff --git a/include/ClapParameter.h b/include/ClapParameter.h new file mode 100644 index 00000000000..4d10f856740 --- /dev/null +++ b/include/ClapParameter.h @@ -0,0 +1,132 @@ +/* + * ClapParameter.h - Declaration of ClapParameter class + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 LMMS_CLAP_PARAMETER_H +#define LMMS_CLAP_PARAMETER_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_CLAP + +#include +#include +#include +#include +#include +#include +#include + +#include "AutomatableModel.h" + +namespace lmms +{ + +class ClapInstance; +class ClapParams; + +class ClapParameter : public QObject +{ + Q_OBJECT; + +public: + + enum class ValueType + { + Undefined, + Bool, + Integer, + Enum, + Float + }; + + ClapParameter(ClapParams* parent, const clap_param_info& info, double value); + + auto model() const -> AutomatableModel* { return m_connectedModel.get(); } + auto valueType() const { return m_valueType; } + auto id() const -> std::string_view { return m_id; } + auto displayName() const -> std::string_view { return m_displayName; } + + auto value() const -> float { return !model() ? m_value : model()->value(); } + void setValue(double v); + + auto modulation() const { return m_modulation; } + void setModulation(double v); + + auto modulatedValue() const -> double + { + return std::clamp(m_value + m_modulation, m_info.min_value, m_info.max_value); + } + + auto isValueValid(const double v) const -> bool + { + return m_info.min_value <= v && v <= m_info.max_value; + } + + auto getShortInfoString() const -> std::string; + auto getInfoString() const -> std::string; + + void setInfo(const clap_param_info& info) noexcept { m_info = info; } + auto isInfoEqualTo(const clap_param_info& info) const -> bool; + auto isInfoCriticallyDifferentTo(const clap_param_info& info) const -> bool; + auto info() noexcept -> clap_param_info& { return m_info; } + auto info() const noexcept -> const clap_param_info& { return m_info; } + + auto isBeingAdjusted() const noexcept -> bool { return m_isBeingAdjusted; } + void setIsAdjusting(bool isAdjusting); + void beginAdjust(); + void endAdjust(); + + //! Checks if the parameter info is valid + static auto check(clap_param_info& info) -> bool; + +signals: + void isBeingAdjustedChanged(); + void infoChanged(); + void valueChanged(); + void modulatedValueChanged(); + +private: + clap_param_info m_info; + + std::string m_id; + std::string m_displayName; + + //! An AutomatableModel is created if the param is to be shown to the user + //std::unique_ptr m_connectedModel; + std::unique_ptr m_connectedModel = nullptr; + + double m_value = 0.0; //!< TODO: Remove? + double m_modulation = 0.0; + + //std::unordered_map m_enumEntries; + + ValueType m_valueType = ValueType::Undefined; + bool m_isBeingAdjusted = false; +}; + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP + +#endif // LMMS_CLAP_PARAMETER_H diff --git a/include/ClapParams.h b/include/ClapParams.h new file mode 100644 index 00000000000..ef823ed8c0d --- /dev/null +++ b/include/ClapParams.h @@ -0,0 +1,160 @@ +/* + * ClapParams.h - Implements CLAP params extension + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 LMMS_CLAP_PARAMS_H +#define LMMS_CLAP_PARAMS_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_CLAP + +#include +#include +#include +#include +#include +#include +#include + +#include "ClapExtension.h" +#include "ClapParameter.h" + +namespace lmms +{ + +class ClapParams final + : public QObject + , public ClapExtension +{ + Q_OBJECT +public: + ClapParams(Model* parent, ClapInstance* instance, + clap::helpers::EventList* eventsIn, clap::helpers::EventList* eventsOut); + ~ClapParams() override = default; + + auto extensionId() const -> std::string_view override { return CLAP_EXT_PARAMS; } + + auto rescan(clap_param_rescan_flags flags) -> bool; + + void idle(); + void processEnd(); + + void saveParamConnections(QDomDocument& doc, QDomElement& elem); + void loadParamConnections(const QDomElement& elem); + + void flushOnMainThread(); + void generatePluginInputEvents(); + void handlePluginOutputEvents(); + void handlePluginOutputEvent(const clap_event_param_gesture* event, bool gestureBegin); + void handlePluginOutputEvent(const clap_event_param_value* event); + + auto getValue(const clap_param_info& info) const -> std::optional; + auto getValueText(const ClapParameter& param) const -> std::string; + + auto parameters() const -> const std::vector& { return m_params; } + auto flushScheduled() const { return m_scheduleFlush; } + + auto automatableCount() const { return m_automatableCount; } + +signals: + //! Called when CLAP plugin changes params and LMMS core needs to update + void paramsChanged(); + + void paramAdjusted(clap_id paramId); + +private: + auto initImpl() noexcept -> bool override; + void deinitImpl() noexcept override; + auto hostExtImpl() const -> const clap_host_params* override; + auto checkSupported(const clap_plugin_params& ext) -> bool override; + + void setModels(); + auto checkValidParamValue(const ClapParameter& param, double value) -> bool; + void setParamValueByHost(ClapParameter& param, double value); + void setParamModulationByHost(ClapParameter& param, double value); + + static auto rescanMayValueChange(std::uint32_t flags) -> bool { return flags & (CLAP_PARAM_RESCAN_ALL | CLAP_PARAM_RESCAN_VALUES); } + static auto rescanMayInfoChange(std::uint32_t flags) -> bool { return flags & (CLAP_PARAM_RESCAN_ALL | CLAP_PARAM_RESCAN_INFO); } + + /** + * clap_host_params implementation + */ + static void clapRescan(const clap_host* host, clap_param_rescan_flags flags); + static void clapClear(const clap_host* host, clap_id param_id, clap_param_clear_flags flags); + static void clapRequestFlush(const clap_host* host); + + std::unordered_map> m_paramMap; + std::vector m_params; //!< Cache for faster iteration + + // TODO: Find better way to handle param and note events + clap::helpers::EventList* m_evIn; //!< owned by ClapInstance + clap::helpers::EventList* m_evOut; //!< owned by ClapInstance + + struct HostToPluginParamQueueValue + { + void* cookie; + double value; + }; + + struct PluginToHostParamQueueValue + { + void update(const PluginToHostParamQueueValue& v) noexcept + { + if (v.hasValue) + { + hasValue = true; + value = v.value; + } + + if (v.hasGesture) + { + hasGesture = true; + isBegin = v.isBegin; + } + } + + bool hasValue = false; + bool hasGesture = false; + bool isBegin = false; + double value = 0; + }; + + clap::helpers::ReducingParamQueue m_hostToPluginValueQueue; + clap::helpers::ReducingParamQueue m_hostToPluginModQueue; + clap::helpers::ReducingParamQueue m_pluginToHostValueQueue; + + std::unordered_map m_isAdjustingParameter; + + bool m_scheduleFlush = false; + + std::size_t m_automatableCount = 0; + + static constexpr bool s_provideCookie = true; +}; + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP + +#endif // LMMS_CLAP_PARAMS_H diff --git a/include/ClapPluginInfo.h b/include/ClapPluginInfo.h new file mode 100644 index 00000000000..9915de2fe36 --- /dev/null +++ b/include/ClapPluginInfo.h @@ -0,0 +1,75 @@ +/* + * ClapPluginInfo.h - Implementation of ClapPluginInfo class + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 LMMS_CLAP_PLUGIN_INFO_H +#define LMMS_CLAP_PLUGIN_INFO_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_CLAP + +#include +#include +#include + +#include "Plugin.h" +#include "lmms_export.h" + +namespace lmms +{ + +//! Represents a CLAP plugin within a .clap file +class LMMS_EXPORT ClapPluginInfo +{ +public: + //! Creates plugin info, populated via a quick scan of the plugin; may fail + static auto create(const clap_plugin_factory& factory, std::uint32_t index) -> std::optional; + + ClapPluginInfo() = delete; + ~ClapPluginInfo() = default; + + ClapPluginInfo(const ClapPluginInfo&) = default; + ClapPluginInfo(ClapPluginInfo&&) noexcept = default; + auto operator=(const ClapPluginInfo&) -> ClapPluginInfo& = default; + auto operator=(ClapPluginInfo&&) noexcept -> ClapPluginInfo& = default; + + auto factory() const -> const clap_plugin_factory& { return *m_factory; }; + auto index() const { return m_index; } + auto type() const { return m_type; } + auto descriptor() const -> const clap_plugin_descriptor& { return *m_descriptor; } + +private: + ClapPluginInfo(const clap_plugin_factory& factory, std::uint32_t index); + + const clap_plugin_factory* m_factory; + std::uint32_t m_index; //!< plugin index within .clap file + const clap_plugin_descriptor* m_descriptor = nullptr; + Plugin::Type m_type = Plugin::Type::Undefined; +}; + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP + +#endif // LMMS_CLAP_PLUGIN_INFO_H diff --git a/include/ClapPresetDatabase.h b/include/ClapPresetDatabase.h new file mode 100644 index 00000000000..a97982ed7d7 --- /dev/null +++ b/include/ClapPresetDatabase.h @@ -0,0 +1,140 @@ +/* + * ClapPresetDatabase.h - Implementation of PresetDatabase for CLAP presets + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 LMMS_CLAP_PRESET_DATABASE_H +#define LMMS_CLAP_PRESET_DATABASE_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_CLAP + +#include +#include +#include +#include +#include +#include +#include + +#include "ClapExtension.h" +#include "PresetDatabase.h" + +struct clap_plugin_entry; + +namespace lmms +{ + +class ClapPresetDatabase : public PresetDatabase +{ +public: + using PresetDatabase::PresetDatabase; + + auto init(const clap_plugin_entry* entry) -> bool; + void deinit(); + + auto supported() const -> bool { return m_factory; } + + static auto toClapLocation(std::string_view location, std::string& ref) + -> std::optional>; + static auto fromClapLocation(const char* location) -> std::string; + static auto fromClapLocation(clap_preset_discovery_location_kind kind, + const char* location, const char* loadKey) -> std::optional; + +private: + auto discoverSetup() -> bool override; + auto discoverFiletypes(std::vector& filetypes) -> bool override; + auto discoverLocations(const SetLocations& func) -> bool override; + auto discoverPresets(const Location& location, std::set& presets) -> bool override; + + auto loadPresets(const Location& location, std::string_view file, std::set& presets) + -> std::vector override; + + class Indexer + { + public: + static auto create(const clap_preset_discovery_factory& factory, std::uint32_t index) + -> std::unique_ptr; + + Indexer() = delete; + Indexer(const clap_preset_discovery_factory& factory, + const clap_preset_discovery_provider_descriptor& descriptor); + ~Indexer() = default; + + //! For PLUGIN presets + auto query(PresetMetadata::Flags flags = PresetMetadata::Flag::None) + -> std::optional>; + + //! For FILE presets; `file` is the full path of the preset file + auto query(std::string_view file, + PresetMetadata::Flags flags = PresetMetadata::Flag::None) -> std::optional>; + + auto filetypeSupported(const std::filesystem::path& path) const -> bool; + + auto provider() const -> const clap_preset_discovery_provider* { return m_provider.get(); } + + auto locations() -> auto& { return m_locations; } + auto filetypes() -> auto& { return m_filetypes; } + + private: + /** + * clap_preset_discovery_indexer implementation + */ + static auto clapDeclareFiletype(const clap_preset_discovery_indexer* indexer, + const clap_preset_discovery_filetype* filetype) -> bool; + static auto clapDeclareLocation(const clap_preset_discovery_indexer* indexer, + const clap_preset_discovery_location* location) -> bool; + static auto clapDeclareSoundpack(const clap_preset_discovery_indexer* indexer, + const clap_preset_discovery_soundpack* soundpack) -> bool; + static auto clapGetExtension(const clap_preset_discovery_indexer* indexer, + const char* extensionId) -> const void*; + + struct ProviderDeleter + { + void operator()(const clap_preset_discovery_provider* p) const noexcept + { + p->destroy(p); + } + }; + + clap_preset_discovery_indexer m_indexer; + std::unique_ptr m_provider; + + std::vector m_locations; + std::vector m_filetypes; + }; + + auto getIndexerFor(const Location& location) const -> Indexer*; + + class MetadataReceiver; + + const clap_preset_discovery_factory* m_factory = nullptr; + std::vector> m_indexers; + std::map, std::less<>> m_filetypeIndexerMap; +}; + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP + +#endif // LMMS_CLAP_PRESET_DATABASE_H diff --git a/include/ClapPresetLoader.h b/include/ClapPresetLoader.h new file mode 100644 index 00000000000..fb896186b10 --- /dev/null +++ b/include/ClapPresetLoader.h @@ -0,0 +1,71 @@ +/* + * ClapPresetLoader.h - Implements CLAP preset load extension + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 LMMS_CLAP_PRESET_LOADER_H +#define LMMS_CLAP_PRESET_LOADER_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_CLAP + +#include +#include + +#include "ClapExtension.h" +#include "PluginPresets.h" + +namespace lmms +{ + +class ClapPresetLoader final + : public ClapExtension + , public PluginPresets +{ +public: + ClapPresetLoader(Model* parent, ClapInstance* instance); + ~ClapPresetLoader() override = default; + + auto extensionId() const -> std::string_view override { return CLAP_EXT_PRESET_LOAD; } + auto extensionIdCompat() const -> std::string_view override { return CLAP_EXT_PRESET_LOAD_COMPAT; } + +private: + auto hostExtImpl() const -> const clap_host_preset_load* override; + auto checkSupported(const clap_plugin_preset_load& ext) -> bool override; + + /** + * clap_host_preset_load implementation + */ + static void clapOnError(const clap_host* host, std::uint32_t locationKind, + const char* location, const char* loadKey, std::int32_t osError, const char* msg); + static void clapLoaded(const clap_host* host, std::uint32_t locationKind, + const char* location, const char* loadKey); + + auto activatePresetImpl(const PresetLoadData& preset) noexcept -> bool override; +}; + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP + +#endif // LMMS_CLAP_PRESET_LOADER_H diff --git a/include/ClapState.h b/include/ClapState.h new file mode 100644 index 00000000000..38372b3e6c7 --- /dev/null +++ b/include/ClapState.h @@ -0,0 +1,111 @@ +/* + * ClapState.h - Implements CLAP state extension + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 LMMS_CLAP_STATE_H +#define LMMS_CLAP_STATE_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_CLAP + +#include +#include +#include +#include + +#include "ClapExtension.h" + +struct clap_plugin_state_context; + +namespace lmms +{ + +class ClapState final : public ClapExtension +{ +public: + using ClapExtension::ClapExtension; + + //! See clap_plugin_state_context_type + enum class Context : std::uint32_t + { + None = 0, + Preset = 1, + Duplicate = 2, + Project = 3 + }; + + auto extensionId() const -> std::string_view override { return CLAP_EXT_STATE; } + + //! Whether the plugin has indicated its state has changes that need to be saved + auto dirty() const { return m_dirty; } + + /** + * Tells plugin to load the given base64-encoded state data + * + * The context (clap_plugin_state_context_type) is used if it's provided and the plugin supports it. + * Returns true if successful + */ + auto load(std::string_view base64, Context context = Context::None) -> bool; + + /** + * Tells plugin to load state data from encodedState() + * + * The context (clap_plugin_state_context_type) is used if it's provided and the plugin supports it. + * Returns true if successful + */ + auto load(Context context = Context::None) -> bool; + + /** + * Tells plugin to save its state data + * + * The context (clap_plugin_state_context_type) is used if it's provided and the plugin supports it. + * Sets and returns encodedState()'s data if successful + */ + auto save(Context context = Context::None) -> std::optional; + + //! Base-64 encoded state data from the last time save() was successfully called + auto encodedState() const -> std::string_view { return m_state; } + +private: + auto initImpl() noexcept -> bool override; + void deinitImpl() noexcept override; + auto hostExtImpl() const -> const clap_host_state* override; + auto checkSupported(const clap_plugin_state& ext) -> bool override; + + /** + * clap_host_state implementation + */ + static void clapMarkDirty(const clap_host* host); + + const clap_plugin_state_context* m_stateContext = nullptr; + + std::string m_state; //!< base64-encoded state cache + bool m_dirty = false; +}; + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP + +#endif // LMMS_CLAP_STATE_H diff --git a/include/ClapSubPluginFeatures.h b/include/ClapSubPluginFeatures.h new file mode 100644 index 00000000000..ef4d137ed02 --- /dev/null +++ b/include/ClapSubPluginFeatures.h @@ -0,0 +1,61 @@ +/* + * ClapSubPluginFeatures.h - derivation from + * Plugin::Descriptor::SubPluginFeatures for + * hosting CLAP plugins + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 LMMS_CLAP_SUBPLUGIN_FEATURES_H +#define LMMS_CLAP_SUBPLUGIN_FEATURES_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_CLAP + +#include "ClapPluginInfo.h" +#include "Plugin.h" +#include "lmms_export.h" + +namespace lmms +{ + +class LMMS_EXPORT ClapSubPluginFeatures : public Plugin::Descriptor::SubPluginFeatures +{ +public: + ClapSubPluginFeatures(Plugin::Type type); + + void fillDescriptionWidget(QWidget* parent, const Key* key) const override; + void listSubPluginKeys(const Plugin::Descriptor* desc, KeyList& kl) const override; + auto additionalFileExtensions(const Key& key) const -> QString override; + auto displayName(const Key& key) const -> QString override; + auto description(const Key& key) const -> QString override; + auto logo(const Key& key) const -> const PixmapLoader* override; + +private: + static auto pluginInfo(const Key& key) -> const ClapPluginInfo*; +}; + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP + +#endif // LMMS_CLAP_SUBPLUGIN_FEATURES_H diff --git a/include/ClapThreadCheck.h b/include/ClapThreadCheck.h new file mode 100644 index 00000000000..09d87bb52e0 --- /dev/null +++ b/include/ClapThreadCheck.h @@ -0,0 +1,63 @@ +/* + * ClapThreadCheck.h - Implements CLAP thread check extension + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 LMMS_CLAP_THREAD_CHECK_H +#define LMMS_CLAP_THREAD_CHECK_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_CLAP + +#include + +#include "ClapExtension.h" + +namespace lmms +{ + +class ClapThreadCheck final : public ClapExtension +{ +public: + using ClapExtension::ClapExtension; + + auto extensionId() const -> std::string_view override { return CLAP_EXT_THREAD_CHECK; } + + static auto isMainThread() -> bool { return clapIsMainThread(nullptr); } + static auto isAudioThread() -> bool { return clapIsAudioThread(nullptr); } + +private: + auto hostExtImpl() const -> const clap_host_thread_check* override; + + /** + * clap_host_thread_check implementation + */ + static auto clapIsMainThread(const clap_host* host) -> bool; + static auto clapIsAudioThread(const clap_host* host) -> bool; +}; + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP + +#endif // LMMS_CLAP_THREAD_CHECK_H diff --git a/include/ClapTimerSupport.h b/include/ClapTimerSupport.h new file mode 100644 index 00000000000..08981cf9c48 --- /dev/null +++ b/include/ClapTimerSupport.h @@ -0,0 +1,75 @@ +/* + * ClapTimerSupport.h - Implements CLAP timer support extension + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 LMMS_CLAP_TIMER_SUPPORT_H +#define LMMS_CLAP_TIMER_SUPPORT_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_CLAP + +#include +#include +#include + +#include "ClapExtension.h" + +namespace lmms +{ + +class ClapTimerSupport final + : public QObject + , public ClapExtension +{ + Q_OBJECT +public: + ClapTimerSupport(ClapInstance* parent); + ~ClapTimerSupport() override = default; + + auto extensionId() const -> std::string_view override { return CLAP_EXT_TIMER_SUPPORT; } + + void killTimers(); + +private: + void deinitImpl() noexcept override; + //auto delayInit() const noexcept -> bool override { return false; } + auto hostExtImpl() const -> const clap_host_timer_support* override; + auto checkSupported(const clap_plugin_timer_support& ext) -> bool override; + + /** + * clap_host_timer_support implementation + */ + static auto clapRegisterTimer(const clap_host* host, std::uint32_t periodMilliseconds, clap_id* timerId) -> bool; + static auto clapUnregisterTimer(const clap_host* host, clap_id timerId) -> bool; + + void timerEvent(QTimerEvent* event) override; + + std::unordered_set m_timerIds; +}; + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP + +#endif // LMMS_CLAP_TIMER_SUPPORT_H diff --git a/include/ClapTransport.h b/include/ClapTransport.h new file mode 100644 index 00000000000..01e4d902ad3 --- /dev/null +++ b/include/ClapTransport.h @@ -0,0 +1,62 @@ +/* + * ClapTransport.h - CLAP transport events + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 LMMS_CLAP_TRANSPORT_H +#define LMMS_CLAP_TRANSPORT_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_CLAP + +#include + +#include "lmms_basics.h" + +namespace lmms +{ + +//! Static class for managing CLAP transport events +class ClapTransport +{ +public: + static void update(); + static void setPlaying(bool isPlaying); + static void setRecording(bool isRecording); + static void setLooping(bool isLooping); + static void setBeatPosition(); + static void setTimePosition(int elapsedMilliseconds); + static void setTempo(bpm_t tempo); + static void setTimeSignature(int num, int denom); + + static auto get() -> const clap_event_transport* { return &s_transport; } + +private: + static clap_event_transport s_transport; +}; + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP + +#endif // LMMS_CLAP_TRANSPORT_H diff --git a/include/ClapViewBase.h b/include/ClapViewBase.h new file mode 100644 index 00000000000..aa91d90a83d --- /dev/null +++ b/include/ClapViewBase.h @@ -0,0 +1,106 @@ +/* + * ClapViewBase.h - Base class for CLAP plugin views + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 LMMS_GUI_CLAP_VIEW_BASE_H +#define LMMS_GUI_CLAP_VIEW_BASE_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_CLAP + +#include +#include +#include + +#include "Controls.h" +#include "lmms_export.h" + +class QPushButton; + +namespace lmms +{ + +class ClapInstance; + +namespace gui +{ + +class ControlLayout; +class PixmapButton; +class PresetSelector; + +class ClapViewParameters : public QWidget +{ +public: + //! @param colNum numbers of columns for the controls + ClapViewParameters(QWidget* parent, ClapInstance* instance, int colNum); + +private: + ClapInstance* m_instance = nullptr; + ControlLayout* m_layout = nullptr; + + std::vector> m_widgets; +}; + +//! Base class for view for one CLAP plugin +class LMMS_EXPORT ClapViewBase +{ +protected: + //! @param pluginWidget A child class which inherits QWidget + ClapViewBase(QWidget* pluginWidget, ClapInstance* instance); + ~ClapViewBase(); + + // these widgets must be connected by child widgets + QPushButton* m_reloadPluginButton = nullptr; + QPushButton* m_toggleUIButton = nullptr; + + void toggleUI(); + void toggleHelp(bool visible); + + // to be called by child virtuals + //! Reconnect models if model changed + void modelChanged(ClapInstance* instance); + +private: + enum class Rows + { + PresetRow, + ButtonRow, + ParametersRow, + LinkChannelsRow + }; + + PresetSelector* m_presetSelector = nullptr; + ComboBox* m_portConfig = nullptr; + ClapViewParameters* m_parametersView = nullptr; +}; + + +} // namespace gui + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP + +#endif // LMMS_GUI_CLAP_VIEW_BASE_H diff --git a/include/ConfigManager.h b/include/ConfigManager.h index 3cba834e198..54ebd8d60df 100644 --- a/include/ConfigManager.h +++ b/include/ConfigManager.h @@ -32,6 +32,8 @@ #include #include +#include "WindowEmbed.h" + #include #include "lmms_export.h" @@ -232,8 +234,7 @@ class LMMS_EXPORT ConfigManager : public QObject static bool enableBlockedPlugins(); - static QStringList availableVstEmbedMethods(); - QString vstEmbedMethod() const; + WindowEmbed::Method vstEmbedMethod() const; // Returns true if the working dir (e.g. ~/lmms) exists on disk. bool hasWorkingDir() const; diff --git a/include/Controls.h b/include/Controls.h index 5ed19027eb2..4cdf50e795a 100644 --- a/include/Controls.h +++ b/include/Controls.h @@ -29,7 +29,6 @@ #include "AutomatableModel.h" #include "ComboBoxModel.h" - class QString; class QWidget; class QLabel; @@ -44,6 +43,7 @@ namespace gui class AutomatableModelView; class Knob; +class CustomTextKnob; class ComboBox; class LedCheckBox; @@ -85,6 +85,24 @@ class KnobControl : public Control }; +class CustomTextKnobControl : public Control +{ + CustomTextKnob* m_knob; + +public: + void setText(const QString& text) override; + QWidget* topWidget() override; + + void setModel(AutomatableModel* model) override; + FloatModel* model() override; + AutomatableModelView* modelView() override; + CustomTextKnob* customTextModelView() { return m_knob; } + + CustomTextKnobControl(QWidget* parent = nullptr); + ~CustomTextKnobControl() override = default; +}; + + class ComboControl : public Control { QWidget* m_widget; diff --git a/include/CustomTextKnob.h b/include/CustomTextKnob.h index 31a58415e6f..630db6807e6 100644 --- a/include/CustomTextKnob.h +++ b/include/CustomTextKnob.h @@ -27,6 +27,8 @@ #include "Knob.h" +#include + namespace lmms::gui { @@ -34,25 +36,47 @@ namespace lmms::gui class LMMS_EXPORT CustomTextKnob : public Knob { protected: - inline void setHintText( const QString & _txt_before, const QString & _txt_after ) {} // inaccessible + inline void setHintText(const QString& txtBefore, const QString& txtAfter) {} // inaccessible public: - CustomTextKnob( KnobType _knob_num, QWidget * _parent = nullptr, const QString & _name = QString(), const QString & _value_text = QString() ); + CustomTextKnob( + KnobType knobNum, + QWidget* parent = nullptr, + const QString& name = QString{}, + const QString& valueText = QString{}); + + //! default ctor + CustomTextKnob( + QWidget* parent = nullptr, + const QString& name = QString{}); - CustomTextKnob( QWidget * _parent = nullptr, const QString & _name = QString(), const QString & _value_text = QString() ); //!< default ctor + CustomTextKnob(const Knob& other) = delete; - CustomTextKnob( const Knob& other ) = delete; + inline void setValueText(const QString& valueText) + { + m_valueText = valueText; + m_valueTextType = ValueTextType::Static; + } - inline void setValueText(const QString & _value_text) + inline void setValueText(const std::function& valueTextFunc) { - m_value_text = _value_text; + m_valueTextFunc = valueTextFunc; + m_valueTextType = ValueTextType::Dynamic; } private: QString displayValue() const override; protected: - QString m_value_text; -} ; + + enum class ValueTextType + { + Static, + Dynamic + } m_valueTextType = ValueTextType::Static; + + QString m_valueText; + std::function m_valueTextFunc; +}; } // namespace lmms::gui diff --git a/include/Engine.h b/include/Engine.h index 7e19e2e8483..d4ccee88882 100644 --- a/include/Engine.h +++ b/include/Engine.h @@ -88,6 +88,13 @@ class LMMS_EXPORT Engine : public QObject } #endif +#ifdef LMMS_HAVE_CLAP + static class ClapManager* getClapManager() + { + return s_clapManager; + } +#endif + static Ladspa2LMMS * getLADSPAManager() { return s_ladspaManager; @@ -140,6 +147,9 @@ class LMMS_EXPORT Engine : public QObject #ifdef LMMS_HAVE_LV2 static class Lv2Manager* s_lv2Manager; +#endif +#ifdef LMMS_HAVE_CLAP + static class ClapManager* s_clapManager; #endif static Ladspa2LMMS* s_ladspaManager; static void* s_dndPluginKey; diff --git a/include/PathUtil.h b/include/PathUtil.h index 9b410d014a0..62d351384eb 100644 --- a/include/PathUtil.h +++ b/include/PathUtil.h @@ -28,30 +28,60 @@ #include "lmms_export.h" #include +#include +#include +#include namespace lmms::PathUtil { enum class Base { Absolute, ProjectDir, FactorySample, UserSample, UserVST, Preset, UserLADSPA, DefaultLADSPA, UserSoundfont, DefaultSoundfont, UserGIG, DefaultGIG, - LocalDir }; + LocalDir, Internal }; //! Return the directory associated with a given base as a QString //! Optionally, if a pointer to boolean is given the method will //! use it to indicate whether the prefix could be resolved properly //! or not. QString LMMS_EXPORT baseLocation(const Base base, bool* error = nullptr); + + //! Return the directory associated with a given base as a std::string. + //! Will return std::nullopt if the prefix could not be resolved. + std::optional LMMS_EXPORT getBaseLocation(Base base); + //! Return the directory associated with a given base as a QDir. //! Optional pointer to boolean to indicate if the prefix could //! be resolved properly. QDir LMMS_EXPORT baseQDir (const Base base, bool* error = nullptr); + //! Return the prefix used to denote this base in path strings - QString LMMS_EXPORT basePrefix(const Base base); + QString LMMS_EXPORT basePrefixQString(const Base base); + + //! Return the prefix used to denote this base in path strings + std::string_view LMMS_EXPORT basePrefix(const Base base); + + //! Return whether the path uses the `base` prefix + bool LMMS_EXPORT hasBase(const QString& path, Base base); + + //! Return whether the path uses the `base` prefix + bool LMMS_EXPORT hasBase(std::string_view path, Base base); + //! Check the prefix of a path and return the base it corresponds to //! Defaults to Base::Absolute Base LMMS_EXPORT baseLookup(const QString & path); + //! Check the prefix of a path and return the base it corresponds to + //! Defaults to Base::Absolute + Base LMMS_EXPORT baseLookup(std::string_view path); + //! Remove the prefix from a path, iff there is one QString LMMS_EXPORT stripPrefix(const QString & path); + + //! Remove the prefix from a path, iff there is one + std::string_view LMMS_EXPORT stripPrefix(std::string_view path); + + //! Return the results of baseLookup() and stripPrefix() + std::pair LMMS_EXPORT parsePath(std::string_view path); + //! Get the filename for a path, handling prefixed paths correctly QString LMMS_EXPORT cleanName(const QString & path); @@ -61,13 +91,22 @@ namespace lmms::PathUtil //! Make this path absolute. If a pointer to boolean is given //! it will indicate whether the path was converted successfully QString LMMS_EXPORT toAbsolute(const QString & input, bool* error = nullptr); + + //! Make this path absolute. Returns std::nullopt upon failure. + std::optional LMMS_EXPORT toAbsolute(std::string_view input); + //! Make this path relative to a given base, return an absolute path if that fails QString LMMS_EXPORT relativeOrAbsolute(const QString & input, const Base base); + //! Make this path relative to any base, choosing the shortest if there are //! multiple options. allowLocal defines whether local paths should be considered. //! Defaults to an absolute path if all bases fail. QString LMMS_EXPORT toShortestRelative(const QString & input, bool allowLocal = false); + //! Make this path relative to any base, choosing the shortest if there are + //! multiple options. allowLocal defines whether local paths should be considered. + //! Defaults to an absolute path if all bases fail. + std::string LMMS_EXPORT toShortestRelative(std::string_view input, bool allowLocal = false); } // namespace lmms::PathUtil #endif // LMMS_PATHUTIL_H diff --git a/include/PluginIssue.h b/include/PluginIssue.h index b0b9b194612..a99051981d6 100644 --- a/include/PluginIssue.h +++ b/include/PluginIssue.h @@ -27,6 +27,7 @@ #include #include +#include namespace lmms { @@ -78,10 +79,21 @@ class PluginIssue bool operator==(const PluginIssue& other) const; bool operator<(const PluginIssue& other) const; friend QDebug operator<<(QDebug stream, const PluginIssue& iss); + friend struct PluginIssueHash; }; QDebug operator<<(QDebug stream, const PluginIssue& iss); +struct PluginIssueHash +{ + inline std::size_t operator()(const PluginIssue& issue) const noexcept + { + auto h1 = std::hash{}(issue.m_info); + auto h2 = static_cast(issue.m_issueType); + return h1 ^ (h2 << 1); + } +}; + } // namespace lmms #endif // LMMS_PLUGIN_ISSUE_H diff --git a/include/PluginPortConfig.h b/include/PluginPortConfig.h new file mode 100644 index 00000000000..41e5c9c7083 --- /dev/null +++ b/include/PluginPortConfig.h @@ -0,0 +1,142 @@ +/* + * PluginPortConfig.h - Specifies how to route audio channels + * in and out of a plugin. + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 LMMS_PLUGIN_PORT_CONFIG_H +#define LMMS_PLUGIN_PORT_CONFIG_H + +#include + +#include "ComboBoxModel.h" +#include "lmms_export.h" +#include "SerializingObject.h" + +namespace lmms +{ + +//! Configure channel routing for a plugin's mono/stereo in/out ports +class LMMS_EXPORT PluginPortConfig + : public QObject + , public SerializingObject +{ + Q_OBJECT + +public: + enum class PortType + { + None, + Mono, + Stereo + }; + + enum class Config + { + None = -1, + MonoMix, // mono ports only + LeftOnly, // mono ports only + RightOnly, // mono ports only + Stereo + }; + + enum class MonoPluginType + { + None, + Input, + Output, + Both + }; + + PluginPortConfig(Model* parent = nullptr); + PluginPortConfig(PortType in, PortType out, Model* parent = nullptr); + + /** + * Getters + */ + auto inputPortType() const { return m_inPort; } + auto outputPortType() const { return m_outPort; } + + template + auto portConfig() const -> Config + { + if constexpr (isInput) + { + switch (m_inPort) + { + default: [[fallthrough]]; + case PortType::None: + return Config::None; + case PortType::Mono: + return static_cast(m_config.value()); + case PortType::Stereo: + return Config::Stereo; + } + } + else + { + switch (m_outPort) + { + default: [[fallthrough]]; + case PortType::None: + return Config::None; + case PortType::Mono: + return static_cast(m_config.value()); + case PortType::Stereo: + return Config::Stereo; + } + } + } + + auto hasMonoPort() const -> bool; + auto monoPluginType() const -> MonoPluginType; + auto model() -> ComboBoxModel* { return &m_config; } + + /** + * Setters + */ + void setPortType(PortType in, PortType out); + auto setPortConfig(Config config) -> bool; + + /** + * SerializingObject implementation + */ + void saveSettings(QDomDocument& doc, QDomElement& elem) override; + void loadSettings(const QDomElement& elem) override; + auto nodeName() const -> QString override { return "port_config"; } + +signals: + void portsChanged(); + +private: + void updateOptions(); + + PortType m_inPort = PortType::None; + PortType m_outPort = PortType::None; + + //! Value is 0..2, which represents { MonoMix, LeftOnly, RightOnly } for non-Stereo plugins + ComboBoxModel m_config; +}; + +} // namespace lmms + +#endif // LMMS_PLUGIN_PORT_CONFIG_H diff --git a/include/PluginPresets.h b/include/PluginPresets.h new file mode 100644 index 00000000000..dea20f48f07 --- /dev/null +++ b/include/PluginPresets.h @@ -0,0 +1,119 @@ +/* + * PluginPresets.h - Preset collection and functionality for a plugin instance + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 LMMS_PLUGIN_PRESETS_H +#define LMMS_PLUGIN_PRESETS_H + +#include +#include +#include +#include + +#include "AutomatableModel.h" +#include "LinkedModelGroups.h" +#include "lmms_export.h" +#include "PresetDatabase.h" + +namespace lmms +{ + +/** + * A collection of referenced presets sourced from a PresetDatabase + */ +class LMMS_EXPORT PluginPresets : public LinkedModelGroup +{ + Q_OBJECT +public: + PluginPresets(Model* parent, PresetDatabase* database, + std::string_view pluginKey = std::string_view{}); + virtual ~PluginPresets() = default; + + auto setPresetDatabase(PresetDatabase* database) -> bool; + auto refreshPresetCollection() -> bool; + + auto activatePreset(const PresetLoadData& preset) -> bool; + auto activatePreset(std::size_t index) -> bool; + auto prevPreset() -> bool; + auto nextPreset() -> bool; + + auto presetDatabase() const -> PresetDatabase* { return m_database; } + auto presets() const -> const auto& { return m_presets; } + auto presetIndex() const { return m_activePreset; } + auto isModified() const { return m_modified; } + auto activePreset() const -> const Preset*; + auto activePresetModel() -> IntModel* { return &m_activePresetModel; } + + /** + * SerializingObject-like functionality + * + * The default implementation just saves/loads the PresetLoadKey + * and ignores any modifications that may have been made. + */ + virtual auto presetNodeName() const -> QString { return "preset"; } // TODO: use "preset#" for linked preset groups? + virtual void saveActivePreset(QDomDocument& doc, QDomElement& element); + virtual void loadActivePreset(const QDomElement& element); + + /** + * Signals + */ +signals: + void activePresetChanged(); + void activePresetModified(); + void presetCollectionChanged(); + + // TODO: Should connect PresetDatabase's dataChanged() to this object's presetCollectionChanged(). + // This way if there are two plugin instances, and a user manually loads a new preset + // in instance #1, it will be added to the preset collection that both PluginPresets share, + // then presetCollectionChanged() will be called, and both objects will update their lists. + +protected: + virtual auto activatePresetImpl(const PresetLoadData& preset) noexcept -> bool = 0; + + void setActivePreset(std::optional index); + + auto findPreset(const PresetLoadData& preset) const -> std::optional; + auto preset(std::size_t index) const -> const Preset*; + +private: + //! The source of the plugin's presets + PresetDatabase* m_database = nullptr; + + //! Non-empty if this is a subplugin; Used to find compatible presets in database + std::string m_pluginKey; + + //! A subset of presets from `m_database` + std::vector m_presets; + + //! `m_preset` index of the active preset + std::optional m_activePreset; + + IntModel m_activePresetModel; // TODO: Remove? + + //! Whether the active preset has been modified + bool m_modified = false; +}; + +} // namespace lmms + +#endif // LMMS_PLUGIN_PRESETS_H diff --git a/include/Preset.h b/include/Preset.h new file mode 100644 index 00000000000..e8b0002350a --- /dev/null +++ b/include/Preset.h @@ -0,0 +1,120 @@ +/* + * Preset.h - A generic preset class for plugins + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 LMMS_PRESET_H +#define LMMS_PRESET_H + +#include +#include +#include + +#include "Flags.h" +#include "lmms_export.h" + +namespace lmms +{ + +//! Contains all information needed for loading a preset associated with a specific plugin or subplugin +struct PresetLoadData +{ + /** + * A string that can be parsed by PathUtil. + * Its meaning depends on the plugin, but it typically represents a preset file path. + * + * Valid examples: + * - "preset:MyPlugin/MyFavoritePresets/foo.preset" + * - "/my/absolute/directory/bar.ext" + * - "preset:foo/preset_database.txt" (could be a container of presets, in which case `loadKey` is non-empty) + * - "internal:" (for presets stored within the plugin's DSO rather than on disk) + */ + std::string location; + + /** + * If non-empty, could be a file offset or any other kind of unique ID. + * Again, it's entirely up to the plugin. + */ + std::string loadKey; + + friend auto operator==(const PresetLoadData& lhs, const PresetLoadData& rhs) -> bool + { + return lhs.location == rhs.location && lhs.loadKey == rhs.loadKey; + } +}; + +//! Stores metadata for use by the preset browser and for categorizing the preset +struct PresetMetadata +{ + std::string displayName; + std::string creator; + std::string description; + std::vector categories; + + enum class Flag + { + None = 0, + FactoryContent = 1 << 0, + UserContent = 1 << 1, + UserFavorite = 1 << 2 + }; + + using Flags = lmms::Flags; + + Flags flags; +}; + +LMMS_DECLARE_OPERATORS_FOR_FLAGS(PresetMetadata::Flag) + +//! Generic preset +class LMMS_EXPORT Preset +{ +public: + auto metadata() -> auto& { return m_metadata; } + auto metadata() const -> auto& { return m_metadata; } + + auto loadData() -> auto& { return m_loadData; } + auto loadData() const -> auto& { return m_loadData; } + + auto keys() -> auto& { return m_keys; } + auto keys() const -> auto& { return m_keys; } + + auto supportsPlugin(std::string_view key) const -> bool; + + //! Enable std::set support + friend auto operator<(const Preset& a, const Preset& b) noexcept -> bool; + +private: + PresetMetadata m_metadata; + PresetLoadData m_loadData; + + //! Subplugin keys that support this preset + //! Empty if all subplugins support the preset or if there are no subplugins + std::vector m_keys; + + //! TODO: Some plugins may cache preset data for faster loading; empty = uncached + //std::string data; +}; + +} // namespace lmms + +#endif // LMMS_PRESET_H diff --git a/include/PresetDatabase.h b/include/PresetDatabase.h new file mode 100644 index 00000000000..b83c172c150 --- /dev/null +++ b/include/PresetDatabase.h @@ -0,0 +1,196 @@ +/* + * PresetDatabase.h - Preset discovery, loading, storage, and query + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 LMMS_PRESET_DATABASE_H +#define LMMS_PRESET_DATABASE_H + +#include +#include +#include + +#include "lmms_export.h" +#include "Preset.h" + +namespace lmms +{ + +/** + * A plugin-specific collection of presets + * + * Contains all the loaded presets for a plugin or its subplugins. + * + * Plugins are expected to inherit this class to implement preset discovery, + * metadata loading, and other functionality. + */ +class LMMS_EXPORT PresetDatabase +{ +public: + struct Filetype + { + std::string name; + std::string description; + std::string extension; //< without dot; if empty, any extension is supported + }; + + //! Represents a preset directory or internal location where presets can be found + struct Location + { + std::string name; + std::string location; //!< PathUtil-compatible + PresetMetadata::Flags flags = PresetMetadata::Flag::None; + + friend auto operator==(const Location& lhs, const Location& rhs) noexcept -> bool + { + return lhs.location == rhs.location; + } + + friend auto operator<(const Location& lhs, const Location& rhs) noexcept -> bool + { + return lhs.location < rhs.location; + } + + friend auto operator<(std::string_view location, const Location& rhs) noexcept -> bool + { + return location < rhs.location; + } + + friend auto operator<(const Location& lhs, std::string_view location) noexcept -> bool + { + return lhs.location < location; + } + }; + + using PresetMap = std::map, std::less<>>; + + PresetDatabase(); + virtual ~PresetDatabase() = default; + + //! Discover presets and populate database; Returns true when successful + auto discover() -> bool; + + //! Load a preset file from disk; Returns empty vector upon failure or if preset(s) were already added + auto loadPresets(std::string_view file) -> std::vector; + + /** + * Query + */ + + //! Returns all presets that can be loaded by the plugin with the given subplugin key + auto findPresets(std::string_view key) const -> std::vector; + + auto findPreset(const PresetLoadData& loadData, std::string_view key = std::string_view{}) const -> const Preset*; + + //! Returns all presets from the given file, loading them if needed. Returns empty vector upon failure. + auto findOrLoadPresets(std::string_view file) -> std::vector; + + /** + * Accessors + */ + + auto presets() const -> auto& { return m_presets; } + auto presets(std::string_view location) const -> const std::set*; + auto presets(std::string_view location) -> std::set*; + auto filetypes() const -> auto& { return m_filetypes; } + auto recentPresetFile() const -> std::string_view { return m_recentPresetFile; } + +//signal: + //! TODO: Need to use this + //void presetLoaded(const Preset& preset); + +protected: + + /** + * 1st step of `discover()` - optional setup + * + * Return true if successful + */ + virtual auto discoverSetup() -> bool { return true; } + + /** + * 2nd step of `discover()` - declare all supported file types + * + * Return true if successful + */ + virtual auto discoverFiletypes(std::vector& filetypes) -> bool = 0; + + //! Function object to make `discoverLocations()` implementation simpler and hide implementation details + class SetLocations + { + public: + friend class PresetDatabase; + void operator()(const std::vector& locations) const; + void operator()(Location location) const; + private: + SetLocations(PresetMap& presets) : m_map{&presets} {} + PresetMap* m_map = nullptr; + }; + + /** + * 3rd step of `discover()` - declare all pre-established preset locations + * + * Return true if successful + */ + virtual auto discoverLocations(const SetLocations& func) -> bool = 0; + + /** + * 4th and final step of `discover()` - populate set of presets at the given location + * + * Return true if successful + */ + virtual auto discoverPresets(const Location& location, std::set& presets) -> bool = 0; + + /** + * Loads presets for the given location from `file`, retrieves any metadata, + * and adds the new presets to `presets`. Returns all the new presets. + * + * Used when the user loads a new preset from disk. + * May also be used by `discoverPresets()`. + * + * The default implementation only works for simple preset files. + * Plugins that support preset containers (where multiple presets could potentially be returned + * from this method) or provide additional preset metadata should reimplement this method. + */ + virtual auto loadPresets(const Location& location, std::string_view file, std::set& presets) + -> std::vector; + + /** + * Gets preset location from `m_presets` which contains `path`, optionally adding it if it doesn't exist + * + * Returns iterator to the `m_presets` location + */ + auto getLocation(std::string_view path, bool add = true) -> PresetMap::iterator; + +private: + + //! Maps locations to presets + PresetMap m_presets; + + std::vector m_filetypes; + + std::string m_recentPresetFile; +}; + +} // namespace lmms + +#endif // LMMS_PRESET_DATABASE_H diff --git a/include/PresetSelector.h b/include/PresetSelector.h new file mode 100644 index 00000000000..1f0543cb508 --- /dev/null +++ b/include/PresetSelector.h @@ -0,0 +1,78 @@ +/* + * PresetSelector.h - A preset selector widget + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 LMMS_GUI_PRESET_SELECTOR_H +#define LMMS_GUI_PRESET_SELECTOR_H + +#include + +#include "lmms_export.h" +#include "AutomatableModelView.h" + +class QLabel; +class QPushButton; + +namespace lmms +{ + +class PluginPresets; + +namespace gui +{ + +class PixmapButton; + +class LMMS_EXPORT PresetSelector : public QToolBar, public IntModelView +{ + Q_OBJECT +public: + PresetSelector(PluginPresets* presets, QWidget* parent = nullptr); + + auto sizeHint() const -> QSize override; + +protected slots: + void updateActivePreset(); + void updateMenu(); + void loadPreset(); + void savePreset(); + void selectPreset(int pos); + +private: + + PluginPresets* m_presets = nullptr; + int m_lastPosInMenu = 0; + + QLabel* m_activePreset = nullptr; + PixmapButton* m_prevPresetButton = nullptr; + PixmapButton* m_nextPresetButton = nullptr; + QPushButton* m_selectPresetButton = nullptr; + PixmapButton* m_loadPresetButton = nullptr; + PixmapButton* m_savePresetButton = nullptr; +}; + +} // namespace gui + +} // namespace lmms + +#endif // LMMS_GUI_PRESET_SELECTOR_H diff --git a/include/SetupDialog.h b/include/SetupDialog.h index 871a80bcd4b..050f2eeed1f 100644 --- a/include/SetupDialog.h +++ b/include/SetupDialog.h @@ -33,6 +33,7 @@ #include "lmmsconfig.h" #include "MidiClient.h" #include "MidiSetupWidget.h" +#include "WindowEmbed.h" class QCheckBox; @@ -165,8 +166,8 @@ private slots: bool m_animateAFP; QLabel * m_vstEmbedLbl; QComboBox* m_vstEmbedComboBox; - QString m_vstEmbedMethod; - QCheckBox * m_vstAlwaysOnTopCheckBox; + WindowEmbed::Method m_vstEmbedMethod; + QCheckBox* m_vstAlwaysOnTopCheckBox; bool m_vstAlwaysOnTop; bool m_disableAutoQuit; diff --git a/include/Song.h b/include/Song.h index f08edfff602..6728ec41e46 100644 --- a/include/Song.h +++ b/include/Song.h @@ -355,6 +355,11 @@ class LMMS_EXPORT Song : public TrackContainer return m_timeSigModel; } + const MeterModel& getTimeSigModel() const + { + return m_timeSigModel; + } + IntModel& tempoModel() { return m_tempoModel; diff --git a/include/WindowEmbed.h b/include/WindowEmbed.h new file mode 100644 index 00000000000..91c973c73a4 --- /dev/null +++ b/include/WindowEmbed.h @@ -0,0 +1,84 @@ +/* + * WindowEmbed.h - Window embedding helper + * + * Copyright (c) 2023 Dalton Messmer + * + * 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 LMMS_WINDOW_EMBED_H +#define LMMS_WINDOW_EMBED_H + +#include +#include + +namespace lmms +{ + +struct WindowEmbed +{ + enum class Method + { + Headless, // no GUI at all + Floating, // detached + Qt, + Win32, + Cocoa, + XEmbed + // ... + }; + + //! The vector returned will not contain None or Headless + static auto availableMethods() -> std::vector; + + //! Returns true if embed methods exist + static auto embeddable() -> bool; + + static auto toString(Method method) -> std::string_view + { + // NOTE: These strings must NOT change - used in files saved to disk + switch (method) + { + case Method::Headless: return "headless"; + case Method::Floating: return "none"; + case Method::Qt: return "qt"; + case Method::Win32: return "win32"; + case Method::Cocoa: return "cocoa"; + case Method::XEmbed: return "xembed"; + } + //assert("invalid window embed method"); + return ""; + } + + static auto toEnum(std::string_view method) -> Method + { + if (method == "headless") { return Method::Headless; } + if (method == "none") { return Method::Floating; } + if (method == "qt") { return Method::Qt; } + if (method == "win32") { return Method::Win32; } + if (method == "cocoa") { return Method::Cocoa; } + if (method == "xembed") { return Method::XEmbed; } + //assert("invalid window embed method"); + return Method::Floating; + } +}; + +} // namespace lmms + +#endif // LMMS_WINDOW_EMBED_H diff --git a/include/lmms_math.h b/include/lmms_math.h index bdadd7ba0c4..07799b511d0 100644 --- a/include/lmms_math.h +++ b/include/lmms_math.h @@ -31,6 +31,7 @@ #include #include #include +#include #include "lmms_constants.h" #include "lmmsconfig.h" @@ -243,6 +244,15 @@ inline int numDigitsAsInt(float f) return digits; } + +//! Taken from N3876 / boost::hash_combine +template +inline void hashCombine(std::size_t& seed, const T& val) noexcept +{ + seed ^= std::hash{}(val) + 0x9e3779b9 + (seed << 6) + (seed >> 2); +} + + template class LinearMap { diff --git a/plugins/ClapEffect/CMakeLists.txt b/plugins/ClapEffect/CMakeLists.txt new file mode 100644 index 00000000000..061a86f4291 --- /dev/null +++ b/plugins/ClapEffect/CMakeLists.txt @@ -0,0 +1,7 @@ +IF(LMMS_HAVE_CLAP) + INCLUDE(BuildPlugin) + BUILD_PLUGIN(clapeffect ClapEffect.cpp ClapFxControls.cpp ClapFxControlDialog.cpp ClapEffect.h ClapFxControls.h ClapFxControlDialog.h + MOCFILES ClapEffect.h ClapFxControls.h ClapFxControlDialog.h + EMBEDDED_RESOURCES logo.png) + target_link_libraries(clapeffect clap) +ENDIF(LMMS_HAVE_CLAP) diff --git a/plugins/ClapEffect/ClapEffect.cpp b/plugins/ClapEffect/ClapEffect.cpp new file mode 100644 index 00000000000..334e5dd6e71 --- /dev/null +++ b/plugins/ClapEffect/ClapEffect.cpp @@ -0,0 +1,113 @@ +/* + * ClapEffect.cpp - Implementation of CLAP effect + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 "ClapEffect.h" + +#include +#include + +#include "ClapInstance.h" +#include "ClapSubPluginFeatures.h" +#include "embed.h" +#include "plugin_export.h" + +namespace lmms +{ + +extern "C" +{ + +Plugin::Descriptor PLUGIN_EXPORT clapeffect_plugin_descriptor = +{ + LMMS_STRINGIFY(PLUGIN_NAME), + "CLAP", + QT_TRANSLATE_NOOP("PluginBrowser", + "plugin for using CLAP effects inside LMMS."), + "Dalton Messmer ", + 0x0100, + Plugin::Type::Effect, + new PluginPixmapLoader("logo"), + nullptr, + new ClapSubPluginFeatures(Plugin::Type::Effect) +}; + +PLUGIN_EXPORT Plugin* lmms_plugin_main(Model* parent, void* data) +{ + using KeyType = Plugin::Descriptor::SubPluginFeatures::Key; + auto effect = std::make_unique(parent, static_cast(data)); + if (!effect || !effect->isValid()) { return nullptr; } + return effect.release(); +} + +} // extern "C" + + +ClapEffect::ClapEffect(Model* parent, const Descriptor::SubPluginFeatures::Key* key) + : Effect{&clapeffect_plugin_descriptor, parent, key} + , m_controls{this, key->attributes["uri"].toStdString()} + , m_tempOutputSamples(Engine::audioEngine()->framesPerPeriod()) +{ + connect(Engine::audioEngine(), &AudioEngine::sampleRateChanged, this, + [&] { m_tempOutputSamples.resize(Engine::audioEngine()->framesPerPeriod()); }); +} + +auto ClapEffect::processImpl(SampleFrame* buf, const fpp_t frames) -> ProcessStatus +{ + ClapInstance* instance = m_controls.m_instance.get(); + assert(instance != nullptr); + assert(frames <= static_cast(m_tempOutputSamples.size())); + + instance->audioPorts().copyBuffersFromCore(buf, frames); + instance->copyModelsFromCore(); + + instance->run(frames); + + instance->copyModelsToCore(); + instance->audioPorts().copyBuffersToCore(m_tempOutputSamples.data(), frames); + + SampleFrame* leftSamples = m_tempOutputSamples.data(); + SampleFrame* rightSamples = m_tempOutputSamples.data(); + switch (instance->audioPorts().portConfig()) + { + case PluginPortConfig::Config::LeftOnly: + rightSamples = buf; break; + case PluginPortConfig::Config::RightOnly: + leftSamples = buf; break; + default: break; + } + + bool corrupt = wetLevel() < 0.f; // #3261 - if wet < 0, bash wet := 0, dry := 1 + const float dry = corrupt ? 1.f : dryLevel(); + const float wet = corrupt ? 0.f : wetLevel(); + + for (fpp_t f = 0; f < frames; ++f) + { + buf[f][0] = dry * buf[f][0] + wet * leftSamples[f][0]; + buf[f][1] = dry * buf[f][1] + wet * rightSamples[f][1]; + } + + return ProcessStatus::ContinueIfNotQuiet; +} + +} // namespace lmms diff --git a/plugins/ClapEffect/ClapEffect.h b/plugins/ClapEffect/ClapEffect.h new file mode 100644 index 00000000000..e65f89d781f --- /dev/null +++ b/plugins/ClapEffect/ClapEffect.h @@ -0,0 +1,60 @@ +/* + * ClapEffect.h - Implementation of CLAP effect + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 LMMS_CLAP_EFFECT_H +#define LMMS_CLAP_EFFECT_H + +#include + +#include "ClapFxControls.h" +#include "Effect.h" + +namespace lmms +{ + +class ClapEffect : public Effect +{ + Q_OBJECT + +public: + ClapEffect(Model* parent, const Descriptor::SubPluginFeatures::Key* key); + + //! Must be checked after ctor or reload + auto isValid() const -> bool { return m_controls.isValid(); } + + auto processImpl(SampleFrame* buf, const fpp_t frames) -> ProcessStatus override; + auto controls() -> EffectControls* override { return &m_controls; } + + auto clapControls() -> ClapFxControls* { return &m_controls; } + auto clapControls() const -> const ClapFxControls* { return &m_controls; } + +private: + ClapFxControls m_controls; + + std::vector m_tempOutputSamples; +}; + +} // namespace lmms + +#endif // LMMS_CLAP_EFFECT_H diff --git a/plugins/ClapEffect/ClapFxControlDialog.cpp b/plugins/ClapEffect/ClapFxControlDialog.cpp new file mode 100644 index 00000000000..e970636f619 --- /dev/null +++ b/plugins/ClapEffect/ClapFxControlDialog.cpp @@ -0,0 +1,65 @@ +/* + * ClapFxControlDialog.cpp - ClapFxControlDialog implementation + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 "ClapFxControlDialog.h" + +#include + +#include "ClapFxControls.h" +#include "PixmapButton.h" + +namespace lmms::gui +{ + +ClapFxControlDialog::ClapFxControlDialog(ClapFxControls* controls) + : EffectControlDialog{controls} + , ClapViewBase{this, controls->m_instance.get()} +{ + if (m_reloadPluginButton) + { + connect(m_reloadPluginButton, &QPushButton::clicked, this, [this] { clapControls()->reload(); }); + } + + if (m_toggleUIButton) + { + connect(m_toggleUIButton, &QPushButton::toggled, this, [this] { toggleUI(); }); + } + + // For Effects, modelChanged only goes to the top EffectView + // We need to call it manually + modelChanged(); +} + +auto ClapFxControlDialog::clapControls() -> ClapFxControls* +{ + return static_cast(m_effectControls); +} + +void ClapFxControlDialog::modelChanged() +{ + ClapViewBase::modelChanged(clapControls()->m_instance.get()); + connect(clapControls(), &ClapFxControls::modelChanged, this, [this] { this->modelChanged(); } ); +} + +} // namespace lmms::gui diff --git a/plugins/ClapEffect/ClapFxControlDialog.h b/plugins/ClapEffect/ClapFxControlDialog.h new file mode 100644 index 00000000000..455213b9622 --- /dev/null +++ b/plugins/ClapEffect/ClapFxControlDialog.h @@ -0,0 +1,55 @@ +/* + * ClapFxControlDialog.h - ClapFxControlDialog implementation + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 LMMS_GUI_CLAP_FX_CONTROL_DIALOG_H +#define LMMS_GUI_CLAP_FX_CONTROL_DIALOG_H + +#include "ClapViewBase.h" +#include "EffectControlDialog.h" + +namespace lmms +{ + +class ClapFxControls; + +namespace gui +{ + +class ClapFxControlDialog : public EffectControlDialog, public ClapViewBase +{ + Q_OBJECT + +public: + ClapFxControlDialog(ClapFxControls* controls); + +private: + auto clapControls() -> ClapFxControls*; + void modelChanged() final; +}; + +} // namespace gui + +} // namespace lmms + +#endif // LMMS_GUI_CLAP_FX_CONTROL_DIALOG_H diff --git a/plugins/ClapEffect/ClapFxControls.cpp b/plugins/ClapEffect/ClapFxControls.cpp new file mode 100644 index 00000000000..ff4bfb2f008 --- /dev/null +++ b/plugins/ClapEffect/ClapFxControls.cpp @@ -0,0 +1,91 @@ +/* + * ClapFxControls.cpp - ClapFxControls implementation + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 "ClapFxControls.h" + +#include + +#include "ClapEffect.h" +#include "ClapFxControlDialog.h" +#include "ClapInstance.h" +#include "Engine.h" + +namespace lmms +{ + +ClapFxControls::ClapFxControls(ClapEffect* effect, const std::string& pluginId) + : EffectControls{effect} + , m_instance{ClapInstance::create(pluginId, this)} + , m_idleTimer{this} +{ + if (isValid()) + { + connect(Engine::audioEngine(), &AudioEngine::sampleRateChanged, this, &ClapFxControls::reload); + connect(&m_idleTimer, &QTimer::timeout, m_instance.get(), &ClapInstance::idle); + m_idleTimer.start(1000 / 30); + } +} + +auto ClapFxControls::isValid() const -> bool +{ + return m_instance != nullptr + && m_instance->isValid(); +} + +void ClapFxControls::reload() +{ + if (m_instance) { m_instance->restart(); } + + emit modelChanged(); +} + +void ClapFxControls::saveSettings(QDomDocument& doc, QDomElement& elem) +{ + if (!m_instance) { return; } + m_instance->saveSettings(doc, elem); +} + +void ClapFxControls::loadSettings(const QDomElement& elem) +{ + if (!m_instance) { return; } + m_instance->loadSettings(elem); +} + +auto ClapFxControls::controlCount() -> int +{ + return m_instance ? m_instance->controlCount() : 0; +} + +auto ClapFxControls::createView() -> gui::EffectControlDialog* +{ + return new gui::ClapFxControlDialog{this}; +} + +void ClapFxControls::changeControl() +{ + // TODO + // engine::getSong()->setModified(); +} + +} // namespace lmms diff --git a/plugins/ClapEffect/ClapFxControls.h b/plugins/ClapEffect/ClapFxControls.h new file mode 100644 index 00000000000..7f5a35a9770 --- /dev/null +++ b/plugins/ClapEffect/ClapFxControls.h @@ -0,0 +1,80 @@ +/* + * ClapFxControls.h - ClapFxControls implementation + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 LMMS_CLAP_FX_CONTROLS_H +#define LMMS_CLAP_FX_CONTROLS_H + +#include + +#include "ClapInstance.h" +#include "EffectControls.h" + +namespace lmms +{ + +class ClapEffect; + +namespace gui +{ + +class ClapFxControlDialog; + +} // namespace gui + + +class ClapFxControls : public EffectControls +{ + Q_OBJECT +public: + ClapFxControls(ClapEffect* effect, const std::string& uri); + + auto isValid() const -> bool; + + void reload(); + + void saveSettings(QDomDocument& doc, QDomElement& elem) override; + void loadSettings(const QDomElement& elem) override; + auto nodeName() const -> QString override { return ClapInstance::ClapNodeName.data(); } + + auto controlCount() -> int override; + auto createView() -> gui::EffectControlDialog* override; + +signals: + void modelChanged(); + +private slots: + void changeControl(); + +private: + std::unique_ptr m_instance; + + QTimer m_idleTimer; + + friend class gui::ClapFxControlDialog; + friend class ClapEffect; +}; + +} // namespace lmms + +#endif // LMMS_CLAP_FX_CONTROLS_H diff --git a/plugins/ClapEffect/logo.png b/plugins/ClapEffect/logo.png new file mode 100644 index 00000000000..98d59f9b46a Binary files /dev/null and b/plugins/ClapEffect/logo.png differ diff --git a/plugins/ClapInstrument/CMakeLists.txt b/plugins/ClapInstrument/CMakeLists.txt new file mode 100644 index 00000000000..3625ab0d323 --- /dev/null +++ b/plugins/ClapInstrument/CMakeLists.txt @@ -0,0 +1,7 @@ +IF(LMMS_HAVE_CLAP) + INCLUDE(BuildPlugin) + BUILD_PLUGIN(clapinstrument ClapInstrument.cpp ClapInstrument.h + MOCFILES ClapInstrument.h + EMBEDDED_RESOURCES logo.png) + target_link_libraries(clapinstrument clap) +ENDIF() diff --git a/plugins/ClapInstrument/ClapInstrument.cpp b/plugins/ClapInstrument/ClapInstrument.cpp new file mode 100644 index 00000000000..034733f2d53 --- /dev/null +++ b/plugins/ClapInstrument/ClapInstrument.cpp @@ -0,0 +1,261 @@ +/* + * ClapInstrument.cpp - Implementation of CLAP instrument + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 "ClapInstrument.h" + +#include +#include +#include + +#include "AudioEngine.h" +#include "ClapLog.h" +#include "ClapSubPluginFeatures.h" +#include "Clipboard.h" +#include "embed.h" +#include "Engine.h" +#include "InstrumentPlayHandle.h" +#include "InstrumentTrack.h" +#include "PixmapButton.h" +#include "plugin_export.h" +#include "StringPairDrag.h" + +namespace lmms +{ + +extern "C" +{ + +Plugin::Descriptor PLUGIN_EXPORT clapinstrument_plugin_descriptor = +{ + LMMS_STRINGIFY(PLUGIN_NAME), + "CLAP", + QT_TRANSLATE_NOOP("PluginBrowser", "plugin for using arbitrary CLAP instruments inside LMMS."), + "Dalton Messmer ", + 0x0100, + Plugin::Type::Instrument, + new PluginPixmapLoader("logo"), + nullptr, + new ClapSubPluginFeatures(Plugin::Type::Instrument) +}; + +PLUGIN_EXPORT Plugin* lmms_plugin_main(Model* parent, void* data) +{ + using KeyType = Plugin::Descriptor::SubPluginFeatures::Key; + auto instrument = std::make_unique(static_cast(parent), static_cast(data)); + if (!instrument || !instrument->isValid()) { return nullptr; } + return instrument.release(); +} + +} // extern "C" + + +/* + * ClapInstrument + */ + +ClapInstrument::ClapInstrument(InstrumentTrack* track, Descriptor::SubPluginFeatures::Key* key) + : Instrument{track, &clapinstrument_plugin_descriptor, key, Flag::IsSingleStreamed | Flag::IsMidiBased} + , m_instance{ClapInstance::create(key->attributes["uri"].toStdString(), this)} + , m_idleTimer{this} +{ + if (isValid()) + { + clearRunningNotes(); + + connect(instrumentTrack()->pitchRangeModel(), &IntModel::dataChanged, + this, &ClapInstrument::updatePitchRange, Qt::DirectConnection); + connect(Engine::audioEngine(), &AudioEngine::sampleRateChanged, this, [this](){ onSampleRateChanged(); }); + + m_idleTimer.moveToThread(QApplication::instance()->thread()); + connect(&m_idleTimer, &QTimer::timeout, this, [this] { if (m_instance) { m_instance->idle(); } }); + m_idleTimer.start(1000 / 30); + + // Now we need a play handle which cares for calling play() + auto iph = new InstrumentPlayHandle{this, track}; + Engine::audioEngine()->addPlayHandle(iph); // TODO: Is this only for non-midi-based instruments? + } +} + +ClapInstrument::~ClapInstrument() +{ + Engine::audioEngine()->removePlayHandlesOfTypes(instrumentTrack(), + PlayHandle::Type::NotePlayHandle | PlayHandle::Type::InstrumentPlayHandle); +} + +auto ClapInstrument::isValid() const -> bool +{ + return m_instance != nullptr + && m_instance->isValid(); +} + +void ClapInstrument::reload() +{ + if (m_instance) { m_instance->restart(); } + clearRunningNotes(); + + emit modelChanged(); +} + +void ClapInstrument::clearRunningNotes() +{ + m_runningNotes.fill(0); +} + +void ClapInstrument::onSampleRateChanged() +{ + reload(); +} + +void ClapInstrument::saveSettings(QDomDocument& doc, QDomElement& elem) +{ + if (!m_instance) { return; } + m_instance->saveSettings(doc, elem); +} + +void ClapInstrument::loadSettings(const QDomElement& elem) +{ + if (!m_instance) { return; } + m_instance->loadSettings(elem); +} + +void ClapInstrument::loadFile(const QString& file) +{ + if (auto database = m_instance->presetLoader().presetDatabase()) + { + auto presets = database->findOrLoadPresets(file.toStdString()); + if (!presets.empty()) + { + m_instance->presetLoader().activatePreset(presets[0]->loadData()); + } + } +} + +auto ClapInstrument::hasNoteInput() const -> bool +{ + return m_instance ? m_instance->hasNoteInput() : false; +} + +auto ClapInstrument::handleMidiEvent(const MidiEvent& event, const TimePos& time, f_cnt_t offset) -> bool +{ + // This function can be called from GUI threads while the plugin is running + // handleMidiInputEvent will use a thread-safe ringbuffer + + if (!m_instance) { return false; } + m_instance->handleMidiInputEvent(event, time, offset); + return true; +} + +void ClapInstrument::play(SampleFrame* buffer) +{ + if (!m_instance) { return; } + + m_instance->copyModelsFromCore(); + + const fpp_t fpp = Engine::audioEngine()->framesPerPeriod(); + m_instance->run(fpp); + + m_instance->copyModelsToCore(); + m_instance->audioPorts().copyBuffersToCore(buffer, fpp); + // TODO: Do bypassed channels need to be zeroed out or does AudioEngine handle it? +} + +auto ClapInstrument::instantiateView(QWidget* parent) -> gui::PluginView* +{ + return new gui::ClapInsView{this, parent}; +} + +void ClapInstrument::updatePitchRange() +{ + if (!m_instance) { return; } + m_instance->logger().log(CLAP_LOG_ERROR, "ClapInstrument::updatePitchRange() [NOT IMPLEMENTED YET]"); + // TODO +} + + +namespace gui +{ + +/* + * ClapInsView + */ + +ClapInsView::ClapInsView(ClapInstrument* instrument, QWidget* parent) + : InstrumentView{instrument, parent} + , ClapViewBase{this, instrument->m_instance.get()} +{ + setAutoFillBackground(true); + if (m_reloadPluginButton) + { + connect(m_reloadPluginButton, &QPushButton::clicked, + this, [this] { this->castModel()->reload();} ); + } + + if (m_toggleUIButton) + { + connect(m_toggleUIButton, &QPushButton::toggled, + this, [this] { toggleUI(); }); + } +} + +void ClapInsView::dragEnterEvent(QDragEnterEvent* dee) +{ + // For mimeType() and MimeType enum class + using namespace Clipboard; + + void (QDragEnterEvent::*reaction)() = &QDragEnterEvent::ignore; + + if (dee->mimeData()->hasFormat(mimeType(MimeType::StringPair))) + { + const QString txt = dee->mimeData()->data(mimeType(MimeType::StringPair)); + if (txt.section(':', 0, 0) == "pluginpresetfile") + { + reaction = &QDragEnterEvent::acceptProposedAction; + } + } + + (dee->*reaction)(); +} + +void ClapInsView::dropEvent(QDropEvent* de) +{ + const QString type = StringPairDrag::decodeKey(de); + const QString value = StringPairDrag::decodeValue(de); + if (type == "pluginpresetfile") + { + castModel()->loadFile(value); + de->accept(); + return; + } + de->ignore(); +} + +void ClapInsView::modelChanged() +{ + ClapViewBase::modelChanged(castModel()->m_instance.get()); + connect(castModel(), &ClapInstrument::modelChanged, this, [this] { this->modelChanged(); } ); +} + +} // namespace gui + +} // namespace lmms diff --git a/plugins/ClapInstrument/ClapInstrument.h b/plugins/ClapInstrument/ClapInstrument.h new file mode 100644 index 00000000000..15d2201ae8a --- /dev/null +++ b/plugins/ClapInstrument/ClapInstrument.h @@ -0,0 +1,119 @@ +/* + * ClapInstrument.h - Implementation of CLAP instrument + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 LMMS_CLAP_INSTRUMENT_H +#define LMMS_CLAP_INSTRUMENT_H + +#include +#include + +#include "ClapInstance.h" +#include "ClapViewBase.h" +#include "Instrument.h" +#include "InstrumentView.h" +#include "Note.h" + + +namespace lmms +{ + +namespace gui +{ + +class ClapInsView; + +} // namespace gui + +class ClapInstrument : public Instrument +{ + Q_OBJECT + +public: + ClapInstrument(InstrumentTrack* track, Descriptor::SubPluginFeatures::Key* key); + ~ClapInstrument() override; + + //! Must be checked after ctor or reload + auto isValid() const -> bool; + + void reload(); + void onSampleRateChanged(); //!< TODO: This should be a virtual method in Plugin that can be overridden + + /* + * Load/Save + */ + void saveSettings(QDomDocument& doc, QDomElement& that) override; + void loadSettings(const QDomElement& that) override; + auto nodeName() const -> QString override { return ClapInstance::ClapNodeName.data(); } + + void loadFile(const QString& file) override; + + /* + * Realtime funcs + */ + auto hasNoteInput() const -> bool override; + auto handleMidiEvent(const MidiEvent& event, const TimePos& time, f_cnt_t offset) -> bool override; + void play(SampleFrame* buffer) override; + + auto instantiateView(QWidget* parent) -> gui::PluginView* override; + +signals: + void modelChanged(); + +private slots: + void updatePitchRange(); + +private: + void clearRunningNotes(); + + std::unique_ptr m_instance; + QTimer m_idleTimer; + + std::array m_runningNotes{}; // TODO: Move to Instrument class + + friend class gui::ClapInsView; +}; + + +namespace gui +{ + +class ClapInsView : public InstrumentView, public ClapViewBase +{ +Q_OBJECT +public: + ClapInsView(ClapInstrument* instrument, QWidget* parent); + +protected: + void dragEnterEvent(QDragEnterEvent* dee) override; + void dropEvent(QDropEvent* de) override; + +private: + void modelChanged() override; +}; + +} // namespace gui + +} // namespace lmms + +#endif // LMMS_CLAP_INSTRUMENT_H diff --git a/plugins/ClapInstrument/logo.png b/plugins/ClapInstrument/logo.png new file mode 100644 index 00000000000..98d59f9b46a Binary files /dev/null and b/plugins/ClapInstrument/logo.png differ diff --git a/plugins/Vestige/Vestige.cpp b/plugins/Vestige/Vestige.cpp index 05716008ef1..f95fbf02bee 100644 --- a/plugins/Vestige/Vestige.cpp +++ b/plugins/Vestige/Vestige.cpp @@ -129,7 +129,7 @@ class VstInstrumentPlugin : public VstPlugin if ( !hasEditor() ) { return; } - if ( embedMethod() != "none" ) { + if (embedMethod() != WindowEmbed::Method::Floating) { m_pluginSubWindow.reset(new gui::vstSubWin( gui::getGUI()->mainWindow()->workspace() )); VstPlugin::createUI( m_pluginSubWindow.get() ); m_pluginSubWindow->setWidget(pluginWidget()); diff --git a/plugins/VstBase/RemoteVstPlugin.cpp b/plugins/VstBase/RemoteVstPlugin.cpp index f8bf39a3755..8e38003041c 100644 --- a/plugins/VstBase/RemoteVstPlugin.cpp +++ b/plugins/VstBase/RemoteVstPlugin.cpp @@ -118,6 +118,7 @@ struct ERect #include "Midi.h" #include "communication.h" #include "IoHelper.h" +#include "WindowEmbed.h" #include "VstSyncData.h" @@ -2453,36 +2454,36 @@ int main( int _argc, char * * _argv ) #else int embedMethodIndex = 2; #endif - std::string embedMethod = _argv[embedMethodIndex]; + auto embedMethod = lmms::WindowEmbed::toEnum(_argv[embedMethodIndex]); - if ( embedMethod == "none" ) + if (embedMethod == lmms::WindowEmbed::Method::Floating) { std::cerr << "Starting detached." << std::endl; EMBED = EMBED_X11 = EMBED_WIN32 = HEADLESS = false; } - else if ( embedMethod == "win32" ) + else if (embedMethod == lmms::WindowEmbed::Method::Win32) { std::cerr << "Starting using Win32-native embedding." << std::endl; EMBED = EMBED_WIN32 = true; EMBED_X11 = HEADLESS = false; } - else if ( embedMethod == "qt" ) + else if (embedMethod == lmms::WindowEmbed::Method::Qt) { std::cerr << "Starting using Qt-native embedding." << std::endl; EMBED = true; EMBED_X11 = EMBED_WIN32 = HEADLESS = false; } - else if ( embedMethod == "xembed" ) + else if (embedMethod == lmms::WindowEmbed::Method::XEmbed) { std::cerr << "Starting using X11Embed protocol." << std::endl; EMBED = EMBED_X11 = true; EMBED_WIN32 = HEADLESS = false; } - else if ( embedMethod == "headless" ) + else if (embedMethod == lmms::WindowEmbed::Method::Headless) { std::cerr << "Starting without UI." << std::endl; HEADLESS = true; EMBED = EMBED_X11 = EMBED_WIN32 = false; } else { - std::cerr << "Unknown embed method " << embedMethod << ". Starting detached instead." << std::endl; + std::cerr << "Unknown embed method " << _argv[embedMethodIndex] << ". Starting detached instead." << std::endl; EMBED = EMBED_X11 = EMBED_WIN32 = HEADLESS = false; } } diff --git a/plugins/VstBase/VstPlugin.cpp b/plugins/VstBase/VstPlugin.cpp index 5dbe7a698ae..0990f56295a 100644 --- a/plugins/VstBase/VstPlugin.cpp +++ b/plugins/VstBase/VstPlugin.cpp @@ -58,6 +58,7 @@ #ifdef LMMS_BUILD_LINUX # include +# undef None #endif namespace PE @@ -124,9 +125,9 @@ enum class ExecutableType VstPlugin::VstPlugin( const QString & _plugin ) : m_plugin( PathUtil::toAbsolute(_plugin) ), m_pluginWindowID( 0 ), - m_embedMethod( (gui::getGUI() != nullptr) + m_embedMethod(gui::getGUI() != nullptr ? ConfigManager::inst()->vstEmbedMethod() - : "headless" ), + : WindowEmbed::Method::Headless), m_version( 0 ), m_currentProgram() { @@ -206,7 +207,7 @@ VstPlugin::~VstPlugin() void VstPlugin::tryLoad( const QString &remoteVstPluginExecutable ) { - init( remoteVstPluginExecutable, false, {m_embedMethod} ); + init( remoteVstPluginExecutable, false, { WindowEmbed::toString(m_embedMethod).data() } ); waitForHostInfoGotten(); if( failed() ) @@ -271,7 +272,7 @@ void VstPlugin::loadSettings( const QDomElement & _this ) void VstPlugin::saveSettings( QDomDocument & _doc, QDomElement & _this ) { - if ( m_embedMethod != "none" ) + if (m_embedMethod != WindowEmbed::Method::Floating) { if( pluginWidget() != nullptr ) { @@ -311,7 +312,7 @@ void VstPlugin::saveSettings( QDomDocument & _doc, QDomElement & _this ) void VstPlugin::toggleUI() { - if ( m_embedMethod == "none" ) + if (m_embedMethod == WindowEmbed::Method::Floating) { RemotePlugin::toggleUI(); } @@ -404,7 +405,7 @@ bool VstPlugin::processMessage( const message & _m ) { case IdVstPluginWindowID: m_pluginWindowID = _m.getInt(); - if( m_embedMethod == "none" + if (m_embedMethod == WindowEmbed::Method::Floating && ConfigManager::inst()->value( "ui", "vstalwaysontop" ).toInt() ) { @@ -631,11 +632,11 @@ void VstPlugin::idleUpdate() void VstPlugin::showUI() { - if ( m_embedMethod == "none" ) + if (m_embedMethod == WindowEmbed::Method::Floating) { RemotePlugin::showUI(); } - else if ( m_embedMethod != "headless" ) + else if (m_embedMethod != WindowEmbed::Method::Headless) { if (! editor()) { qWarning() << "VstPlugin::showUI called before VstPlugin::createUI"; @@ -646,7 +647,7 @@ void VstPlugin::showUI() void VstPlugin::hideUI() { - if ( m_embedMethod == "none" ) + if (m_embedMethod == WindowEmbed::Method::Floating) { RemotePlugin::hideUI(); } @@ -735,7 +736,7 @@ void VstPlugin::createUI( QWidget * parent ) QWidget* container = nullptr; - if (m_embedMethod == "qt" ) + if (m_embedMethod == WindowEmbed::Method::Qt) { QWindow* vw = QWindow::fromWinId(m_pluginWindowID); container = QWidget::createWindowContainer(vw, parent ); @@ -743,7 +744,7 @@ void VstPlugin::createUI( QWidget * parent ) } else #ifdef LMMS_BUILD_WIN32 - if (m_embedMethod == "win32" ) + if (m_embedMethod == WindowEmbed::Method::Win32) { QWidget * helper = new QWidget; QHBoxLayout * l = new QHBoxLayout( helper ); @@ -774,7 +775,7 @@ void VstPlugin::createUI( QWidget * parent ) #endif #ifdef LMMS_BUILD_LINUX - if (m_embedMethod == "xembed" ) + if (m_embedMethod == WindowEmbed::Method::XEmbed) { if (parent) { @@ -787,7 +788,7 @@ void VstPlugin::createUI( QWidget * parent ) } else #endif { - qCritical() << "Unknown embed method" << m_embedMethod; + qCritical() << "Unknown embed method" << WindowEmbed::toString(m_embedMethod).data(); return; } @@ -799,7 +800,7 @@ void VstPlugin::createUI( QWidget * parent ) bool VstPlugin::eventFilter(QObject *obj, QEvent *event) { - if (embedMethod() == "qt" && obj == m_pluginWidget) + if (embedMethod() == WindowEmbed::Method::Qt && obj == m_pluginWidget) { if (event->type() == QEvent::Show) { RemotePlugin::showUI(); @@ -809,7 +810,7 @@ bool VstPlugin::eventFilter(QObject *obj, QEvent *event) return false; } -QString VstPlugin::embedMethod() const +WindowEmbed::Method VstPlugin::embedMethod() const { return m_embedMethod; } diff --git a/plugins/VstBase/VstPlugin.h b/plugins/VstBase/VstPlugin.h index 03e732970e2..b25c37bc476 100644 --- a/plugins/VstBase/VstPlugin.h +++ b/plugins/VstBase/VstPlugin.h @@ -33,6 +33,7 @@ #include "JournallingObject.h" #include "RemotePlugin.h" +#include "WindowEmbed.h" #include "vstbase_export.h" @@ -121,7 +122,7 @@ class VSTBASE_EXPORT VstPlugin : public RemotePlugin, public JournallingObject virtual void createUI(QWidget *parent); bool eventFilter(QObject *obj, QEvent *event) override; - QString embedMethod() const; + WindowEmbed::Method embedMethod() const; public slots: void setTempo( lmms::bpm_t _bpm ); @@ -152,7 +153,7 @@ public slots: QPointer m_pluginWidget; int m_pluginWindowID; QSize m_pluginGeometry; - const QString m_embedMethod; + const WindowEmbed::Method m_embedMethod; QString m_name; int m_version; diff --git a/plugins/VstEffect/VstEffectControlDialog.cpp b/plugins/VstEffect/VstEffectControlDialog.cpp index a5b67f5f33a..ab4ff683e90 100644 --- a/plugins/VstEffect/VstEffectControlDialog.cpp +++ b/plugins/VstEffect/VstEffectControlDialog.cpp @@ -69,7 +69,7 @@ VstEffectControlDialog::VstEffectControlDialog( VstEffectControls * _ctl ) : _ctl->m_effect->m_plugin != nullptr ) { m_plugin = _ctl->m_effect->m_plugin; - embed_vst = m_plugin->embedMethod() != "none"; + embed_vst = m_plugin->embedMethod() != WindowEmbed::Method::Floating; if (embed_vst) { if (m_plugin->hasEditor() && ! m_plugin->pluginWidget()) { diff --git a/src/3rdparty/CMakeLists.txt b/src/3rdparty/CMakeLists.txt index e5cb6252756..618f97b3311 100644 --- a/src/3rdparty/CMakeLists.txt +++ b/src/3rdparty/CMakeLists.txt @@ -10,6 +10,7 @@ target_include_directories(jack_headers INTERFACE jack2/common) ADD_SUBDIRECTORY(hiir) ADD_SUBDIRECTORY(weakjack) +ADD_SUBDIRECTORY(clap) # The lockless ring buffer library is linked as part of the core add_library(ringbuffer OBJECT diff --git a/src/3rdparty/clap/CMakeLists.txt b/src/3rdparty/clap/CMakeLists.txt new file mode 100644 index 00000000000..7a71e01898e --- /dev/null +++ b/src/3rdparty/clap/CMakeLists.txt @@ -0,0 +1,6 @@ +if(LMMS_HAVE_CLAP) + add_library(clap INTERFACE) + target_include_directories(clap INTERFACE + ${CMAKE_CURRENT_SOURCE_DIR}/clap/include + ${CMAKE_CURRENT_SOURCE_DIR}/clap-helpers/include) +endif() diff --git a/src/3rdparty/clap/clap b/src/3rdparty/clap/clap new file mode 160000 index 00000000000..df8f16c69ba --- /dev/null +++ b/src/3rdparty/clap/clap @@ -0,0 +1 @@ +Subproject commit df8f16c69ba1c1a15fb105f0c5a2e5b9ac6be742 diff --git a/src/3rdparty/clap/clap-helpers b/src/3rdparty/clap/clap-helpers new file mode 160000 index 00000000000..7b53a685e11 --- /dev/null +++ b/src/3rdparty/clap/clap-helpers @@ -0,0 +1 @@ +Subproject commit 7b53a685e11465154b4ccba3065224dbcbf8a893 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7645e49e34e..9fc08fd696b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -150,6 +150,10 @@ if(LMMS_HAVE_OGGVORBIS) list(APPEND EXTRA_LIBRARIES Vorbis::vorbisenc Vorbis::vorbisfile) endif() +IF(LMMS_HAVE_CLAP) + set(CLAP_LIBRARIES clap) +ENDIF() + SET(LMMS_REQUIRED_LIBS ${LMMS_REQUIRED_LIBS} ${CMAKE_THREAD_LIBS_INIT} ${QT_LIBRARIES} @@ -163,6 +167,9 @@ SET(LMMS_REQUIRED_LIBS ${LMMS_REQUIRED_LIBS} ${LV2_LIBRARIES} ${SUIL_LIBRARIES} ${LILV_LIBRARIES} + ${CLAP_LIBRARIES} + ${SAMPLERATE_LIBRARIES} + ${SNDFILE_LIBRARIES} ${FFTW3F_LIBRARIES} SampleRate::samplerate SndFile::sndfile diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 1e2c4f3cfdb..98783a8fb17 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -11,6 +11,7 @@ set(LMMS_SRCS core/BandLimitedWave.cpp core/base64.cpp core/BufferManager.cpp + core/Clip.cpp core/Clipboard.cpp core/ComboBoxModel.cpp core/ConfigManager.cpp @@ -24,7 +25,6 @@ set(LMMS_SRCS core/EnvelopeAndLfoParameters.cpp core/fft_helpers.cpp core/FileSearch.cpp - core/Mixer.cpp core/ImportFilter.cpp core/InlineAutomation.cpp core/Instrument.cpp @@ -43,6 +43,7 @@ set(LMMS_SRCS core/Metronome.cpp core/MicroTimer.cpp core/Microtuner.cpp + core/Mixer.cpp core/MixHelpers.cpp core/Model.cpp core/ModelVisitor.cpp @@ -57,8 +58,12 @@ set(LMMS_SRCS core/Piano.cpp core/PlayHandle.cpp core/Plugin.cpp - core/PluginIssue.cpp core/PluginFactory.cpp + core/PluginIssue.cpp + core/PluginPortConfig.cpp + core/PluginPresets.cpp + core/Preset.cpp + core/PresetDatabase.cpp core/PresetPreviewPlayHandle.cpp core/ProjectJournal.cpp core/ProjectRenderer.cpp @@ -76,6 +81,7 @@ set(LMMS_SRCS core/LmmsSemaphore.cpp core/SerializingObject.cpp core/Song.cpp + core/StepRecorder.cpp core/TempoSyncKnobModel.cpp core/ThreadPool.cpp core/Timeline.cpp @@ -85,10 +91,9 @@ set(LMMS_SRCS core/TrackContainer.cpp core/UpgradeExtendedNoteRange.h core/UpgradeExtendedNoteRange.cpp - core/Clip.cpp core/ValueBuffer.cpp core/VstSyncController.cpp - core/StepRecorder.cpp + core/WindowEmbed.cpp core/audio/AudioAlsa.cpp core/audio/AudioDevice.cpp @@ -107,6 +112,25 @@ set(LMMS_SRCS core/audio/AudioSampleRecorder.cpp core/audio/AudioSdl.cpp + core/clap/ClapAudioPorts.cpp + core/clap/ClapExtension.cpp + core/clap/ClapFile.cpp + core/clap/ClapGui.cpp + core/clap/ClapInstance.cpp + core/clap/ClapLog.cpp + core/clap/ClapManager.cpp + core/clap/ClapNotePorts.cpp + core/clap/ClapParameter.cpp + core/clap/ClapParams.cpp + core/clap/ClapPluginInfo.cpp + core/clap/ClapPresetDatabase.cpp + core/clap/ClapPresetLoader.cpp + core/clap/ClapState.cpp + core/clap/ClapSubPluginFeatures.cpp + core/clap/ClapThreadCheck.cpp + core/clap/ClapTimerSupport.cpp + core/clap/ClapTransport.cpp + core/lv2/Lv2Basics.cpp core/lv2/Lv2ControlBase.cpp core/lv2/Lv2Evbuf.cpp diff --git a/src/core/ConfigManager.cpp b/src/core/ConfigManager.cpp index d3c973020bf..3a43221e6b6 100644 --- a/src/core/ConfigManager.cpp +++ b/src/core/ConfigManager.cpp @@ -194,30 +194,21 @@ bool ConfigManager::enableBlockedPlugins() return (envVar && *envVar); } -QStringList ConfigManager::availableVstEmbedMethods() +WindowEmbed::Method ConfigManager::vstEmbedMethod() const { - QStringList methods; - methods.append("none"); - methods.append("qt"); -#ifdef LMMS_BUILD_WIN32 - methods.append("win32"); -#endif -#ifdef LMMS_BUILD_LINUX - if (static_cast(QApplication::instance())-> - platformName() == "xcb") - { - methods.append("xembed"); - } -#endif - return methods; -} + const auto methods = WindowEmbed::availableMethods(); + const auto defaultMethod = methods.empty() ? WindowEmbed::Method::Floating : methods.back(); + const auto defaultMethodStr = QString{WindowEmbed::toString(defaultMethod).data()}; + const auto currentMethodStr = value("ui", "vstembedmethod", defaultMethodStr); + const auto currentMethod = WindowEmbed::toEnum(currentMethodStr.toStdString()); -QString ConfigManager::vstEmbedMethod() const -{ - QStringList methods = availableVstEmbedMethods(); - QString defaultMethod = *(methods.end() - 1); - QString currentMethod = value( "ui", "vstembedmethod", defaultMethod ); - return methods.contains(currentMethod) ? currentMethod : defaultMethod; + // If floating, use that + if (currentMethod == WindowEmbed::Method::Floating) { return currentMethod; } + + // If embedding, use method from configuration if available, else default method + return std::find(methods.begin(), methods.end(), currentMethod) != methods.end() + ? currentMethod + : defaultMethod; } bool ConfigManager::hasWorkingDir() const diff --git a/src/core/DataFile.cpp b/src/core/DataFile.cpp index 8bd3a776bfc..05a040f2c7d 100644 --- a/src/core/DataFile.cpp +++ b/src/core/DataFile.cpp @@ -508,7 +508,8 @@ bool DataFile::copyResources(const QString& resourcesDir) } // Update attribute path to point to the bundle file - QString newAtt = PathUtil::basePrefix(PathUtil::Base::LocalDir) + "resources/" + finalFileName; + QString newAtt = PathUtil::basePrefixQString(PathUtil::Base::LocalDir) + + "resources/" + finalFileName; el.setAttribute(*res, newAtt); } ++res; @@ -568,12 +569,7 @@ bool DataFile::hasLocalPlugins(QDomElement parent /* = QDomElement()*/, bool fir for (int i = 0; i < attributes.size(); ++i) { QDomNode attribute = attributes.item(i); - QDomAttr attr = attribute.toAttr(); - if (attr.value().startsWith(PathUtil::basePrefix(PathUtil::Base::LocalDir), - Qt::CaseInsensitive)) - { - return true; - } + if (PathUtil::hasBase(attribute.toAttr().value(), PathUtil::Base::LocalDir)) { return true; } } } diff --git a/src/core/Engine.cpp b/src/core/Engine.cpp index 9435eb69c06..1dbeb46041a 100644 --- a/src/core/Engine.cpp +++ b/src/core/Engine.cpp @@ -29,6 +29,7 @@ #include "Mixer.h" #include "Ladspa2LMMS.h" #include "Lv2Manager.h" +#include "ClapManager.h" #include "PatternStore.h" #include "Plugin.h" #include "PresetPreviewPlayHandle.h" @@ -49,6 +50,9 @@ ProjectJournal * Engine::s_projectJournal = nullptr; #ifdef LMMS_HAVE_LV2 Lv2Manager * Engine::s_lv2Manager = nullptr; #endif +#ifdef LMMS_HAVE_CLAP +ClapManager* Engine::s_clapManager = nullptr; +#endif Ladspa2LMMS * Engine::s_ladspaManager = nullptr; void* Engine::s_dndPluginKey = nullptr; @@ -76,6 +80,11 @@ void Engine::init( bool renderOnly ) s_lv2Manager = new Lv2Manager; s_lv2Manager->initPlugins(); #endif +#ifdef LMMS_HAVE_CLAP + s_clapManager = new ClapManager; + s_clapManager->initPlugins(); +#endif + s_ladspaManager = new Ladspa2LMMS; s_projectJournal->setJournalling( true ); @@ -108,6 +117,9 @@ void Engine::destroy() #ifdef LMMS_HAVE_LV2 deleteHelper( &s_lv2Manager ); +#endif +#ifdef LMMS_HAVE_CLAP + deleteHelper( &s_clapManager ); #endif deleteHelper( &s_ladspaManager ); diff --git a/src/core/PathUtil.cpp b/src/core/PathUtil.cpp index 03ec465a929..9bb57181e47 100644 --- a/src/core/PathUtil.cpp +++ b/src/core/PathUtil.cpp @@ -9,9 +9,9 @@ namespace lmms::PathUtil { - auto relativeBases = std::array{ Base::ProjectDir, Base::FactorySample, Base::UserSample, Base::UserVST, Base::Preset, + constexpr auto relativeBases = std::array{ Base::ProjectDir, Base::FactorySample, Base::UserSample, Base::UserVST, Base::Preset, Base::UserLADSPA, Base::DefaultLADSPA, Base::UserSoundfont, Base::DefaultSoundfont, Base::UserGIG, Base::DefaultGIG, - Base::LocalDir }; + Base::LocalDir, Base::Internal }; QString baseLocation(const Base base, bool* error /* = nullptr*/) { @@ -50,11 +50,22 @@ namespace lmms::PathUtil if (error) { *error = (!s || projectPath.isEmpty()); } break; } + case Base::Internal: + if (error) { *error = true; } + [[fallthrough]]; default : return QString(""); } return QDir::cleanPath(loc) + "/"; } + std::optional getBaseLocation(Base base) + { + bool error = false; + const auto str = baseLocation(base, &error); + if (error) { return std::nullopt; } + return str.toStdString(); + } + QDir baseQDir (const Base base, bool* error /* = nullptr*/) { if (base == Base::Absolute) @@ -65,51 +76,103 @@ namespace lmms::PathUtil return QDir(baseLocation(base, error)); } - QString basePrefix(const Base base) + QString basePrefixQString(const Base base) + { + const auto prefix = basePrefix(base); + return QString::fromLatin1(prefix.data(), prefix.size()); + } + + std::string_view basePrefix(const Base base) { switch (base) { - case Base::ProjectDir : return QStringLiteral("userprojects:"); - case Base::FactorySample : return QStringLiteral("factorysample:"); - case Base::UserSample : return QStringLiteral("usersample:"); - case Base::UserVST : return QStringLiteral("uservst:"); - case Base::Preset : return QStringLiteral("preset:"); - case Base::UserLADSPA : return QStringLiteral("userladspa:"); - case Base::DefaultLADSPA : return QStringLiteral("defaultladspa:"); - case Base::UserSoundfont : return QStringLiteral("usersoundfont:"); - case Base::DefaultSoundfont : return QStringLiteral("defaultsoundfont:"); - case Base::UserGIG : return QStringLiteral("usergig:"); - case Base::DefaultGIG : return QStringLiteral("defaultgig:"); - case Base::LocalDir : return QStringLiteral("local:"); - default : return QStringLiteral(""); + case Base::ProjectDir : return "userprojects:"; + case Base::FactorySample : return "factorysample:"; + case Base::UserSample : return "usersample:"; + case Base::UserVST : return "uservst:"; + case Base::Preset : return "preset:"; + case Base::UserLADSPA : return "userladspa:"; + case Base::DefaultLADSPA : return "defaultladspa:"; + case Base::UserSoundfont : return "usersoundfont:"; + case Base::DefaultSoundfont : return "defaultsoundfont:"; + case Base::UserGIG : return "usergig:"; + case Base::DefaultGIG : return "defaultgig:"; + case Base::LocalDir : return "local:"; + case Base::Internal : return "internal:"; + default : return ""; + } + } + + bool hasBase(const QString& path, Base base) + { + if (base == Base::Absolute) + { + return baseLookup(path) == base; } + + return path.startsWith(basePrefixQString(base), Qt::CaseInsensitive); + } + + bool hasBase(std::string_view path, Base base) + { + if (base == Base::Absolute) + { + return baseLookup(path) == base; + } + + auto prefix = basePrefix(base); + return path.rfind(prefix, 0) == 0; } Base baseLookup(const QString & path) { - for (auto base: relativeBases) + for (auto base : relativeBases) { - QString prefix = basePrefix(base); + QString prefix = basePrefixQString(base); if ( path.startsWith(prefix) ) { return base; } } return Base::Absolute; } - - + Base baseLookup(std::string_view path) + { + for (auto base : relativeBases) + { + const auto prefix = basePrefix(base); + if (path.rfind(prefix, 0) == 0) { return base; } + } + return Base::Absolute; + } QString stripPrefix(const QString & path) { return path.mid( basePrefix(baseLookup(path)).length() ); } - QString cleanName(const QString & path) + std::string_view stripPrefix(std::string_view path) { - return stripPrefix(QFileInfo(path).baseName()); + path.remove_prefix(basePrefix(baseLookup(path)).length()); + return path; } + std::pair parsePath(std::string_view path) + { + for (auto base : relativeBases) + { + auto prefix = basePrefix(base); + if (path.rfind(prefix, 0) == 0) + { + path.remove_prefix(prefix.length()); + return { base, path }; + } + } + return { Base::Absolute, path }; + } - + QString cleanName(const QString & path) + { + return stripPrefix(QFileInfo(path).baseName()); + } QString oldRelativeUpgrade(const QString & input) { @@ -129,33 +192,42 @@ namespace lmms::PathUtil if (vstInfo.exists()) { assumedBase = Base::UserVST; } //Assume we've found the correct base location, return the full path - return basePrefix(assumedBase) + input; + return basePrefixQString(assumedBase) + input; } - - - QString toAbsolute(const QString & input, bool* error /* = nullptr*/) { - //First, do no harm to absolute paths + // First, check if it's Internal + if (hasBase(input, Base::Internal)) { return input; } + + // Secondly, do no harm to absolute paths QFileInfo inputFileInfo = QFileInfo(input); if (inputFileInfo.isAbsolute()) { if (error) { *error = false; } return input; } - //Next, handle old relative paths with no prefix + + // Next, handle old relative paths with no prefix QString upgraded = input.contains(":") ? input : oldRelativeUpgrade(input); Base base = baseLookup(upgraded); return baseLocation(base, error) + upgraded.remove(0, basePrefix(base).length()); } + std::optional toAbsolute(std::string_view input) + { + bool error = false; + const auto str = toAbsolute(QString::fromUtf8(input.data(), input.size()), &error); + if (error) { return std::nullopt; } + return str.toStdString(); + } + QString relativeOrAbsolute(const QString & input, const Base base) { if (input.isEmpty()) { return input; } QString absolutePath = toAbsolute(input); - if (base == Base::Absolute) { return absolutePath; } + if (base == Base::Absolute || base == Base::Internal) { return absolutePath; } bool error; QString relativePath = baseQDir(base, &error).relativeFilePath(absolutePath); // Return the relative path if it didn't result in a path starting with .. @@ -167,6 +239,8 @@ namespace lmms::PathUtil QString toShortestRelative(const QString & input, bool allowLocal /* = false*/) { + if (hasBase(input, Base::Internal)) { return input; } + QFileInfo inputFileInfo = QFileInfo(input); QString absolutePath = inputFileInfo.isAbsolute() ? input : toAbsolute(input); @@ -185,7 +259,34 @@ namespace lmms::PathUtil shortestPath = otherPath; } } - return basePrefix(shortestBase) + relativeOrAbsolute(absolutePath, shortestBase); + return basePrefixQString(shortestBase) + relativeOrAbsolute(absolutePath, shortestBase); + } + + std::string toShortestRelative(std::string_view input, bool allowLocal) + { + if (hasBase(input, Base::Internal)) { return std::string{input}; } + + const auto qstr = QString::fromUtf8(input.data(), input.size()); + QFileInfo inputFileInfo = QFileInfo(qstr); + QString absolutePath = inputFileInfo.isAbsolute() ? qstr : toAbsolute(qstr); + + Base shortestBase = Base::Absolute; + QString shortestPath = relativeOrAbsolute(absolutePath, shortestBase); + for (auto base : relativeBases) + { + // Skip local paths when searching for the shortest relative if those + // are not allowed for that resource + if (base == Base::LocalDir && !allowLocal) { continue; } + + QString otherPath = relativeOrAbsolute(absolutePath, base); + if (otherPath.length() < shortestPath.length()) + { + shortestBase = base; + shortestPath = otherPath; + } + } + + return (basePrefixQString(shortestBase) + relativeOrAbsolute(absolutePath, shortestBase)).toStdString(); } } // namespace lmms::PathUtil diff --git a/src/core/PluginPortConfig.cpp b/src/core/PluginPortConfig.cpp new file mode 100644 index 00000000000..0c6ab09ac52 --- /dev/null +++ b/src/core/PluginPortConfig.cpp @@ -0,0 +1,191 @@ +/* + * PluginPortConfig.cpp - Specifies how to route audio channels + * in and out of a plugin. + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 "PluginPortConfig.h" + +#include +#include +#include + +#include "Model.h" + +namespace lmms +{ + +PluginPortConfig::PluginPortConfig(Model* parent) + : QObject{parent} + , m_config{parent, tr("L/R channel configuration")} +{ +} + +PluginPortConfig::PluginPortConfig(PortType in, PortType out, Model* parent) + : QObject{parent} + , m_inPort{in} + , m_outPort{out} + , m_config{parent, tr("L/R channel configuration")} +{ +} + +auto PluginPortConfig::hasMonoPort() const -> bool +{ + return m_inPort == PortType::Mono || m_outPort == PortType::Mono; +} + +auto PluginPortConfig::monoPluginType() const -> MonoPluginType +{ + if (m_inPort == PortType::Mono) + { + if (m_outPort == PortType::Mono) + { + return MonoPluginType::Both; + } + return MonoPluginType::Input; + } + else if (m_outPort == PortType::Mono) + { + return MonoPluginType::Output; + } + return MonoPluginType::None; +} + +void PluginPortConfig::setPortType(PortType in, PortType out) +{ + if (in == PortType::None && out == PortType::None) { return; } + if (m_inPort == in && m_outPort == out) { return; } + + m_inPort = in; + m_outPort = out; + + updateOptions(); + + emit portsChanged(); +} + +auto PluginPortConfig::setPortConfig(Config config) -> bool +{ + assert(config != Config::None); + if (m_inPort != PortType::Mono && m_outPort != PortType::Mono) + { + if (config != Config::Stereo) { return false; } + m_config.setValue(0); + } + else + { + if (config == Config::Stereo) { return false; } + m_config.setValue(static_cast(config)); + } + + return true; +} + +void PluginPortConfig::saveSettings(QDomDocument& doc, QDomElement& elem) +{ + // Only plugins with a mono in/out need to be saved + //if (m_inPort != PortType::Mono && m_outPort != PortType::Mono) { return; } + + elem.setAttribute("in", static_cast(m_inPort)); // probably not needed, but just in case + elem.setAttribute("out", static_cast(m_outPort)); // ditto + m_config.saveSettings(doc, elem, "config"); +} + +void PluginPortConfig::loadSettings(const QDomElement& elem) +{ + //const auto inPort = static_cast(elem.attribute("in", "0").toInt()); + //const auto outPort = static_cast(elem.attribute("out", "0").toInt()); + m_config.loadSettings(elem, "config"); +} + +void PluginPortConfig::updateOptions() +{ + m_config.clear(); + + const auto monoType = monoPluginType(); + if (monoType == PluginPortConfig::MonoPluginType::None) + { + m_config.addItem(tr("Stereo")); + return; + } + + const auto hasInputPort = inputPortType() != PluginPortConfig::PortType::None; + const auto hasOutputPort = outputPortType() != PluginPortConfig::PortType::None; + + // 1. Mono mix + QString itemText; + switch (monoType) + { + case PluginPortConfig::MonoPluginType::Input: + itemText = tr("Downmix to mono"); break; + case PluginPortConfig::MonoPluginType::Output: + itemText = tr("Upmix to stereo"); break; + case PluginPortConfig::MonoPluginType::Both: + itemText = tr("Mono mix"); break; + default: break; + } + m_config.addItem(itemText); + + // 2. Left only + itemText = QString{}; + switch (monoType) + { + case PluginPortConfig::MonoPluginType::Input: + itemText = hasOutputPort + ? tr("L in (R bypass)") + : tr("Left in"); + break; + case PluginPortConfig::MonoPluginType::Output: + itemText = hasInputPort + ? tr("L out (R bypass)") + : tr("Left only"); + break; + case PluginPortConfig::MonoPluginType::Both: + itemText = tr("L only (R bypass)"); + break; + default: break; + } + m_config.addItem(itemText); + + // 3. Right only + itemText = QString{}; + switch (monoType) + { + case PluginPortConfig::MonoPluginType::Input: + itemText = hasOutputPort + ? tr("R in (L bypass)") + : tr("Right in"); + break; + case PluginPortConfig::MonoPluginType::Output: + itemText = hasInputPort + ? tr("R out (L bypass)") + : tr("Right only"); + break; + case PluginPortConfig::MonoPluginType::Both: + itemText = tr("R only (L bypass)"); + break; + default: break; + } + m_config.addItem(itemText); +} + +} // namespace lmms diff --git a/src/core/PluginPresets.cpp b/src/core/PluginPresets.cpp new file mode 100644 index 00000000000..eefedc789cd --- /dev/null +++ b/src/core/PluginPresets.cpp @@ -0,0 +1,230 @@ +/* + * PluginPresets.cpp - Preset collection and functionality for a plugin instance + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 "PluginPresets.h" + +#include + +#include +#include +#include + +namespace lmms +{ + +PluginPresets::PluginPresets(Model* parent, PresetDatabase* database, std::string_view pluginKey) + : LinkedModelGroup{parent} + , m_pluginKey{pluginKey} +{ + setPresetDatabase(database); + // TODO: Connect preset database changed signal to refreshPresetCollection() +} + +auto PluginPresets::setPresetDatabase(PresetDatabase* database) -> bool +{ + if (!database) { return false; } + + m_database = database; + m_presets = database->findPresets(m_pluginKey); + + m_activePresetModel.setRange(-1, m_presets.size() - 1); + + emit presetCollectionChanged(); + + setActivePreset(std::nullopt); + return true; +} + +auto PluginPresets::refreshPresetCollection() -> bool +{ + if (!m_database) { return false; } + + std::optional loadData; + if (m_activePreset) { loadData = m_presets.at(*m_activePreset)->loadData(); } + + m_presets = m_database->findPresets(m_pluginKey); + + m_activePresetModel.setRange(-1, m_presets.size() - 1); + + // Find the previously active preset in the new presets vector + if (loadData) + { + const auto newIndex = findPreset(*loadData); + if (!newIndex) + { + // Failed to find preset in new vector + setActivePreset(std::nullopt); + return false; + } + + m_activePreset = newIndex; + m_activePresetModel.setValue(*newIndex); + } + else + { + m_activePresetModel.setValue(-1); + } + + emit presetCollectionChanged(); + + return true; +} + +auto PluginPresets::activatePreset(const PresetLoadData& preset) -> bool +{ + if (auto index = findPreset(preset)) + { + return activatePreset(*index); + } + return false; +} + +auto PluginPresets::activatePreset(std::size_t index) -> bool +{ + if (!m_database || index >= m_presets.size()) { return false; } + + const auto preset = m_presets[index]; + assert(preset != nullptr); + + // TODO: Check if current preset has been modified? (In case user wants to save it) + + if (!activatePresetImpl(preset->loadData())) { return false; } + + setActivePreset(index); + + return true; +} + +auto PluginPresets::prevPreset() -> bool +{ + if (!m_activePreset) { return activatePreset(0); } + + if (m_presets.empty()) { return false; } + const auto newIndex = *m_activePreset != 0 + ? (*m_activePreset - 1) % m_presets.size() + : m_presets.size() - 1; + + return activatePreset(newIndex); +} + +auto PluginPresets::nextPreset() -> bool +{ + if (!m_activePreset) { return activatePreset(0); } + + if (m_presets.empty()) { return false; } + return activatePreset((*m_activePreset + 1) % m_presets.size()); +} + +auto PluginPresets::activePreset() const -> const Preset* +{ + if (!m_activePreset) { return nullptr; } + return m_presets.at(*m_activePreset); +} + +void PluginPresets::saveActivePreset(QDomDocument& doc, QDomElement& element) +{ + if (auto preset = activePreset()) + { + const auto& [location, loadKey] = preset->loadData(); + + QDomElement presetElement = doc.createElement(presetNodeName()); + element.appendChild(presetElement); +#if 0 + qDebug().nospace() << "Saving active preset: location: \"" << location.data() << "\" loadKey: \"" + << loadKey.data() << "\""; +#endif + presetElement.setAttribute("location", QString::fromUtf8(location.data(), location.size())); + presetElement.setAttribute("loadKey", QString::fromUtf8(loadKey.data(), loadKey.size())); + } +} + +void PluginPresets::loadActivePreset(const QDomElement& element) +{ + QDomElement presetElement = element.firstChildElement(presetNodeName()); + if (presetElement.isNull()) + { + setActivePreset(std::nullopt); + return; + } + + const auto location = presetElement.attribute("location"); + const auto loadKey = presetElement.attribute("loadKey"); + + if (location.isEmpty() && loadKey.isEmpty()) + { + setActivePreset(std::nullopt); + return; + } + +#if 0 + qDebug().nospace() << "Loading active preset: location: \"" << location.data() << "\" loadKey: \"" + << loadKey.data() << "\""; +#endif + + // TODO: The needed preset may not be discovered at this point + + const auto loadData = PresetLoadData{location.toStdString(), loadKey.toStdString()}; + if (auto index = findPreset(loadData)) + { + if (!activatePreset(*index)) + { + qWarning() << "Failed to load preset!"; + } + } + else + { + qWarning() << "Failed to find preset!"; + } +} + +void PluginPresets::setActivePreset(std::optional index) +{ + const auto oldIndex = m_activePreset; + m_activePreset = index; + m_activePresetModel.setValue(index ? *index : -1); + m_modified = false; + + if (index != oldIndex) + { + emit activePresetChanged(); + } +} + +auto PluginPresets::findPreset(const PresetLoadData& preset) const -> std::optional +{ + const auto it = std::find_if(m_presets.begin(), m_presets.end(), [&](const Preset* p) { + return p && p->loadData() == preset; + }); + + if (it == m_presets.end()) { return std::nullopt; } + return std::distance(m_presets.begin(), it); +} + +auto PluginPresets::preset(std::size_t index) const -> const Preset* +{ + if (index >= m_presets.size()) { return nullptr; } + return m_presets[index]; +} + +} // namespace lmms diff --git a/src/core/Preset.cpp b/src/core/Preset.cpp new file mode 100644 index 00000000000..336150bffee --- /dev/null +++ b/src/core/Preset.cpp @@ -0,0 +1,47 @@ +/* + * Preset.cpp - A generic preset class for plugins + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 "Preset.h" + +#include + +namespace lmms +{ + +auto Preset::supportsPlugin(std::string_view key) const -> bool +{ + if (m_keys.empty()) { return true; } + return std::find(m_keys.begin(), m_keys.end(), key) != m_keys.end(); +} + +auto operator<(const Preset& a, const Preset& b) noexcept -> bool +{ + // TODO: Better way to do this? + auto res = a.m_loadData.location.compare(b.m_loadData.location); + if (res < 0) { return true; } + if (res > 0) { return false; } + return a.m_loadData.loadKey < b.m_loadData.loadKey; +} + +} // namespace lmms diff --git a/src/core/PresetDatabase.cpp b/src/core/PresetDatabase.cpp new file mode 100644 index 00000000000..88bceb9d522 --- /dev/null +++ b/src/core/PresetDatabase.cpp @@ -0,0 +1,196 @@ +/* + * PresetDatabase.cpp - Preset discovery, loading, storage, and query + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 "PresetDatabase.h" + +#include +#include +#include + +#include "ConfigManager.h" +#include "PathUtil.h" + +namespace lmms +{ + +PresetDatabase::PresetDatabase() + : m_recentPresetFile{ConfigManager::inst()->userPresetsDir().toStdString()} +{ +} + +auto PresetDatabase::discover() -> bool +{ + if (!discoverSetup()) { return false; } + if (!discoverFiletypes(m_filetypes)) { return false; } + + auto func = SetLocations{m_presets}; + if (!discoverLocations(func)) { return false; } + + bool success = false; + for (auto& [location, presets] : m_presets) + { + success = discoverPresets(location, presets) || success; + } + + return success; +} + +auto PresetDatabase::loadPresets(std::string_view file) -> std::vector +{ + m_recentPresetFile = file; + + auto& [location, presets] = *getLocation(file); + return loadPresets(location, file, presets); +} + +auto PresetDatabase::loadPresets(const Location& location, std::string_view file, std::set& presets) + -> std::vector +{ + // This is the default method - plugins should override this + + auto preset = Preset{}; + preset.loadData() = { std::string{file}, "" }; + + preset.metadata().displayName = std::filesystem::u8path(file).filename().u8string(); + + auto [it, added] = presets.emplace(std::move(preset)); + if (!added) { return {}; } + + return { &*it }; +} + +auto PresetDatabase::findPresets(std::string_view key) const -> std::vector +{ + std::vector ret; + for (const auto& mapPair : m_presets) + { + for (const auto& preset : mapPair.second) + { + if (preset.supportsPlugin(key)) + { + ret.push_back(&preset); + } + } + } + return ret; +} + +auto PresetDatabase::findPreset(const PresetLoadData& loadData, std::string_view key) const -> const Preset* +{ + if (auto it = m_presets.find(loadData.location); it != m_presets.end()) + { + auto it2 = std::find_if(it->second.begin(), it->second.end(), [&](const Preset& p) { + return p.loadData().loadKey == loadData.loadKey && p.supportsPlugin(key); + }); + return it2 != it->second.end() ? &*it2 : nullptr; // TODO: Is it2.base() standard? + } + return nullptr; +} + +auto PresetDatabase::findOrLoadPresets(std::string_view file) -> std::vector +{ + m_recentPresetFile = file; + + auto& [location, presets] = *getLocation(file); + loadPresets(location, file, presets); + + std::vector results; + results.reserve(presets.size()); + for (const auto& preset : presets) + { + results.push_back(&preset); + } + return results; +} + +auto PresetDatabase::getLocation(std::string_view path, bool add) -> PresetMap::iterator +{ + auto isSubpath = [](std::string_view path, std::string_view base) -> bool { + const auto mismatchPair = std::mismatch(path.begin(), path.end(), base.begin(), base.end()); + return mismatchPair.second == base.end(); + }; + + // First, create shortened path + const auto shortPath = PathUtil::toShortestRelative(path); + + // Next, find the preset directory it belongs to (if any) + std::vector matches; + for (auto it = m_presets.begin(); it != m_presets.end(); ++it) + { + const auto& location = it->first.location; + if (isSubpath(shortPath, location)) + { + matches.push_back(it); + } + } + + if (!matches.empty()) + { + // Location already exists - return the longest (most specific) directory + return *std::max_element(matches.begin(), matches.end(), + [](PresetMap::iterator a, PresetMap::iterator b) { + return a->first.location.size() < b->first.location.size(); + }); + } + + // Else, need to add new location + if (!add) { return m_presets.end(); } + + // Use parent directory + const auto parentPath = std::filesystem::u8path(PathUtil::toAbsolute(path).value()).parent_path().u8string(); + auto newLocation = Location { + std::string{}, // name + std::string{PathUtil::toShortestRelative(parentPath)}, // directory + PresetMetadata::Flag::UserContent // assume unknown directories are user content + }; + + return m_presets.emplace(std::move(newLocation), std::set{}).first; +} + +auto PresetDatabase::presets(std::string_view location) const -> const std::set* +{ + const auto it = m_presets.find(location); + return it != m_presets.end() ? &it->second : nullptr; +} + +auto PresetDatabase::presets(std::string_view location) -> std::set* +{ + const auto it = m_presets.find(location); + return it != m_presets.end() ? &it->second : nullptr; +} + +void PresetDatabase::SetLocations::operator()(const std::vector& locations) const +{ + for (auto& location : locations) + { + m_map->emplace(location, std::set{}); + } +} + +void PresetDatabase::SetLocations::operator()(Location location) const +{ + m_map->emplace(std::move(location), std::set{}); +} + +} // namespace lmms diff --git a/src/core/Song.cpp b/src/core/Song.cpp index 4e6bf6f583d..0e6f43ca7f7 100644 --- a/src/core/Song.cpp +++ b/src/core/Song.cpp @@ -34,6 +34,7 @@ #include "AutomationTrack.h" #include "AutomationEditor.h" +#include "ClapTransport.h" #include "ConfigManager.h" #include "ControllerRackView.h" #include "ControllerConnection.h" @@ -164,6 +165,10 @@ void Song::setTempo() m_vstSyncController.setTempo( tempo ); +#ifdef LMMS_HAVE_CLAP + ClapTransport::setTempo(tempo); +#endif + emit tempoChanged( tempo ); } @@ -179,6 +184,10 @@ void Song::setTimeSignature() m_vstSyncController.setTimeSignature( getTimeSigModel().getNumerator(), getTimeSigModel().getDenominator() ); + +#ifdef LMMS_HAVE_CLAP + ClapTransport::setTimeSignature(getTimeSigModel().getNumerator(), getTimeSigModel().getDenominator()); +#endif } @@ -256,7 +265,17 @@ void Song::processNextBuffer() // Ensure playback begins within the loop if it is enabled if (loopEnabled) { enforceLoop(timeline.loopBegin(), timeline.loopEnd()); } - // Inform VST plugins and sample tracks if the user moved the play head + +#ifdef LMMS_HAVE_CLAP + // If looping is enabled, or we're playing a pattern track or a MIDI clip + // TODO: Looping might not be handled correctly + ClapTransport::setLooping(loopEnabled + || (m_playMode == PlayMode::Pattern) + || (m_playMode == PlayMode::MidiClip && m_loopMidiClip) + ); +#endif + + // Inform VST plugins if the user moved the play head if (getPlayPos().jumped()) { m_vstSyncController.setPlaybackJumped(true); @@ -326,6 +345,11 @@ void Song::processNextBuffer() m_vstSyncController.setAbsolutePosition(getPlayPos().getTicks() + getPlayPos().currentFrame() / static_cast(framesPerTick)); m_vstSyncController.update(); + +#ifdef LMMS_HAVE_CLAP + ClapTransport::setBeatPosition(); + ClapTransport::setTimePosition(getMilliseconds()); +#endif } if (static_cast(frameOffsetInTick) == 0) @@ -498,6 +522,10 @@ void Song::playSong() m_vstSyncController.setPlaybackState( true ); +#ifdef LMMS_HAVE_CLAP + ClapTransport::setPlaying(true); +#endif + savePlayStartPosition(); emit playbackStateChanged(); @@ -537,6 +565,10 @@ void Song::playPattern() m_vstSyncController.setPlaybackState( true ); +#ifdef LMMS_HAVE_CLAP + ClapTransport::setPlaying(true); +#endif + savePlayStartPosition(); emit playbackStateChanged(); @@ -633,6 +665,10 @@ void Song::togglePause() m_vstSyncController.setPlaybackState( m_playing ); +#ifdef LMMS_HAVE_CLAP + ClapTransport::setPlaying(m_playing); +#endif + emit playbackStateChanged(); } @@ -689,6 +725,12 @@ void Song::stop() + getPlayPos().currentFrame() / (double) Engine::framesPerTick() ); +#ifdef LMMS_HAVE_CLAP + ClapTransport::setPlaying(m_exporting); + ClapTransport::setBeatPosition(); + ClapTransport::setTimePosition(getMilliseconds()); +#endif + // remove all note-play-handles that are active Engine::audioEngine()->clear(); @@ -770,6 +812,10 @@ void Song::stopExport() m_exporting = false; m_vstSyncController.setPlaybackState( m_playing ); + +#ifdef LMMS_HAVE_CLAP + ClapTransport::setPlaying(m_playing); +#endif } diff --git a/src/core/Track.cpp b/src/core/Track.cpp index e44475d9374..3fbdb899842 100644 --- a/src/core/Track.cpp +++ b/src/core/Track.cpp @@ -163,7 +163,7 @@ Track * Track::create( const QDomElement & element, TrackContainer * tc ) Track* Track::clone() { // Save track to temporary XML and load it to create a new identical track - QDomDocument doc; + QDomDocument doc{"clonedtrack"}; QDomElement parent = doc.createElement("clonedtrack"); saveState(doc, parent); Track* t = create(parent.firstChild().toElement(), m_trackContainer); diff --git a/src/core/WindowEmbed.cpp b/src/core/WindowEmbed.cpp new file mode 100644 index 00000000000..ae3dba4aa18 --- /dev/null +++ b/src/core/WindowEmbed.cpp @@ -0,0 +1,62 @@ +/* + * WindowEmbed.cpp - Window embedding helper + * + * Copyright (c) 2023 Dalton Messmer + * + * 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 "WindowEmbed.h" + +#include +#include +#include + +#include "lmmsconfig.h" + +namespace lmms +{ + +auto WindowEmbed::availableMethods() -> std::vector +{ + auto methods = std::vector { + Method::Qt, +#ifdef LMMS_BUILD_APPLE + Method::Cocoa, +#endif +#ifdef LMMS_BUILD_WIN32 + Method::Win32, +#endif + }; + +#ifdef LMMS_BUILD_LINUX + if (static_cast(QApplication::instance())->platformName() == "xcb") + { + methods.push_back(Method::XEmbed); + } +#endif + return methods; +} + +auto WindowEmbed::embeddable() -> bool +{ + return !availableMethods().empty(); +} + +} // namespace lmms diff --git a/src/core/clap/ClapAudioPorts.cpp b/src/core/clap/ClapAudioPorts.cpp new file mode 100644 index 00000000000..8e6f884423a --- /dev/null +++ b/src/core/clap/ClapAudioPorts.cpp @@ -0,0 +1,423 @@ +/* + * ClapAudioPorts.cpp - Implements CLAP audio ports extension + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 "ClapAudioPorts.h" + +#ifdef LMMS_HAVE_CLAP + +#include + +#include "ClapInstance.h" +#include "AudioEngine.h" + +namespace lmms +{ + +namespace +{ + template + inline void copyBuffersHostToPlugin(const SampleFrame* hostBuf, float** pluginBuf, + unsigned channel, fpp_t frames) + { + for (fpp_t f = 0; f < frames; ++f) + { + pluginBuf[0][f] = hostBuf[f][channel]; + if constexpr (stereo) { pluginBuf[1][f] = hostBuf[f][channel + 1]; } + } + } + + inline void copyBuffersStereoHostToMonoPlugin(const SampleFrame* hostBuf, float** pluginBuf, + unsigned channel, fpp_t frames) + { + for (fpp_t f = 0; f < frames; ++f) + { + pluginBuf[0][f] = (hostBuf[f][channel] + hostBuf[f][channel + 1]) / 2.0f; + } + } + + template + inline void copyBuffersPluginToHost(float** pluginBuf, SampleFrame* hostBuf, + std::uint64_t constantMask, unsigned channel, fpp_t frames) + { + const bool isLeftConstant = (constantMask & (1 << 0)) != 0; + if constexpr (stereo) + { + const bool isRightConstant = (constantMask & (1 << 1)) != 0; + for (fpp_t f = 0; f < frames; ++f) + { + hostBuf[f][channel] = pluginBuf[0][isLeftConstant ? 0 : f]; + hostBuf[f][channel + 1] = pluginBuf[1][isRightConstant ? 0 : f]; + } + } + else + { + for (fpp_t f = 0; f < frames; ++f) + { + hostBuf[f][channel] = pluginBuf[0][isLeftConstant ? 0 : f]; + } + } + } + + inline void copyBuffersMonoPluginToStereoHost(float** pluginBuf, SampleFrame* hostBuf, + std::uint64_t constantMask, unsigned channel, fpp_t frames) + { + const bool isConstant = (constantMask & (1 << 0)) != 0; + for (fpp_t f = 0; f < frames; ++f) + { + hostBuf[f][channel] = hostBuf[f][channel + 1] = pluginBuf[0][isConstant ? 0 : f]; + } + } +} // namespace + +ClapAudioPorts::ClapAudioPorts(ClapInstance* parent) + : ClapExtension{parent} + , PluginPortConfig{parent->parent()} +{ +} + +ClapAudioPorts::~ClapAudioPorts() +{ + deinit(); +} + +auto ClapAudioPorts::init(clap_process& process) noexcept -> bool +{ + // TODO: Refactor so that regular init() method can be used (Possible lazy extension init issues) + + // NOTE: I'm using this init() method instead of implementing initImpl() because I need the `process` parameter + if (!ClapExtension::init()) + { + logger().log(CLAP_LOG_ERROR, "Plugin does not implement the required audio port extension"); + return false; + } + + // Clear everything just to be safe + m_issues.clear(); + m_audioIn.clear(); + m_audioOut.clear(); + m_audioInActive = m_audioOutActive = nullptr; + m_audioPortsIn.clear(); + m_audioPortsOut.clear(); + m_audioPortInActive = m_audioPortOutActive = nullptr; + m_audioInBuffers.clear(); + m_audioOutBuffers.clear(); + + // Effect, Instrument, and Tool are the only options + const bool needInputPort = instance()->info().type() != Plugin::Type::Instrument; + constexpr bool needOutputPort = true; + + bool hasStereoInput = false; + bool hasStereoOutput = false; + + auto readPorts = [&]( + std::vector& audioPorts, + std::vector& audioBuffers, + std::vector& rawAudioBuffers, + bool isInput) -> AudioPort* + { + if (isInput && !needInputPort) + { + logger().log(CLAP_LOG_DEBUG, "Skipping plugin's audio input ports (not needed)"); + return nullptr; + } + + if (!isInput && !needOutputPort) + { + logger().log(CLAP_LOG_DEBUG, "Skipping plugin's audio output ports (not needed)"); + return nullptr; + } + + const auto portCount = pluginExt()->count(this->plugin(), isInput); + + if (isInput) + { + hasStereoInput = false; // initialize + + //if (portCount == 0 && m_pluginInfo->getType() == Plugin::PluginTypes::Effect) + // m_issues.emplace_back( ... ); + } + else + { + hasStereoOutput = false; // initialize + + if (portCount == 0 && needOutputPort) + { + m_issues.emplace_back(PluginIssueType::NoOutputChannel); + } + //if (portCount > 2) + // m_issues.emplace_back(PluginIssueType::tooManyOutputChannels, std::to_string(outCount)); + } + +#if 0 + { + std::string msg = (isInput ? "Input ports: " : "Output ports: ") + std::to_string(portCount); + logger()->log(CLAP_LOG_DEBUG, msg); + } +#endif + + clap_id monoPort = CLAP_INVALID_ID; + clap_id stereoPort = CLAP_INVALID_ID; + //clap_id mainPort = CLAP_INVALID_ID; + for (std::uint32_t idx = 0; idx < portCount; ++idx) + { + auto info = clap_audio_port_info{}; + info.id = CLAP_INVALID_ID; + info.in_place_pair = CLAP_INVALID_ID; + if (!pluginExt()->get(this->plugin(), idx, isInput, &info)) + { + logger().log(CLAP_LOG_ERROR, "Unknown error calling clap_plugin_audio_ports.get()"); + m_issues.emplace_back(PluginIssueType::PortHasNoDef); + return nullptr; + } + + if (idx == 0 && !(info.flags & CLAP_AUDIO_PORT_IS_MAIN)) + { + logger().log(CLAP_LOG_DEBUG, "Plugin audio port #0 is not main"); + } + + //if (info.flags & CLAP_AUDIO_PORT_IS_MAIN) + // mainPort = idx; + + auto type = PluginPortConfig::PortType::None; + if (info.port_type) + { + auto portType = std::string_view{info.port_type}; + if (portType == CLAP_PORT_MONO) + { + assert(info.channel_count == 1); + type = PluginPortConfig::PortType::Mono; + if (monoPort == CLAP_INVALID_ID) { monoPort = idx; } + } + + if (portType == CLAP_PORT_STEREO) + { + assert(info.channel_count == 2); + type = PluginPortConfig::PortType::Stereo; + if (stereoPort == CLAP_INVALID_ID) { stereoPort = idx; } + } + } + else + { + if (info.channel_count == 1) + { + type = PluginPortConfig::PortType::Mono; + if (monoPort == CLAP_INVALID_ID) { monoPort = idx; } + } + else if (info.channel_count == 2) + { + type = PluginPortConfig::PortType::Stereo; + if (stereoPort == CLAP_INVALID_ID) { stereoPort = idx; } + } + } + +#if 0 + { + std::string msg = "---audio port---\nid: "; + msg += std::to_string(info.id); + msg += "\nname: "; + msg += info.name; + msg += "\nflags: "; + msg += std::to_string(info.flags); + msg += "\nchannel count: "; + msg += std::to_string(info.channel_count); + msg += "\ntype: "; + msg += (info.port_type ? info.port_type : "(null)"); + msg += "\nin place pair: "; + msg += std::to_string(info.in_place_pair); + logger()->log(CLAP_LOG_DEBUG, msg); + } +#endif + + audioPorts.emplace_back(AudioPort{info, idx, isInput, type, false}); + } + + assert(portCount == audioPorts.size()); + audioBuffers.reserve(audioPorts.size()); + for (std::uint32_t port = 0; port < audioPorts.size(); ++port) + { + auto& buffer = audioBuffers.emplace_back(); + + const auto channelCount = audioPorts[port].info.channel_count; + if (channelCount <= 0) + { + logger().log(CLAP_LOG_WARNING, "Audio port channel count is zero"); + //return nullptr; + } + + buffer.channel_count = channelCount; + if (isInput && port != monoPort && (port != stereoPort || stereoPort == CLAP_INVALID_ID)) + { + // This input port will not be used by LMMS + // TODO: Will a mono port ever need to be used if a stereo port is available? + buffer.constant_mask = static_cast(-1); + } + else + { + buffer.constant_mask = 0; + } + + auto& rawBuffer = rawAudioBuffers.emplace_back(channelCount, DEFAULT_BUFFER_SIZE); + buffer.data32 = rawBuffer.data(); + buffer.data64 = nullptr; // Not supported by LMMS + buffer.latency = 0; // TODO: latency extension + } + + if (stereoPort != CLAP_INVALID_ID) + { + if (isInput) { hasStereoInput = true; } else { hasStereoOutput = true; } + auto port = &audioPorts[stereoPort]; + port->used = true; + return port; + } + + if (monoPort != CLAP_INVALID_ID) + { + auto port = &audioPorts[monoPort]; + port->used = true; + return port; + } + + // Missing a required port type that LMMS supports - i.e. an effect where the only input is surround sound + { + std::string msg = std::string{isInput ? "An input" : "An output"} + + " audio port is required, but plugin has none that are usable"; + logger().log(CLAP_LOG_ERROR, msg); + } + m_issues.emplace_back(PluginIssueType::UnknownPortType); // TODO: Add better entry to PluginIssueType + return nullptr; + }; + + m_audioPortInActive = readPorts(m_audioPortsIn, m_audioIn, m_audioInBuffers, true); + if (!m_issues.empty() || (!m_audioPortInActive && needInputPort)) + { + return false; + } + + process.audio_inputs = m_audioIn.data(); + process.audio_inputs_count = m_audioPortsIn.size(); + m_audioInActive = m_audioPortInActive ? &m_audioIn[m_audioPortInActive->index] : nullptr; + + m_audioPortOutActive = readPorts(m_audioPortsOut, m_audioOut, m_audioOutBuffers, false); + if (!m_issues.empty() || (!m_audioPortOutActive && needOutputPort)) + { + return false; + } + + process.audio_outputs = m_audioOut.data(); + process.audio_outputs_count = m_audioPortsOut.size(); + m_audioOutActive = m_audioPortOutActive ? &m_audioOut[m_audioPortOutActive->index] : nullptr; + + if (needInputPort) + { + if (!hasStereoInput && m_audioPortInActive->type != PluginPortConfig::PortType::Mono) + { + return false; + } + if (hasStereoInput && m_audioPortInActive->type != PluginPortConfig::PortType::Stereo) + { + return false; + } + } + + if (needOutputPort) + { + if (!hasStereoOutput && m_audioPortOutActive->type != PluginPortConfig::PortType::Mono) + { + return false; + } + if (hasStereoOutput && m_audioPortOutActive->type != PluginPortConfig::PortType::Stereo) + { + return false; + } + } + + setPortType( + m_audioPortInActive ? m_audioPortInActive->type : PluginPortConfig::PortType::None, + m_audioPortOutActive ? m_audioPortOutActive->type : PluginPortConfig::PortType::None); + + return true; +} + +auto ClapAudioPorts::checkSupported(const clap_plugin_audio_ports& ext) -> bool +{ + return ext.count && ext.get; +} + +void ClapAudioPorts::copyBuffersFromCore(const SampleFrame* buffer, fpp_t frames) +{ + switch (portConfig()) + { + case PluginPortConfig::Config::None: + break; + case PluginPortConfig::Config::Stereo: + // LMMS stereo to CLAP stereo input + copyBuffersHostToPlugin(buffer, m_audioInActive->data32, 0, frames); + break; + case PluginPortConfig::Config::MonoMix: + // LMMS stereo downmix to mono for CLAP mono input + copyBuffersStereoHostToMonoPlugin(buffer, m_audioInActive->data32, 0, frames); + break; + case PluginPortConfig::Config::LeftOnly: + // LMMS left channel to CLAP mono input; LMMS right channel bypassed + copyBuffersHostToPlugin(buffer, m_audioInActive->data32, 0, frames); + break; + case PluginPortConfig::Config::RightOnly: + // LMMS right channel to CLAP mono input; LMMS left channel bypassed + copyBuffersHostToPlugin(buffer, m_audioInActive->data32, 1, frames); + break; + } +} + +void ClapAudioPorts::copyBuffersToCore(SampleFrame* buffer, fpp_t frames) const +{ + switch (portConfig()) + { + case PluginPortConfig::Config::None: + break; + case PluginPortConfig::Config::Stereo: + // CLAP stereo output to LMMS stereo + copyBuffersPluginToHost(m_audioOutActive->data32, buffer, + m_audioOutActive->constant_mask, 0, frames); + break; + case PluginPortConfig::Config::MonoMix: + // CLAP mono output upmix to stereo for LMMS stereo + copyBuffersMonoPluginToStereoHost(m_audioOutActive->data32, buffer, + m_audioOutActive->constant_mask, 0, frames); + break; + case PluginPortConfig::Config::LeftOnly: + // CLAP mono output to LMMS left channel + copyBuffersPluginToHost(m_audioOutActive->data32, buffer, + m_audioOutActive->constant_mask, 0, frames); + break; + case PluginPortConfig::Config::RightOnly: + // CLAP mono output to LMMS right channel + copyBuffersPluginToHost(m_audioOutActive->data32, buffer, + m_audioOutActive->constant_mask, 1, frames); + break; + } +} + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP diff --git a/src/core/clap/ClapExtension.cpp b/src/core/clap/ClapExtension.cpp new file mode 100644 index 00000000000..35eb2544c95 --- /dev/null +++ b/src/core/clap/ClapExtension.cpp @@ -0,0 +1,77 @@ +/* + * ClapExtension.cpp - Base class templates for implementing CLAP extensions + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 "ClapExtension.h" + +#ifdef LMMS_HAVE_CLAP + +#include "ClapInstance.h" +#include "ClapLog.h" + +namespace lmms +{ + +auto detail::ClapExtensionHelper::host() const -> const clap_host* +{ + return m_instance->host(); +} + +auto detail::ClapExtensionHelper::plugin() const -> const clap_plugin* +{ + return m_instance->plugin(); +} + +auto detail::ClapExtensionHelper::logger() const -> const ClapLog& +{ + return instance()->logger(); +} + +auto detail::ClapExtensionHelper::fromHost(const clap_host* host) -> ClapInstance* +{ + if (!host) + { + ClapLog::globalLog(CLAP_LOG_ERROR, "A plugin passed an invalid host pointer"); + return nullptr; + } + + auto h = static_cast(host->host_data); + if (!h) + { + ClapLog::globalLog(CLAP_LOG_ERROR, "A plugin invalidated the host context pointer"); + return nullptr; + } + + if (!h->plugin()) + { + ClapLog::globalLog(CLAP_LOG_ERROR, "A plugin is calling the host API during factory.create_plugin(). " + "It needs to wait for plugin.init() first."); + return nullptr; + } + + return h; +} + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP diff --git a/src/core/clap/ClapFile.cpp b/src/core/clap/ClapFile.cpp new file mode 100644 index 00000000000..0e0e0d87cd5 --- /dev/null +++ b/src/core/clap/ClapFile.cpp @@ -0,0 +1,127 @@ +/* + * ClapFile.cpp - Implementation of ClapFile class + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 "ClapFile.h" + +#ifdef LMMS_HAVE_CLAP + +#include "ClapLog.h" +#include "ClapManager.h" + +namespace lmms +{ + +ClapFile::ClapFile(std::filesystem::path filename) + : m_filename{std::move(filename.make_preferred())} +{ +} + +ClapFile::~ClapFile() +{ + unload(); +} + +auto ClapFile::load() -> bool +{ + // Do not allow reloading yet + if (m_library && m_library->isLoaded()) { return false; } + + // TODO: Replace QLibrary with in-house non-Qt alternative + const auto file = filename().u8string(); + m_library = std::make_unique(QString::fromUtf8(file.c_str(), file.size())); + if (!m_library->load()) + { + ClapLog::globalLog(CLAP_LOG_ERROR, m_library->errorString().toStdString()); + return false; + } + + m_entry.reset(reinterpret_cast(m_library->resolve("clap_entry"))); + if (!m_entry) + { + std::string msg = "Unable to resolve entry point in '" + filename().string() + "'"; + ClapLog::globalLog(CLAP_LOG_ERROR, msg); + m_library->unload(); + return false; + } + + if (!m_entry->init(file.c_str())) + { + std::string msg = "Failed to initialize '" + filename().string() + "'"; + ClapLog::globalLog(CLAP_LOG_ERROR, msg); + m_entry = nullptr; // Prevent deinit() from being called + return false; + } + + m_factory = static_cast(m_entry->get_factory(CLAP_PLUGIN_FACTORY_ID)); + if (!m_factory) + { + std::string msg = "Failed to retrieve plugin factory from '" + filename().string() + "'"; + ClapLog::globalLog(CLAP_LOG_ERROR, msg); + return false; + } + + m_pluginCount = m_factory->get_plugin_count(m_factory); + if (m_pluginCount == 0) + { + std::string msg = "Plugin file '" + filename().string() + "' contains no plugins"; + ClapLog::globalLog(CLAP_LOG_DEBUG, msg); + return false; + } + + m_pluginInfo.clear(); + for (std::uint32_t idx = 0; idx < m_pluginCount; ++idx) + { + if (auto plugin = ClapPluginInfo::create(*m_factory, idx)) + { + m_pluginInfo.emplace_back(std::move(plugin)); + } + } + + m_presetDatabase = std::make_unique(); + m_presetDatabase->init(m_entry.get()); + + return true; +} + +void ClapFile::unload() noexcept +{ + // NOTE: Need to destroy any plugin instances from this .clap file before + // calling this method. This should be okay as long as the ClapManager + // singleton is destroyed after any Instrument/Effect objects. + // TODO: Enforce this? + + m_presetDatabase.reset(); + + m_entry.reset(); + + if (m_library) + { + m_library->unload(); + m_library = nullptr; + } +} + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP diff --git a/src/core/clap/ClapGui.cpp b/src/core/clap/ClapGui.cpp new file mode 100644 index 00000000000..aa1166a98c7 --- /dev/null +++ b/src/core/clap/ClapGui.cpp @@ -0,0 +1,233 @@ +/* + * ClapGui.cpp - Implements CLAP gui extension + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 "ClapGui.h" + +#ifdef LMMS_HAVE_CLAP + +#include + +#include "ClapInstance.h" +#include "GuiApplication.h" +#include "MainWindow.h" +#include "SubWindow.h" + +namespace lmms +{ + +ClapGui::ClapGui(ClapInstance* instance) + : ClapExtension{instance} +{ + const auto windowId = gui::getGUI()->mainWindow()->winId(); + +#if defined(LMMS_BUILD_WIN32) + m_embedMethod = WindowEmbed::Method::Win32; + m_window.api = CLAP_WINDOW_API_WIN32; + m_window.win32 = reinterpret_cast(windowId); +#elif defined(LMMS_BUILD_APPLE) + m_embedMethod = WindowEmbed::Method::Cocoa; + m_window.api = CLAP_WINDOW_API_COCOA; + m_window.cocoa = reinterpret_cast(windowId); +#elif defined(LMMS_BUILD_LINUX) + m_embedMethod = WindowEmbed::Method::XEmbed; + m_window.api = CLAP_WINDOW_API_X11; + m_window.x11 = windowId; +#else + instance->log(CLAP_LOG_ERROR, "The host does not implement the CLAP gui extension for this platform"); + m_window.api = nullptr; +#endif +} + +auto ClapGui::initImpl() noexcept -> bool +{ + if (!m_window.api) { return false; } // unsupported host platform + + m_supportsEmbed = windowSupported(*pluginExt(), false) + && pluginExt()->is_api_supported(plugin(), m_window.api, false); + + m_supportsFloating = windowSupported(*pluginExt(), true) + && pluginExt()->is_api_supported(plugin(), m_window.api, true); + + if (!m_supportsEmbed) + { + if (!m_supportsFloating) + { + logger().log(CLAP_LOG_ERROR, "Plugin does not support any GUI API that the host implements"); + return false; + } + + // No choice but to use floating windows + m_embedMethod = WindowEmbed::Method::Floating; + } + + return true; +} + +void ClapGui::deinitImpl() noexcept +{ + destroy(); +} + +auto ClapGui::hostExtImpl() const -> const clap_host_gui* +{ + static clap_host_gui ext { + &clapResizeHintsChanged, + &clapRequestResize, + &clapRequestShow, + &clapRequestHide, + &clapRequestClosed + }; + return &ext; +} + +auto ClapGui::checkSupported(const clap_plugin_gui& ext) -> bool +{ + return ext.is_api_supported && ext.get_preferred_api && ext.create && ext.destroy + && ext.set_scale && ext.get_size && ext.show && ext.hide + && (windowSupported(ext, true) || windowSupported(ext, false)); +} + +auto ClapGui::windowSupported(const clap_plugin_gui& ext, bool floating) -> bool +{ + // NOTE: This method only checks if the needed API functions are implemented. + // Still need to call clap_plugin_gui.is_api_supported() + + if (floating) + { + // Needed for floating windows + return ext.set_transient && ext.suggest_title; + } + else + { + // Needed for embedded windows + return ext.can_resize && ext.get_resize_hints && ext.adjust_size && ext.set_size && ext.set_parent; + } +} + +auto ClapGui::create() -> bool +{ + assert(supported()); + destroy(); + + if (!pluginExt()->create(plugin(), m_window.api, isFloating())) + { + logger().log(CLAP_LOG_ERROR, "Failed to create the plugin GUI"); + return false; + } + + m_created = true; + assert(m_visible == false); + + if (isFloating()) + { + pluginExt()->set_transient(plugin(), &m_window); + if (const auto name = instance()->info().descriptor().name; name && name[0] != '\0') + { + pluginExt()->suggest_title(plugin(), name); + } + } + else + { + std::uint32_t width = 0; + std::uint32_t height = 0; + + if (!pluginExt()->get_size(plugin(), &width, &height)) + { + logger().log(CLAP_LOG_PLUGIN_MISBEHAVING, "Could not get the size of the plugin gui"); + m_created = false; + pluginExt()->destroy(plugin()); + return false; + } + + //mainWindow()->resizePluginView(width, height); + + if (!pluginExt()->set_parent(plugin(), &m_window)) + { + logger().log(CLAP_LOG_ERROR, "Failed to embed the plugin GUI"); + m_created = false; + pluginExt()->destroy(plugin()); + return false; + } + } + + return true; +} + +void ClapGui::destroy() +{ + if (supported() && m_created) + { + pluginExt()->destroy(plugin()); + } + + m_created = false; + m_visible = false; +} + +void ClapGui::clapResizeHintsChanged(const clap_host* host) +{ + ClapLog::globalLog(CLAP_LOG_ERROR, "ClapGui::clapResizeHintsChanged() [NOT IMPLEMENTED YET]"); + // TODO +} + +auto ClapGui::clapRequestResize(const clap_host* host, std::uint32_t width, std::uint32_t height) -> bool +{ + ClapLog::globalLog(CLAP_LOG_ERROR, "ClapGui::clapRequestResize() [NOT IMPLEMENTED YET]"); + // TODO + + return true; +} + +auto ClapGui::clapRequestShow(const clap_host* host) -> bool +{ + ClapLog::globalLog(CLAP_LOG_ERROR, "ClapGui::clapRequestShow() [NOT IMPLEMENTED YET]"); + // TODO + + return true; +} + +auto ClapGui::clapRequestHide(const clap_host* host) -> bool +{ + ClapLog::globalLog(CLAP_LOG_ERROR, "ClapGui::clapRequestHide() [NOT IMPLEMENTED YET]"); + // TODO + + return true; +} + +void ClapGui::clapRequestClosed(const clap_host* host, bool wasDestroyed) +{ + if (!wasDestroyed) { return; } + + auto h = fromHost(host); + if (!h) { return; } + auto& gui = h->gui(); + + gui.pluginExt()->destroy(gui.plugin()); + gui.m_created = false; + gui.m_visible = false; +} + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP diff --git a/src/core/clap/ClapInstance.cpp b/src/core/clap/ClapInstance.cpp new file mode 100644 index 00000000000..dcd3c12a947 --- /dev/null +++ b/src/core/clap/ClapInstance.cpp @@ -0,0 +1,812 @@ +/* + * ClapInstance.cpp - Implementation of ClapInstance class + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 "ClapInstance.h" + +#ifdef LMMS_HAVE_CLAP + +#include +#include +#include +#include +#include +#include + +#include "AudioEngine.h" +#include "ClapManager.h" +#include "ClapTransport.h" +#include "Engine.h" +#include "lmmsversion.h" +#include "MidiEvent.h" + +namespace lmms +{ + +//! Container for everything required to store MIDI events going to the plugin +// TODO: Move to MidiEvent.h for both LV2 and CLAP? +struct MidiInputEvent +{ + MidiEvent ev; + TimePos time; + f_cnt_t offset; +}; + +auto ClapInstance::create(const std::string& pluginId, Model* parent) -> std::unique_ptr +{ + // CLAP API requires main thread for plugin loading + assert(ClapThreadCheck::isMainThread()); + + const auto manager = Engine::getClapManager(); + const auto info = manager->pluginInfo(pluginId); + if (!info) + { + std::string msg = "No plugin found for ID \"" + pluginId + "\""; + ClapLog::globalLog(CLAP_LOG_ERROR, msg); + return nullptr; + } + + ClapTransport::update(); + + ClapLog::globalLog(CLAP_LOG_DEBUG, "Creating CLAP instance"); + + auto instance = std::make_unique(Access{}, *info, parent); + if (!instance->start()) + { + ClapLog::globalLog(CLAP_LOG_ERROR, "Failed instantiating CLAP instance"); + return nullptr; + } + + return instance; +} + +ClapInstance::ClapInstance(Access, const ClapPluginInfo& pluginInfo, Model* parent) + : QObject{parent} + , m_pluginInfo{pluginInfo} + , m_host { + CLAP_VERSION, // clap_version + this, // host_data // NOTE: Need to update if class is copied/moved + "LMMS", // name + "LMMS contributors", // vendor + "https://lmms.io/", // url + LMMS_VERSION, // version + &clapGetExtension, // get_extension + &clapRequestCallback, // request_callback + &clapRequestProcess, // request_process + &clapRequestRestart // request_restart + } + , m_midiInputBuf{s_maxMidiInputEvents} + , m_midiInputReader{m_midiInputBuf} + , m_params{parent, this, &m_evIn, &m_evOut} + , m_presetLoader{parent, this} +{ + m_process.steady_time = 0; + m_process.transport = ClapTransport::get(); +} + +ClapInstance::~ClapInstance() +{ +#if 0 + logger().log(CLAP_LOG_DEBUG, "ClapInstance::~ClapInstance"); +#endif + + destroy(); +} + +void ClapInstance::copyModelsFromCore() +{ + // TODO: Handle parameter events similar to midi input? (with ringbuffer) + + if (!hasNoteInput()) { return; } + + // TODO: Midi events and parameter events may not be ordered according to increasing sample offset + // The event.header.time values need to be sorted in increasing order. + + while (m_midiInputReader.read_space() > 0) + { + const auto [event, ignore, offset] = m_midiInputReader.read(1)[0]; + (void)ignore; + switch (event.type()) + { + case MidiNoteOff: + processNote(offset, event.channel(), event.key(), event.velocity(), false); + break; + case MidiNoteOn: + processNote(offset, event.channel(), event.key(), event.velocity(), true); + break; + case MidiKeyPressure: + processKeyPressure(offset, event.channel(), event.key(), event.velocity()); + break; + default: + break; + } + } +} + +void ClapInstance::copyModelsToCore() +{ + //m_params.rescan(CLAP_PARAM_RESCAN_VALUES); +} + +void ClapInstance::run(fpp_t frames) +{ + processBegin(frames); + process(frames); + processEnd(frames); +} + +void ClapInstance::handleMidiInputEvent(const MidiEvent& event, const TimePos& time, f_cnt_t offset) +{ + // TODO: Use MidiInputEvent from LV2 code, moved to common location? + + if (!hasNoteInput()) { return; } + + const auto ev = MidiInputEvent{event, time, offset}; + + // Acquire lock and spin + while (m_ringLock.test_and_set(std::memory_order_acquire)) {} + + const auto written = m_midiInputBuf.write(&ev, 1); + + m_ringLock.clear(std::memory_order_release); + + if (written != 1) + { + logger().log(CLAP_LOG_WARNING, "MIDI ringbuffer is too small! Discarding MIDI event."); + } +} + +auto ClapInstance::controlCount() const -> std::size_t +{ + return m_params.automatableCount(); // TODO: + 1 control if port config != Stereo? +} + +auto ClapInstance::hasNoteInput() const -> bool +{ + return m_notePorts.hasInput(); +} + +void ClapInstance::saveSettings(QDomDocument& doc, QDomElement& elem) +{ + elem.setAttribute("version", "0"); + + audioPorts().saveSettings(doc, elem); + params().saveParamConnections(doc, elem); + + // The CLAP standard strongly recommends using the state extension + // instead of manually saving parameter values + if (!state().supported()) { return; } + + // TODO: Integrate save/load context into LMMS better + const auto context = elem.ownerDocument().doctype().name() == "clonedtrack" + ? ClapState::Context::Duplicate + : ClapState::Context::Project; + + const auto savedState = state().save(context).value_or(""); + elem.setAttribute("state", QString::fromUtf8(savedState.data(), savedState.size())); +} + +void ClapInstance::loadSettings(const QDomElement& elem) +{ + [[maybe_unused]] const auto version = elem.attribute("version", "0").toInt(); + + audioPorts().loadSettings(elem); + params().loadParamConnections(elem); + + // The CLAP standard strongly recommends using the state extension + // instead of manually saving parameter values + if (!state().supported()) { return; } + + // TODO: Integrate save/load context into LMMS better + const auto context = elem.ownerDocument().doctype().name() == "clonedtrack" + ? ClapState::Context::Duplicate + : ClapState::Context::Project; + + const auto savedState = elem.attribute("state", "").toStdString(); + if (!state().load(savedState, context)) { return; } + + // Parameters may have changed in the plugin; + // Those values need to be reflected in host + params().rescan(CLAP_PARAM_RESCAN_VALUES); // TODO: Is this correct? +} + +void ClapInstance::destroy() +{ + //idle(); // TODO: ??? May throw an exception, which should not happen in ClapInstance dtor + + unload(); +} + +auto ClapInstance::isValid() const -> bool +{ + return m_plugin != nullptr && !isErrorState(); +} + +auto ClapInstance::start() -> bool +{ + if (!load()) { return false; } + if (!init()) { return false; } + return activate(); +} + +auto ClapInstance::restart() -> bool +{ +#if 0 + { + std::string msg = "Restarting plugin instance: " + std::string{m_pluginInfo->descriptor()->name}; + logger().log(CLAP_LOG_INFO, msg); + } +#endif + + if (!deactivate()) { return false; } + return activate(); +} + +auto ClapInstance::load() -> bool +{ +#if 0 + { + std::string msg = "Loading plugin instance: " + std::string{m_pluginInfo->descriptor()->name}; + logger().log(CLAP_LOG_INFO, msg); + } +#endif + + assert(isCurrentStateValid(PluginState::None)); + + // Create plugin instance, destroying any previous plugin instance first + const auto& factory = info().factory(); + m_plugin = factory.create_plugin(&factory, host(), info().descriptor().id); + if (!m_plugin) + { + logger().log(CLAP_LOG_ERROR, "Failed to create plugin instance"); + // TODO: Set state to NoneWithError? + return false; + } + + setPluginState(PluginState::Loaded); + return true; +} + +auto ClapInstance::unload() -> bool +{ +#if 0 + { + std::string msg = "Unloading plugin instance: " + std::string{m_pluginInfo->descriptor()->name}; + logger().log(CLAP_LOG_INFO, msg); + } +#endif + + assert(ClapThreadCheck::isMainThread()); + + // Deinitialize extensions + m_audioPorts.deinit(); + //m_gui.deinit(); + m_log.deinit(); + m_notePorts.deinit(); + m_params.deinit(); + m_presetLoader.deinit(); + m_state.deinit(); + m_timerSupport.deinit(); + + deactivate(); + + if (m_plugin) + { + m_plugin->destroy(m_plugin); + m_plugin = nullptr; + } + + setPluginState(PluginState::None); + return true; +} + +auto ClapInstance::init() -> bool +{ + assert(ClapThreadCheck::isMainThread()); + + if (isErrorState()) { return false; } + assert(isCurrentStateValid(PluginState::Loaded)); + + if (m_pluginState != PluginState::Loaded) { return false; } + + m_audioPorts.beginPluginInit(); + m_gui.beginPluginInit(); + m_log.beginPluginInit(); + m_notePorts.beginPluginInit(); + m_params.beginPluginInit(); + m_presetLoader.beginPluginInit(); + m_state.beginPluginInit(); + m_threadCheck.beginPluginInit(); + m_timerSupport.beginPluginInit(); + + const bool success = m_plugin->init(m_plugin); + + m_audioPorts.endPluginInit(); + m_gui.endPluginInit(); + m_log.endPluginInit(); + m_notePorts.endPluginInit(); + m_params.endPluginInit(); + m_presetLoader.endPluginInit(); + m_state.endPluginInit(); + m_threadCheck.endPluginInit(); + m_timerSupport.endPluginInit(); + + if (!success) + { + { + std::string msg = "Could not init the plugin with id: " + std::string{info().descriptor().id}; + logger().log(CLAP_LOG_ERROR, msg); + } + setPluginState(PluginState::LoadedWithError); + m_plugin->destroy(m_plugin); + m_plugin = nullptr; + return false; + } + + if (!m_audioPorts.init(m_process)) + { + setPluginState(PluginState::LoadedWithError); + return false; + } + + // TODO: What if this is the 2nd instance of a mono plugin? + //m_gui.init(); + + m_notePorts.init(); + if (!hasNoteInput() && info().type() == Plugin::Type::Instrument) + { + logger().log(CLAP_LOG_WARNING, "Plugin is instrument but doesn't implement note ports extension"); + } + + if (!m_params.init()) + { + logger().log(CLAP_LOG_DEBUG, "Plugin does not support params extension"); + } + + m_presetLoader.init(); + m_state.init(); + m_timerSupport.init(); + + setPluginState(PluginState::Inactive); + + return true; +} + +auto ClapInstance::activate() -> bool +{ + assert(ClapThreadCheck::isMainThread()); + + if (isErrorState()) { return false; } + assert(isCurrentStateValid(PluginState::Inactive)); + + const auto sampleRate = static_cast(Engine::audioEngine()->outputSampleRate()); + static_assert(DEFAULT_BUFFER_SIZE > MINIMUM_BUFFER_SIZE); + + assert(!isActive()); + if (!m_plugin->activate(m_plugin, sampleRate, MINIMUM_BUFFER_SIZE, DEFAULT_BUFFER_SIZE)) + { + setPluginState(PluginState::InactiveWithError); + return false; + } + + m_scheduleProcess = true; + setPluginState(PluginState::ActiveAndSleeping); + return true; +} + +auto ClapInstance::deactivate() -> bool +{ + // NOTE: This method assumes that process() cannot be called concurrently + + assert(ClapThreadCheck::isMainThread()); + if (!isActive()) { return false; } + + // TODO: m_timerSupport.killTimers()? + + // stop_processing() needs to be called on the audio thread, + // but the main thread will hang if I try to use m_scheduleDeactivate + // and poll until the process() event is called - it never seems to be called + // TODO: Could try spoofing the thread check extension instead of + // creating a thread here, but maybe that wouldn't be safe + auto thread = std::thread{[this] { + if (m_pluginState == PluginState::ActiveAndProcessing) + { + m_plugin->stop_processing(m_plugin); + } + setPluginState(PluginState::ActiveAndReadyToDeactivate); + }}; + thread.join(); + + m_plugin->deactivate(m_plugin); + setPluginState(PluginState::Inactive); + + return true; +} + +auto ClapInstance::processBegin(std::uint32_t frames) -> bool +{ + m_process.frames_count = frames; + return false; +} + +void ClapInstance::processNote(f_cnt_t offset, std::int8_t channel, std::int16_t key, std::uint8_t velocity, bool isOn) +{ + assert(ClapThreadCheck::isAudioThread()); + assert(channel >= 0 && channel <= 15); + assert(key >= 0 && key <= 127); + assert(velocity >= 0 && velocity <= 127); + + // NOTE: I've read that Bitwig Studio always sends CLAP dialect note events regardless of plugin's note dialect + switch (CLAP_NOTE_DIALECT_CLAP) + { + case CLAP_NOTE_DIALECT_CLAP: + { + clap_event_note ev; + ev.header.space_id = CLAP_CORE_EVENT_SPACE_ID; + ev.header.type = isOn ? CLAP_EVENT_NOTE_ON : CLAP_EVENT_NOTE_OFF; + ev.header.time = static_cast(offset); + ev.header.flags = 0; + ev.header.size = sizeof(ev); + ev.note_id = -1; // TODO + ev.port_index = static_cast(m_notePorts.portIndex()); + ev.channel = channel; + ev.key = key; + ev.velocity = velocity / 127.0; + + m_evIn.push(&ev.header); + break; + } + case CLAP_NOTE_DIALECT_MIDI: + { + clap_event_midi ev; + ev.header.space_id = CLAP_CORE_EVENT_SPACE_ID; + ev.header.type = CLAP_EVENT_MIDI; + ev.header.time = static_cast(offset); + ev.header.flags = 0; + ev.header.size = sizeof(ev); + ev.port_index = m_notePorts.portIndex(); + ev.data[0] = static_cast((isOn ? 0x90 : 0x80) | (channel & 0x0F)); + ev.data[1] = static_cast(key); + ev.data[2] = static_cast(velocity / 127.0); + + m_evIn.push(&ev.header); + break; + } + default: + assert(false); + break; + } +} + +void ClapInstance::processKeyPressure(f_cnt_t offset, std::int8_t channel, std::int16_t key, std::uint8_t pressure) +{ + assert(ClapThreadCheck::isAudioThread()); + + switch (m_notePorts.dialect()) + { + case CLAP_NOTE_DIALECT_CLAP: + { + clap_event_note_expression ev; + ev.header.space_id = CLAP_CORE_EVENT_SPACE_ID; + ev.header.type = CLAP_EVENT_NOTE_EXPRESSION; + ev.header.time = static_cast(offset); + ev.header.flags = 0; + ev.header.size = sizeof(ev); + ev.expression_id = CLAP_NOTE_EXPRESSION_VOLUME; + ev.note_id = 0; // TODO + ev.port_index = static_cast(m_notePorts.portIndex()); + ev.channel = channel; + ev.key = key; + ev.value = pressure / 32.0; // 0..127 --> 0..4 + + m_evIn.push(&ev.header); + break; + } + case CLAP_NOTE_DIALECT_MIDI: + { + clap_event_midi ev; + ev.header.space_id = CLAP_CORE_EVENT_SPACE_ID; + ev.header.type = CLAP_EVENT_MIDI; + ev.header.time = static_cast(offset); + ev.header.flags = 0; + ev.header.size = sizeof(ev); + ev.port_index = m_notePorts.portIndex(); + ev.data[0] = static_cast(MidiEventTypes::MidiKeyPressure | channel); + ev.data[1] = static_cast(key); + ev.data[2] = static_cast(pressure / 127.0); + + m_evIn.push(&ev.header); + break; + } + default: + assert(false); + break; + } +} + +auto ClapInstance::process(std::uint32_t frames) -> bool +{ + assert(ClapThreadCheck::isAudioThread()); + if (!m_plugin) { return false; } + + // Can't process a plugin that is not active + if (!isActive()) { return false; } + + // Do we want to deactivate the plugin? + if (m_scheduleDeactivate) + { + m_scheduleDeactivate = false; + if (m_pluginState == PluginState::ActiveAndProcessing) + { + m_plugin->stop_processing(m_plugin); + } + setPluginState(PluginState::ActiveAndReadyToDeactivate); + return true; + } + + // We can't process a plugin which failed to start processing + if (m_pluginState == PluginState::ActiveWithError) { return false; } + + m_process.in_events = m_evIn.clapInputEvents(); + m_process.out_events = m_evOut.clapOutputEvents(); + + m_evOut.clear(); + m_params.generatePluginInputEvents(); + + if (isSleeping()) + { + if (!m_scheduleProcess && m_evIn.empty()) + { + // The plugin is sleeping, there is no request to wake it up + // and there are no events to process + return true; + } + + m_scheduleProcess = false; + if (!m_plugin->start_processing(m_plugin)) + { + // The plugin failed to start processing + setPluginState(PluginState::ActiveWithError); + return false; + } + + setPluginState(PluginState::ActiveAndProcessing); + } + + [[maybe_unused]] int32_t status = CLAP_PROCESS_SLEEP; + if (isProcessing()) + { + status = m_plugin->process(m_plugin, &m_process); + } + + m_params.handlePluginOutputEvents(); + + m_evOut.clear(); + m_evIn.clear(); + + m_params.processEnd(); + + // TODO: send plugin to sleep if possible + + return true; +} + +auto ClapInstance::processEnd(std::uint32_t frames) -> bool +{ + m_process.frames_count = frames; + m_process.steady_time += frames; + return false; +} + +auto ClapInstance::isActive() const -> bool +{ + switch (m_pluginState) + { + case PluginState::ActiveAndSleeping: [[fallthrough]]; + case PluginState::ActiveWithError: [[fallthrough]]; + case PluginState::ActiveAndProcessing: [[fallthrough]]; + case PluginState::ActiveAndReadyToDeactivate: + return true; + default: + return false; + } +} + +auto ClapInstance::isProcessing() const -> bool +{ + return m_pluginState == PluginState::ActiveAndProcessing; +} + +auto ClapInstance::isSleeping() const -> bool +{ + return m_pluginState == PluginState::ActiveAndSleeping; +} + +auto ClapInstance::isErrorState() const -> bool +{ + return m_pluginState == PluginState::None + || m_pluginState == PluginState::LoadedWithError + || m_pluginState == PluginState::InactiveWithError + || m_pluginState == PluginState::ActiveWithError; +} + +void ClapInstance::setPluginState(PluginState state) +{ + // Assert that it's okay to transition to the desired next state from the current state + assert(isNextStateValid(state) && "Invalid state transition"); + + m_pluginState = state; + if (!ClapManager::debugging()) { return; } + + switch (state) + { + case PluginState::None: + logger().log(CLAP_LOG_DEBUG, "Set state to None"); break; + case PluginState::Loaded: + logger().log(CLAP_LOG_DEBUG, "Set state to Loaded"); break; + case PluginState::LoadedWithError: + logger().log(CLAP_LOG_DEBUG, "Set state to LoadedWithError"); break; + case PluginState::Inactive: + logger().log(CLAP_LOG_DEBUG, "Set state to Inactive"); break; + case PluginState::InactiveWithError: + logger().log(CLAP_LOG_DEBUG, "Set state to InactiveWithError"); break; + case PluginState::ActiveAndSleeping: + logger().log(CLAP_LOG_DEBUG, "Set state to ActiveAndSleeping"); break; + case PluginState::ActiveAndProcessing: + logger().log(CLAP_LOG_DEBUG, "Set state to ActiveAndProcessing"); break; + case PluginState::ActiveWithError: + logger().log(CLAP_LOG_DEBUG, "Set state to ActiveWithError"); break; + case PluginState::ActiveAndReadyToDeactivate: + logger().log(CLAP_LOG_DEBUG, "Set state to ActiveAndReadyToDeactivate"); break; + } +} + +void ClapInstance::idle() +{ + assert(ClapThreadCheck::isMainThread()); + if (isErrorState()) { return; } + + m_params.idle(); + + if (m_scheduleMainThreadCallback) + { + m_scheduleMainThreadCallback = false; + m_plugin->on_main_thread(m_plugin); + } + + if (m_scheduleRestart) + { + deactivate(); + m_scheduleRestart = false; + activate(); + } +} + +auto ClapInstance::isNextStateValid(PluginState next) const -> bool +{ + switch (next) + { + case PluginState::None: + return m_pluginState == PluginState::Inactive + || m_pluginState == PluginState::InactiveWithError + || m_pluginState == PluginState::Loaded + || m_pluginState == PluginState::LoadedWithError + || m_pluginState == PluginState::None; // TODO: NoneWithError? + case PluginState::Loaded: + return m_pluginState == PluginState::None; + case PluginState::LoadedWithError: + return m_pluginState == PluginState::Loaded; + case PluginState::Inactive: + return m_pluginState == PluginState::Loaded + || m_pluginState == PluginState::ActiveAndReadyToDeactivate; + case PluginState::InactiveWithError: + return m_pluginState == PluginState::Inactive; + case PluginState::ActiveAndSleeping: + return m_pluginState == PluginState::Inactive + || m_pluginState == PluginState::ActiveAndProcessing; + case PluginState::ActiveAndProcessing: + return m_pluginState == PluginState::ActiveAndSleeping; + case PluginState::ActiveWithError: + return m_pluginState == PluginState::ActiveAndProcessing; + case PluginState::ActiveAndReadyToDeactivate: + return m_pluginState == PluginState::ActiveAndProcessing + || m_pluginState == PluginState::ActiveAndSleeping + || m_pluginState == PluginState::ActiveWithError; + break; + default: + throw std::runtime_error{"CLAP plugin state error"}; + } + return false; +} + +auto ClapInstance::clapGetExtension(const clap_host* host, const char* extensionId) -> const void* +{ + auto h = detail::ClapExtensionHelper::fromHost(host); + if (!h || !extensionId) { return nullptr; } + +#if 0 + { + std::string msg = "Plugin requested host extension: "; + msg += (extensionId ? extensionId : "(NULL)"); + h->logger().log(CLAP_LOG_DEBUG, msg); + } +#endif + + if (h->m_pluginState == PluginState::None) + { + h->logger().log(CLAP_LOG_PLUGIN_MISBEHAVING, "Plugin may not request host extension before plugin.init()"); + return nullptr; + } + + const auto id = std::string_view{extensionId}; + //if (id == CLAP_EXT_AUDIO_PORTS) { return h->audioPorts().hostExt(); } + //if (id == CLAP_EXT_GUI) { return h->gui().hostExt(); } + if (id == CLAP_EXT_LATENCY) { return &s_clapLatency; } + if (id == CLAP_EXT_LOG) { return h->logger().hostExt(); } + if (id == CLAP_EXT_NOTE_PORTS) { return h->notePorts().hostExt(); } + if (id == CLAP_EXT_PARAMS) { return h->params().hostExt(); } + if (id == CLAP_EXT_PRESET_LOAD) { return h->presetLoader().hostExt(); } + if (id == CLAP_EXT_PRESET_LOAD_COMPAT) { return h->presetLoader().hostExt(); } + if (id == CLAP_EXT_STATE) { return h->state().hostExt(); } + if (id == CLAP_EXT_THREAD_CHECK) { return h->m_threadCheck.hostExt(); } + if (id == CLAP_EXT_TIMER_SUPPORT) { return h->timerSupport().hostExt(); } + + return nullptr; +} + +void ClapInstance::clapRequestCallback(const clap_host* host) +{ + const auto h = detail::ClapExtensionHelper::fromHost(host); + if (!h) { return; } + h->m_scheduleMainThreadCallback = true; +} + +void ClapInstance::clapRequestProcess(const clap_host* host) +{ + auto h = detail::ClapExtensionHelper::fromHost(host); + if (!h) { return; } + h->m_scheduleProcess = true; +} + +void ClapInstance::clapRequestRestart(const clap_host* host) +{ + auto h = detail::ClapExtensionHelper::fromHost(host); + if (!h) { return; } + h->m_scheduleRestart = true; + h->logger().log(CLAP_LOG_DEBUG, ClapThreadCheck::isAudioThread() + ? "Req. restart on Audio thread" : "Req. restart on Main thread"); +} + +void ClapInstance::clapLatencyChanged([[maybe_unused]] const clap_host* host) +{ + /* + * LMMS currently does not use latency data, but implementing this extension + * fixes a crash that would occur in plugins built using the DISTRHO plugin + * framework prior to this commit: + * https://github.com/DISTRHO/DPF/commit/4f11f8cc49b24ede1735a16606e7bad5a52ab41d + */ +} + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP diff --git a/src/core/clap/ClapLog.cpp b/src/core/clap/ClapLog.cpp new file mode 100644 index 00000000000..b2910796fe5 --- /dev/null +++ b/src/core/clap/ClapLog.cpp @@ -0,0 +1,136 @@ +/* + * ClapLog.cpp - Implements CLAP log extension + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 "ClapLog.h" + +#ifdef LMMS_HAVE_CLAP + +#include + +#include "ClapInstance.h" +#include "ClapManager.h" + +namespace lmms +{ + +namespace +{ + auto logHelper(clap_log_severity severity, std::ostream*& ostr) -> std::string_view + { + ostr = &std::cerr; + std::string_view severityStr; + switch (severity) + { + case CLAP_LOG_DEBUG: + severityStr = "DEBUG"; + ostr = &std::cout; + break; + case CLAP_LOG_INFO: + severityStr = "INFO"; + ostr = &std::cout; + break; + case CLAP_LOG_WARNING: + severityStr = "WARNING"; + ostr = &std::cout; + break; + case CLAP_LOG_ERROR: + severityStr = "ERROR"; + break; + case CLAP_LOG_FATAL: + severityStr = "FATAL"; + break; + case CLAP_LOG_HOST_MISBEHAVING: + severityStr = "HOST_MISBEHAVING"; + break; + case CLAP_LOG_PLUGIN_MISBEHAVING: + severityStr = "PLUGIN_MISBEHAVING"; + break; + default: + severityStr = "UNKNOWN"; + break; + } + return severityStr; + } +} // namespace + +auto ClapLog::hostExtImpl() const -> const clap_host_log* +{ + static clap_host_log ext { + &clapLog + }; + return &ext; +} + +void ClapLog::log(clap_log_severity severity, std::string_view msg) const +{ + if (severity == CLAP_LOG_DEBUG && !ClapManager::debugging()) { return; } + + std::ostream* ostr; + const auto severityStr = logHelper(severity, ostr); + + *ostr << "[" << severityStr << "] [" << instance()->info().descriptor().id << "] " << msg << "\n"; +} + +void ClapLog::log(clap_log_severity severity, const char* msg) const +{ + log(severity, std::string_view{msg ? msg : ""}); +} + +void ClapLog::globalLog(clap_log_severity severity, std::string_view msg) +{ + if (severity == CLAP_LOG_DEBUG && !ClapManager::debugging()) { return; } + + std::ostream* ostr; + const auto severityStr = logHelper(severity, ostr); + + *ostr << "[" << severityStr << "] [*] " << msg << "\n"; +} + +void ClapLog::plainLog(clap_log_severity severity, std::string_view msg) +{ + if (severity == CLAP_LOG_DEBUG && !ClapManager::debugging()) { return; } + + std::ostream* ostr; + logHelper(severity, ostr); + + *ostr << msg << "\n"; +} + +void ClapLog::plainLog(std::string_view msg) +{ + std::cout << msg << "\n"; +} + +void ClapLog::clapLog(const clap_host_t* host, clap_log_severity severity, const char* msg) +{ + // Thread-safe + // TODO: Specify log message origin in the log message? (host vs. plugin) + auto h = fromHost(host); + if (!h) { return; } + h->logger().log(severity, msg); +} + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP diff --git a/src/core/clap/ClapManager.cpp b/src/core/clap/ClapManager.cpp new file mode 100644 index 00000000000..9598289241b --- /dev/null +++ b/src/core/clap/ClapManager.cpp @@ -0,0 +1,331 @@ +/* + * ClapManager.cpp - Implementation of ClapManager class + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 "ClapManager.h" + +#ifdef LMMS_HAVE_CLAP + +#include +#include +#include +#include +#include +#include + +#include "ClapLog.h" +#include "ClapTransport.h" +#include "lmmsversion.h" + +namespace lmms +{ + +namespace +{ + auto expandHomeDir(std::string_view dir) -> std::filesystem::path + { +#if defined(LMMS_BUILD_LINUX) || defined(LMMS_BUILD_APPLE) + if (!dir.empty() && dir[0] == '~') + { + if (auto home = std::getenv("HOME")) + { + const auto pos = dir.find_first_not_of(R"(/\)", 1); + if (pos == std::string_view::npos) { return home; } + dir.remove_prefix(pos); + return std::filesystem::u8path(home) / dir; + } + } +#endif + return std::filesystem::u8path(dir); + } +} // namespace + +ClapManager::ClapManager() +{ + const char* debug = std::getenv("LMMS_CLAP_DEBUG"); + s_debugging = debug && *debug; + ClapLog::plainLog(CLAP_LOG_DEBUG, "CLAP host debugging enabled"); +} + +ClapManager::~ClapManager() +{ + ClapLog::plainLog(CLAP_LOG_DEBUG, "ClapManager::~ClapManager()"); + + // NOTE: All plugin instances need to be deactivated and destroyed first + + // Deinit the .clap files and unload the shared libraries + m_files.clear(); + + ClapLog::plainLog(CLAP_LOG_DEBUG, "ClapManager::~ClapManager() end"); +} + +void ClapManager::initPlugins() +{ + findSearchPaths(); + if (debugging()) + { + ClapLog::plainLog("CLAP search paths:"); + for (const auto& searchPath : m_searchPaths) + { + std::string msg = "-" + searchPath.string(); + ClapLog::plainLog(msg); + } + ClapLog::plainLog("Found .clap files:"); + } + loadClapFiles(searchPaths()); +} + +void ClapManager::findSearchPaths() +{ + m_searchPaths.clear(); + + // Parses a string of paths, adding results to m_searchPaths + auto parsePaths = [this](const char* pathString) { + if (!pathString) { return; } + auto paths = std::string_view{pathString}; + std::size_t pos = 0; + do + { + if (paths.size() <= pos) { break; } + paths.remove_prefix(pos); + pos = paths.find(LADSPA_PATH_SEPERATOR); + if (pos == 0) { continue; } + auto path = expandHomeDir(paths.substr(0, pos)); + if (std::error_code ec; std::filesystem::is_directory(path, ec)) + { + path = std::filesystem::canonical(path.make_preferred(), ec); + if (ec) { continue; } + m_searchPaths.emplace(std::move(path)); + } + } while (pos++ != std::string_view::npos); + }; + +#if defined(LMMS_BUILD_WIN32) || defined(LMMS_BUILD_WIN64) + auto toUtf8 = [](const std::wstring& str) -> std::string { + std::wstring_convert> conv; + return conv.to_bytes(str); + }; + + // Use LMMS_CLAP_PATH to override all of CLAP's default search paths + if (auto paths = _wgetenv(L"LMMS_CLAP_PATH")) + { + parsePaths(toUtf8(paths).c_str()); + return; + } + + // Get CLAP_PATH paths + if (auto paths = _wgetenv(L"CLAP_PATH")) + { + parsePaths(toUtf8(paths).c_str()); + } +#else + // Use LMMS_CLAP_PATH to override all of CLAP's default search paths + if (auto paths = std::getenv("LMMS_CLAP_PATH")) + { + parsePaths(paths); + return; + } + + // Get CLAP_PATH paths + parsePaths(std::getenv("CLAP_PATH")); +#endif + + // Add OS-dependent search paths +#ifdef LMMS_BUILD_LINUX + // ~/.clap + // /usr/lib/clap + std::error_code ec; + auto path = expandHomeDir("~/.clap"); + if (std::filesystem::is_directory(path, ec)) + { + m_searchPaths.emplace(std::move(path)); + } + path = "/usr/lib/clap"; + if (std::filesystem::is_directory(path, ec)) + { + m_searchPaths.emplace(std::move(path)); + } +#elif defined(LMMS_BUILD_WIN32) || defined(LMMS_BUILD_WIN64) + // %COMMONPROGRAMFILES%\CLAP + // %LOCALAPPDATA%\Programs\Common\CLAP + std::error_code ec; + if (auto commonProgFiles = _wgetenv(L"COMMONPROGRAMFILES")) // TODO: Use wstring + { + auto path = std::filesystem::path{commonProgFiles} / "CLAP"; + if (std::filesystem::is_directory(path, ec)) + { + m_searchPaths.emplace(std::move(path.make_preferred())); + } + } + if (auto localAppData = _wgetenv(L"LOCALAPPDATA")) + { + auto path = std::filesystem::path{localAppData} / "Programs/Common/CLAP"; + if (std::filesystem::is_directory(path, ec)) + { + m_searchPaths.emplace(std::move(path.make_preferred())); + } + } +#elif defined(LMMS_BUILD_APPLE) + // /Library/Audio/Plug-Ins/CLAP + // ~/Library/Audio/Plug-Ins/CLAP + std::error_code ec; + auto path = std::filesystem::path{"/Library/Audio/Plug-Ins/CLAP"}; + if (std::filesystem::is_directory(path, ec)) + { + m_searchPaths.emplace(std::move(path)); + } + path = expandHomeDir("~/Library/Audio/Plug-Ins/CLAP"); + if (std::filesystem::is_directory(path, ec)) + { + m_searchPaths.emplace(std::move(path)); + } +#endif +} + +void ClapManager::loadClapFiles(const UniquePaths& searchPaths) +{ + if (!m_files.empty()) { return; } // Cannot unload CLAP plugins yet + + m_files.clear(); + m_uriInfoMap.clear(); + m_uriFileIndexMap.clear(); + m_pluginInfo.clear(); + + const auto startTime = std::chrono::steady_clock::now(); + + // Search `searchPaths` for files (or macOS bundles) with ".clap" extension + std::size_t totalClapFiles = 0; + std::size_t totalPlugins = 0; + for (const auto& path : searchPaths) + { + for (const auto& entry : std::filesystem::recursive_directory_iterator{path}) + { +#if defined(LMMS_BUILD_APPLE) + // NOTE: macOS uses a bundle rather than a regular file + if (std::error_code ec; !entry.is_directory(ec)) { continue; } +#else + if (std::error_code ec; !entry.is_regular_file(ec)) { continue; } +#endif + const auto& entryPath = entry.path(); + if (entryPath.extension() != ".clap") { continue; } + + ++totalClapFiles; + + if (debugging()) + { + std::string msg = "\n\n~~~CLAP FILE~~~\nfilename: "; + msg += entryPath.u8string(); + ClapLog::plainLog(msg); + } + + auto& file = m_files.emplace_back(std::move(entryPath)); + if (!file.load()) + { + std::string msg = "Failed to load '"; + msg += file.filename().u8string(); + msg += "'"; + ClapLog::globalLog(CLAP_LOG_ERROR, msg); + m_files.pop_back(); // Remove/unload invalid clap file + continue; + } + + totalPlugins += file.pluginCount(); + bool loadedFromThisFile = false; + for (auto& plugin : file.pluginInfo({})) + { + assert(plugin.has_value()); + const auto id = std::string{plugin->descriptor().id}; + const bool added = m_uriInfoMap.emplace(id, *plugin).second; + if (!added) + { + if (debugging()) + { + std::string msg = "Plugin ID '"; + msg += plugin->descriptor().id; + msg += "' in the plugin file '"; + msg += entryPath.string(); + msg += "' is identical to an ID from a previously loaded plugin file."; + ClapLog::globalLog(CLAP_LOG_INFO, msg); + ClapLog::globalLog(CLAP_LOG_INFO, "Skipping the duplicate plugin"); + } + plugin.reset(); // invalidate duplicate plugin + continue; + } + + m_uriFileIndexMap.emplace(id, m_files.size() - 1); + + m_pluginInfo.push_back(&plugin.value()); + loadedFromThisFile = true; + } + + if (loadedFromThisFile && file.presetDatabase()) + { + file.presetDatabase()->discover(); + } + } + } + + { + const auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - startTime); + std::string msg = "CLAP plugin SUMMARY: "; + msg += std::to_string(m_pluginInfo.size()) + " out of " + std::to_string(totalPlugins); + msg += " plugins in " + std::to_string(m_files.size()) + " out of " + std::to_string(totalClapFiles); + msg += " plugin files loaded in " + std::to_string(elapsed.count()) + " msecs."; + ClapLog::plainLog(msg); + } + + if (debugging()) + { + ClapLog::plainLog( + "If you don't want to see all this debug output, please set\n" + " environment variable \"LMMS_CLAP_DEBUG\" to empty or\n" + " do not set it."); + } + else if (m_files.size() != totalClapFiles || m_pluginInfo.size() != totalPlugins) + { + ClapLog::plainLog("For details about not loaded plugins, please set\n" + " environment variable \"LMMS_CLAP_DEBUG\" to nonempty."); + } +} + +auto ClapManager::pluginInfo(const std::string& uri) const -> const ClapPluginInfo* +{ + const auto iter = m_uriInfoMap.find(uri); + return iter != m_uriInfoMap.end() ? &iter->second : nullptr; +} + +auto ClapManager::presetDatabase(const std::string& uri) -> ClapPresetDatabase* +{ + const auto iter = m_uriFileIndexMap.find(uri); + if (iter == m_uriFileIndexMap.end()) { return nullptr; } + + assert(iter->second < m_files.size()); + auto& file = m_files[iter->second]; + + return file.presetDatabase(); +} + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP diff --git a/src/core/clap/ClapNotePorts.cpp b/src/core/clap/ClapNotePorts.cpp new file mode 100644 index 00000000000..4cc52c71a9c --- /dev/null +++ b/src/core/clap/ClapNotePorts.cpp @@ -0,0 +1,173 @@ +/* + * ClapNotePorts.cpp - Implements CLAP note ports extension + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 "ClapNotePorts.h" + +#ifdef LMMS_HAVE_CLAP + +#include +#include +#include + +#include "ClapInstance.h" + +namespace lmms +{ + +auto ClapNotePorts::initImpl() noexcept -> bool +{ + m_portIndex = 0; + m_dialect = 0; + clapRescan(host(), CLAP_NOTE_PORTS_RESCAN_ALL); + return true; +} + +auto ClapNotePorts::hostExtImpl() const -> const clap_host_note_ports* +{ + static clap_host_note_ports ext { + &clapSupportedDialects, + &clapRescan + }; + return &ext; +} + +auto ClapNotePorts::checkSupported(const clap_plugin_note_ports& ext) -> bool +{ + return ext.count && ext.get; +} + +auto ClapNotePorts::clapSupportedDialects([[maybe_unused]] const clap_host* host) -> std::uint32_t +{ + return CLAP_NOTE_DIALECT_CLAP | CLAP_NOTE_DIALECT_MIDI; +} + +void ClapNotePorts::clapRescan(const clap_host* host, std::uint32_t flags) +{ + auto h = fromHost(host); + if (!h) { return; } + auto& notePorts = h->notePorts(); + + if (!notePorts.supported()) { return; } + + if (flags & CLAP_NOTE_PORTS_RESCAN_ALL) + { + if (h->isActive()) + { + h->logger().log(CLAP_LOG_PLUGIN_MISBEHAVING, "Host cannot rescan note ports while plugin is active"); + return; + } + + /* + * I'm using a priority system to choose the note port we use. + * This may not be very useful in practice. + * + * Highest to lowest priority: + * - CLAP preferred + * - MIDI preferred + * - CLAP supported + * - MIDI supported + * - (no note input support) + */ + class PriorityHelper + { + public: + using IndexDialectPair = std::pair; + void check(std::uint16_t index, const clap_note_port_info& info) + { + // #2 priority: MIDI preferred + if (info.preferred_dialect == CLAP_NOTE_DIALECT_MIDI) + { + m_cache[0] = { index, CLAP_NOTE_DIALECT_MIDI }; + m_best = std::min(0u, m_best); + } + // #3 priority: CLAP supported + else if (info.supported_dialects & CLAP_NOTE_DIALECT_CLAP) + { + m_cache[1] = { index, CLAP_NOTE_DIALECT_CLAP }; + m_best = std::min(1u, m_best); + } + // #4 priority: MIDI supported + else if (info.supported_dialects & CLAP_NOTE_DIALECT_MIDI) + { + m_cache[2] = { index, CLAP_NOTE_DIALECT_MIDI }; + m_best = std::min(2u, m_best); + } + } + auto getBest() -> IndexDialectPair + { + if (m_best == m_cache.size()) + { + // No note port supported by host + return {0, 0}; + } + return m_cache[m_best]; + } + private: + std::array m_cache; // priority 2 thru 4 + unsigned m_best = static_cast(m_cache.size()); // best port seen so far + } priorityHelper; + + const auto count = notePorts.pluginExt()->count(h->plugin(), true); + assert(count < (std::numeric_limits::max)()); // just in case + for (std::uint16_t idx = 0; idx < static_cast(count); ++idx) + { + auto info = clap_note_port_info{}; + info.id = CLAP_INVALID_ID; + if (!notePorts.pluginExt()->get(h->plugin(), idx, true, &info)) + { + h->logger().log(CLAP_LOG_ERROR, "Failed to read note port info"); + return; + } + + // Just in case + if (info.id == CLAP_INVALID_ID) + { + h->logger().log(CLAP_LOG_PLUGIN_MISBEHAVING, "Note port info contains invalid id"); + continue; + } + + // Check for #1 priority option: CLAP preferred + if (info.preferred_dialect == CLAP_NOTE_DIALECT_CLAP) + { + notePorts.m_portIndex = idx; + notePorts.m_dialect = CLAP_NOTE_DIALECT_CLAP; + return; + } + + // Check for match with lesser-priority options + priorityHelper.check(idx, info); + } + + std::tie(notePorts.m_portIndex, notePorts.m_dialect) = priorityHelper.getBest(); + // TODO: Also set the 2nd supported note dialect? Or use a boolean to indicate both CLAP and MIDI are supported? + } + else if (flags & CLAP_NOTE_PORTS_RESCAN_NAMES) + { + // Not implemented + } +} + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP diff --git a/src/core/clap/ClapParameter.cpp b/src/core/clap/ClapParameter.cpp new file mode 100644 index 00000000000..16ba6895839 --- /dev/null +++ b/src/core/clap/ClapParameter.cpp @@ -0,0 +1,223 @@ +/* + * ClapParameter.cpp - Implementation of ClapParameter class + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 "ClapParameter.h" + +#ifdef LMMS_HAVE_CLAP + +#include +#include +#include + +#include "ClapInstance.h" +#include "ClapParams.h" + +namespace lmms +{ + +ClapParameter::ClapParameter(ClapParams* parent, const clap_param_info& info, double value) + : QObject{parent} + , m_info{info} + , m_id{"p" + std::to_string(m_info.id)} + , m_displayName{m_info.name} + , m_value{value} +{ + // Assume ClapParam::check() has already been called at this point + +#if 0 + { + std::string msg = "Parameter --- id: '"; + msg += info.id; + msg += "'; name: '"; + msg += info.name; + msg += "'; module: '"; + msg += info.module; + msg += "'; flags: "; + msg += std::to_string(info.flags); + parent->logger().log(CLAP_LOG_DEBUG, msg); + } +#endif + + // If the user cannot control this param, no AutomatableModel is needed + const auto flags = m_info.flags; + if ((flags & CLAP_PARAM_IS_HIDDEN) || (flags & CLAP_PARAM_IS_READONLY)) { return; } + + if (value > m_info.max_value || value < m_info.min_value) + { + throw std::logic_error{"CLAP param: Error: value is out of range"}; + //value = std::clamp(value, m_info.min_value, m_info.max_value); + } + + const auto name = QString::fromUtf8(displayName().data(), displayName().size()); + + if ((flags & CLAP_PARAM_IS_STEPPED) || (flags & CLAP_PARAM_IS_BYPASS)) + { + const auto minVal = static_cast(std::trunc(m_info.min_value)); + const auto valueInt = static_cast(std::trunc(value)); + const auto maxVal = static_cast(std::trunc(m_info.max_value)); + + if (minVal == 0 && maxVal == 1) + { + m_connectedModel = std::make_unique(valueInt, nullptr, name); + m_valueType = ValueType::Bool; + } + else + { + if (flags & CLAP_PARAM_IS_BYPASS) + { + parent->logger().log(CLAP_LOG_PLUGIN_MISBEHAVING, "Bypass parameter doesn't have range [0, 1]"); + } + + m_connectedModel = std::make_unique(valueInt, minVal, maxVal, nullptr, name); + m_valueType = (flags & CLAP_PARAM_IS_ENUM) ? ValueType::Enum : ValueType::Integer; + } + } + else + { + // Allow ~1000 steps + double stepSize = (m_info.max_value - m_info.min_value) / 1000.0; + + // Make multiples of 0.01 (or 0.1 for larger values) + const double minStep = (stepSize >= 1.0) ? 0.1 : 0.01; + stepSize -= std::fmod(stepSize, minStep); + stepSize = std::max(stepSize, minStep); + + m_connectedModel = std::make_unique( + static_cast(value), + static_cast(m_info.min_value), + static_cast(m_info.max_value), + static_cast(stepSize), + nullptr, name); + + m_valueType = ValueType::Float; + } +} + +void ClapParameter::setValue(double v) +{ + // TODO: Use the model instead of m_value? + if (m_connectedModel) + { + // TODO: Use setAutomatedValue()?? + m_connectedModel->setValue(static_cast(v)); + } + else + { + if (m_value == v) { return; } + m_value = v; + emit valueChanged(); + } + +} + +void ClapParameter::setModulation(double v) +{ + if (m_modulation == v) { return; } + m_modulation = v; + emit modulatedValueChanged(); +} + +auto ClapParameter::getShortInfoString() const -> std::string +{ + return "id: " + std::to_string(m_info.id) + ", name: '" + std::string{m_info.name} + + "', module: '" + std::string{m_info.module} + "'"; +} + +auto ClapParameter::getInfoString() const -> std::string +{ + return getShortInfoString() + ", min: " + std::to_string(m_info.min_value) + + ", max: " + std::to_string(m_info.max_value); +} + +auto ClapParameter::isInfoEqualTo(const clap_param_info& info) const -> bool +{ + return info.cookie == m_info.cookie + && info.default_value == m_info.default_value + && info.max_value == m_info.max_value + && info.min_value == m_info.min_value + && info.flags == m_info.flags + && info.id == m_info.id + && !::std::strncmp(info.name, m_info.name, sizeof(info.name)) + && !::std::strncmp(info.module, m_info.module, sizeof(info.module)); +} + +auto ClapParameter::isInfoCriticallyDifferentTo(const clap_param_info& info) const -> bool +{ + assert(m_info.id == info.id); + constexpr std::uint32_t criticalFlags = CLAP_PARAM_IS_AUTOMATABLE | CLAP_PARAM_IS_AUTOMATABLE_PER_NOTE_ID + | CLAP_PARAM_IS_AUTOMATABLE_PER_KEY | CLAP_PARAM_IS_AUTOMATABLE_PER_CHANNEL + | CLAP_PARAM_IS_AUTOMATABLE_PER_PORT | CLAP_PARAM_IS_MODULATABLE + | CLAP_PARAM_IS_MODULATABLE_PER_NOTE_ID | CLAP_PARAM_IS_MODULATABLE_PER_KEY + | CLAP_PARAM_IS_MODULATABLE_PER_CHANNEL | CLAP_PARAM_IS_MODULATABLE_PER_PORT + | CLAP_PARAM_IS_READONLY | CLAP_PARAM_REQUIRES_PROCESS; + return (m_info.flags & criticalFlags) == (info.flags & criticalFlags) + || m_info.min_value != m_info.min_value || m_info.max_value != m_info.max_value; +} + +void ClapParameter::setIsAdjusting(bool isAdjusting) +{ + if (isAdjusting && !m_isBeingAdjusted) { beginAdjust(); } + else if (!isAdjusting && m_isBeingAdjusted) { endAdjust(); } +} + +void ClapParameter::beginAdjust() +{ + assert(!m_isBeingAdjusted); + m_isBeingAdjusted = true; + emit isBeingAdjustedChanged(); +} + +void ClapParameter::endAdjust() +{ + assert(m_isBeingAdjusted); + m_isBeingAdjusted = false; + emit isBeingAdjustedChanged(); +} + +auto ClapParameter::check(clap_param_info& info) -> bool +{ + if (info.min_value > info.max_value) + { + ClapLog::globalLog(CLAP_LOG_PLUGIN_MISBEHAVING, "Parameter's min value is greater than its max value"); + // TODO: Use PluginIssueType::MinGreaterMax ?? + return false; + } + + if (info.default_value > info.max_value || info.default_value < info.min_value) + { + std::string msg = "Parameter's default value is out of range\ndefault: " + std::to_string(info.default_value) + + "; min: " + std::to_string(info.min_value) + "; max: " + std::to_string(info.max_value); + ClapLog::globalLog(CLAP_LOG_PLUGIN_MISBEHAVING, msg); + + // TODO: Use PluginIssueType::DefaultValueNotInRange ?? + //info.default_value = std::clamp(info.default_value, info.min_value, info.max_value); + return false; + } + + return true; +} + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP diff --git a/src/core/clap/ClapParams.cpp b/src/core/clap/ClapParams.cpp new file mode 100644 index 00000000000..c5b64dc6321 --- /dev/null +++ b/src/core/clap/ClapParams.cpp @@ -0,0 +1,563 @@ +/* + * ClapParams.cpp - Implements CLAP params extension + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 "ClapParams.h" + +#ifdef LMMS_HAVE_CLAP + +#include +#include + +#include "ClapInstance.h" + +namespace lmms +{ + +ClapParams::ClapParams(Model* parent, ClapInstance* instance, + clap::helpers::EventList* eventsIn, clap::helpers::EventList* eventsOut) + : QObject{parent} + , ClapExtension{instance} + , m_evIn{eventsIn} + , m_evOut{eventsOut} +{ +} + +auto ClapParams::initImpl() noexcept -> bool +{ + if (!rescan(CLAP_PARAM_RESCAN_ALL)) { return false; } + setModels(); + return true; +} + +void ClapParams::deinitImpl() noexcept +{ + m_paramMap.clear(); + m_params.clear(); +} + +auto ClapParams::hostExtImpl() const -> const clap_host_params* +{ + static clap_host_params ext { + &clapRescan, + &clapClear, + &clapRequestFlush + }; + return &ext; +} + +auto ClapParams::checkSupported(const clap_plugin_params& ext) -> bool +{ + // NOTE: value_to_text and text_to_value are not strictly required + return ext.count && ext.get_info && ext.get_value && ext.flush; +} + +auto ClapParams::rescan(clap_param_rescan_flags flags) -> bool +{ + // TODO: Update LinkedModelGroup when parameters are added/removed? + assert(ClapThreadCheck::isMainThread()); + + if (!supported()) { return false; } + + // 1. It is forbidden to use CLAP_PARAM_RESCAN_ALL if the plugin is active + if (instance()->isActive() && (flags & CLAP_PARAM_RESCAN_ALL)) + { + logger().log(CLAP_LOG_WARNING, "clap_host_params.recan(CLAP_PARAM_RESCAN_ALL) " + "was called while the plugin is active"); + return false; + } + + // 2. Scan the params + auto count = pluginExt()->count(plugin()); + + std::unordered_set paramIds(count * 2); + bool needToUpdateParamsCache = false; + + for (std::uint32_t idx = 0; idx < count; ++idx) + { + clap_param_info info{}; + info.id = CLAP_INVALID_ID; + + if (!pluginExt()->get_info(plugin(), idx, &info)) + { + logger().log(CLAP_LOG_WARNING, "clap_plugin_params.get_info() returned false!"); + return false; // TODO: continue? + } + + if (!ClapParameter::check(info)) { return false; } // TODO: continue? + + if (info.id == CLAP_INVALID_ID) + { + std::string msg = "clap_plugin_params.get_info() reported a parameter with id = CLAP_INVALID_ID\n" + " 2. name: " + std::string{info.name} + ", module: " + std::string{info.module}; + logger().log(CLAP_LOG_WARNING, msg.c_str()); + return false; // TODO: continue? + } + + auto it = m_paramMap.find(info.id); + + if (!paramIds.insert(info.id).second) + { + // Parameter was declared twice + assert(it != m_paramMap.end()); + std::string msg = "the parameter with id: " + std::to_string(info.id) + " was declared twice.\n" + " 1. name: " + std::string{it->second->info().name} + ", module: " + + std::string{it->second->info().module} + "\n" + " 2. name: " + std::string{info.name} + ", module: " + std::string{info.module}; + logger().log(CLAP_LOG_WARNING, msg.c_str()); + return false; // TODO: continue? + } + + if (it == m_paramMap.end()) + { + if (!(flags & CLAP_PARAM_RESCAN_ALL)) + { + std::string msg = "A new parameter was declared, but the flag CLAP_PARAM_RESCAN_ALL " + "was not specified; id: " + std::to_string(info.id) + + ", name: " + std::string{info.name} + ", module: " + std::string{info.module}; + logger().log(CLAP_LOG_WARNING, msg.c_str()); + return false; // TODO: continue? + } + + if (const auto value = getValue(info)) + { + auto param = std::make_unique(this, info, *value); + checkValidParamValue(*param, *value); + m_paramMap.insert_or_assign(info.id, std::move(param)); + needToUpdateParamsCache = true; + } + else + { + return false; // TODO: continue? + } + } + else + { + // Update param info + if (!it->second->isInfoEqualTo(info)) + { + if (!rescanMayInfoChange(flags)) + { + std::string msg = "a parameter's info did change, but the flag CLAP_PARAM_RESCAN_INFO " + "was not specified; id: " + std::to_string(info.id) + + ", name: " + std::string{info.name} + ", module: " + std::string{info.module}; + logger().log(CLAP_LOG_WARNING, msg.c_str()); + return false; // TODO: continue? + } + + if (!(flags & CLAP_PARAM_RESCAN_ALL) && !it->second->isInfoCriticallyDifferentTo(info)) + { + std::string msg = "a parameter's info has critical changes, but the flag CLAP_PARAM_RESCAN_ALL " + "was not specified; id: " + std::to_string(info.id) + + ", name: " + std::string{info.name} + ", module: " + std::string{info.module}; + logger().log(CLAP_LOG_WARNING, msg.c_str()); + return false; // TODO: continue? + } + + it->second->setInfo(info); + } + + if (const auto value = getValue(info)) + { + if (it->second->value() != *value) + { + if (!rescanMayValueChange(flags)) + { + std::string msg = "a parameter's value did change but, but the flag " + "CLAP_PARAM_RESCAN_VALUES was not specified; id: " + std::to_string(info.id) + + ", name: " + std::string{info.name} + ", module: " + std::string{info.module}; + logger().log(CLAP_LOG_WARNING, msg.c_str()); + return false; // TODO: continue? + } + + // Update param value + checkValidParamValue(*it->second, *value); + it->second->setValue(*value); + it->second->setModulation(*value); + } + } + else + { + return false; // TODO: continue? + } + } + } + + // 3. Remove parameters which are gone + for (auto it = m_paramMap.begin(); it != m_paramMap.end();) + { + if (paramIds.find(it->first) == paramIds.end()) + { + if (!(flags & CLAP_PARAM_RESCAN_ALL)) + { + const auto& info = it->second->info(); + std::string msg = "a parameter was removed, but the flag CLAP_PARAM_RESCAN_ALL " + "was not specified; id: " + std::to_string(info.id) + + ", name: " + std::string{info.name} + + ", module: " + std::string{info.module}; + logger().log(CLAP_LOG_WARNING, msg.c_str()); + return false; + } + it = m_paramMap.erase(it); + needToUpdateParamsCache = true; + } + else { ++it; } + } + + if (needToUpdateParamsCache) + { + m_automatableCount = 0; + m_params.resize(m_paramMap.size()); + int idx = 0; + for (const auto& elem : m_paramMap) + { + m_params[idx] = elem.second.get(); + if (m_params[idx]->model() != nullptr) { ++m_automatableCount; } + ++idx; + } + } + + if (flags & CLAP_PARAM_RESCAN_ALL) { emit paramsChanged(); } + + return true; +} + +void ClapParams::idle() +{ + // Try to send events to the audio engine + m_hostToPluginValueQueue.producerDone(); + m_hostToPluginModQueue.producerDone(); + + m_pluginToHostValueQueue.consume( + [this](clap_id paramId, const PluginToHostParamQueueValue& value) { + const auto it = m_paramMap.find(paramId); + if (it == m_paramMap.end()) + { + std::string msg = "Plugin produced a CLAP_EVENT_PARAM_SET with an unknown param id: " + + std::to_string(paramId); + logger().log(CLAP_LOG_WARNING, msg.c_str()); + return; + } + + if (value.hasValue) { it->second->setValue(value.value); } + if (value.hasGesture) { it->second->setIsAdjusting(value.isBegin); } + + emit paramAdjusted(paramId); + } + ); + + if (m_scheduleFlush && !instance()->isActive()) + { + flushOnMainThread(); + } +} + +void ClapParams::processEnd() +{ + m_pluginToHostValueQueue.producerDone(); +} + +void ClapParams::saveParamConnections(QDomDocument& doc, QDomElement& elem) +{ + auto models = doc.createElement("automated_models"); + elem.appendChild(models); + + for (auto& param : m_params) + { + assert(param != nullptr); + const auto model = param->model(); + if (!model || !model->isAutomatedOrControlled()) { continue; } + + // TODO: Add method to AutomatableModel which only saves automation / controls? + model->saveSettings(doc, models, QString::fromUtf8(param->id().data(), param->id().size())); + } +} + +void ClapParams::loadParamConnections(const QDomElement& elem) +{ + auto models = elem.firstChildElement("automated_models"); + if (models.isNull()) { return; } + + for (auto& param : m_params) + { + assert(param != nullptr); + const auto model = param->model(); + if (!model) { continue; } + + // TODO: Add method to AutomatableModel which only loads automation / controls? + model->loadSettings(models, QString::fromUtf8(param->id().data(), param->id().size())); + } +} + +void ClapParams::flushOnMainThread() +{ + assert(ClapThreadCheck::isMainThread()); + assert(!instance()->isActive()); + + if (!supported()) + { + logger().log(CLAP_LOG_WARNING, "Attempted to flush parameters on main thread, " + "but plugin does not support params extension"); + return; + } + + m_scheduleFlush = false; + + m_evIn->clear(); + m_evOut->clear(); + + generatePluginInputEvents(); + + pluginExt()->flush(plugin(), m_evIn->clapInputEvents(), m_evOut->clapOutputEvents()); + + handlePluginOutputEvents(); + + m_evOut->clear(); + m_pluginToHostValueQueue.producerDone(); +} + +void ClapParams::generatePluginInputEvents() +{ + m_hostToPluginValueQueue.consume( + [this](clap_id paramId, const HostToPluginParamQueueValue& value) { + clap_event_param_value ev; + ev.header.time = 0; + ev.header.type = CLAP_EVENT_PARAM_VALUE; + ev.header.space_id = CLAP_CORE_EVENT_SPACE_ID; + ev.header.flags = 0; + ev.header.size = sizeof(ev); + ev.param_id = paramId; + ev.cookie = s_provideCookie ? value.cookie : nullptr; + ev.port_index = 0; + ev.key = -1; + ev.channel = -1; + ev.note_id = -1; + ev.value = value.value; + m_evIn->push(&ev.header); + }); + + m_hostToPluginModQueue.consume( + [this](clap_id paramId, const HostToPluginParamQueueValue& value) { + clap_event_param_mod ev; + ev.header.time = 0; + ev.header.type = CLAP_EVENT_PARAM_MOD; + ev.header.space_id = CLAP_CORE_EVENT_SPACE_ID; + ev.header.flags = 0; + ev.header.size = sizeof(ev); + ev.param_id = paramId; + ev.cookie = s_provideCookie ? value.cookie : nullptr; + ev.port_index = 0; + ev.key = -1; + ev.channel = -1; + ev.note_id = -1; + ev.amount = value.value; + m_evIn->push(&ev.header); + }); +} + +void ClapParams::handlePluginOutputEvents() +{ + // TODO: Are LMMS models being updated with values here? + for (std::uint32_t idx = 0; idx < m_evOut->size(); ++idx) + { + auto header = m_evOut->get(idx); + switch (header->type) + { + case CLAP_EVENT_PARAM_GESTURE_BEGIN: + { + auto event = reinterpret_cast(header); + handlePluginOutputEvent(event, true); + break; + } + case CLAP_EVENT_PARAM_GESTURE_END: + { + auto event = reinterpret_cast(header); + handlePluginOutputEvent(event, false); + break; + } + case CLAP_EVENT_PARAM_VALUE: + { + auto event = reinterpret_cast(header); + handlePluginOutputEvent(event); + break; + } + default: + break; + } + } +} + +void ClapParams::handlePluginOutputEvent(const clap_event_param_gesture* event, bool gestureBegin) +{ + bool& isAdj = m_isAdjustingParameter[event->param_id]; + + if (isAdj == gestureBegin) + { + logger().log(CLAP_LOG_PLUGIN_MISBEHAVING, gestureBegin + ? "The plugin sent BEGIN_ADJUST twice" + : "The plugin sent END_ADJUST without a preceding BEGIN_ADJUST"); + } + isAdj = gestureBegin; + + PluginToHostParamQueueValue v; + v.hasGesture = true; + v.isBegin = gestureBegin; + m_pluginToHostValueQueue.setOrUpdate(event->param_id, v); +} + +void ClapParams::handlePluginOutputEvent(const clap_event_param_value* event) +{ + PluginToHostParamQueueValue v; + v.hasValue = true; + v.value = event->value; + m_pluginToHostValueQueue.setOrUpdate(event->param_id, v); +} + +void ClapParams::setModels() +{ + for (auto param : m_params) + { + if (!param || !param->model()) { continue; } + + const auto uri = QString::fromUtf8(param->id().data()); + + // Tell plugin when param value changes in host + auto updateParam = [this, param]() { + setParamValueByHost(*param, param->model()->value()); + }; + + // This is used for updating input parameters instead of copyModelsFromCore() + connect(param->model(), &Model::dataChanged, this, updateParam); + + // Initially assign model value to param value + updateParam(); + } +} + +auto ClapParams::checkValidParamValue(const ClapParameter& param, double value) -> bool +{ + if (!param.isValueValid(value)) + { + std::string msg = "Invalid value for param. " + param.getInfoString() + "; value: " + std::to_string(value); + logger().log(CLAP_LOG_WARNING, msg); + return false; + } + return true; +} + +auto ClapParams::getValue(const clap_param_info& info) const -> std::optional +{ + assert(ClapThreadCheck::isMainThread()); + assert(supported()); + + double value = 0.0; + if (!pluginExt()->get_value(plugin(), info.id, &value)) + { + std::string msg = "Failed to get the parameter value. id: " + std::to_string(info.id) + + ", name: " + std::string{info.name} + ", module: " + std::string{info.module}; + logger().log(CLAP_LOG_PLUGIN_MISBEHAVING, msg); + return std::nullopt; + } + + return value; +} + +auto ClapParams::getValueText(const ClapParameter& param) const -> std::string +{ + const auto valueStr = std::to_string(param.value()); + if (!pluginExt()->value_to_text) + { + return valueStr; + } + + auto buffer = std::array{}; + if (!pluginExt()->value_to_text(plugin(), param.info().id, param.value(), buffer.data(), CLAP_NAME_SIZE - 1)) + { + return valueStr; + } + + if (valueStr == buffer.data()) + { + // No point in displaying two identical strings + return valueStr; + } + + // Use CLAP-provided string + internal value in brackets for automation purposes + return std::string{buffer.data()} + "\n[" + valueStr + "]"; +} + +void ClapParams::setParamValueByHost(ClapParameter& param, double value) +{ + assert(ClapThreadCheck::isMainThread()); + + param.setValue(value); + + m_hostToPluginValueQueue.set(param.info().id, {param.info().cookie, value}); + m_hostToPluginValueQueue.producerDone(); + clapRequestFlush(host()); +} + +void ClapParams::setParamModulationByHost(ClapParameter& param, double value) +{ + assert(ClapThreadCheck::isMainThread()); + + param.setModulation(value); + + m_hostToPluginModQueue.set(param.info().id, {param.info().cookie, value}); + m_hostToPluginModQueue.producerDone(); + clapRequestFlush(host()); +} + +void ClapParams::clapRescan(const clap_host* host, clap_param_rescan_flags flags) +{ + auto h = fromHost(host); + if (!h) { return; } + h->params().rescan(flags); +} + +void ClapParams::clapClear(const clap_host* host, clap_id param_id, clap_param_clear_flags flags) +{ + assert(ClapThreadCheck::isMainThread()); + ClapLog::globalLog(CLAP_LOG_ERROR, "ClapParams::clapClear() [NOT IMPLEMENTED YET]"); + // TODO +} + +void ClapParams::clapRequestFlush(const clap_host* host) +{ + auto h = fromHost(host); + if (!h) { return; } + auto& params = h->params(); + + if (!h->isActive() && ClapThreadCheck::isMainThread()) + { + // Perform the flush immediately + params.flushOnMainThread(); + return; + } + + params.m_scheduleFlush = true; +} + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP diff --git a/src/core/clap/ClapPluginInfo.cpp b/src/core/clap/ClapPluginInfo.cpp new file mode 100644 index 00000000000..ef97a176f46 --- /dev/null +++ b/src/core/clap/ClapPluginInfo.cpp @@ -0,0 +1,124 @@ +/* + * ClapPluginInfo.cpp - Implementation of ClapPluginInfo class + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 "ClapPluginInfo.h" + +#ifdef LMMS_HAVE_CLAP + +#include "ClapLog.h" +#include "ClapManager.h" + +namespace lmms +{ + +auto ClapPluginInfo::create(const clap_plugin_factory& factory, std::uint32_t index) -> std::optional +{ + auto info = std::optional{ClapPluginInfo{factory, index}}; + return info->type() != Plugin::Type::Undefined ? info : std::nullopt; +} + +ClapPluginInfo::ClapPluginInfo(const clap_plugin_factory& factory, std::uint32_t index) + : m_factory{&factory} + , m_index{index} +{ + m_descriptor = m_factory->get_plugin_descriptor(m_factory, m_index); + if (!m_descriptor) + { + ClapLog::globalLog(CLAP_LOG_ERROR, "No plugin descriptor"); + return; + } + + if (!m_descriptor->id || !m_descriptor->name) + { + ClapLog::globalLog(CLAP_LOG_ERROR, "Invalid plugin descriptor"); + return; + } + + if (!clap_version_is_compatible(m_descriptor->clap_version)) + { + std::string msg = "Plugin '"; + msg += m_descriptor->id; + msg += "' uses unsupported CLAP version '"; + msg += std::to_string(m_descriptor->clap_version.major) + "." + + std::to_string(m_descriptor->clap_version.minor) + "." + + std::to_string(m_descriptor->clap_version.revision); + msg += "'. Must be at least 1.0.0."; + ClapLog::globalLog(CLAP_LOG_ERROR, msg); + return; + } + + if (ClapManager::debugging()) + { + std::string msg = "name: "; + msg += m_descriptor->name; + msg += "\nid: "; + msg += m_descriptor->id; + msg += "\nversion: "; + msg += (m_descriptor->version ? m_descriptor->version : ""); + msg += "\nCLAP version: "; + msg += std::to_string(m_descriptor->clap_version.major) + "." + + std::to_string(m_descriptor->clap_version.minor) + "." + + std::to_string(m_descriptor->clap_version.revision); + msg += "\ndescription: "; + msg += (m_descriptor->description ? m_descriptor->description : ""); + ClapLog::plainLog(CLAP_LOG_DEBUG, msg); + } + + auto features = m_descriptor->features; + while (features && *features) + { + auto feature = std::string_view{*features}; + if (ClapManager::debugging()) + { + std::string msg = "feature: " + std::string{feature}; + ClapLog::plainLog(CLAP_LOG_DEBUG, msg); + } + + if (feature == CLAP_PLUGIN_FEATURE_INSTRUMENT) + { + m_type = Plugin::Type::Instrument; + } + else if (feature == CLAP_PLUGIN_FEATURE_AUDIO_EFFECT + || feature == "effect" /* non-standard, but used by Surge XT Effects */) + { + m_type = Plugin::Type::Effect; + } + /*else if (feature == CLAP_PLUGIN_FEATURE_ANALYZER) + { + m_type = Plugin::Type::Tool; + }*/ + ++features; + } + + if (m_type == Plugin::Type::Undefined) + { + std::string msg = "Plugin '" + std::string{m_descriptor->id} + + "' is not recognized as an instrument or audio effect"; + ClapLog::globalLog(CLAP_LOG_INFO, msg); + } +} + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP diff --git a/src/core/clap/ClapPresetDatabase.cpp b/src/core/clap/ClapPresetDatabase.cpp new file mode 100644 index 00000000000..684fed513d2 --- /dev/null +++ b/src/core/clap/ClapPresetDatabase.cpp @@ -0,0 +1,812 @@ +/* + * ClapPresetDatabase.cpp - Implementation of PresetDatabase for CLAP presets + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 "ClapPresetDatabase.h" + +#ifdef LMMS_HAVE_CLAP + +#include +#include +#include +#include + +#include "ClapLog.h" +#include "lmmsversion.h" +#include "PathUtil.h" + +namespace lmms +{ + +namespace +{ + //! Converts clap_preset_discovery_flags to PresetMetadata::Flags + auto convertFlags(std::uint32_t flags) -> PresetMetadata::Flags + { + PresetMetadata::Flags result = PresetMetadata::Flag::None; + if (flags & CLAP_PRESET_DISCOVERY_IS_FACTORY_CONTENT) { result |= PresetMetadata::Flag::FactoryContent; } + if (flags & CLAP_PRESET_DISCOVERY_IS_USER_CONTENT) { result |= PresetMetadata::Flag::UserContent; } + if (flags & CLAP_PRESET_DISCOVERY_IS_FAVORITE) { result |= PresetMetadata::Flag::UserFavorite; } + return result; + } +} // namespace + +class ClapPresetDatabase::MetadataReceiver +{ +public: + MetadataReceiver() = delete; + MetadataReceiver(const Indexer& indexer); + + //! For PLUGIN presets + auto query(PresetMetadata::Flags flags = PresetMetadata::Flag::None) + -> std::optional>; + + //! For FILE presets; `file` is the full path of the preset file + auto query(std::string_view file, + PresetMetadata::Flags flags = PresetMetadata::Flag::None) -> std::optional>; + + auto errorMessage() const -> auto& { return m_error; } + +private: + /** + * clap_preset_discovery_metadata_receiver implementation + */ + static void clapOnError(const clap_preset_discovery_metadata_receiver* receiver, + std::int32_t osError, const char* errorMessage); + static auto clapBeginPreset(const clap_preset_discovery_metadata_receiver* receiver, + const char* name, const char* loadKey) -> bool; + static void clapAddPluginId(const clap_preset_discovery_metadata_receiver* receiver, + const clap_universal_plugin_id* pluginId); + static void clapSetSoundpackId(const clap_preset_discovery_metadata_receiver* receiver, + const char* soundpackId); + static void clapSetFlags(const clap_preset_discovery_metadata_receiver* receiver, std::uint32_t flags); + static void clapAddCreator(const clap_preset_discovery_metadata_receiver* receiver, const char* creator); + static void clapSetDescription(const clap_preset_discovery_metadata_receiver* receiver, + const char* description); + static void clapSetTimestamps(const clap_preset_discovery_metadata_receiver* receiver, + clap_timestamp creationTime, clap_timestamp modificationTime); + static void clapAddFeature(const clap_preset_discovery_metadata_receiver* receiver, + const char* feature); + static void clapAddExtraInfo(const clap_preset_discovery_metadata_receiver* receiver, + const char* key, const char* value); + + static auto from(const clap_preset_discovery_metadata_receiver* receiver) -> MetadataReceiver*; + + clap_preset_discovery_metadata_receiver m_receiver; + + const Indexer* m_indexer = nullptr; + + std::string_view m_location; //!< Used during the call to query() + PresetMetadata::Flags m_flags; //!< Used during the call to query() + std::vector m_presets; //!< Used during the call to query() + + std::string m_error; + bool m_skip = false; //!< Used to ignore remaining metadata if the current preset is invalid +}; + +auto ClapPresetDatabase::init(const clap_plugin_entry* entry) -> bool +{ + if (!entry) { return false; } + + m_factory = static_cast( + entry->get_factory(CLAP_PRESET_DISCOVERY_FACTORY_ID)); + + if (!m_factory) + { + // Try using compatibility ID if it exists + m_factory = static_cast( + entry->get_factory(CLAP_PRESET_DISCOVERY_FACTORY_ID_COMPAT)); + } + + return m_factory; +} + +void ClapPresetDatabase::deinit() +{ + m_indexers.clear(); +} + +auto ClapPresetDatabase::discoverSetup() -> bool +{ + if (!m_factory) + { + // This .clap file doesn't provide preset support + return false; + } + + const auto providerCount = m_factory->count(m_factory); + if (providerCount == 0) { return false; } + + bool success = false; + for (std::uint32_t idx = 0; idx < providerCount; ++idx) + { + if (auto indexer = Indexer::create(*m_factory, idx)) + { + success = true; + auto& indexerRef = m_indexers.emplace_back(std::move(indexer)); + for (auto& filetype : indexerRef->filetypes()) + { + m_filetypeIndexerMap[filetype.extension].push_back(indexerRef.get()); + } + } + else + { + ClapLog::globalLog(CLAP_LOG_WARNING, "Failed to create preset indexer"); + } + } + + return success; +} + +auto ClapPresetDatabase::discoverFiletypes(std::vector& filetypes) -> bool +{ + for (const auto& indexer : m_indexers) + { + for (const auto& filetype : indexer->filetypes()) + { + filetypes.push_back(filetype); + } + } + return true; +} + +auto ClapPresetDatabase::discoverLocations(const SetLocations& func) -> bool +{ + for (const auto& indexer : m_indexers) + { + for (const auto& location : indexer->locations()) + { + func(location); + } + } + return true; +} + +auto ClapPresetDatabase::discoverPresets(const Location& location, std::set& presets) -> bool +{ + const auto preferredIndexer = getIndexerFor(location); + + // Handle PLUGIN presets (internal presets) + if (PathUtil::baseLookup(location.location) == PathUtil::Base::Internal) + { + if (!preferredIndexer) + { + ClapLog::globalLog(CLAP_LOG_ERROR, "No known indexer supports internal presets"); + return false; + } + + auto newPresets = preferredIndexer->query(location.flags); + if (!newPresets) { return false; } + for (auto& preset : *newPresets) + { + presets.insert(std::move(preset)); + } + + return true; + } + + // Presets must be FILE presets + const auto fullPath = PathUtil::toAbsolute(location.location).value_or(""); + if (fullPath.empty()) + { + // Shouldn't ever happen? + ClapLog::globalLog(CLAP_LOG_ERROR, "Failed to get absolute path for preset directory"); + return false; + } + + // First handle case where location is a file + if (std::error_code ec; std::filesystem::is_regular_file(fullPath, ec)) + { + // Use preferred indexer if possible + if (preferredIndexer) + { + auto newPresets = preferredIndexer->query(fullPath, location.flags); + if (!newPresets) { return false; } + for (auto& preset : *newPresets) + { + presets.insert(std::move(preset)); + } + return true; + } + + // Else, just try whichever indexer works + bool success = false; + for (const auto& indexer : m_indexers) + { + auto newPresets = indexer->query(fullPath, location.flags); + if (!newPresets) { continue; } + success = true; + for (auto& preset : *newPresets) + { + presets.insert(std::move(preset)); + } + } + return success; + } + + // Location is a directory - need to search for preset files + if (std::error_code ec; !std::filesystem::is_directory(fullPath, ec)) + { + ClapLog::globalLog(CLAP_LOG_WARNING, "Preset directory \"" + fullPath + "\" does not exist"); + return false; + } + + // TODO: Move to Indexer? + auto getPresets = [&](Indexer& indexer) -> bool { + bool success = false; + + for (const auto& entry : std::filesystem::recursive_directory_iterator{fullPath}) + { + const auto entryPath = entry.path().string(); + + if (std::error_code ec; !entry.is_regular_file(ec)) { continue; } + + if (!indexer.filetypeSupported(entryPath)) { continue; } + + auto newPresets = indexer.query(entryPath, location.flags); + if (!newPresets) { continue; } + success = true; + for (auto& preset : *newPresets) + { + presets.insert(std::move(preset)); + } + } + + return success; + }; + + + // TODO: Use m_filetypeIndexerMap instead of preferredIndexer? + + // Use preferred indexer if possible + if (preferredIndexer) + { + return getPresets(*preferredIndexer); + } + + // Else, just try whichever indexer works + bool success = false; + for (const auto& indexer : m_indexers) + { + success = getPresets(*indexer) || success; + } + return success; +} + +auto ClapPresetDatabase::loadPresets(const Location& location, std::string_view file, + std::set& presets) -> std::vector +{ + const auto filePath = std::filesystem::u8path(file); + if (std::error_code ec; !std::filesystem::is_regular_file(filePath, ec)) { return {}; } + + auto getPresets = [&](Indexer& indexer) -> std::vector { + if (!indexer.filetypeSupported(filePath)) { return {}; } + + auto newPresets = indexer.query(file, location.flags); + if (!newPresets) { return {}; } + + std::vector results; + for (auto& preset : *newPresets) + { + auto [it, added] = presets.emplace(std::move(preset)); + if (added) + { + results.push_back(&*it); + } + else + { + std::string msg = "The preset \"" + it->metadata().displayName + "\" was already loaded"; + ClapLog::globalLog(CLAP_LOG_WARNING, msg); + } + } + return results; + }; + + if (const auto preferredIndexer = getIndexerFor(location)) + { + return getPresets(*preferredIndexer); + } + + std::vector results; + for (const auto& indexer : m_indexers) + { + results = getPresets(*indexer); + if (!results.empty()) { break; } + } + + return results; +} + +auto ClapPresetDatabase::getIndexerFor(const Location& location) const -> Indexer* +{ + for (const auto& indexer : m_indexers) + { + const auto& locations = indexer->locations(); + const auto it = std::find(locations.begin(), locations.end(), location); + if (it == locations.end()) { return nullptr; } + return indexer.get(); // exact match + } + + return nullptr; +} + +auto ClapPresetDatabase::toClapLocation(std::string_view location, std::string& ref) + -> std::optional> +{ + const auto base = PathUtil::baseLookup(location); + + clap_preset_discovery_location_kind locationKind; + const char* locationOut = nullptr; + switch (base) + { + case PathUtil::Base::Internal: + locationKind = CLAP_PRESET_DISCOVERY_LOCATION_PLUGIN; + //locationOut = nullptr; + break; + case PathUtil::Base::Absolute: + locationKind = CLAP_PRESET_DISCOVERY_LOCATION_FILE; + locationOut = location.data(); + break; + default: + { + locationKind = CLAP_PRESET_DISCOVERY_LOCATION_FILE; + if (auto temp = PathUtil::toAbsolute(location)) + { + ref = std::move(*temp); + locationOut = ref.c_str(); + } + else + { + ClapLog::globalLog(CLAP_LOG_ERROR, "Failed to get absolute path for preset"); + return std::nullopt; + } + break; + } + } + + return std::pair{ locationKind, locationOut }; +} + +auto ClapPresetDatabase::fromClapLocation(const char* location) -> std::string +{ + return location + ? PathUtil::toShortestRelative(std::string_view{location}) + : std::string{PathUtil::basePrefix(PathUtil::Base::Internal)}; +} + +auto ClapPresetDatabase::fromClapLocation([[maybe_unused]] clap_preset_discovery_location_kind kind, + const char* location, const char* loadKey) -> std::optional +{ + if (!location && !loadKey) { return std::nullopt; } + + return PresetLoadData { + fromClapLocation(location), + loadKey ? loadKey : std::string{} + }; +} + +auto ClapPresetDatabase::Indexer::create(const clap_preset_discovery_factory& factory, std::uint32_t index) + -> std::unique_ptr +{ + const auto desc = factory.get_descriptor(&factory, index); + if (!desc || !desc->id) { return nullptr; } + + if (!clap_version_is_compatible(desc->clap_version)) { return nullptr; } + + auto indexer = std::make_unique(factory, *desc); + return indexer->m_provider ? std::move(indexer) : nullptr; +} + +ClapPresetDatabase::Indexer::Indexer(const clap_preset_discovery_factory& factory, + const clap_preset_discovery_provider_descriptor& descriptor) + : m_indexer { + CLAP_VERSION, + "LMMS", + "LMMS contributors", + "https://lmms.io/", + LMMS_VERSION, + this, + &clapDeclareFiletype, + &clapDeclareLocation, + &clapDeclareSoundpack, + &clapGetExtension + } + , m_provider{factory.create(&factory, &m_indexer, descriptor.id), ProviderDeleter{}} +{ + if (!m_provider) + { + ClapLog::globalLog(CLAP_LOG_ERROR, "Failed to create preset discovery provider"); + return; + } + + if (!m_provider->init || !m_provider->get_metadata) + { + ClapLog::globalLog(CLAP_LOG_ERROR, "Plugin does not fully implement preset discovery provider"); + m_provider.reset(); + return; + } + + if (!m_provider->init(m_provider.get())) + { + ClapLog::globalLog(CLAP_LOG_ERROR, "Failed to initialize preset discovery provider"); + m_provider.reset(); + return; + } +} + +auto ClapPresetDatabase::Indexer::query(PresetMetadata::Flags flags) + -> std::optional> +{ + auto receiver = MetadataReceiver{*this}; + return receiver.query(flags); +} + +auto ClapPresetDatabase::Indexer::query(std::string_view file, PresetMetadata::Flags flags) + -> std::optional> +{ + auto receiver = MetadataReceiver{*this}; + return receiver.query(file, flags); +} + +auto ClapPresetDatabase::Indexer::filetypeSupported(const std::filesystem::path& path) const -> bool +{ + if (m_filetypes.empty() || m_filetypes[0].extension.empty()) { return true; } + + const auto extension = path.extension().string(); + if (extension.empty()) { return false; } + + auto extView = std::string_view{extension}; + extView.remove_prefix(1); // remove dot + return std::find_if(m_filetypes.begin(), m_filetypes.end(), + [&](const Filetype& f) { return f.extension == extView; }) != m_filetypes.end(); +} + +auto ClapPresetDatabase::Indexer::clapDeclareFiletype(const clap_preset_discovery_indexer* indexer, + const clap_preset_discovery_filetype* filetype) -> bool +{ + if (!indexer || !filetype) + { + ClapLog::globalLog(CLAP_LOG_PLUGIN_MISBEHAVING, "Preset provider declared a file type incorrectly"); + return false; + } + + auto self = static_cast(indexer->indexer_data); + if (!self) + { + ClapLog::globalLog(CLAP_LOG_PLUGIN_MISBEHAVING, "Plugin modified the preset indexer's context pointer"); + return false; + } + + auto ft = Filetype{}; + ft.name = filetype->name ? filetype->name : QObject::tr("Preset").toStdString(); + if (filetype->description) { ft.description = filetype->description; } + if (filetype->file_extension) { ft.extension = filetype->file_extension; } + + self->m_filetypes.push_back(std::move(ft)); + return true; +} + +auto ClapPresetDatabase::Indexer::clapDeclareLocation(const clap_preset_discovery_indexer* indexer, + const clap_preset_discovery_location* location) -> bool +{ + if (!indexer || !location) + { + ClapLog::globalLog(CLAP_LOG_PLUGIN_MISBEHAVING, "Preset provider declared a location incorrectly"); + return false; + } + + auto self = static_cast(indexer->indexer_data); + if (!self) + { + ClapLog::globalLog(CLAP_LOG_PLUGIN_MISBEHAVING, "Plugin modified the preset indexer's context pointer"); + return false; + } + + switch (location->kind) + { + case CLAP_PRESET_DISCOVERY_LOCATION_PLUGIN: + if (location->location) + { + ClapLog::globalLog(CLAP_LOG_PLUGIN_MISBEHAVING, + "Preset with PLUGIN location kind must have null location"); + return false; + } + break; + case CLAP_PRESET_DISCOVERY_LOCATION_FILE: + { + if (!location->location) + { + ClapLog::globalLog(CLAP_LOG_PLUGIN_MISBEHAVING, + "Preset with FILE location kind cannot have null location"); + return false; + } + + // A FILE location could be a directory or a file + if (std::error_code ec; !std::filesystem::exists(location->location, ec)) + { + std::string msg = "Preset location \"" + std::string{location->location} + "\" does not exist"; + ClapLog::globalLog(CLAP_LOG_WARNING, msg); + return false; + } + break; + } + default: + ClapLog::globalLog(CLAP_LOG_PLUGIN_MISBEHAVING, "Invalid preset location kind"); + return false; + } + + auto loc = Location { + location->name ? location->name : std::string{}, + fromClapLocation(location->location), + convertFlags(location->flags) + }; + + self->m_locations.push_back(std::move(loc)); + + return true; +} + +auto ClapPresetDatabase::Indexer::clapDeclareSoundpack(const clap_preset_discovery_indexer* indexer, + const clap_preset_discovery_soundpack* soundpack) -> bool +{ + // TODO: Implement later? + return true; +} + +auto ClapPresetDatabase::Indexer::clapGetExtension(const clap_preset_discovery_indexer* indexer, + const char* extensionId) -> const void* +{ + // LMMS does not have any custom indexer extensions + return nullptr; +} + +ClapPresetDatabase::MetadataReceiver::MetadataReceiver(const Indexer& indexer) + : m_receiver { + this, + &clapOnError, + &clapBeginPreset, + &clapAddPluginId, + &clapSetSoundpackId, + &clapSetFlags, + &clapAddCreator, + &clapSetDescription, + &clapSetTimestamps, + &clapAddFeature, + &clapAddExtraInfo + } + , m_indexer{&indexer} +{ +} + +auto ClapPresetDatabase::MetadataReceiver::query(PresetMetadata::Flags flags) + -> std::optional> +{ + const auto provider = m_indexer->provider(); + if (!provider) { return std::nullopt; } + + m_location = PathUtil::basePrefix(PathUtil::Base::Internal); + m_flags = flags; + m_presets.clear(); + if (!provider->get_metadata(provider, CLAP_PRESET_DISCOVERY_LOCATION_PLUGIN, nullptr, &m_receiver)) + { + std::string msg = "Failed to get metadata from the preset discovery provider"; + if (!errorMessage().empty()) { msg += ": " + errorMessage(); } + ClapLog::globalLog(CLAP_LOG_ERROR, msg.c_str()); + return std::nullopt; + } + + return std::move(m_presets); +} + +auto ClapPresetDatabase::MetadataReceiver::query(std::string_view file, + PresetMetadata::Flags flags) -> std::optional> +{ + const auto provider = m_indexer->provider(); + if (!provider) { return std::nullopt; } + + /* + * For preset files, PresetLoadData will usually be { file, "" }, + * but if the preset file contains multiple presets, it will be { file, loadKey }. + */ + + m_location = file; + m_flags = flags; + m_presets.clear(); + if (!provider->get_metadata(provider, CLAP_PRESET_DISCOVERY_LOCATION_FILE, file.data(), &m_receiver)) + { + std::string msg = "Failed to get metadata from the preset discovery provider"; + if (!errorMessage().empty()) { msg += ": " + errorMessage(); } + ClapLog::globalLog(CLAP_LOG_ERROR, msg.c_str()); + return std::nullopt; + } + + return std::move(m_presets); +} + +void ClapPresetDatabase::MetadataReceiver::clapOnError( + const clap_preset_discovery_metadata_receiver* receiver, std::int32_t osError, const char* errorMessage) +{ + if (auto self = from(receiver)) + { + self->m_error = "{ os_error=" + std::to_string(osError) + ", error_message=\"" + + (errorMessage ? errorMessage : std::string{}) + "\" }"; + } +} + +auto ClapPresetDatabase::MetadataReceiver::clapBeginPreset( + const clap_preset_discovery_metadata_receiver* receiver, const char* name, const char* loadKey) -> bool +{ + if (!loadKey) { return false; } + auto self = from(receiver); + if (!self) { return false; } + + self->m_skip = false; + + auto& preset = self->m_presets.emplace_back(); + preset.metadata().displayName = name ? name : std::string{}; + preset.metadata().flags = self->m_flags; // may be overridden by clapSetFlags() + preset.loadData().location = self->m_location; + preset.loadData().loadKey = loadKey ? loadKey : std::string{}; + + return true; +} + +void ClapPresetDatabase::MetadataReceiver::clapAddPluginId( + const clap_preset_discovery_metadata_receiver* receiver, const clap_universal_plugin_id* pluginId) +{ + if (!pluginId || !pluginId->abi || !pluginId->id) + { + ClapLog::globalLog(CLAP_LOG_WARNING, + "Plugin called clap_preset_discovery_metadata_receiver.add_plugin_id() with invalid arguments"); + return; + } + + auto self = from(receiver); + if (!self || self->m_skip) { return; } + + auto& presets = self->m_presets; + if (presets.empty()) + { + ClapLog::globalLog(CLAP_LOG_PLUGIN_MISBEHAVING, + "Preset discovery provider called add_plugin_id() called before begin_preset()"); + return; + } + + if (pluginId->abi != std::string_view{"clap"}) + { + ClapLog::globalLog(CLAP_LOG_WARNING, "Preset must use the \"clap\" abi"); + + // Remove the current preset + self->m_skip = true; // Skip any more metadata calls until next begin_preset() + presets.pop_back(); + return; + } + + presets.back().keys().push_back(pluginId->id); +} + +void ClapPresetDatabase::MetadataReceiver::clapSetSoundpackId( + const clap_preset_discovery_metadata_receiver* receiver, const char* soundpackId) +{ + // [UNIMPLEMENTED] +} + +void ClapPresetDatabase::MetadataReceiver::clapSetFlags( + const clap_preset_discovery_metadata_receiver* receiver, std::uint32_t flags) +{ + auto self = from(receiver); + if (!self || self->m_skip) { return; } + + auto& presets = self->m_presets; + if (presets.empty()) + { + ClapLog::globalLog(CLAP_LOG_PLUGIN_MISBEHAVING, + "Preset discovery provider called set_flags() called before begin_preset()"); + return; + } + + presets.back().metadata().flags = convertFlags(flags); +} + +void ClapPresetDatabase::MetadataReceiver::clapAddCreator( + const clap_preset_discovery_metadata_receiver* receiver, const char* creator) +{ + if (!creator) { return; } + auto self = from(receiver); + if (!self || self->m_skip) { return; } + + auto& presets = self->m_presets; + if (presets.empty()) + { + ClapLog::globalLog(CLAP_LOG_PLUGIN_MISBEHAVING, + "Preset discovery provider called add_creator() called before begin_preset()"); + return; + } + + presets.back().metadata().creator = creator; +} + +void ClapPresetDatabase::MetadataReceiver::clapSetDescription( + const clap_preset_discovery_metadata_receiver* receiver, const char* description) +{ + if (!description) { return; } + auto self = from(receiver); + if (!self || self->m_skip) { return; } + + auto& presets = self->m_presets; + if (presets.empty()) + { + ClapLog::globalLog(CLAP_LOG_PLUGIN_MISBEHAVING, + "Preset discovery provider called set_description() called before begin_preset()"); + return; + } + + presets.back().metadata().description = description; +} + +void ClapPresetDatabase::MetadataReceiver::clapSetTimestamps( + const clap_preset_discovery_metadata_receiver* receiver, + clap_timestamp creationTime, clap_timestamp modificationTime) +{ + // [UNIMPLEMENTED] +} + +void ClapPresetDatabase::MetadataReceiver::clapAddFeature( + const clap_preset_discovery_metadata_receiver* receiver, const char* feature) +{ + if (!feature) { return; } + auto self = from(receiver); + if (!self || self->m_skip) { return; } + + auto& presets = self->m_presets; + if (presets.empty()) + { + ClapLog::globalLog(CLAP_LOG_PLUGIN_MISBEHAVING, + "Preset discovery provider called add_feature() called before begin_preset()"); + return; + } + + presets.back().metadata().categories.push_back(feature); +} + +void ClapPresetDatabase::MetadataReceiver::clapAddExtraInfo( + const clap_preset_discovery_metadata_receiver* receiver, const char* key, const char* value) +{ + // [UNIMPLEMENTED] +} + +auto ClapPresetDatabase::MetadataReceiver::from( + const clap_preset_discovery_metadata_receiver* receiver) -> MetadataReceiver* +{ + if (!receiver || !receiver->receiver_data) + { + ClapLog::globalLog(CLAP_LOG_ERROR, + "Preset discovery metadata receiver's context pointer was invalidated by the plugin."); + return nullptr; + } + return static_cast(receiver->receiver_data); +} + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP diff --git a/src/core/clap/ClapPresetLoader.cpp b/src/core/clap/ClapPresetLoader.cpp new file mode 100644 index 00000000000..385d82d0bb0 --- /dev/null +++ b/src/core/clap/ClapPresetLoader.cpp @@ -0,0 +1,139 @@ +/* + * ClapPresetLoader.cpp - Implements CLAP preset load extension + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 "ClapPresetLoader.h" + +#ifdef LMMS_HAVE_CLAP + +#include + +#include "ClapInstance.h" +#include "ClapManager.h" +#include "ClapPresetDatabase.h" +#include "Engine.h" + +namespace lmms +{ + +ClapPresetLoader::ClapPresetLoader(Model* parent, ClapInstance* inst) + : ClapExtension{inst} + , PluginPresets{parent, nullptr, inst->info().descriptor().id} +{ + assert(instance() && "ClapExtension was not constructed properly"); + + const auto mgr = Engine::getClapManager(); + const auto id = instance()->info().descriptor().id; + setPresetDatabase(mgr->presetDatabase(id)); +} + +auto ClapPresetLoader::hostExtImpl() const -> const clap_host_preset_load* +{ + static clap_host_preset_load ext { + &clapOnError, + &clapLoaded + }; + return &ext; +} + +auto ClapPresetLoader::checkSupported(const clap_plugin_preset_load& ext) -> bool +{ + return ext.from_location; +} + +auto ClapPresetLoader::activatePresetImpl(const PresetLoadData& preset) noexcept -> bool +{ + assert(ClapThreadCheck::isMainThread()); + if (!supported()) { return false; } + + std::string temp; + const auto location = ClapPresetDatabase::toClapLocation(preset.location, temp); + if (!location) { return false; } + +#if 0 + { + std::string msg = "About to activate preset: kind:" + std::to_string(location->first) + + "; location: \"" + std::string{location->second ? location->second : "(NULL)"} + + "\"; load key: \"" + preset.loadKey + "\""; + logger().log(CLAP_LOG_INFO, msg); + } +#endif + + if (!pluginExt()->from_location(plugin(), location->first, location->second, preset.loadKey.c_str())) + { + logger().log(CLAP_LOG_ERROR, "Failed to load preset"); + return false; + } + + if (!instance()->params().supported()) { return true; } + + return instance()->params().rescan(CLAP_PARAM_RESCAN_VALUES); // TODO: Is this correct? +} + +void ClapPresetLoader::clapOnError(const clap_host* host, std::uint32_t locationKind, + const char* location, const char* loadKey, std::int32_t osError, const char* msg) +{ + const auto h = fromHost(host); + if (!h) { return; } + + const std::string text = "Preset load error: location kind: " + std::to_string(locationKind) + + "; location: \"" + std::string{location ? location : ""} + "\"; load key: \"" + + std::string{loadKey ? loadKey : ""} + "\"; OS error: " + std::to_string(osError) + + "; msg: \"" + std::string{msg ? msg : ""} + "\""; + + h->logger().log(CLAP_LOG_ERROR, text); +} + +void ClapPresetLoader::clapLoaded(const clap_host* host, std::uint32_t locationKind, + const char* location, const char* loadKey) +{ + const auto h = fromHost(host); + if (!h) { return; } + auto& self = h->presetLoader(); + +#if 0 + const std::string text = "Loaded preset: location kind: " + std::to_string(locationKind) + + "; location: \"" + std::string{location ? location : ""} + "\"; load key: \"" + + std::string{loadKey ? loadKey : ""} + "\""; + + h->logger().log(CLAP_LOG_INFO, text); +#endif + + const auto loadData = ClapPresetDatabase::fromClapLocation( + static_cast(locationKind), location, loadKey); + if (!loadData) { return; } + + const auto index = self.findPreset(*loadData); + if (!index) + { + // TODO: Can we assume preset always exists in `m_presets`? + h->logger().log(CLAP_LOG_ERROR, "Could not find preset in database"); + return; + } + + self.setActivePreset(*index); +} + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP diff --git a/src/core/clap/ClapState.cpp b/src/core/clap/ClapState.cpp new file mode 100644 index 00000000000..31891fdb742 --- /dev/null +++ b/src/core/clap/ClapState.cpp @@ -0,0 +1,223 @@ +/* + * ClapState.cpp - Implements CLAP state extension + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 "ClapState.h" + +#ifdef LMMS_HAVE_CLAP + +#include +#include +#include +#include + +#include "ClapInstance.h" +#include "base64.h" + +namespace lmms +{ + +static_assert(static_cast(ClapState::Context::Preset) == CLAP_STATE_CONTEXT_FOR_PRESET); +static_assert(static_cast(ClapState::Context::Duplicate) == CLAP_STATE_CONTEXT_FOR_DUPLICATE); +static_assert(static_cast(ClapState::Context::Project) == CLAP_STATE_CONTEXT_FOR_PROJECT); + +auto ClapState::initImpl() noexcept -> bool +{ + m_stateContext = static_cast( + plugin()->get_extension(plugin(), CLAP_EXT_STATE_CONTEXT)); + + if (m_stateContext && (!m_stateContext->load || !m_stateContext->save)) + { + m_stateContext = nullptr; + logger().log(CLAP_LOG_WARNING, "State context extension is not fully implemented"); + return true; + } + + if (m_stateContext) + { + logger().log(CLAP_LOG_INFO, "State context extension is supported"); + } + + return true; +} + +void ClapState::deinitImpl() noexcept +{ + m_stateContext = nullptr; +} + +auto ClapState::hostExtImpl() const -> const clap_host_state* +{ + static clap_host_state ext { + &clapMarkDirty + }; + return &ext; +} + +auto ClapState::checkSupported(const clap_plugin_state& ext) -> bool +{ + return ext.load && ext.save; +} + +auto ClapState::load(std::string_view base64, Context context) -> bool +{ + assert(ClapThreadCheck::isMainThread()); + + if (!supported()) { return false; } + + class IStream + { + public: + IStream(std::string_view base64) + : m_state{QByteArray::fromBase64(QByteArray::fromRawData(base64.data(), base64.size()))} + { + } + + //! Implements clap_istream.read + static auto clapRead(const clap_istream* stream, void* buffer, std::uint64_t size) -> std::int64_t + { + if (!stream || !buffer || size == 0) { return -1; } // error + auto self = static_cast(stream->ctx); + if (!self) { return -1; } // error + + const auto bytesLeft = self->m_state.size() - self->m_readPos; + if (bytesLeft == 0) { return 0; } // end of file + const auto readAmount = std::min(bytesLeft, size); + + auto ptr = static_cast(buffer); + std::memcpy(ptr, self->m_state.data() + self->m_readPos, readAmount); + self->m_readPos += readAmount; + + return readAmount; + } + private: + QByteArray m_state; //!< unencoded state data + std::uint64_t m_readPos = 0; + } stream{base64}; + + const auto clapStream = clap_istream { + &stream, + &IStream::clapRead + }; + + const auto success = m_stateContext && context != Context::None + ? m_stateContext->load(plugin(), &clapStream, static_cast(context)) + : pluginExt()->load(plugin(), &clapStream); + + if (!success) + { + if (auto h = fromHost(host())) + { + h->logger().log(CLAP_LOG_WARNING, "Plugin failed to load its state"); + } + return false; + } + + m_dirty = false; + + return true; +} + +auto ClapState::load(Context context) -> bool +{ + return load(encodedState(), context); +} + +auto ClapState::save(Context context) -> std::optional +{ + assert(ClapThreadCheck::isMainThread()); + + if (!supported()) { return std::nullopt; } + + struct OStream + { + //! Implements clap_ostream.write + static auto clapWrite(const clap_ostream* stream, const void* buffer, std::uint64_t size) -> std::int64_t + { + if (!stream || !buffer) { return -1; } // error + auto self = static_cast(stream->ctx); + if (!self) { return -1; } // error + if (size == 0) { return 0; } + + const auto ptr = static_cast(buffer); + + self->state.reserve(self->state.size() + size); + for (std::uint64_t idx = 0; idx < size; ++idx) + { + self->state.push_back(ptr[idx]); + } + + return size; + } + + std::vector state; + } stream; + + const auto clapStream = clap_ostream { + &stream, + &OStream::clapWrite + }; + + const auto success = m_stateContext && context != Context::None + ? m_stateContext->save(plugin(), &clapStream, static_cast(context)) + : pluginExt()->save(plugin(), &clapStream); + + if (!success) + { + logger().log(CLAP_LOG_WARNING, "Plugin failed to save its state"); + return std::nullopt; + } + + QString base64; + base64::encode(stream.state.data(), stream.state.size(), base64); + + m_state = base64.toStdString(); + m_dirty = false; + + return encodedState(); +} + +void ClapState::clapMarkDirty(const clap_host* host) +{ + auto h = fromHost(host); + if (!h) { return; } + auto& state = h->state(); + + if (!ClapThreadCheck::isMainThread()) + { + h->logger().log(CLAP_LOG_PLUGIN_MISBEHAVING, "Called state.mark_dirty() from wrong thread"); + } + + if (!state.supported()) + { + h->logger().log(CLAP_LOG_PLUGIN_MISBEHAVING, "Plugin called clap_host_state.set_dirty()" + " but the plugin does not provide a complete clap_plugin_state interface."); + return; + } + + state.m_dirty = true; +} + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP diff --git a/src/core/clap/ClapSubPluginFeatures.cpp b/src/core/clap/ClapSubPluginFeatures.cpp new file mode 100644 index 00000000000..9aace1c115b --- /dev/null +++ b/src/core/clap/ClapSubPluginFeatures.cpp @@ -0,0 +1,153 @@ +/* + * ClapSubPluginFeatures.cpp - derivation from + * Plugin::Descriptor::SubPluginFeatures for + * hosting CLAP plugins + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 "ClapSubPluginFeatures.h" + +#ifdef LMMS_HAVE_CLAP + +#include +#include +#include + +#include "ClapManager.h" +#include "Engine.h" + +namespace lmms +{ + + +ClapSubPluginFeatures::ClapSubPluginFeatures(Plugin::Type type) + : SubPluginFeatures{type} +{ +} + +void ClapSubPluginFeatures::fillDescriptionWidget(QWidget* parent, const Key* key) const +{ + const auto& descriptor = pluginInfo(*key)->descriptor(); + + auto label = new QLabel(parent); + label->setText(QWidget::tr("Name: ") + QString::fromUtf8(descriptor.name)); + + auto versionLabel = new QLabel(parent); + versionLabel->setText(QWidget::tr("Version: ") + QString::fromUtf8(descriptor.version)); + + auto urlLabel = new QLabel(parent); + urlLabel->setText(QWidget::tr("URL: ") + QString::fromUtf8(descriptor.url)); + + if (descriptor.manual_url && descriptor.manual_url[0] != '\0') + { + auto urlLabel = new QLabel(parent); + urlLabel->setText(QWidget::tr("Manual URL: ") + QString::fromUtf8(descriptor.manual_url)); + } + + if (descriptor.support_url && descriptor.support_url[0] != '\0') + { + auto urlLabel = new QLabel(parent); + urlLabel->setText(QWidget::tr("Support URL: ") + QString::fromUtf8(descriptor.support_url)); + } + + auto author = new QWidget(parent); + auto l = new QHBoxLayout(author); + l->setContentsMargins(0, 0, 0, 0); + l->setSpacing(0); + + auto authorLabel = new QLabel(author); + authorLabel->setText(QWidget::tr("Author: ")); + authorLabel->setAlignment(Qt::AlignTop); + + auto authorContent = new QLabel(author); + authorContent->setText(QString::fromUtf8(descriptor.vendor)); + authorContent->setWordWrap(true); + + l->addWidget(authorLabel); + l->addWidget(authorContent, 1); + + auto copyright = new QWidget(parent); + l = new QHBoxLayout(copyright); + l->setContentsMargins(0, 0, 0, 0); + l->setSpacing(0); + copyright->setMinimumWidth(parent->minimumWidth()); + + auto copyrightLabel = new QLabel(copyright); + copyrightLabel->setText(QWidget::tr("Copyright: ")); + copyrightLabel->setAlignment(Qt::AlignTop); + + auto copyrightContent = new QLabel(copyright); + copyrightContent->setText(""); + copyrightContent->setWordWrap(true); + l->addWidget(copyrightLabel); + l->addWidget(copyrightContent, 1); + + // Possibly TODO: project, plugin type, number of channels +} + +void ClapSubPluginFeatures::listSubPluginKeys(const Plugin::Descriptor* desc, KeyList& kl) const +{ + for (const auto& file : Engine::getClapManager()->files()) + { + for (const auto& info : file.pluginInfo()) + { + if (!info || info->type() != m_type) { continue; } + + const auto& plugin = info->descriptor(); + Key::AttributeMap atm; + atm["uri"] = QString::fromUtf8(plugin.id); + + kl.push_back(Key{desc, QString::fromUtf8(plugin.name), atm}); + } + } +} + +auto ClapSubPluginFeatures::additionalFileExtensions([[maybe_unused]] const Key& key) const -> QString +{ + // CLAP only loads .clap files + return QString{}; +} + +auto ClapSubPluginFeatures::displayName(const Key& key) const -> QString +{ + return QString::fromUtf8(pluginInfo(key)->descriptor().name); +} + +auto ClapSubPluginFeatures::description(const Key& key) const -> QString +{ + return QString::fromUtf8(pluginInfo(key)->descriptor().description); +} + +auto ClapSubPluginFeatures::logo([[maybe_unused]] const Key& key) const -> const PixmapLoader* +{ + return nullptr; +} + +auto ClapSubPluginFeatures::pluginInfo(const Key& key) -> const ClapPluginInfo* +{ + const auto uri = key.attributes["uri"].toStdString(); + return Engine::getClapManager()->pluginInfo(uri); +} + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP diff --git a/src/core/clap/ClapThreadCheck.cpp b/src/core/clap/ClapThreadCheck.cpp new file mode 100644 index 00000000000..b3b83c69bad --- /dev/null +++ b/src/core/clap/ClapThreadCheck.cpp @@ -0,0 +1,57 @@ +/* + * ClapThreadCheck.cpp - Implements CLAP thread check extension + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 "ClapThreadCheck.h" + +#ifdef LMMS_HAVE_CLAP + +#include +#include + +namespace lmms +{ + +auto ClapThreadCheck::hostExtImpl() const -> const clap_host_thread_check* +{ + static clap_host_thread_check ext { + &clapIsMainThread, + &clapIsAudioThread + }; + return &ext; +} + +auto ClapThreadCheck::clapIsMainThread([[maybe_unused]] const clap_host* host) -> bool +{ + return QThread::currentThread() == QCoreApplication::instance()->thread(); +} + +auto ClapThreadCheck::clapIsAudioThread([[maybe_unused]] const clap_host* host) -> bool +{ + // Assume any non-GUI thread is an audio thread + return QThread::currentThread() != QCoreApplication::instance()->thread(); +} + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP diff --git a/src/core/clap/ClapTimerSupport.cpp b/src/core/clap/ClapTimerSupport.cpp new file mode 100644 index 00000000000..6855043cc23 --- /dev/null +++ b/src/core/clap/ClapTimerSupport.cpp @@ -0,0 +1,151 @@ +/* + * ClapTimerSupport.cpp - Implements CLAP timer support extension + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 "ClapTimerSupport.h" + +#ifdef LMMS_HAVE_CLAP + +#include +#include +#include + +#include "ClapInstance.h" + +namespace lmms +{ + +ClapTimerSupport::ClapTimerSupport(ClapInstance* parent) + : QObject{parent} + , ClapExtension{parent} +{ +} + +void ClapTimerSupport::deinitImpl() noexcept +{ + killTimers(); +} + +auto ClapTimerSupport::hostExtImpl() const -> const clap_host_timer_support* +{ + static clap_host_timer_support ext { + &clapRegisterTimer, + &clapUnregisterTimer + }; + return &ext; +} + +auto ClapTimerSupport::checkSupported(const clap_plugin_timer_support& ext) -> bool +{ + return ext.on_timer; +} + +void ClapTimerSupport::killTimers() +{ + for (int timerId : m_timerIds) + { + killTimer(timerId); + } + m_timerIds.clear(); +} + +auto ClapTimerSupport::clapRegisterTimer(const clap_host* host, + std::uint32_t periodMilliseconds, clap_id* timerId) -> bool +{ + assert(ClapThreadCheck::isMainThread()); + const auto h = fromHost(host); + if (!h) { return false; } + + if (!timerId) + { + h->logger().log(CLAP_LOG_PLUGIN_MISBEHAVING, "register_timer()'s `timerId` cannot be null"); + return false; + } + *timerId = CLAP_INVALID_ID; + + auto& timerSupport = h->timerSupport(); + if (!timerSupport.supported()) + { + h->logger().log(CLAP_LOG_PLUGIN_MISBEHAVING, + "Plugin cannot register a timer when it does not implement the timer support extension"); + return false; + } + + // Period should be no lower than 15 ms (arbitrary) + periodMilliseconds = std::max(15, periodMilliseconds); + + const auto id = timerSupport.startTimer(periodMilliseconds, + periodMilliseconds >= 2000 ? Qt::CoarseTimer : Qt::PreciseTimer); + if (id <= 0) + { + h->logger().log(CLAP_LOG_WARNING, "Failed to start timer"); + return false; + } + + *timerId = static_cast(id); + timerSupport.m_timerIds.insert(*timerId); + + return true; +} + +auto ClapTimerSupport::clapUnregisterTimer(const clap_host* host, clap_id timerId) -> bool +{ + assert(ClapThreadCheck::isMainThread()); + const auto h = fromHost(host); + if (!h) { return false; } + + if (timerId == 0 || timerId == CLAP_INVALID_ID) + { + h->logger().log(CLAP_LOG_PLUGIN_MISBEHAVING, "Invalid timer id"); + return false; + } + + auto& timerSupport = h->timerSupport(); + if (auto it = timerSupport.m_timerIds.find(timerId); it != timerSupport.m_timerIds.end()) + { + timerSupport.killTimer(static_cast(timerId)); + timerSupport.m_timerIds.erase(it); + return true; // assume successful + } + else + { + const auto msg = "Unrecognized timer id: " + std::to_string(timerId); + h->logger().log(CLAP_LOG_PLUGIN_MISBEHAVING, msg); + return false; + } +} + +void ClapTimerSupport::timerEvent(QTimerEvent* event) +{ + assert(ClapThreadCheck::isMainThread()); + const auto timerId = static_cast(event->timerId()); + assert(timerId > 0 && "Timer must be active"); + if (pluginExt() && plugin()) + { + pluginExt()->on_timer(plugin(), timerId); + } +} + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP diff --git a/src/core/clap/ClapTransport.cpp b/src/core/clap/ClapTransport.cpp new file mode 100644 index 00000000000..5d689fa4e48 --- /dev/null +++ b/src/core/clap/ClapTransport.cpp @@ -0,0 +1,151 @@ +/* + * ClapTransport.cpp - CLAP transport events + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 "ClapTransport.h" + +#ifdef LMMS_HAVE_CLAP + +#include "Engine.h" +#include "Song.h" + +namespace lmms +{ + +namespace +{ + +template +inline void setBit(T& number, bool value) noexcept +{ + static_assert(mask && !(mask & (mask - 1)), "mask must have single bit set"); + constexpr auto bitPos = [=] { + // constexpr log2 + unsigned pos = 0; + auto x = mask; + while (x != 0) { + x >>= 1; + ++pos; + } + return pos - 1; + }(); + static_assert(bitPos < sizeof(T) * 8, "mask is too big for T"); + number = (number & ~mask) | (static_cast(value) << bitPos); +} + +} // namespace + +clap_event_transport ClapTransport::s_transport = {}; + +void ClapTransport::update() +{ + // TODO: If the main thread calls this while any of the setters + // are called, a data race could occur + + s_transport = {}; + s_transport.header.size = sizeof(clap_event_transport); + s_transport.header.type = CLAP_EVENT_TRANSPORT; + + const Song* song = Engine::getSong(); + if (!song) { return; } + + s_transport.flags = 0; + + setPlaying(song->isPlaying()); + setRecording(song->isRecording()); + //setLooping(song->isLooping()); + + // TODO: Pre-roll, isLooping, tempo_inc + + setBeatPosition(); + setTimePosition(song->getMilliseconds()); + + setTempo(song->getTempo()); + setTimeSignature(song->getTimeSigModel().getNumerator(), song->getTimeSigModel().getDenominator()); +} + +void ClapTransport::setPlaying(bool isPlaying) +{ + setBit(s_transport.flags, isPlaying); +} + +void ClapTransport::setRecording(bool isRecording) +{ + setBit(s_transport.flags, isRecording); +} + +void ClapTransport::setLooping(bool isLooping) +{ + setBit(s_transport.flags, isLooping); + // TODO: loop_start_* and loop_end_* +} + +void ClapTransport::setBeatPosition() +{ + const Song* song = Engine::getSong(); + if (!song) { return; } + + s_transport.flags |= static_cast(CLAP_TRANSPORT_HAS_BEATS_TIMELINE); + + // Logic taken from TimeDisplayWidget.cpp + // NOTE: If the time signature changes during the song, this info may be misleading + const auto tick = song->getPlayPos().getTicks(); + const auto ticksPerBar = song->ticksPerBar(); + const auto timeSigNum = song->getTimeSigModel().getNumerator(); + const auto barsElapsed = static_cast(tick / ticksPerBar); // zero-based + + s_transport.bar_number = barsElapsed; + + const auto barsElapsedInBeats = (barsElapsed * timeSigNum) + 1; // one-based + + s_transport.bar_start = barsElapsedInBeats << 31; // same as multiplication by CLAP_BEATTIME_FACTOR + + const auto beatWithinBar = 1.0 * (tick % ticksPerBar) / (ticksPerBar / timeSigNum); // zero-based + const auto beatsElapsed = barsElapsedInBeats + beatWithinBar; // one-based + + s_transport.song_pos_beats = std::lround(CLAP_BEATTIME_FACTOR * beatsElapsed); +} + +void ClapTransport::setTimePosition(int elapsedMilliseconds) +{ + s_transport.flags |= static_cast(CLAP_TRANSPORT_HAS_SECONDS_TIMELINE); + s_transport.song_pos_seconds = std::lround(CLAP_SECTIME_FACTOR * (elapsedMilliseconds / 1000.0)); +} + +void ClapTransport::setTempo(bpm_t tempo) +{ + s_transport.flags |= static_cast(CLAP_TRANSPORT_HAS_TEMPO); + s_transport.tempo = static_cast(tempo); + // TODO: tempo_inc +} + +void ClapTransport::setTimeSignature(int num, int denom) +{ + s_transport.flags |= static_cast(CLAP_TRANSPORT_HAS_TIME_SIGNATURE); + s_transport.tsig_num = static_cast(num); + s_transport.tsig_denom = static_cast(denom); +} + +} // namespace lmms + +#endif // LMMS_HAVE_CLAP diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index fe4a2c462b2..b5e14085ae3 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -4,6 +4,7 @@ SET(LMMS_SRCS gui/AudioAlsaSetupWidget.cpp gui/AudioDeviceSetupWidget.cpp gui/AutomatableModelView.cpp + gui/ClapViewBase.cpp gui/ControlLayout.cpp gui/ControllerDialog.cpp gui/ControllerRackView.cpp @@ -31,6 +32,7 @@ SET(LMMS_SRCS gui/ModelView.cpp gui/PeakControllerDialog.cpp gui/PluginBrowser.cpp + gui/PresetSelector.cpp gui/ProjectNotes.cpp gui/RowTableView.cpp gui/SampleLoader.cpp diff --git a/src/gui/ClapViewBase.cpp b/src/gui/ClapViewBase.cpp new file mode 100644 index 00000000000..99ff69b6d80 --- /dev/null +++ b/src/gui/ClapViewBase.cpp @@ -0,0 +1,243 @@ +/* + * ClapViewBase.cpp - Base class for CLAP plugin views + * + * Copyright (c) 2024 Dalton Messmer + * + * 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 "ClapViewBase.h" + +#ifdef LMMS_HAVE_CLAP + +#include +#include +#include +#include +#include +#include + +#include "AudioEngine.h" +#include "ClapInstance.h" +#include "ClapManager.h" +#include "ComboBox.h" +#include "ControlLayout.h" +#include "CustomTextKnob.h" +#include "embed.h" +#include "Engine.h" +#include "FontHelper.h" +#include "GuiApplication.h" +#include "lmms_math.h" +#include "MainWindow.h" +#include "PixmapButton.h" +#include "PresetSelector.h" +#include "SubWindow.h" + +namespace lmms::gui +{ + +ClapViewParameters::ClapViewParameters(QWidget* parent, ClapInstance* instance, int colNum) + : QWidget{parent} + , m_instance{instance} + , m_layout{new ControlLayout{this}} +{ + setFocusPolicy(Qt::StrongFocus); + + auto addControl = [&](std::unique_ptr ctrl, std::string_view display) { + if (!ctrl) { return; } + + auto box = new QWidget{this}; + auto boxLayout = new QHBoxLayout{box}; + boxLayout->addWidget(ctrl->topWidget()); + + // Required, so the Layout knows how to sort/filter widgets by string + box->setObjectName(QString::fromUtf8(display.data(), display.size())); + m_layout->addWidget(box); + + m_widgets.push_back(std::move(ctrl)); + }; + + for (auto param : m_instance->params().parameters()) + { + if (!param || !param->model()) { continue; } + + std::unique_ptr control; + + switch (param->valueType()) + { + case ClapParameter::ValueType::Bool: + control = std::make_unique(); + break; + case ClapParameter::ValueType::Integer: [[fallthrough]]; + case ClapParameter::ValueType::Enum: + { + const int digits = std::max( + numDigitsAsInt(param->info().min_value), + numDigitsAsInt(param->info().max_value)); + + if (digits < 3) + { + control = std::make_unique(digits); + } + else + { + control = std::make_unique(); + } + break; + } + // TODO: Add support for enum controls + case ClapParameter::ValueType::Float: + { + control = std::make_unique(); + + // CustomTextKnob calls this lambda to update value text + auto customTextKnob = dynamic_cast(control->modelView()); + customTextKnob->setValueText([=] { + return QString::fromUtf8(m_instance->params().getValueText(*param).c_str()); + }); + + break; + } + default: + throw std::runtime_error{"Invalid CLAP param value type"}; + } + + if (!control) { continue; } + + control->setModel(param->model()); // TODO? + + // This is the param name seen in the GUI + control->setText(QString::fromUtf8(param->displayName().data())); + + // TODO: Group parameters according to module path? + if (param->info().module[0] != '\0') + { + control->topWidget()->setToolTip(QString::fromUtf8(param->info().module)); + } + + addControl(std::move(control), param->displayName()); + } +} + +ClapViewBase::ClapViewBase(QWidget* pluginWidget, ClapInstance* instance) +{ + constexpr int controlsPerRow = 6; + auto grid = new QGridLayout{pluginWidget}; + + auto btnBox = std::make_unique(); + if (ClapManager::debugging()) + { + m_reloadPluginButton = new QPushButton{QObject::tr("Reload Plugin"), pluginWidget}; + btnBox->addWidget(m_reloadPluginButton); + } + + if (instance->gui().supported()) + { + m_toggleUIButton = new QPushButton{QObject::tr("Show GUI"), pluginWidget}; + m_toggleUIButton->setCheckable(true); + m_toggleUIButton->setChecked(false); + m_toggleUIButton->setIcon(embed::getIconPixmap("zoom")); + m_toggleUIButton->setFont(adjustedToPixelSize(m_toggleUIButton->font(), SMALL_FONT_SIZE)); + btnBox->addWidget(m_toggleUIButton); + } + + if (instance->presetLoader().supported()) + { + auto presetBox = std::make_unique(); + m_presetSelector = new PresetSelector{&instance->presetLoader(), pluginWidget}; + presetBox->addWidget(m_presetSelector); + grid->addLayout(presetBox.release(), static_cast(Rows::PresetRow), 0, 1, 1); + } + + if (instance->audioPorts().hasMonoPort()) + { + m_portConfig = new ComboBox{pluginWidget}; + m_portConfig->setFixedSize(128, ComboBox::DEFAULT_HEIGHT); + + QString inputType; + switch (instance->audioPorts().inputPortType()) + { + case PluginPortConfig::PortType::None: break; + case PluginPortConfig::PortType::Mono: + inputType += QObject::tr("mono in"); break; + case PluginPortConfig::PortType::Stereo: + inputType += QObject::tr("stereo in"); break; + default: break; + } + + QString outputType; + switch (instance->audioPorts().outputPortType()) + { + case PluginPortConfig::PortType::None: break; + case PluginPortConfig::PortType::Mono: + outputType += QObject::tr("mono out"); break; + case PluginPortConfig::PortType::Stereo: + outputType += QObject::tr("stereo out"); break; + default: break; + } + + QString pluginType; + if (inputType.isEmpty()) { pluginType = outputType; } + else if (outputType.isEmpty()) { pluginType = inputType; } + else { pluginType = QString{"%1, %2"}.arg(inputType, outputType); } + + m_portConfig->setToolTip(QObject::tr("L/R channel config for %1 plugin").arg(pluginType)); + m_portConfig->setModel(instance->audioPorts().model()); + btnBox->addWidget(m_portConfig); + } + + btnBox->addStretch(1); + + pluginWidget->setAcceptDrops(true); + + if (m_reloadPluginButton || m_toggleUIButton || m_portConfig) + { + grid->addLayout(btnBox.release(), static_cast(Rows::ButtonRow), 0, 1, controlsPerRow); + } + + m_parametersView = new ClapViewParameters{pluginWidget, instance, controlsPerRow}; + grid->addWidget(m_parametersView, static_cast(Rows::ParametersRow), 0); +} + + +ClapViewBase::~ClapViewBase() +{ + // TODO: hide UI if required +} + +void ClapViewBase::toggleUI() +{ +} + +void ClapViewBase::modelChanged(ClapInstance* instance) +{ + // reconnect models + if (m_toggleUIButton) + { + m_toggleUIButton->setChecked(instance->gui().supported()); + } + + //m_presetsView->modelChanged(&ctrlBase->presetsGroup()); + + // TODO: How to handle presets? +} + +} // namespace lmms::gui + +#endif // LMMS_HAVE_CLAP diff --git a/src/gui/Controls.cpp b/src/gui/Controls.cpp index 209b0fce1f9..32e7b536478 100644 --- a/src/gui/Controls.cpp +++ b/src/gui/Controls.cpp @@ -32,11 +32,13 @@ #include "LcdSpinBox.h" #include "LedCheckBox.h" #include "Knob.h" +#include "CustomTextKnob.h" namespace lmms::gui { +// KnobControl void KnobControl::setText(const QString &text) { m_knob->setLabel(text); } @@ -55,6 +57,27 @@ KnobControl::KnobControl(QWidget *parent) : m_knob(new Knob(parent)) {} +// CustomTextKnobControl + +void CustomTextKnobControl::setText(const QString &text) { m_knob->setLabel(text); } + +QWidget *CustomTextKnobControl::topWidget() { return m_knob; } + +void CustomTextKnobControl::setModel(AutomatableModel *model) +{ + m_knob->setModel(model->dynamicCast(true)); +} + +FloatModel *CustomTextKnobControl::model() { return m_knob->model(); } + +AutomatableModelView* CustomTextKnobControl::modelView() { return m_knob; } + +CustomTextKnobControl::CustomTextKnobControl(QWidget *parent) : + m_knob(new CustomTextKnob(parent)) {} + + +// ComboControl + void ComboControl::setText(const QString &text) { m_label->setText(text); } void ComboControl::setModel(AutomatableModel *model) @@ -79,6 +102,7 @@ ComboControl::ComboControl(QWidget *parent) : } +// CheckControl void CheckControl::setText(const QString &text) { m_label->setText(text); } @@ -104,7 +128,7 @@ CheckControl::CheckControl(QWidget *parent) : } - +// LcdControl void LcdControl::setText(const QString &text) { m_lcd->setLabel(text); } diff --git a/src/gui/PluginBrowser.cpp b/src/gui/PluginBrowser.cpp index 2594bdab374..30d00b5059b 100644 --- a/src/gui/PluginBrowser.cpp +++ b/src/gui/PluginBrowser.cpp @@ -36,6 +36,7 @@ #include "embed.h" #include "Engine.h" #include "InstrumentTrack.h" +#include "Instrument.h" #include "Song.h" #include "StringPairDrag.h" #include "TrackContainerView.h" @@ -307,8 +308,17 @@ void PluginDescWidget::openInNewInstrumentTrack(QString value) { TrackContainer* tc = Engine::getSong(); auto it = dynamic_cast(Track::create(Track::Type::Instrument, tc)); - auto ilt = new InstrumentLoaderThread(this, it, value); - ilt->start(); + + if (value == "clapinstrument") + { + // Special case for CLAP, since CLAP API requires plugin to load on main thread + it->loadInstrument(value, nullptr, true /*always DnD*/); + } + else + { + auto ilt = new InstrumentLoaderThread(this, it, value); + ilt->start(); + } } diff --git a/src/gui/PresetSelector.cpp b/src/gui/PresetSelector.cpp new file mode 100644 index 00000000000..1e504027d42 --- /dev/null +++ b/src/gui/PresetSelector.cpp @@ -0,0 +1,256 @@ +/* + * PresetSelector.cpp - A simple preset selector for PluginPresets + * + * Copyright (c) 2024 Dalton Messmer + * + * 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. + * + */ + +// TODO: Use this for LV2 and VST presets + +#include "PresetSelector.h" + +#include +#include +#include +#include +#include + +#include "embed.h" +#include "FileDialog.h" +#include "PathUtil.h" +#include "PixmapButton.h" +#include "PluginPresets.h" + +namespace lmms::gui +{ + +PresetSelector::PresetSelector(PluginPresets* presets, QWidget* parent) + : QToolBar{parent} + , IntModelView{presets, parent} + , m_presets{presets} +{ + setSizePolicy({QSizePolicy::MinimumExpanding, QSizePolicy::Fixed}); + + m_activePreset = new QLabel{this}; + updateActivePreset(); + + connect(m_presets, &PluginPresets::activePresetChanged, this, &PresetSelector::updateActivePreset); + connect(m_presets, &PluginPresets::activePresetModified, this, &PresetSelector::updateActivePreset); + + m_prevPresetButton = new PixmapButton{this}; + m_prevPresetButton->setCheckable(false); + m_prevPresetButton->setCursor(Qt::PointingHandCursor); + m_prevPresetButton->setActiveGraphic(embed::getIconPixmap("stepper-left-press")); + m_prevPresetButton->setInactiveGraphic(embed::getIconPixmap("stepper-left")); + m_prevPresetButton->setToolTip(tr("Previous preset")); + m_prevPresetButton->setFixedSize(16, 16); + + connect(m_prevPresetButton, &PixmapButton::clicked, this, [&] { m_presets->prevPreset(); }); + + m_nextPresetButton = new PixmapButton{this}; + m_nextPresetButton->setCheckable(false); + m_nextPresetButton->setCursor(Qt::PointingHandCursor); + m_nextPresetButton->setActiveGraphic(embed::getIconPixmap("stepper-right-press")); + m_nextPresetButton->setInactiveGraphic(embed::getIconPixmap("stepper-right")); + m_nextPresetButton->setToolTip(tr("Next preset")); + m_nextPresetButton->setFixedSize(16, 16); + + connect(m_nextPresetButton, &PixmapButton::clicked, this, [&] { m_presets->nextPreset(); }); + + m_selectPresetButton = new QPushButton{this}; + m_selectPresetButton->setCheckable(false); + m_selectPresetButton->setCursor(Qt::PointingHandCursor); + m_selectPresetButton->setIcon(embed::getIconPixmap("stepper-down")); + m_selectPresetButton->setToolTip(tr("Select preset")); + auto menu = new QMenu{}; + m_selectPresetButton->setMenu(menu); + m_selectPresetButton->setFixedSize(16, 16); + + connect(menu, &QMenu::aboutToShow, this, &PresetSelector::updateMenu); + connect(m_presets, &PluginPresets::presetCollectionChanged, this, &PresetSelector::updateMenu); + + m_loadPresetButton = new PixmapButton{this}; + m_loadPresetButton->setCheckable(false); + m_loadPresetButton->setCursor(Qt::PointingHandCursor); + m_loadPresetButton->setActiveGraphic(embed::getIconPixmap("project_open", 21, 21)); + m_loadPresetButton->setInactiveGraphic(embed::getIconPixmap("project_open", 21, 21)); + m_loadPresetButton->setToolTip(tr("Load preset")); + m_loadPresetButton->setFixedSize(21, 21); + + connect(m_loadPresetButton, &PixmapButton::clicked, this, &PresetSelector::loadPreset); + + m_savePresetButton = new PixmapButton{this}; + m_savePresetButton->setCheckable(false); + m_savePresetButton->setCursor(Qt::PointingHandCursor); + m_savePresetButton->setActiveGraphic(embed::getIconPixmap("project_save", 21, 21)); + m_savePresetButton->setInactiveGraphic(embed::getIconPixmap("project_save", 21, 21)); + m_savePresetButton->setToolTip(tr("Save preset")); + m_savePresetButton->setFixedSize(21, 21); + + connect(m_savePresetButton, &PixmapButton::clicked, this, &PresetSelector::savePreset); + + auto widget = new QWidget{this}; + auto layout = new QHBoxLayout{widget}; + layout->addWidget(m_activePreset, Qt::AlignmentFlag::AlignLeft); + layout->addStretch(1); + layout->addWidget(m_prevPresetButton, Qt::AlignmentFlag::AlignRight); + layout->addWidget(m_nextPresetButton); + layout->addWidget(m_selectPresetButton); + layout->addWidget(m_loadPresetButton); + layout->addWidget(m_savePresetButton); + widget->setLayout(layout); + + addWidget(widget); +} + +auto PresetSelector::sizeHint() const -> QSize +{ + // See InstrumentViewFixedSize + return QSize{250, 24}; +} + +void PresetSelector::updateActivePreset() +{ + if (!m_presets) { return; } + + const auto preset = m_presets->activePreset(); + if (!preset) + { + m_activePreset->setText(tr("(No active preset)")); + return; + } + + const auto presetIndex = m_presets->presetIndex().value() + 1; + const auto indexText = QString{"%1/%2"}.arg(presetIndex).arg(m_presets->presets().size()); + + const auto displayName = QString::fromStdString(preset->metadata().displayName); + const auto text = m_presets->isModified() + ? QString{"%1: %2*"}.arg(indexText).arg(displayName) + : QString{"%1: %2"}.arg(indexText).arg(displayName); + + m_activePreset->setText(text); +} + +void PresetSelector::updateMenu() +{ + if (!m_selectPresetButton) { return; } + + const auto menu = m_selectPresetButton->menu(); + menu->setStyleSheet("QMenu { menu-scrollable: 1; }"); + menu->clear(); + + if (!m_presets) { return; } + + const auto& presets = m_presets->presets(); + for (int idx = 0; idx < static_cast(presets.size()); ++idx) + { + auto presetAction = new QAction{this}; + connect(presetAction, &QAction::triggered, this, [this, idx] { selectPreset(idx); }); + + const auto isActive = static_cast(m_presets->presetIndex().value_or(-1)) == idx; + if (isActive) + { + auto font = presetAction->font(); + font.setBold(true); + font.setItalic(true); + presetAction->setFont(font); + } + + const auto name = QString::fromStdString(presets[idx]->metadata().displayName); + presetAction->setText(QString{"%1. %2"}.arg(QString::number(idx + 1), name)); + + const auto icon = isActive ? "sample_file" : "edit_copy"; + presetAction->setIcon(embed::getIconPixmap(icon, 16, 16)); + + menu->addAction(presetAction); + } + + // TODO: Scroll to active preset? +} + +void PresetSelector::loadPreset() +{ + if (!m_presets) { return; } + + const auto database = m_presets->presetDatabase(); + if (!database) { return; } + + auto openFileDialog = gui::FileDialog(nullptr, QObject::tr("Open audio file")); + + // Change dir to position of previously opened file + const auto recentFile = QString::fromUtf8( + database->recentPresetFile().data(), database->recentPresetFile().size()); + openFileDialog.setDirectory(recentFile); + openFileDialog.setFileMode(gui::FileDialog::ExistingFiles); + + // Set filters + auto fileTypes = QStringList{}; + auto allFileTypes = QStringList{}; + auto nameFilters = QStringList{}; + + for (const auto& filetype : database->filetypes()) + { + const auto name = QString::fromStdString(filetype.name); + const auto extension = QString::fromStdString(filetype.extension); + const auto displayExtension = QString{"*.%1"}.arg(extension); + fileTypes.append(QString{"%1 (%2)"}.arg(gui::FileDialog::tr("%1 files").arg(name), displayExtension)); + allFileTypes.append(displayExtension); + } + + nameFilters.append(QString{"%1 (%2)"}.arg(gui::FileDialog::tr("All preset files"), allFileTypes.join(" "))); + nameFilters.append(fileTypes); + nameFilters.append(QString("%1 (*)").arg(gui::FileDialog::tr("Other files"))); + + openFileDialog.setNameFilters(nameFilters); + + if (!recentFile.isEmpty()) + { + // Select previously opened file + openFileDialog.selectFile(QFileInfo{recentFile}.fileName()); + } + + if (openFileDialog.exec() != QDialog::Accepted) { return; } + + if (openFileDialog.selectedFiles().isEmpty()) { return; } + + auto presets = database->loadPresets(openFileDialog.selectedFiles()[0].toStdString()); + if (presets.empty()) + { + QMessageBox::warning(this, tr("Preset load failure"), + tr("Failed to load preset(s) or preset(s) were already loaded")); + } +} + +void PresetSelector::savePreset() +{ + // [NOT IMPLEMENTED YET] + QMessageBox::warning(this, tr("Save preset"), tr("This feature is not implemented yet")); +} + +void PresetSelector::selectPreset(int pos) +{ + if (!m_presets || pos < 0) { return; } + + m_presets->activatePreset(static_cast(pos)); + + // QWidget::update(); +} + +} // namespace lmms::gui diff --git a/src/gui/editors/TrackContainerView.cpp b/src/gui/editors/TrackContainerView.cpp index e85f4e86666..871f69d7672 100644 --- a/src/gui/editors/TrackContainerView.cpp +++ b/src/gui/editors/TrackContainerView.cpp @@ -396,8 +396,18 @@ void TrackContainerView::dropEvent( QDropEvent * _de ) if( type == "instrument" ) { auto it = dynamic_cast(Track::create(Track::Type::Instrument, m_tc)); - auto ilt = new InstrumentLoaderThread(this, it, value); - ilt->start(); + + if (value == "clapinstrument") + { + // Special case for CLAP, since CLAP API requires plugin to load on main thread + it->loadInstrument(value, nullptr, true /*always DnD*/); + } + else + { + auto ilt = new InstrumentLoaderThread(this, it, value); + ilt->start(); + } + //it->toggledInstrumentTrackButton( true ); _de->accept(); } diff --git a/src/gui/modals/EffectSelectDialog.cpp b/src/gui/modals/EffectSelectDialog.cpp index 65976059fca..1558daa14e4 100644 --- a/src/gui/modals/EffectSelectDialog.cpp +++ b/src/gui/modals/EffectSelectDialog.cpp @@ -24,10 +24,6 @@ */ #include "EffectSelectDialog.h" -#include "DummyEffect.h" -#include "EffectChain.h" -#include "embed.h" -#include "PluginFactory.h" #include #include @@ -38,6 +34,11 @@ #include #include +#include "DummyEffect.h" +#include "EffectChain.h" +#include "embed.h" +#include "lmmsconfig.h" +#include "PluginFactory.h" namespace lmms::gui { @@ -103,8 +104,20 @@ EffectSelectDialog::EffectSelectDialog(QWidget* parent) : QVBoxLayout* leftSectionLayout = new QVBoxLayout(); - QStringList buttonLabels = { tr("All"), "LMMS", "LADSPA", "LV2", "VST" }; - QStringList buttonSearchString = { "", "LMMS", "LADSPA", "LV2", "VST" }; + const auto buttonStringsCommon = QStringList { + "LMMS", +#ifdef LMMS_HAVE_CLAP + "CLAP", +#endif + "LADSPA", +#ifdef LMMS_HAVE_LV2 + "LV2", +#endif + "VST" + }; + + QStringList buttonLabels = QStringList{ tr("All") } + buttonStringsCommon; + QStringList buttonSearchString = QStringList{ "" } + buttonStringsCommon; for (int i = 0; i < buttonLabels.size(); ++i) { @@ -246,7 +259,7 @@ void EffectSelectDialog::rowChanged(const QModelIndex& idx, const QModelIndex&) logoLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); logoLabel->setMaximumSize(64, 64); - hbox->addWidget(logoLabel); + hbox->addWidget(logoLabel, 0, Qt::AlignTop); } auto textualInfoWidget = new QWidget(m_descriptionWidget); diff --git a/src/gui/modals/SetupDialog.cpp b/src/gui/modals/SetupDialog.cpp index d71ede03f53..67255659960 100644 --- a/src/gui/modals/SetupDialog.cpp +++ b/src/gui/modals/SetupDialog.cpp @@ -423,27 +423,36 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : m_vstEmbedComboBox = new QComboBox(pluginsBox); - QStringList embedMethods = ConfigManager::availableVstEmbedMethods(); - m_vstEmbedComboBox->addItem(tr("No embedding"), "none"); - if(embedMethods.contains("qt")) + m_vstEmbedComboBox->addItem(tr("No embedding"), static_cast(WindowEmbed::Method::Floating)); + for (const auto embedMethod : WindowEmbed::availableMethods()) { - m_vstEmbedComboBox->addItem(tr("Embed using Qt API"), "qt"); - } - if(embedMethods.contains("win32")) - { - m_vstEmbedComboBox->addItem(tr("Embed using native Win32 API"), "win32"); - } - if(embedMethods.contains("xembed")) - { - m_vstEmbedComboBox->addItem(tr("Embed using XEmbed protocol"), "xembed"); + switch (embedMethod) + { + case WindowEmbed::Method::Qt: + m_vstEmbedComboBox->addItem(tr("Embed using Qt API"), + static_cast(WindowEmbed::Method::Qt)); + break; + case WindowEmbed::Method::Win32: + m_vstEmbedComboBox->addItem(tr("Embed using native Win32 API"), + static_cast(WindowEmbed::Method::Win32)); + break; + case WindowEmbed::Method::XEmbed: + m_vstEmbedComboBox->addItem(tr("Embed using XEmbed protocol"), + static_cast(WindowEmbed::Method::XEmbed)); + break; + default: + break; + } } - m_vstEmbedComboBox->setCurrentIndex(m_vstEmbedComboBox->findData(m_vstEmbedMethod)); + + m_vstEmbedComboBox->setCurrentIndex(m_vstEmbedComboBox->findData(static_cast(m_vstEmbedMethod))); connect(m_vstEmbedComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(vstEmbedMethodChanged())); pluginsLayout->addWidget(m_vstEmbedComboBox); m_vstAlwaysOnTopCheckBox = addCheckBox(tr("Keep plugin windows on top when not embedded"), pluginsBox, pluginsLayout, m_vstAlwaysOnTop, SLOT(toggleVSTAlwaysOnTop(bool)), false); + vstEmbedMethodChanged(); addCheckBox(tr("Keep effects running even without input"), pluginsBox, pluginsLayout, m_disableAutoQuit, SLOT(toggleDisableAutoQuit(bool)), false); @@ -954,7 +963,7 @@ void SetupDialog::accept() ConfigManager::inst()->setValue("ui", "animateafp", QString::number(m_animateAFP)); ConfigManager::inst()->setValue("ui", "vstembedmethod", - m_vstEmbedComboBox->currentData().toString()); + WindowEmbed::toString(m_vstEmbedMethod).data()); ConfigManager::inst()->setValue("ui", "vstalwaysontop", QString::number(m_vstAlwaysOnTop)); ConfigManager::inst()->setValue("ui", "disableautoquit", @@ -1149,8 +1158,8 @@ void SetupDialog::toggleAnimateAFP(bool enabled) void SetupDialog::vstEmbedMethodChanged() { - m_vstEmbedMethod = m_vstEmbedComboBox->currentData().toString(); - m_vstAlwaysOnTopCheckBox->setVisible(m_vstEmbedMethod == "none"); + m_vstEmbedMethod = WindowEmbed::Method{m_vstEmbedComboBox->currentData().toInt()}; + m_vstAlwaysOnTopCheckBox->setVisible(m_vstEmbedMethod == WindowEmbed::Method::Floating); } diff --git a/src/gui/widgets/CustomTextKnob.cpp b/src/gui/widgets/CustomTextKnob.cpp index a4edde47ccb..f7f21441582 100644 --- a/src/gui/widgets/CustomTextKnob.cpp +++ b/src/gui/widgets/CustomTextKnob.cpp @@ -28,17 +28,22 @@ namespace lmms::gui { -CustomTextKnob::CustomTextKnob( KnobType _knob_num, QWidget * _parent, const QString & _name, const QString & _value_text ) : - Knob( _knob_num, _parent, _name ), - m_value_text( _value_text ) {} +CustomTextKnob::CustomTextKnob(KnobType knobNum, QWidget* parent, const QString& name, const QString& valueText) + : Knob(knobNum, parent, name), m_valueText(valueText) {} -CustomTextKnob::CustomTextKnob( QWidget * _parent, const QString & _name, const QString & _value_text ) : //!< default ctor - Knob( _parent, _name ), - m_value_text( _value_text ) {} +CustomTextKnob::CustomTextKnob(QWidget* parent, const QString& name) + : Knob(parent, name) {} QString CustomTextKnob::displayValue() const { - return m_description.trimmed() + m_value_text; + if (m_valueTextType == ValueTextType::Static) + { + return m_description.trimmed() + m_valueText; + } + else + { + return m_valueTextFunc(); + } } diff --git a/src/lmmsconfig.h.in b/src/lmmsconfig.h.in index 867a04a4535..1054249f1d6 100644 --- a/src/lmmsconfig.h.in +++ b/src/lmmsconfig.h.in @@ -16,6 +16,7 @@ #cmakedefine LMMS_HOST_PPC64 #cmakedefine LMMS_HAVE_ALSA +#cmakedefine LMMS_HAVE_CLAP #cmakedefine LMMS_HAVE_FLUIDSYNTH #cmakedefine LMMS_HAVE_JACK #cmakedefine LMMS_HAVE_JACK_PRENAME diff --git a/src/tracks/PatternTrack.cpp b/src/tracks/PatternTrack.cpp index 697a7c2a8fb..08f1cf1d8ef 100644 --- a/src/tracks/PatternTrack.cpp +++ b/src/tracks/PatternTrack.cpp @@ -160,13 +160,13 @@ void PatternTrack::saveTrackSpecificSettings(QDomDocument& doc, QDomElement& _th /* _this.setAttribute( "current", s_infoMap[this] == engine::getPatternEditor()->currentPattern() );*/ if( s_infoMap[this] == 0 && - _this.parentNode().parentNode().nodeName() != "clonedtrack" && + _this.parentNode().parentNode().nodeName() != "clonedtrack" && // TODO: Does this work? _this.parentNode().parentNode().nodeName() != "journaldata" ) { Engine::patternStore()->saveState(doc, _this); } // If we are creating drag-n-drop data for Track::clone() only save pattern ID, not pattern content - if (_this.parentNode().parentNode().nodeName() == "clonedtrack") + if (_this.parentNode().parentNode().nodeName() == "clonedtrack") // TODO: Does this work? { _this.setAttribute("sourcepattern", s_infoMap[this]); } diff --git a/tests/src/core/RelativePathsTest.cpp b/tests/src/core/RelativePathsTest.cpp index 089ab2e8ae0..be01826e1c9 100644 --- a/tests/src/core/RelativePathsTest.cpp +++ b/tests/src/core/RelativePathsTest.cpp @@ -43,7 +43,7 @@ private slots: QString absPath = fi.absoluteFilePath(); QString oldRelPath = "drums/kick01.ogg"; - QString relPath = PathUtil::basePrefix(PathUtil::Base::FactorySample) + "drums/kick01.ogg"; + QString relPath = PathUtil::basePrefixQString(PathUtil::Base::FactorySample) + "drums/kick01.ogg"; QString fuzPath = absPath; fuzPath.replace(relPath, "drums/.///kick01.ogg"); @@ -61,10 +61,10 @@ private slots: //Empty paths should stay empty QString empty = QString(""); - QCOMPARE(PathUtil::stripPrefix(""), empty); - QCOMPARE(PathUtil::cleanName(""), empty); - QCOMPARE(PathUtil::toAbsolute(""), empty); - QCOMPARE(PathUtil::toShortestRelative(""), empty); + QCOMPARE(PathUtil::stripPrefix(empty), empty); + QCOMPARE(PathUtil::cleanName(empty), empty); + QCOMPARE(PathUtil::toAbsolute(empty), empty); + QCOMPARE(PathUtil::toShortestRelative(empty), empty); } };