diff --git a/Brewfile b/Brewfile index 05f89420960..6d4a5f4b910 100644 --- a/Brewfile +++ b/Brewfile @@ -13,6 +13,9 @@ brew "libsoundio" brew "libvorbis" brew "lilv" brew "lv2" +brew "suil" +brew "serd" +brew "sratom" brew "pkgconf" brew "portaudio" brew "qt@5" diff --git a/CMakeLists.txt b/CMakeLists.txt index f439815cb03..e7e02afe7ed 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,6 +81,8 @@ OPTION(WANT_JACK "Include JACK (Jack Audio Connection Kit) support" ON) OPTION(WANT_WEAKJACK "Loosely link JACK libraries" ON) OPTION(WANT_LV2 "Include Lv2 plugins" ON) OPTION(WANT_SUIL "Include SUIL for LV2 plugin UIs" ON) +OPTION(WANT_SERD "Include SERD for LV2 debugging" ON) +OPTION(WANT_SRATOM "Include SRATOM for LV2 debugging" ON) OPTION(WANT_MP3LAME "Include MP3/Lame support" ON) OPTION(WANT_OGGVORBIS "Include OGG/Vorbis support" ON) OPTION(WANT_PULSEAUDIO "Include PulseAudio support" ON) @@ -259,6 +261,11 @@ if(WANT_LV2) else() set(LMMS_HAVE_LV2 TRUE) set(STATUS_LV2 "OK") + IF (DEFINED LV2_VERSION AND LV2_VERSION VERSION_GREATER_EQUAL "1.17.2") + SET(LMMS_HAVE_LV2_1_17_2 TRUE) + ELSE() + SET(LMMS_HAVE_LV2_1_17_2 FALSE) + ENDIF() endif() else() set(STATUS_LV2 "not built as requested") @@ -281,6 +288,38 @@ ELSE(WANT_SUIL) SET(STATUS_SUIL "not built as requested") ENDIF(WANT_SUIL) +IF(WANT_SERD) + IF(PKG_CONFIG_FOUND) + PKG_CHECK_MODULES(SERD serd-0) + IF(SERD_FOUND) + SET(LMMS_HAVE_SERD TRUE) + SET(STATUS_SERD "OK") + ELSE() + SET(STATUS_SERD "not found, install it or set PKG_CONFIG_PATH appropriately") + ENDIF() + ELSE() + SET(STATUS_SERD "not found, requires pkg-config") + ENDIF() +ELSE(WANT_SERD) + SET(STATUS_SERD "not built as requested") +ENDIF(WANT_SERD) + +IF(WANT_SRATOM) + IF(PKG_CONFIG_FOUND) + PKG_CHECK_MODULES(SRATOM sratom-0) + IF(SRATOM_FOUND) + SET(LMMS_HAVE_SRATOM TRUE) + SET(STATUS_SRATOM "OK") + ELSE() + SET(STATUS_SRATOM "not found, install it or set PKG_CONFIG_PATH appropriately") + ENDIF() + ELSE() + SET(STATUS_SRATOM "not found, requires pkg-config") + ENDIF() +ELSE(WANT_SRATOM) + SET(STATUS_SRATOM "not built as requested") +ENDIF(WANT_SRATOM) + IF(WANT_CALF) SET(LMMS_HAVE_CALF TRUE) SET(STATUS_CALF "OK") @@ -848,6 +887,8 @@ MESSAGE( "----------------\n" "* Lv2 plugins : ${STATUS_LV2}\n" "* SUIL for plugin UIs : ${STATUS_SUIL}\n" +"* SERD for Lv2 debugging : ${STATUS_SERD}\n" +"* SRATOM for Lv2 debugging : ${STATUS_SRATOM}\n" "* ZynAddSubFX instrument : ${STATUS_ZYN}\n" "* Carla Patchbay & Rack : ${STATUS_CARLA}\n" "* SoundFont2 player : ${STATUS_FLUIDSYNTH}\n" diff --git a/include/AudioEngine.h b/include/AudioEngine.h index 49b511a1744..92a8ffb9c90 100644 --- a/include/AudioEngine.h +++ b/include/AudioEngine.h @@ -354,6 +354,11 @@ class LMMS_EXPORT AudioEngine : public QObject AudioDevice * tryAudioDevices(); MidiClient * tryMidiClients(); + const AudioDevice* audioDev() const + { + return m_audioDev; + } + void renderStageNoteSetup(); void renderStageInstruments(); void renderStageEffects(); diff --git a/include/AudioJack.h b/include/AudioJack.h index e13b4a5ef17..ab87e97f6e9 100644 --- a/include/AudioJack.h +++ b/include/AudioJack.h @@ -53,11 +53,6 @@ namespace lmms class MidiJack; -namespace gui -{ -class LcdSpinBox; -} - class AudioJack : public QObject, public AudioDevice { @@ -82,13 +77,10 @@ class AudioJack : public QObject, public AudioDevice { public: setupWidget(QWidget* parent); - ~setupWidget() override; - void saveSettings() override; private: QLineEdit* m_clientName; - gui::LcdSpinBox* m_channels; }; private slots: @@ -96,6 +88,7 @@ private slots: private: bool initJackClient(); + void resizeInputBuffer(jack_nframes_t nframes); void startProcessing() override; void stopProcessing() override; @@ -116,8 +109,11 @@ private slots: std::atomic m_midiClient; std::vector m_outputPorts; + std::vector m_inputPorts; jack_default_audio_sample_t** m_tempOutBufs; + std::vector m_inputFrameBuffer; SampleFrame* m_outBuf; + SampleFrame* m_inBuf; f_cnt_t m_framesDoneInCurBuf; f_cnt_t m_framesToDoInCurBuf; diff --git a/include/GuiApplication.h b/include/GuiApplication.h index 16680934f68..364968e69f4 100644 --- a/include/GuiApplication.h +++ b/include/GuiApplication.h @@ -50,7 +50,7 @@ class LMMS_EXPORT GuiApplication : public QObject { Q_OBJECT; public: - explicit GuiApplication(); + explicit GuiApplication(int* argc, char*** argv); ~GuiApplication() override; static GuiApplication* instance(); diff --git a/include/InstrumentTrackWindow.h b/include/InstrumentTrackWindow.h index 6f0245875f2..fe6c6061130 100644 --- a/include/InstrumentTrackWindow.h +++ b/include/InstrumentTrackWindow.h @@ -70,8 +70,6 @@ class InstrumentTrackWindow : public QWidget, public ModelView, InstrumentTrackWindow( InstrumentTrackView * _tv ); ~InstrumentTrackWindow() override; - void resizeEvent(QResizeEvent* event) override; - // parent for all internal tab-widgets TabWidget * tabWidgetParent() diff --git a/include/InstrumentView.h b/include/InstrumentView.h index 40014a11f32..12bcf14b262 100644 --- a/include/InstrumentView.h +++ b/include/InstrumentView.h @@ -64,11 +64,16 @@ class LMMS_EXPORT InstrumentView : public PluginView //! Instrument view with fixed LMMS-default size class LMMS_EXPORT InstrumentViewFixedSize : public InstrumentView { +protected: QSize sizeHint() const override { return QSize(250, 250); } QSize minimumSizeHint() const override { return sizeHint(); } public: - using InstrumentView::InstrumentView; + InstrumentViewFixedSize(Instrument* instrument, QWidget* parent) + : InstrumentView(instrument, parent) + { + setFixedSize(sizeHint()); + } ~InstrumentViewFixedSize() override = default; } ; diff --git a/include/LinkedModelGroupViews.h b/include/LinkedModelGroupViews.h index cf5aabd0d46..b1666950201 100644 --- a/include/LinkedModelGroupViews.h +++ b/include/LinkedModelGroupViews.h @@ -1,7 +1,7 @@ /* * LinkedModelGroupViews.h - view for groups of linkable models * - * Copyright (c) 2019-2019 Johannes Lorenz + * Copyright (c) 2019-2024 Johannes Lorenz * * This file is part of LMMS - https://lmms.io * @@ -29,6 +29,8 @@ #include #include +class QVBoxLayout; + namespace lmms { @@ -83,11 +85,14 @@ class LinkedModelGroupView : public QWidget void removeFocusFromSearchBar(); + const LinkedModelGroup* model() const { return m_model; } + private: class LinkedModelGroup* m_model; //! column number in surrounding grid in LinkedModelGroupsView std::size_t m_colNum; + QVBoxLayout* m_vbox; class ControlLayout* m_layout; std::map> m_widgets; }; diff --git a/include/Lv2Basics.h b/include/Lv2Basics.h index 5b286586875..a1915951d52 100644 --- a/include/Lv2Basics.h +++ b/include/Lv2Basics.h @@ -78,6 +78,20 @@ QString qStringFromPortName(const LilvPlugin* plug, const LilvPort* port); //! Return port name as std::string, everything will be freed automatically std::string stdStringFromPortName(const LilvPlugin* plug, const LilvPort* port); +//! Control change event, sent through ring buffers for UI updates +struct Lv2UiControlChange +{ + uint32_t index; + uint32_t protocol; + uint32_t size; + // Followed immediately by bytes of data +}; + +float lv2UiRefreshRate(); +float lv2UiScaleFactor(); + +const bool lv2Dump = false; + } // namespace lmms #endif // LMMS_HAVE_LV2 diff --git a/include/Lv2ControlBase.h b/include/Lv2ControlBase.h index 8ee235ad898..17a2cdafc4c 100644 --- a/include/Lv2ControlBase.h +++ b/include/Lv2ControlBase.h @@ -87,7 +87,6 @@ class LMMS_EXPORT Lv2ControlBase : public LinkedModelGroups const Lv2Proc *control(std::size_t idx) const { return m_procs[idx].get(); } bool hasGui() const { return m_hasGUI; } - void setHasGui(bool val) { m_hasGUI = val; } protected: /* diff --git a/include/Lv2Features.h b/include/Lv2Features.h index 69a456bbde3..3d8ade7603a 100644 --- a/include/Lv2Features.h +++ b/include/Lv2Features.h @@ -1,7 +1,7 @@ /* * Lv2Features.h - Lv2Features class * - * Copyright (c) 2020-2020 Johannes Lorenz + * Copyright (c) 2020-2024 Johannes Lorenz * * This file is part of LMMS - https://lmms.io * @@ -33,6 +33,7 @@ #include #include #include "Lv2Manager.h" +#include namespace lmms @@ -62,7 +63,7 @@ class Lv2Features //! Register only plugin-common features void initCommon(); //! Return reference to feature data with given URI featName - void*& operator[](const char* featName); + LV2_Feature& operator[](const char* featName); //! Fill m_features and m_featurePointers with all features void createFeatureVectors(); //! Return LV2_Feature pointer vector, suited for lilv_plugin_instantiate @@ -73,13 +74,16 @@ class Lv2Features //! Clear everything void clear(); + //! data features is a UI feature, it is not in the map + LV2_Extension_Data_Feature m_extData; + private: //! feature storage std::vector m_features; //! pointers to m_features, required for lilv_plugin_instantiate std::vector m_featurePointers; //! features + data, ordered by URI - std::map m_featureByUri; + std::map m_featureByUri; }; diff --git a/include/Lv2Manager.h b/include/Lv2Manager.h index 8a9f5168448..b9313fd01e0 100644 --- a/include/Lv2Manager.h +++ b/include/Lv2Manager.h @@ -34,6 +34,13 @@ #include #include +#ifdef LMMS_HAVE_SERD +#include +#ifdef LMMS_HAVE_SRATOM +#include +#endif // LMMS_HAVE_SRATOM +#endif // LMMS_HAVE_SERD + #include "Lv2Basics.h" #include "Lv2UridCache.h" #include "Lv2UridMap.h" @@ -78,7 +85,7 @@ namespace lmms //! Class to keep track of all LV2 plugins -class Lv2Manager +class LMMS_EXPORT Lv2Manager { public: void initPlugins(); @@ -149,12 +156,20 @@ class Lv2Manager //! Since we do not generally support UI right now, this will always return false... static bool wantUi(); +#if defined(LMMS_HAVE_SRATOM) && defined(LMMS_HAVE_SERD) + Sratom* sratom; //!< Atom serialiser + Sratom* ui_sratom; //!< Atom serialiser for UI thread +#endif + private: // general data bool m_debug; //!< if set, debug output will be printed LilvWorld* m_world; Lv2InfoMap m_lv2InfoMap; std::set m_supportedFeatureURIs; +#ifdef LMMS_HAVE_SERD + SerdEnv* env; +#endif // feature data that are common for all Lv2Proc UridMap m_uridMap; diff --git a/include/Lv2Options.h b/include/Lv2Options.h index 69d294cbe1f..58f44ff3ca5 100644 --- a/include/Lv2Options.h +++ b/include/Lv2Options.h @@ -66,7 +66,7 @@ class Lv2Options //! Mark option as supported static void supportOption(LV2_URID key); - //! Initialize an option + //! Initialize an option from Cache ID template void initOption(Lv2UridCache::Id key, Arg&& value, LV2_Options_Context context = LV2_OPTIONS_INSTANCE, @@ -88,7 +88,7 @@ class Lv2Options private: //! Initialize an option internally - void initOption(LV2_URID key, + void initOption(LV2_URID keyAsUrid, uint32_t size, LV2_URID type, std::shared_ptr value, diff --git a/include/Lv2Proc.h b/include/Lv2Proc.h index f315c5d7a89..e64185203d4 100644 --- a/include/Lv2Proc.h +++ b/include/Lv2Proc.h @@ -1,7 +1,7 @@ /* * Lv2Proc.h - Lv2 processor class * - * Copyright (c) 2019-2022 Johannes Lorenz + * Copyright (c) 2019-2024 Johannes Lorenz * * This file is part of LMMS - https://lmms.io * @@ -54,9 +54,11 @@ class SampleFrame; // forward declare port structs/enums namespace Lv2Ports { - struct Audio; struct PortBase; + struct AtomSeq; + struct Audio; + struct Control; enum class Type; enum class Flow; @@ -80,6 +82,7 @@ class Lv2Proc : public LinkedModelGroup ~Lv2Proc() override; void reload(); void onSampleRateChanged(); + void onSettingsLoaded(); /* port access @@ -97,14 +100,6 @@ class Lv2Proc : public LinkedModelGroup StereoPortRef& outPorts() { return m_outPorts; } const StereoPortRef& outPorts() const { return m_outPorts; } template - void foreach_port(const Functor& ftor) - { - for (std::unique_ptr& port : m_ports) - { - ftor(port.get()); - } - } - template void foreach_port(const Functor& ftor) const { for (const std::unique_ptr& port : m_ports) @@ -157,11 +152,58 @@ class Lv2Proc : public LinkedModelGroup const TimePos &time, f_cnt_t offset); /* - misc + ui + */ + void connectUiEventsReaderTo(LocklessRingBuffer& uiEvents) + { + m_uiEventsReader.emplace(uiEvents); + } + void connectToPluginEvents(std::optional>& pluginEventsReader) + { + pluginEventsReader.emplace(m_pluginEvents); + } + //! This can enable RealTime safety violations, only use this to instantiate the instance feature + const LilvInstance* getInstanceForInstanceFeatureOnly() const { return m_instance; } + LocklessRingBuffer& getPluginEventsForInitializationOnly() { return m_pluginEvents; } + static constexpr std::size_t uiEventsBufsize() + { + // source: Jalv (MSG_BUFFER_SIZE) + // Ardour: Uses dynamic stack allocation (g_alloca) + // => TODO: switch to alloca/STACKALLOC + return 1024; + } + static constexpr std::size_t uiNBufferCycles() + { + // source: jalv (N_BUFFER_CYCLES=16) + // Ardour is similar (at least 8, LV2Plugin::write_from_ui). */ + return 16; + } + static constexpr std::size_t uiMidiBufsize() + { + // source: Ardour uses the capacity of the event buffers (LV2Plugin::write_from_ui) + // Jalv is lower (jalv->midi_buf_size = 4096 default for jack, + // but it can be changed by jack: jack_port_type_get_buffer_size) + return defaultEvbufSize(); + } + + /* + features save for non-realtime access + */ + const LV2_Feature* mapFeature() { return &m_features[LV2_URID__map]; } + const LV2_Feature* unmapFeature() { return &m_features[LV2_URID__unmap]; } + const LV2_Feature* optionsFeature() { return &m_features[LV2_OPTIONS__options]; } + LV2_Extension_Data_Feature* extdataFeature() { return &m_features.m_extData; } + + /* + metadata */ - class AutomatableModel *modelAtPort(const QString &uri); // unused currently std::size_t controlCount() const { return LinkedModelGroup::modelNum(); } bool hasNoteInput() const; + LilvUIs* getUis() const { return lilv_plugin_get_uis(m_plugin); } + const char* pluginUri() const { return lilv_node_as_uri(lilv_plugin_get_uri(m_plugin)); } + std::size_t getIdOfPort(const char* symbol) const; + QString portname(std::size_t idx) const; + uint32_t portNum() const; protected: /* @@ -201,8 +243,17 @@ class Lv2Proc : public LinkedModelGroup //! MIDI ringbuffer reader ringbuffer_reader_t m_midiInputReader; + // ui + LocklessRingBuffer m_pluginEvents; + std::optional> m_uiEventsReader; + bool isUiActive() const; + void applyUiEvents(uint32_t nframes); + bool sendToUi(uint32_t port_index, uint32_t type, uint32_t size, const void* body); + bool sendToUi(uint32_t port_index, const Lv2Ports::Control* ctrl); + uint32_t m_eventDeltaT; + // other - static int32_t defaultEvbufSize() { return 1 << 15; /* ardour uses this*/ } + static constexpr int32_t defaultEvbufSize() { return 1 << 15; /* ardour uses this*/ } //! models for the controls, sorted by port symbols //! @note These are not owned, but rather link to the models in diff --git a/include/Lv2UridCache.h b/include/Lv2UridCache.h index b64003a2ee7..7ea6add9ea1 100644 --- a/include/Lv2UridCache.h +++ b/include/Lv2UridCache.h @@ -1,7 +1,7 @@ /* * Lv2UridCache.h - Lv2UridCache definition * - * Copyright (c) 2020-2020 Johannes Lorenz + * Copyright (c) 2020-2024 Johannes Lorenz * * This file is part of LMMS - https://lmms.io * @@ -30,6 +30,8 @@ #ifdef LMMS_HAVE_LV2 #include +#include +#include namespace lmms @@ -45,12 +47,16 @@ class Lv2UridCache // keep it alphabetically (except "size" at the end) atom_Float, atom_Int, - bufsz_minBlockLength, bufsz_maxBlockLength, + bufsz_minBlockLength, bufsz_nominalBlockLength, bufsz_sequenceSize, midi_MidiEvent, param_sampleRate, + ui_backgroundColor, + ui_foregroundColor, + ui_scaleFactor, + ui_updateRate, // exception to alphabetic ordering - keep at the end: size }; @@ -58,13 +64,39 @@ class Lv2UridCache template struct IdForType; + static LV2_URID noUrid() { return 0; } + //! Return URID for a cache ID - uint32_t operator[](Id id) const; + LV2_URID operator[](Id id) const; + //! Return name of an ID + static const char* nameOfId(Id id) + { + return s_idNames[static_cast(id)].data(); + } Lv2UridCache(class UridMap& mapper); private: - uint32_t m_cache[static_cast(Id::size)]; + LV2_URID m_cache[static_cast(Id::size)]; + + // must match Id enum! + static constexpr std::string_view s_idNames[static_cast(Id::size)] = + { + "atom_Float", + "atom_Int", + "bufsz_maxBlockLength", + "bufsz_minBlockLength", + "bufsz_nominalBlockLength", + "bufsz_sequenceSize", + "midi_MidiEvent", + "param_sampleRate", + "ui_backgroundColor", + "ui_foregroundColor", + "ui_scaleFactor", + "ui_updateRate" + }; + + static void checkIdNamesConsistency(); }; template<> struct Lv2UridCache::IdForType { static constexpr auto value = Id::atom_Float; }; diff --git a/include/Lv2UridMap.h b/include/Lv2UridMap.h index 23f8f758a44..1bba6a737ad 100644 --- a/include/Lv2UridMap.h +++ b/include/Lv2UridMap.h @@ -1,7 +1,7 @@ /* * Lv2UridMap.cpp - Lv2UridMap class * - * Copyright (c) 2019 Johannes Lorenz + * Copyright (c) 2019-2024 Johannes Lorenz * * This file is part of LMMS - https://lmms.io * @@ -63,6 +63,8 @@ class UridMap LV2_URID map(const char* uri); //! unmap feature function const char* unmap(LV2_URID urid); + //! debug dumping + void dump(); // access the features LV2_URID_Map* mapFeature() { return &m_mapFeature; } diff --git a/include/Lv2ViewBase.h b/include/Lv2ViewBase.h index 43086849cb6..c8b6a253329 100644 --- a/include/Lv2ViewBase.h +++ b/include/Lv2ViewBase.h @@ -29,11 +29,18 @@ #ifdef LMMS_HAVE_LV2 +#include +#include +#include +#ifdef LMMS_HAVE_SUIL + #include +#endif #include "LinkedModelGroupViews.h" +#include "LocklessRingBuffer.h" #include "lmms_export.h" #include "Lv2Basics.h" - +#include "Lv2Features.h" class QPushButton; class QMdiSubWindow; @@ -45,22 +52,68 @@ namespace lmms class Lv2Proc; class Lv2ControlBase; +namespace Lv2Ports { + struct PortBase; +} + namespace gui { -class LedCheckBox; - //! View for one processor, Lv2ViewBase contains 2 of those for mono plugins class Lv2ViewProc : public LinkedModelGroupView { public: //! @param colNum numbers of columns for the controls Lv2ViewProc(QWidget *parent, Lv2Proc *proc, int colNum); - ~Lv2ViewProc() override = default; + ~Lv2ViewProc() override; + + QSize uiWidgetSize() const; + bool isResizable() const { + qDebug() << "isResizable?" << m_isResizable; + return m_isResizable; /*m_isResizable;*/ /* TODO: temporary fix, see Discord, asked H-S */ } + void update(); private: + Lv2Proc* proc() { return (Lv2Proc*)model(); } + const Lv2Proc* proc() const { return (const Lv2Proc*)model(); } + + static const char* hostUiTypeUri(); + bool calculateIsResizable() const; + + void initUi(); + void writeToPlugin(uint32_t port_index, + uint32_t buffer_size, uint32_t protocol, const void* buffer); + void uiPortEvent(uint32_t port_index, uint32_t buffer_size, uint32_t protocol, + const void* buffer); + std::tuple selectPluginUi(LilvUIs* uis) const; + + Lv2Features m_uiFeatures; + + LV2_URID m_atomEventTransfer; static AutoLilvNode uri(const char *uriStr); + + LocklessRingBuffer m_uiEvents; + std::optional> m_pluginEventsReader; + std::vector m_uiEventBuf; + class Timer* m_timer = nullptr; + + const LilvUI* m_ui = nullptr; + const LilvNode* m_uiType; + bool m_isResizable = false; +#ifdef LMMS_HAVE_SUIL + SuilHost* m_uiHost; //!< Plugin UI host support + SuilInstance* m_uiInstance; //!< Plugin UI instance (shared library) + QWidget* m_uiInstanceWidget = nullptr; +#endif +#ifdef LMMS_HAVE_LV2_1_17_2 + LV2UI_Request_Value m_requestValue; + LV2UI_Request_Value_Status requestValue( + LV2_URID key, + LV2_URID type, + const LV2_Feature* const* features); +#endif + void touch(uint32_t portIndex, bool grabbed); }; @@ -100,6 +153,9 @@ class LMMS_EXPORT Lv2ViewBase : public LinkedModelGroupsView // to be called by child virtuals //! Reconnect models if model changed void modelChanged(Lv2ControlBase* ctrlBase); + + QSize uiWidgetSize() const { return m_procView->uiWidgetSize(); } + bool isResizable() const { return m_procView->isResizable(); } private: enum Rows diff --git a/plugins/Lv2Effect/CMakeLists.txt b/plugins/Lv2Effect/CMakeLists.txt index ca565817212..6a423d4f03b 100644 --- a/plugins/Lv2Effect/CMakeLists.txt +++ b/plugins/Lv2Effect/CMakeLists.txt @@ -2,6 +2,12 @@ IF(LMMS_HAVE_LV2) include_directories(SYSTEM ${LV2_INCLUDE_DIRS}) include_directories(SYSTEM ${Lilv_INCLUDE_DIRS}) include_directories(SYSTEM ${SUIL_INCLUDE_DIRS}) + IF(SERD_INCLUDE_DIRS) + include_directories(SYSTEM ${SERD_INCLUDE_DIRS}) + ENDIF() + IF(SRATOM_INCLUDE_DIRS) + include_directories(SYSTEM ${SRATOM_INCLUDE_DIRS}) + ENDIF() INCLUDE(BuildPlugin) BUILD_PLUGIN(lv2effect Lv2Effect.cpp Lv2FxControls.cpp Lv2FxControlDialog.cpp Lv2Effect.h Lv2FxControls.h Lv2FxControlDialog.h MOCFILES Lv2Effect.h Lv2FxControls.h Lv2FxControlDialog.h diff --git a/plugins/Lv2Effect/Lv2Effect.cpp b/plugins/Lv2Effect/Lv2Effect.cpp index afa69fe1373..393eb06556d 100644 --- a/plugins/Lv2Effect/Lv2Effect.cpp +++ b/plugins/Lv2Effect/Lv2Effect.cpp @@ -26,6 +26,7 @@ #include +#include "Lv2Manager.h" #include "Lv2SubPluginFeatures.h" #include "embed.h" @@ -63,6 +64,9 @@ Lv2Effect::Lv2Effect(Model* parent, const Descriptor::SubPluginFeatures::Key *ke m_controls(this, key->attributes["uri"]), m_tmpOutputSmps(Engine::audioEngine()->framesPerPeriod()) { + // even if no input, we may need to force this effect to run permanently + // this is required for permanent DSP<->UI communication + if(Lv2Manager::wantUi()) { startRunning(); } } @@ -91,7 +95,9 @@ Effect::ProcessStatus Lv2Effect::processImpl(SampleFrame* buf, const fpp_t frame buf[f][1] = d * buf[f][1] + w * m_tmpOutputSmps[f][1]; } - return ProcessStatus::ContinueIfNotQuiet; + return Lv2Manager::wantUi() + ? ProcessStatus::Continue + : ProcessStatus::ContinueIfNotQuiet; } diff --git a/plugins/Lv2Effect/Lv2FxControlDialog.cpp b/plugins/Lv2Effect/Lv2FxControlDialog.cpp index 73890937c04..cddb3e780b3 100644 --- a/plugins/Lv2Effect/Lv2FxControlDialog.cpp +++ b/plugins/Lv2Effect/Lv2FxControlDialog.cpp @@ -36,6 +36,7 @@ Lv2FxControlDialog::Lv2FxControlDialog(Lv2FxControls *controls) : EffectControlDialog(controls), Lv2ViewBase(this, controls) { + setMinimumSize(440, 60); if (m_reloadPluginButton) { connect(m_reloadPluginButton, &QPushButton::clicked, this, [this](){ lv2Controls()->reload(); }); @@ -81,4 +82,12 @@ void Lv2FxControlDialog::hideEvent(QHideEvent *event) } + + +QSize Lv2FxControlDialog::sizeHint() const +{ + return Lv2ViewBase::uiWidgetSize(); +} + + } // namespace lmms::gui diff --git a/plugins/Lv2Effect/Lv2FxControlDialog.h b/plugins/Lv2Effect/Lv2FxControlDialog.h index f38c0364bfa..adad74bf02c 100644 --- a/plugins/Lv2Effect/Lv2FxControlDialog.h +++ b/plugins/Lv2Effect/Lv2FxControlDialog.h @@ -47,6 +47,9 @@ class Lv2FxControlDialog : public EffectControlDialog, public Lv2ViewBase Lv2FxControls *lv2Controls(); void modelChanged() final; void hideEvent(QHideEvent *event) override; + bool isResizable() const final { return Lv2ViewBase::isResizable(); } + QSize sizeHint() const override; + QSize minimumSizeHint() const override { return sizeHint(); } }; diff --git a/plugins/Lv2Instrument/CMakeLists.txt b/plugins/Lv2Instrument/CMakeLists.txt index a316a0fa7c1..7c0b159fead 100644 --- a/plugins/Lv2Instrument/CMakeLists.txt +++ b/plugins/Lv2Instrument/CMakeLists.txt @@ -2,6 +2,12 @@ IF(LMMS_HAVE_LV2) include_directories(SYSTEM ${LV2_INCLUDE_DIRS}) include_directories(SYSTEM ${Lilv_INCLUDE_DIRS}) include_directories(SYSTEM ${SUIL_INCLUDE_DIRS}) + IF(SERD_INCLUDE_DIRS) + include_directories(SYSTEM ${SERD_INCLUDE_DIRS}) + ENDIF() + IF(SRATOM_INCLUDE_DIRS) + include_directories(SYSTEM ${SRATOM_INCLUDE_DIRS}) + ENDIF() INCLUDE(BuildPlugin) BUILD_PLUGIN(lv2instrument Lv2Instrument.cpp Lv2Instrument.h MOCFILES Lv2Instrument.h EMBEDDED_RESOURCES logo.png) ENDIF(LMMS_HAVE_LV2) diff --git a/plugins/Lv2Instrument/Lv2Instrument.cpp b/plugins/Lv2Instrument/Lv2Instrument.cpp index 8da2d913ed3..19f57a03add 100644 --- a/plugins/Lv2Instrument/Lv2Instrument.cpp +++ b/plugins/Lv2Instrument/Lv2Instrument.cpp @@ -310,6 +310,14 @@ void Lv2InsView::modelChanged() } + + +QSize Lv2InsView::sizeHint() const +{ + return Lv2ViewBase::uiWidgetSize(); +} + + } // namespace gui extern "C" diff --git a/plugins/Lv2Instrument/Lv2Instrument.h b/plugins/Lv2Instrument/Lv2Instrument.h index 9fbc7b7f699..ad8dc66509e 100644 --- a/plugins/Lv2Instrument/Lv2Instrument.h +++ b/plugins/Lv2Instrument/Lv2Instrument.h @@ -97,8 +97,6 @@ private slots: std::array m_runningNotes = {}; #endif void clearRunningNotes(); - - friend class gui::Lv2InsView; }; @@ -119,6 +117,9 @@ Q_OBJECT private: void modelChanged() override; + bool isResizable() const final { return Lv2ViewBase::isResizable(); } + QSize sizeHint() const override; + QSize minimumSizeHint() const override { return sizeHint(); } }; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index eb69fd1a24b..05692bb9d65 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -28,6 +28,14 @@ IF(LMMS_BUILD_APPLE AND CMAKE_CXX_COMPILER_ID MATCHES "Clang") SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++") ENDIF() +IF(SERD_INCLUDE_DIRS) + include_directories(SYSTEM ${SERD_INCLUDE_DIRS}) +ENDIF() + +IF(SRATOM_INCLUDE_DIRS) + include_directories(SYSTEM ${SRATOM_INCLUDE_DIRS}) +ENDIF() + ADD_SUBDIRECTORY(common) ADD_SUBDIRECTORY(core) ADD_SUBDIRECTORY(gui) @@ -88,6 +96,7 @@ IF(SUIL_INCLUDE_DIRS) include_directories(SYSTEM ${SUIL_INCLUDE_DIRS}) ENDIF() + # Use libraries in non-standard directories (e.g., another version of Qt) IF(LMMS_BUILD_LINUX) LINK_LIBRARIES(-Wl,--enable-new-dtags) @@ -174,6 +183,8 @@ SET(LMMS_REQUIRED_LIBS ${LMMS_REQUIRED_LIBS} ${JACK_LIBRARIES} ${LV2_LIBRARIES} ${SUIL_LIBRARIES} + ${SERD_LIBRARIES} + ${SRATOM_LIBRARIES} ${LILV_LIBRARIES} ${FFTW3F_LIBRARIES} SampleRate::samplerate diff --git a/src/core/audio/AudioJack.cpp b/src/core/audio/AudioJack.cpp index b6da7c84591..429c4ddcb85 100644 --- a/src/core/audio/AudioJack.cpp +++ b/src/core/audio/AudioJack.cpp @@ -34,7 +34,6 @@ #include "ConfigManager.h" #include "Engine.h" #include "GuiApplication.h" -#include "LcdSpinBox.h" #include "MainWindow.h" #include "MidiJack.h" @@ -44,19 +43,14 @@ namespace lmms AudioJack::AudioJack(bool& successful, AudioEngine* audioEngineParam) : AudioDevice( - // clang-format off - std::clamp( - ConfigManager::inst()->value("audiojack", "channels").toInt(), - DEFAULT_CHANNELS, - DEFAULT_CHANNELS - ), - // clang-format on + DEFAULT_CHANNELS, audioEngineParam) , m_client(nullptr) , m_active(false) , m_midiClient(nullptr) , m_tempOutBufs(new jack_default_audio_sample_t*[channels()]) , m_outBuf(new SampleFrame[audioEngine()->framesPerPeriod()]) + , m_inBuf(new SampleFrame[audioEngine()->framesPerPeriod()]) , m_framesDoneInCurBuf(0) , m_framesToDoInCurBuf(0) { @@ -66,6 +60,8 @@ AudioJack::AudioJack(bool& successful, AudioEngine* audioEngineParam) if (successful) { connect(this, SIGNAL(zombified()), this, SLOT(restartAfterZombified()), Qt::QueuedConnection); } + + m_supportsCapture = true; } @@ -90,6 +86,7 @@ AudioJack::~AudioJack() delete[] m_tempOutBufs; delete[] m_outBuf; + delete[] m_inBuf; } @@ -154,6 +151,16 @@ bool AudioJack::initJackClient() clientName.toLatin1().constData(), jack_get_client_name(m_client)); } + resizeInputBuffer(jack_get_buffer_size(m_client)); + + // set buffer-size callback + jack_set_buffer_size_callback(m_client, + [](jack_nframes_t nframes, void* udata) -> int { + static_cast(udata)->resizeInputBuffer(nframes); + return 0; + }, + this); + // set process-callback jack_set_process_callback(m_client, staticProcessCallback, this); @@ -167,6 +174,10 @@ bool AudioJack::initJackClient() QString name = QString("master out ") + ((ch % 2) ? "R" : "L") + QString::number(ch / 2 + 1); m_outputPorts.push_back( jack_port_register(m_client, name.toLatin1().constData(), JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0)); + + QString input_name = QString("master in ") + ((ch % 2) ? "R" : "L") + QString::number(ch / 2 + 1); + m_inputPorts.push_back(jack_port_register(m_client, input_name.toLatin1().constData(), JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0)); + if (m_outputPorts.back() == nullptr) { printf("no more JACK-ports available!\n"); @@ -180,6 +191,14 @@ bool AudioJack::initJackClient() +void AudioJack::resizeInputBuffer(jack_nframes_t nframes) +{ + m_inputFrameBuffer.resize(nframes); +} + + + + void AudioJack::startProcessing() { if (m_active || m_client == nullptr) @@ -290,7 +309,6 @@ void AudioJack::renamePort(AudioBusHandle* port) int AudioJack::processCallback(jack_nframes_t nframes) { - // do midi processing first so that midi input can // add to the following sound processing if (m_midiClient && nframes > 0) @@ -356,6 +374,16 @@ int AudioJack::processCallback(jack_nframes_t nframes) } } + for (int c = 0; c < channels(); ++c) + { + jack_default_audio_sample_t* jack_input_buffer = (jack_default_audio_sample_t*) jack_port_get_buffer(m_inputPorts[c], nframes); + + for (jack_nframes_t frame = 0; frame < nframes; frame++) + { + m_inputFrameBuffer[frame][c] = static_cast(jack_input_buffer[frame]); + } + } + audioEngine()->pushInputFrames (m_inputFrameBuffer.data(), nframes); return 0; } @@ -390,24 +418,6 @@ AudioJack::setupWidget::setupWidget(QWidget* parent) m_clientName = new QLineEdit(cn, this); form->addRow(tr("Client name"), m_clientName); - - auto m = new gui::LcdSpinBoxModel(/* this */); - m->setRange(DEFAULT_CHANNELS, DEFAULT_CHANNELS); - m->setStep(2); - m->setValue(ConfigManager::inst()->value("audiojack", "channels").toInt()); - - m_channels = new gui::LcdSpinBox(1, this); - m_channels->setModel(m); - - form->addRow(tr("Channels"), m_channels); -} - - - - -AudioJack::setupWidget::~setupWidget() -{ - delete m_channels->model(); } @@ -416,7 +426,6 @@ AudioJack::setupWidget::~setupWidget() void AudioJack::setupWidget::saveSettings() { ConfigManager::inst()->setValue("audiojack", "clientname", m_clientName->text()); - ConfigManager::inst()->setValue("audiojack", "channels", QString::number(m_channels->value())); } diff --git a/src/core/lv2/Lv2Basics.cpp b/src/core/lv2/Lv2Basics.cpp index ab5db6ef106..166ab747cbf 100644 --- a/src/core/lv2/Lv2Basics.cpp +++ b/src/core/lv2/Lv2Basics.cpp @@ -1,7 +1,7 @@ /* * Lv2Basics.cpp - basic Lv2 functions * - * Copyright (c) 2019-2020 Johannes Lorenz + * Copyright (c) 2019-2024 Johannes Lorenz * * This file is part of LMMS - https://lmms.io * @@ -24,7 +24,10 @@ #include "Lv2Basics.h" -#ifdef LMMS_HAVE_LV2 +#include +#include + +#ifdef LMMS_HAVE_LV2 namespace lmms { @@ -48,6 +51,16 @@ std::string stdStringFromPortName(const LilvPlugin* plug, const LilvPort* port) lilv_node_as_string(AutoLilvNode(lilv_port_get_name(plug, port)).get())); } +float lv2UiRefreshRate() +{ + return (float)QGuiApplication::primaryScreen()->refreshRate(); +} + +float lv2UiScaleFactor() +{ + return (float)QGuiApplication::primaryScreen()->devicePixelRatio(); +} + } // namespace lmms #endif // LMMS_HAVE_LV2 diff --git a/src/core/lv2/Lv2ControlBase.cpp b/src/core/lv2/Lv2ControlBase.cpp index 27e6348ae5a..9f4780bd578 100644 --- a/src/core/lv2/Lv2ControlBase.cpp +++ b/src/core/lv2/Lv2ControlBase.cpp @@ -181,7 +181,8 @@ void Lv2ControlBase::saveSettings(QDomDocument &doc, QDomElement &that) void Lv2ControlBase::loadSettings(const QDomElement &that) { LinkedModelGroups::loadSettings(that); - + qDebug() << "Lv2ControlBase::loadSettings"; + for(const auto& p : m_procs) { p->onSettingsLoaded(); } // TODO: load state if supported by plugin } diff --git a/src/core/lv2/Lv2Features.cpp b/src/core/lv2/Lv2Features.cpp index 25ee115443a..45ff532de14 100644 --- a/src/core/lv2/Lv2Features.cpp +++ b/src/core/lv2/Lv2Features.cpp @@ -1,7 +1,7 @@ /* * Lv2Features.cpp - Lv2Features implementation * - * Copyright (c) 2020-2020 Johannes Lorenz + * Copyright (c) 2020-2024 Johannes Lorenz * * This file is part of LMMS - https://lmms.io * @@ -50,7 +50,7 @@ Lv2Features::Lv2Features() // create (yet empty) map feature URI -> feature for(auto uri : man->supportedFeatureURIs()) { - m_featureByUri.emplace(uri, nullptr); + m_featureByUri.emplace(uri, LV2_Feature { uri.data(), nullptr }); } } @@ -61,8 +61,8 @@ void Lv2Features::initCommon() { Lv2Manager* man = Engine::getLv2Manager(); // init m_featureByUri with the plugin-common features - operator[](LV2_URID__map) = man->uridMap().mapFeature(); - operator[](LV2_URID__unmap) = man->uridMap().unmapFeature(); + operator[](LV2_URID__map).data = man->uridMap().mapFeature(); + operator[](LV2_URID__unmap).data = man->uridMap().unmapFeature(); } @@ -82,7 +82,7 @@ void Lv2Features::createFeatureVectors() vector creation (This can be done in Lv2Proc::initPluginSpecificFeatures or in Lv2Features::initCommon) */ - m_features.push_back(LV2_Feature{(const char*)uri.data(), (void*)feature}); + m_features.push_back(feature); } // create pointer vector (for lilv_plugin_instantiate) @@ -97,7 +97,7 @@ void Lv2Features::createFeatureVectors() -void *&Lv2Features::operator[](const char *featName) +LV2_Feature& Lv2Features::operator[](const char *featName) { auto itr = m_featureByUri.find(featName); Q_ASSERT(itr != m_featureByUri.end()); @@ -113,7 +113,7 @@ void Lv2Features::clear() for (auto& [uri, feature] : m_featureByUri) { (void) uri; - feature = nullptr; + feature.data = nullptr; } } diff --git a/src/core/lv2/Lv2Manager.cpp b/src/core/lv2/Lv2Manager.cpp index 9807379e44c..e45724ee791 100644 --- a/src/core/lv2/Lv2Manager.cpp +++ b/src/core/lv2/Lv2Manager.cpp @@ -32,6 +32,11 @@ #include #include #include + +#include +#include +#define NS_XSD "http://www.w3.org/2001/XMLSchema#" + #include #include @@ -68,6 +73,65 @@ const std::set Lv2Manager::unstablePlugins = "http://drobilla.net/plugins/blop/square", "http://drobilla.net/plugins/blop/triangle", + // LSP mono effects have no advantage over stereo effects, + // since LMMS is always stereo (if you want different effects + // for both sides, these effects offer "lr" variants) + "http://lsp-plug.in/plugins/lv2/ab_tester_x2_mono", + "http://lsp-plug.in/plugins/lv2/ab_tester_x4_mono", + "http://lsp-plug.in/plugins/lv2/ab_tester_x8_mono", + "http://lsp-plug.in/plugins/lv2/art_delay_mono", + "http://lsp-plug.in/plugins/lv2/autogain_mono", + "http://lsp-plug.in/plugins/lv2/beat_breather_mono", + "http://lsp-plug.in/plugins/lv2/chorus_mono", + "http://lsp-plug.in/plugins/lv2/clipper_mono", + "http://lsp-plug.in/plugins/lv2/comp_delay_mono", + "http://lsp-plug.in/plugins/lv2/compressor_mono", + "http://lsp-plug.in/plugins/lv2/crossover_mono", + "http://lsp-plug.in/plugins/lv2/dyna_processor_mono", + "http://lsp-plug.in/plugins/lv2/expander_mono", + "http://lsp-plug.in/plugins/lv2/filter_mono", + "http://lsp-plug.in/plugins/lv2/flanger_mono", + "http://lsp-plug.in/plugins/lv2/gate_mono", + "http://lsp-plug.in/plugins/lv2/gott_compressor_mono", + "http://lsp-plug.in/plugins/lv2/graph_equalizer_x16_mono", + "http://lsp-plug.in/plugins/lv2/graph_equalizer_x32_mono", + "http://lsp-plug.in/plugins/lv2/impulse_responses_mono", + "http://lsp-plug.in/plugins/lv2/impulse_reverb_mono", + "http://lsp-plug.in/plugins/lv2/limiter_mono", + "http://lsp-plug.in/plugins/lv2/loud_comp_mono", + "http://lsp-plug.in/plugins/lv2/mb_clipper_mono", + "http://lsp-plug.in/plugins/lv2/mb_compressor_mono", + "http://lsp-plug.in/plugins/lv2/mb_dyna_processor_mono", + "http://lsp-plug.in/plugins/lv2/mb_expander_mono", + "http://lsp-plug.in/plugins/lv2/mb_gate_mono", + "http://lsp-plug.in/plugins/lv2/mb_limiter_mono", + "http://lsp-plug.in/plugins/lv2/mixer_x16_mono", + "http://lsp-plug.in/plugins/lv2/mixer_x4_mono", + "http://lsp-plug.in/plugins/lv2/mixer_x8_mono", + "http://lsp-plug.in/plugins/lv2/oscillator_mono", + "http://lsp-plug.in/plugins/lv2/para_equalizer_x16_mono", + "http://lsp-plug.in/plugins/lv2/para_equalizer_x32_mono", + "http://lsp-plug.in/plugins/lv2/para_equalizer_x8_mono", + "http://lsp-plug.in/plugins/lv2/profiler_mono", + "http://lsp-plug.in/plugins/lv2/room_builder_mono", + "http://lsp-plug.in/plugins/lv2/sampler_mono", + "http://lsp-plug.in/plugins/lv2/sc_autogain_mono", + "http://lsp-plug.in/plugins/lv2/sc_compressor_mono", + "http://lsp-plug.in/plugins/lv2/sc_dyna_processor_mono", + "http://lsp-plug.in/plugins/lv2/sc_expander_mono", + "http://lsp-plug.in/plugins/lv2/sc_gate_mono", + "http://lsp-plug.in/plugins/lv2/sc_gott_compressor_mono", + "http://lsp-plug.in/plugins/lv2/sc_limiter_mono", + "http://lsp-plug.in/plugins/lv2/sc_mb_compressor_mono", + "http://lsp-plug.in/plugins/lv2/sc_mb_dyna_processor_mono", + "http://lsp-plug.in/plugins/lv2/sc_mb_expander_mono", + "http://lsp-plug.in/plugins/lv2/sc_mb_gate_mono", + "http://lsp-plug.in/plugins/lv2/sc_mb_limiter_mono", + "http://lsp-plug.in/plugins/lv2/slap_delay_mono", + "http://lsp-plug.in/plugins/lv2/surge_filter_mono", + "http://lsp-plug.in/plugins/lv2/trigger_midi_mono", + "http://lsp-plug.in/plugins/lv2/trigger_mono", + // unstable "urn:juced:DrumSynth" }; @@ -174,6 +238,22 @@ Lv2Manager::Lv2Manager() : m_world = lilv_world_new(); lilv_world_load_all(m_world); +#ifdef LMMS_HAVE_SERD + env = serd_env_new(nullptr); + serd_env_set_prefix_from_strings( + env, (const uint8_t*)"patch", (const uint8_t*)LV2_PATCH_PREFIX); + serd_env_set_prefix_from_strings( + env, (const uint8_t*)"time", (const uint8_t*)LV2_TIME_PREFIX); + serd_env_set_prefix_from_strings( + env, (const uint8_t*)"xsd", (const uint8_t*)NS_XSD); +#ifdef LMMS_HAVE_SRATOM + sratom = sratom_new(uridMap().mapFeature()); + ui_sratom = sratom_new(uridMap().mapFeature()); + sratom_set_env(sratom, env); + sratom_set_env(ui_sratom, env); +#endif +#endif // LMMS_HAVE_SERD + m_supportedFeatureURIs.insert(LV2_URID__map); m_supportedFeatureURIs.insert(LV2_URID__unmap); m_supportedFeatureURIs.insert(LV2_OPTIONS__options); @@ -189,13 +269,21 @@ Lv2Manager::Lv2Manager() : auto supportOpt = [this](Lv2UridCache::Id id) { - Lv2Options::supportOption(uridCache()[id]); + uint32_t urid = uridCache()[id]; + if(urid != Lv2UridCache::noUrid()) + { + Lv2Options::supportOption(urid); + } }; supportOpt(Lv2UridCache::Id::param_sampleRate); supportOpt(Lv2UridCache::Id::bufsz_maxBlockLength); supportOpt(Lv2UridCache::Id::bufsz_minBlockLength); supportOpt(Lv2UridCache::Id::bufsz_nominalBlockLength); supportOpt(Lv2UridCache::Id::bufsz_sequenceSize); + supportOpt(Lv2UridCache::Id::ui_updateRate); + supportOpt(Lv2UridCache::Id::ui_scaleFactor); + supportOpt(Lv2UridCache::Id::ui_backgroundColor); + supportOpt(Lv2UridCache::Id::ui_foregroundColor); } @@ -337,7 +425,11 @@ AutoLilvNodes Lv2Manager::findNodes(const LilvNode *subject, bool Lv2Manager::wantUi() { +#ifdef LMMS_HAVE_SUIL + return (ConfigManager::inst()->vstEmbedMethod() != "none"); +#else return false; +#endif } diff --git a/src/core/lv2/Lv2Options.cpp b/src/core/lv2/Lv2Options.cpp index 7b4528cb606..5ba35f456f1 100644 --- a/src/core/lv2/Lv2Options.cpp +++ b/src/core/lv2/Lv2Options.cpp @@ -27,6 +27,7 @@ #ifdef LMMS_HAVE_LV2 #include +#include namespace lmms @@ -73,24 +74,31 @@ void Lv2Options::createOptionVectors() -void Lv2Options::initOption(LV2_URID key, uint32_t size, LV2_URID type, +void Lv2Options::initOption(LV2_URID keyAsUrid, uint32_t size, LV2_URID type, std::shared_ptr value, LV2_Options_Context context, uint32_t subject) { - Q_ASSERT(isOptionSupported(key)); - - LV2_Options_Option opt; - opt.key = key; - opt.context = context; - opt.subject = subject; - opt.size = size; - opt.type = type; - opt.value = value.get(); - - const auto optResult = m_optionByUrid.emplace(key, opt); - const auto valResult = m_optionValues.emplace(key, std::move(value)); - Q_ASSERT(optResult.second); - Q_ASSERT(valResult.second); + if(isOptionSupported(keyAsUrid)) + { + LV2_Options_Option opt; + opt.key = keyAsUrid; + opt.context = context; + opt.subject = subject; + opt.size = size; + opt.type = type; + opt.value = value.get(); + + const auto optResult = m_optionByUrid.emplace(keyAsUrid, opt); + const auto valResult = m_optionValues.emplace(keyAsUrid, std::move(value)); + Q_ASSERT(optResult.second); + Q_ASSERT(valResult.second); + } + else + { + // Lv2Manager did not call supportOpt with this key + UridMap& uridMap = Engine::getLv2Manager()->uridMap(); + qDebug() << "Note: Lv2 Option " << uridMap.unmap(keyAsUrid) << " supported by LMMS, but not by your system"; + } } diff --git a/src/core/lv2/Lv2Proc.cpp b/src/core/lv2/Lv2Proc.cpp index e656f0cf19e..29f217e26cf 100644 --- a/src/core/lv2/Lv2Proc.cpp +++ b/src/core/lv2/Lv2Proc.cpp @@ -196,7 +196,8 @@ Lv2Proc::Lv2Proc(const LilvPlugin *plugin, Model* parent) : m_plugin(plugin), m_workLock(1), m_midiInputBuf(m_maxMidiInputEvents), - m_midiInputReader(m_midiInputBuf) + m_midiInputReader(m_midiInputBuf), + m_pluginEvents(uiMidiBufsize() * uiNBufferCycles()) { createPorts(); initPlugin(); @@ -215,6 +216,29 @@ void Lv2Proc::reload() { Lv2ProcSuspender(this); } +void Lv2Proc::onSettingsLoaded() +{ + // Savefile has been loaded into models, but not into corresponding ports yet + copyModelsFromCore(); + + // All control ports may have changed -> Tell UI + if (isUiActive()) + { + for (uint32_t p = 0; p < portNum(); ++p) + { + const Lv2Ports::PortBase& port = *m_ports[p]; + if (port.m_flow == Lv2Ports::Flow::Input && + port.m_type == Lv2Ports::Type::Control) + { + sendToUi(p, Lv2Ports::dcast(&port)); + } + } + } +} + + + + void Lv2Proc::dumpPorts() { std::size_t num = 0; @@ -228,6 +252,94 @@ void Lv2Proc::dumpPorts() +void Lv2Proc::applyUiEvents(uint32_t nframes) +{ + if (!isUiActive()) { return; } + Lv2UiControlChange ev; + const size_t space = m_uiEventsReader->read_space(); + Lv2Manager* mgr = Engine::getLv2Manager(); + LV2_URID atom_eventTransfer = mgr->uridMap().map(LV2_ATOM__eventTransfer); + for (size_t i = 0; i < space; i += sizeof(ev) + ev.size) + { + // read ControlChange + if (space - i < sizeof(ev)) { break; } + m_uiEventsReader->read(sizeof(ev)).copy((char*)&ev, sizeof(ev)); + + // fill body + char body[uiEventsBufsize()]; + { + auto sequence = m_uiEventsReader->read(ev.size); + if (sequence.size() == ev.size) + { + sequence.copy(body, ev.size); + } + else + { + qWarning() << "error: Error reading from UI ring buffer"; + break; + } + } + + assert(ev.index < portNum()); + struct Lv2Ports::PortBase& port = *m_ports[ev.index]; + if (ev.protocol == 0) + { + assert(ev.size == sizeof(float)); + struct FloatToModelVisitor : public ModelVisitor + { + const std::vector* m_scalePointMap; // in + float m_val; // in + void visit(FloatModel& m) override { m.setValue(m_val); qDebug() << "set val" << m_val; } + void visit(IntModel& m) override { m.setValue(m_val); } + void visit(BoolModel& m) override { m.setValue(m_val); } + void visit(ComboBoxModel& m) override + { + float bestDist = std::numeric_limits::max(); + std::size_t bestIdx = std::numeric_limits::max(); + for (std::size_t idx = 0; idx < m_scalePointMap->size(); ++idx) + { + float dist = std::fabs((*m_scalePointMap)[idx] - m_val); + if(dist < bestDist) + { + bestDist = dist; + bestIdx = idx; + } + } + assert(bestIdx != std::numeric_limits::max()); + m.setValue(static_cast(bestIdx)); + } + }; + Lv2Ports::Control* ctrl = Lv2Ports::dcast(&port); + // update value in port + ctrl->m_val = *(float*)body; + // update value in model + FloatToModelVisitor ftmv; + ftmv.m_scalePointMap = &ctrl->m_scalePointMap; + ftmv.m_val = *(float*)body; + ctrl->m_connectedModel->accept(ftmv); + qDebug() << m_ports[ev.index]->uri(); + qDebug() << m_ports[ev.index]->name(); + } + else if (ev.protocol == atom_eventTransfer) + { + Lv2Ports::AtomSeq* ato = Lv2Ports::dcast(&port); + assert(ato); + LV2_Evbuf_Iterator e = lv2_evbuf_end(ato->m_buf.get()); + const LV2_Atom* const atom = (const LV2_Atom*)body; + assert(atom); + lv2_evbuf_write(&e, nframes, atom->type, atom->size, + (const uint8_t*)LV2_ATOM_BODY_CONST(atom)); + } + else + { + qDebug() << "error: Unknown control change protocol" << ev.protocol; + } + } +} + + + + void Lv2Proc::copyModelsFromCore() { struct FloatFromModelVisitor : public ConstModelVisitor @@ -291,7 +403,7 @@ void Lv2Proc::copyModelsFromCore() std::size_t bufsize = writeToByteSeq(ev.ev, buf.data(), buf.size()); if(bufsize) { - lv2_evbuf_write(&iter, atomStamp, type, bufsize, buf.data()); + lv2_evbuf_write(&iter, atomStamp, type, (uint32_t)bufsize, buf.data()); } } } @@ -369,8 +481,60 @@ void Lv2Proc::copyBuffersToCore(SampleFrame* buf, +bool Lv2Proc::sendToUi(uint32_t port_index, + uint32_t type, uint32_t size, const void* body) +{ + if (!isUiActive()) { return false; } + + char evbuf[sizeof(Lv2UiControlChange) + sizeof(LV2_Atom)]; + Lv2UiControlChange* ev = (Lv2UiControlChange*)evbuf; + ev->index = port_index; + ev->protocol = Engine::getLv2Manager()->uridMap().map(LV2_ATOM__eventTransfer); + ev->size = sizeof(LV2_Atom) + size; + + LV2_Atom* atom = (LV2_Atom*)(ev + 1); + atom->type = type; + atom->size = size; + + const bool enoughSpace = m_pluginEvents.capacity() >= sizeof(evbuf) + size; + if (enoughSpace) + { + m_pluginEvents.write(evbuf, sizeof(evbuf)); + m_pluginEvents.write((const char*)body, size); + } + else { qWarning() << "Plugin => UI buffer overflow for port" << port_index << "!"; } + return enoughSpace; +} + + + + +bool Lv2Proc::sendToUi(uint32_t port_index, const Lv2Ports::Control* ctrl) +{ + qDebug () << "sendToUi(): Want to set float port" << port_index << "to" << ctrl->m_val; + + if (!isUiActive()) { return false; } + assert(ctrl); + char buf[sizeof(Lv2UiControlChange) + sizeof(float)]; + Lv2UiControlChange* ev = (Lv2UiControlChange*)buf; + ev->index = port_index; + ev->protocol = 0; // = float value + ev->size = sizeof(float); + *(float*)(ev + 1) = ctrl->m_val; + const bool enoughSpace = m_pluginEvents.capacity() >= sizeof(buf); + qDebug () << "sendToUi(): Setting float port" << port_index << "to" << ctrl->m_val; + if (enoughSpace) { m_pluginEvents.write(buf, sizeof(buf)); } + else { qWarning() << "Plugin => UI buffer overflow for port" << port_index << "!"; } + return enoughSpace; +} + + + + void Lv2Proc::run(fpp_t frames) { + applyUiEvents(frames); + if (m_worker) { // Process any worker replies @@ -384,6 +548,75 @@ void Lv2Proc::run(fpp_t frames) // Notify the plugin the run() cycle is finished m_worker->notifyPluginThatRunFinished(); } + + // Check if it's time to send updates to the UI + m_eventDeltaT += frames; + bool sendUiUpdates = false; + uint32_t updateFrames = (uint32_t)(Engine::audioEngine()->outputSampleRate() / lv2UiRefreshRate()); + if (isUiActive() && (m_eventDeltaT > updateFrames)) + { + sendUiUpdates = true; + m_eventDeltaT = 0; + } + + for (uint32_t p = 0; p < portNum(); ++p) + { + const Lv2Ports::PortBase& port = *m_ports[p]; + if (port.m_flow == Lv2Ports::Flow::Output && + port.m_type == Lv2Ports::Type::AtomSeq) + { + // plugin writes all kind of information to AtomSeq output, + // so we forward this to the UI + // example: User presses key -> MIDI gets to Atom in port -> Plugin plays sound + // -> MIDI gets send to Atom out port -> UI visualizes sound + const Lv2Ports::AtomSeq* atomSeq = Lv2Ports::dcast(&port); + /*void* buf = nullptr; + if (port->sys_port) + { + buf = jack_port_get_buffer(port->sys_port, nframes); + jack_midi_clear_buffer(buf); + }*/ + for (LV2_Evbuf_Iterator i = lv2_evbuf_begin(&*atomSeq->m_buf); + lv2_evbuf_is_valid(i); // see note (*) on bottom of file + i = lv2_evbuf_next(i)) + { + // Get event from LV2 buffer + uint32_t frames = 0; + uint32_t type = 0; + uint32_t size = 0; + uint8_t* body = NULL; + lv2_evbuf_get(i, &frames, &type, &size, &body); + + /*if (buf && type == jalv->urids.midi_MidiEvent) + { + // Write MIDI event to Jack output + jack_midi_event_write(buf, frames, body, size); + }*/ + sendToUi(p, type, size, body); // forward event to UI + } + } + else if (sendUiUpdates && + port.m_flow == Lv2Ports::Flow::Output && + port.m_type == Lv2Ports::Type::Control) + { + const Lv2Ports::Control* ctrl = Lv2Ports::dcast(&port); + assert(ctrl); + char buf[sizeof(Lv2UiControlChange) + sizeof(float)]; + Lv2UiControlChange* ev = (Lv2UiControlChange*)buf; + ev->index = p; + ev->protocol = 0; + ev->size = sizeof(float); + *(float*)(ev + 1) = ctrl->m_val; + if (m_pluginEvents.capacity() >= sizeof(buf)) + { + m_pluginEvents.write(buf, sizeof(buf)); + } + else + { + qWarning() << "Plugin => UI buffer overflow!"; + } + } + } } @@ -424,11 +657,13 @@ void Lv2Proc::handleMidiInputEvent(const MidiEvent &event, const TimePos &time, +#if 0 AutomatableModel *Lv2Proc::modelAtPort(const QString &uri) { const auto itr = m_connectedModels.find(uri.toUtf8().data()); return itr != m_connectedModels.end() ? itr->second : nullptr; } +#endif @@ -442,6 +677,8 @@ void Lv2Proc::initPlugin() m_instance = lilv_plugin_instantiate(m_plugin, Engine::audioEngine()->outputSampleRate(), m_features.featurePointers()); + + m_features.m_extData.data_access = lilv_instance_get_descriptor(m_instance)->extension_data; if (m_instance) { @@ -462,9 +699,7 @@ void Lv2Proc::initPlugin() { qCritical() << "Failed to create an instance of" << qStringFromPluginNode(m_plugin, lilv_plugin_get_name) - << "(URI:" - << lilv_node_as_uri(lilv_plugin_get_uri(m_plugin)) - << ")"; + << "(URI:" << pluginUri() << ")"; throw std::runtime_error("Failed to create Lv2 processor"); } } @@ -497,6 +732,47 @@ bool Lv2Proc::hasNoteInput() const +std::size_t Lv2Proc::getIdOfPort(const char* symbol) const +{ + std::size_t idx = 0; + for (const std::unique_ptr& port : m_ports) + { + if (!strcmp(port.get()->uri().toUtf8().data(), symbol)) + { + return idx; + } + ++idx; + } + return -1; +} + + + + +QString Lv2Proc::portname(std::size_t idx) const { return m_ports[idx].get()->name(); } + + + + +uint32_t Lv2Proc::portNum() const +{ + return m_ports.size(); +} + + + + +bool Lv2Proc::isUiActive() const +{ + // m_uiEventsReader is connected in Lv2ViewProc CTOR + // not connected => Lv2ViewProc CTOR has not been completed + // connected => Lv2ViewProc CTOR has not been completed "sufficiently" + return !!m_uiEventsReader; +} + + + + void Lv2Proc::initMOptions() { /* @@ -512,13 +788,24 @@ void Lv2Proc::initMOptions() float sampleRate = Engine::audioEngine()->outputSampleRate(); int32_t blockLength = Engine::audioEngine()->framesPerPeriod(); int32_t sequenceSize = defaultEvbufSize(); - + // Create dummy object to find out the color + // I wish there was a more clean way... + /*gui::SubWindow tmpSubWindow; + int32_t backgroundColor = tmpSubWindow.palette().color(QPalette::Window).rgba(); + int32_t foregroundColor = tmpSubWindow.palette().color(QPalette::WindowText).rgba();*/ + int32_t backgroundColor = 0xffffffff; + int32_t foregroundColor = 0x000000ff; + qDebug() << backgroundColor << " <-> " << foregroundColor; using Id = Lv2UridCache::Id; m_options.initOption(Id::param_sampleRate, sampleRate); m_options.initOption(Id::bufsz_maxBlockLength, blockLength); m_options.initOption(Id::bufsz_minBlockLength, blockLength); m_options.initOption(Id::bufsz_nominalBlockLength, blockLength); m_options.initOption(Id::bufsz_sequenceSize, sequenceSize); + m_options.initOption(Id::ui_updateRate, lv2UiRefreshRate()); + m_options.initOption(Id::ui_scaleFactor, lv2UiScaleFactor()); + m_options.initOption(Id::ui_backgroundColor, backgroundColor); + m_options.initOption(Id::ui_foregroundColor, foregroundColor); m_options.createOptionVectors(); } @@ -529,7 +816,7 @@ void Lv2Proc::initPluginSpecificFeatures() { // options initMOptions(); - m_features[LV2_OPTIONS__options] = const_cast(m_options.feature()); + m_features[LV2_OPTIONS__options].data = const_cast(m_options.feature()); // worker (if plugin has worker extension) Lv2Manager* mgr = Engine::getLv2Manager(); @@ -537,7 +824,7 @@ void Lv2Proc::initPluginSpecificFeatures() { bool threaded = !Engine::audioEngine()->renderOnly(); m_worker.emplace(&m_workLock, threaded); - m_features[LV2_WORKER__schedule] = m_worker->feature(); + m_features[LV2_WORKER__schedule].data = m_worker->feature(); // note: the worker interface can not be instantiated yet - it requires m_instance. see initPlugin() } } @@ -576,9 +863,7 @@ void Lv2Proc::createPort(std::size_t portNum) { qWarning() << "Warning: Plugin" << qStringFromPluginNode(m_plugin, lilv_plugin_get_name) - << "(URI:" - << lilv_node_as_uri(lilv_plugin_get_uri(m_plugin)) - << ") has a default value for port" + << "(URI:" << pluginUri() << ") has a default value for port" << dispName << "which is not in range [min, max]."; } @@ -698,7 +983,7 @@ void Lv2Proc::createPort(std::size_t portNum) lv2_evbuf_new(static_cast(minimumSize), mgr->uridMap().map(LV2_ATOM__Chunk), mgr->uridMap().map(LV2_ATOM__Sequence))); - + port = atomPort; break; } @@ -918,4 +1203,23 @@ AutoLilvNode Lv2Proc::uri(const char *uriStr) } // namespace lmms +/* + *Note: lv2_evbuf_is_valid is called only for output ports here + output ports are created with "evbuf->buf.atom.type == evbuf->atom_Chunk", + so lv2_evbuf_is_valid would always return false + + However, the atom.type can get overwritten by the plugin, e.g. like + in the following backtrace: + + memcpy + lv2_atom_forge_raw + lv2_atom_forge_write + lv2_atom_forge_sequence_head + lsp::lv2::Extensions::forge_sequence_head + lsp::lv2::Wrapper::transmit_atoms + lsp::lv2::Wrapper::run + lsp::lv2::run + lilv_instance_run +*/ + #endif // LMMS_HAVE_LV2 diff --git a/src/core/lv2/Lv2UridCache.cpp b/src/core/lv2/Lv2UridCache.cpp index 7d3a14c9349..0aee1cc75ae 100644 --- a/src/core/lv2/Lv2UridCache.cpp +++ b/src/core/lv2/Lv2UridCache.cpp @@ -1,7 +1,7 @@ /* * Lv2UridCache.cpp - Lv2UridCache implementation * - * Copyright (c) 2020-2020 Johannes Lorenz + * Copyright (c) 2020-2024 Johannes Lorenz * * This file is part of LMMS - https://lmms.io * @@ -26,10 +26,12 @@ #ifdef LMMS_HAVE_LV2 +#include #include #include #include #include +#include #include #include "Lv2UridMap.h" @@ -43,21 +45,31 @@ namespace lmms { -uint32_t Lv2UridCache::operator[](Lv2UridCache::Id id) const +LV2_URID Lv2UridCache::operator[](Lv2UridCache::Id id) const { Q_ASSERT(id != Id::size); return m_cache[static_cast(id)]; } + + + Lv2UridCache::Lv2UridCache(UridMap &mapper) { - const uint32_t noIdYet = 0; - std::fill_n(m_cache, static_cast(Id::size), noIdYet); + checkIdNamesConsistency(); + + const LV2_URID noUridYet = std::numeric_limits::max(); + std::fill_n(m_cache, static_cast(Id::size), noUridYet); auto init = [this, &mapper](Id id, const char* uridStr) { m_cache[static_cast(id)] = mapper.map(uridStr); }; + auto initNoUrid = [this](Id id) + { + m_cache[static_cast(id)] = noUrid(); + }; + (void)initNoUrid; init(Id::atom_Float, LV2_ATOM__Float); init(Id::atom_Int, LV2_ATOM__Int); @@ -67,8 +79,44 @@ Lv2UridCache::Lv2UridCache(UridMap &mapper) init(Id::bufsz_sequenceSize, LV2_BUF_SIZE__sequenceSize); init(Id::midi_MidiEvent, LV2_MIDI__MidiEvent); init(Id::param_sampleRate, LV2_PARAMETERS__sampleRate); +#ifdef LV2_UI__backgroundColor + init(Id::ui_backgroundColor, LV2_UI__backgroundColor); +#else + initNoUrid(Id::ui_backgroundColor); +#endif +#ifdef LV2_UI__foregroundColor + init(Id::ui_foregroundColor, LV2_UI__foregroundColor); +#else + initNoUrid(Id::ui_foregroundColor); +#endif + init(Id::ui_updateRate, LV2_UI__updateRate); +#ifdef LV2_UI__scaleFactor + init(Id::ui_scaleFactor, LV2_UI__scaleFactor); +#else + initNoUrid(Id::ui_scaleFactor); +#endif - for(uint32_t urid : m_cache) { Q_ASSERT(urid != noIdYet); } + for(LV2_URID urid : m_cache) + { + // If you hit this assert, then you added an ID for which you did not call "init" + Q_ASSERT(urid != noUridYet); + } +} + + + + +void Lv2UridCache::checkIdNamesConsistency() +{ + // make sure sizes match + static_assert(sizeof(s_idNames)/sizeof(std::string_view) == static_cast(Id::size)); + // all array elements are (non-default-)initialized + assert(s_idNames[static_cast(Id::size)][0]); + // alphabetical order + for(std::size_t i = 1; i < static_cast(Id::size); ++i) + { + assert(s_idNames[i-1] + * Copyright (c) 2019-2024 Johannes Lorenz * * This file is part of LMMS - https://lmms.io * @@ -22,6 +22,7 @@ * */ +#include #include "Lv2UridMap.h" @@ -99,6 +100,17 @@ const char *UridMap::unmap(LV2_URID urid) return (idx < m_unMap.size()) ? m_unMap[idx] : nullptr; } +void UridMap::dump() +{ + std::lock_guard guard (m_MapMutex); + qDebug() << "UridMap dump"; + qDebug() << "============"; + for(const auto & [str, urid] : m_map) + { + qDebug() << static_cast(urid) << str.c_str(); + } +} + } // namespace lmms diff --git a/src/core/main.cpp b/src/core/main.cpp index fb54feeab0b..9d47e02a99d 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -805,7 +805,7 @@ int main( int argc, char * * argv ) { using namespace lmms::gui; - new GuiApplication(); + new GuiApplication(&argc, &argv); // re-intialize RNG - shared libraries might have srand() or // srandom() calls in their init procedure diff --git a/src/gui/GuiApplication.cpp b/src/gui/GuiApplication.cpp index 7e62a3141cd..4b145683ba0 100644 --- a/src/gui/GuiApplication.cpp +++ b/src/gui/GuiApplication.cpp @@ -48,6 +48,7 @@ #include #include #include +#include #include #ifdef LMMS_BUILD_WIN32 @@ -59,6 +60,10 @@ #include #endif +#ifdef LMMS_HAVE_SUIL + #include +#endif + namespace lmms { @@ -81,7 +86,7 @@ GuiApplication* GuiApplication::instance() -GuiApplication::GuiApplication() +GuiApplication::GuiApplication(int *argc, char ***argv) { // Immediately register our SIGINT handler createSocketNotifier(); @@ -115,6 +120,34 @@ GuiApplication::GuiApplication() QApplication::setAttribute(Qt::AA_DontShowIconsInMenus, true); #endif +#ifdef LMMS_HAVE_SUIL + if(qgetenv("SUIL_MODULE_DIR").isEmpty()) + { + // Load Suil modules from a bundled application +#if defined(LMMS_BUILD_WIN32) + if(qApp->applicationDirPath().contains("/Program Files/")) { + qputenv("SUIL_MODULE_DIR", qApp->applicationDirPath().append("/../suil-0/").toUtf8()); + } +#elif defined(LMMS_BUILD_APPLE) + if(qApp->applicationDirPath().endsWith("/Contents/MacOS")) { + qputenv("SUIL_MODULE_DIR", qApp->applicationDirPath().append("/../Frameworks/suil-0/").toUtf8()); + } +#else + if(qApp->applicationDirPath().contains("/squashfs-root/") || + qApp->applicationDirPath().contains("/.mount_lmms-") || + qApp->applicationDirPath().startsWith("/opt/lmms/")) { + qputenv("SUIL_MODULE_DIR", qApp->applicationDirPath().append("/../lib/suil-0/").toUtf8()); + } +#endif + } + else { qDebug() << "Using SUIL_MODULE_DIR from commandline:" << qgetenv("SUIL_MODULE_DIR"); } + // Note: The suil_init documentation says "This function should be called + // as early as possible, before any other GUI" - This place here is after the LMMS + // arguments have been passed. We do so because qApp->applicationDirPath() (see above) + // can not be called before GuiApplication has been inited. + suil_init(argc, argv, SUIL_ARG_NONE); +#endif // LMMS_HAVE_SUIL + // Show splash screen QSplashScreen splashScreen( embed::getIconPixmap( "splash" ) ); splashScreen.setFixedSize(splashScreen.pixmap().size()); diff --git a/src/gui/LinkedModelGroupViews.cpp b/src/gui/LinkedModelGroupViews.cpp index f77edcdb9de..9430f18e0a2 100644 --- a/src/gui/LinkedModelGroupViews.cpp +++ b/src/gui/LinkedModelGroupViews.cpp @@ -25,6 +25,7 @@ #include "LinkedModelGroupViews.h" #include +#include #include "Controls.h" #include "ControlLayout.h" #include "LinkedModelGroups.h" @@ -43,8 +44,11 @@ LinkedModelGroupView::LinkedModelGroupView(QWidget* parent, QWidget(parent), m_model(model), m_colNum(colNum), - m_layout(new ControlLayout(this)) + m_vbox(new QVBoxLayout(this)) { + auto layoutWidget = new QWidget(); + m_vbox->addWidget(layoutWidget, 0); + m_layout = new ControlLayout(layoutWidget); // This is required to remove the focus of the line edit // when e.g. another spin box is being clicked. // Removing the focus is wanted because in many cases, the user wants to @@ -87,7 +91,7 @@ void LinkedModelGroupView::addControl(Control* ctrl, const std::string& id, if (ctrl) { auto box = new QWidget(this); - auto boxLayout = new QHBoxLayout(box); + auto boxLayout = new QHBoxLayout(); boxLayout->addWidget(ctrl->topWidget()); if (removable) @@ -109,6 +113,7 @@ void LinkedModelGroupView::addControl(Control* ctrl, const std::string& id, // required, so the Layout knows how to sort/filter widgets by string box->setObjectName(QString::fromStdString(display)); + box->setLayout(boxLayout); m_layout->addWidget(box); // take ownership of control and add it diff --git a/src/gui/Lv2ViewBase.cpp b/src/gui/Lv2ViewBase.cpp index 0cd6a0ee4b7..97f57cf43d0 100644 --- a/src/gui/Lv2ViewBase.cpp +++ b/src/gui/Lv2ViewBase.cpp @@ -1,7 +1,7 @@ /* * Lv2ViewBase.cpp - base class for Lv2 plugin views * - * Copyright (c) 2018-2023 Johannes Lorenz + * Copyright (c) 2018-2024 Johannes Lorenz * * This file is part of LMMS - https://lmms.io * @@ -29,8 +29,13 @@ #include #include #include +#include #include #include +#include +#include +#include +#include #include #include "AudioEngine.h" @@ -52,84 +57,461 @@ namespace lmms::gui { -Lv2ViewProc::Lv2ViewProc(QWidget* parent, Lv2Proc* proc, int colNum) : - LinkedModelGroupView (parent, proc, colNum) +class Timer : public QTimer +{ +public: + explicit Timer(Lv2ViewProc* viewProc) + : QTimer(viewProc) + , m_viewProc(viewProc) + {} + + void timerEvent(QTimerEvent*) override + { + if (m_viewProc) { m_viewProc->update(); } + } + +private: + Lv2ViewProc* m_viewProc = nullptr; +}; + + + + +void Lv2ViewProc::uiPortEvent( + uint32_t port_index, + uint32_t buffer_size, + uint32_t protocol, + const void* buffer) { - class SetupTheWidget : public Lv2Ports::ConstVisitor +#ifdef LMMS_HAVE_SUIL + if (m_uiInstance) { - public: - QWidget* m_parent; // input - const LilvNode* m_commentUri; // input - Control* m_control = nullptr; // output - void visit(const Lv2Ports::Control& port) override + suil_instance_port_event(m_uiInstance, port_index, buffer_size, protocol, buffer); + } + else +#endif + { + // LMMS UI is updated automatically + } +} + + + + +void Lv2ViewProc::initUi() +{ + // Set initial control port values + int i = 0; + proc()->foreach_port([&i,this](Lv2Ports::PortBase* port){ + auto ctrl = Lv2Ports::dcast(port); + if (ctrl) { - if (port.m_flow == Lv2Ports::Flow::Input) + //qDebug() << "Setting port" << i << "to" << ctrl->m_val; + uiPortEvent(i, sizeof(float), 0, &ctrl->m_val); + } + i++; + }); +} + + + + +// write UI event to ui_events (to be read by plugin) +void Lv2ViewProc::writeToPlugin(uint32_t port_index, + uint32_t buffer_size, + uint32_t protocol, + const void* buffer) +{ + Lv2Manager* mgr = Engine::getLv2Manager(); + + if (protocol != 0 && protocol != m_atomEventTransfer) + { + qWarning( + "UI write with unsupported protocol %u (%s), should be %u (%s) or 0", + protocol, + mgr->uridMap().unmap(protocol), + m_atomEventTransfer, + mgr->uridMap().unmap(m_atomEventTransfer) + ); + return; + } + + if (port_index >= proc()->portNum()) + { + qWarning() << "UI write to out of range port index" << port_index; + return; + } + +#if defined(LMMS_HAVE_SERD) && defined(LMMS_HAVE_SRATOM) + if (lv2Dump && protocol == m_atomEventTransfer) + { + const LV2_Atom* atom = (const LV2_Atom*)buffer; + mgr->uridMap().dump(); + std::unique_ptr str( + sratom_to_turtle( + mgr->sratom, mgr->uridMap().unmapFeature(), + "lmms:", nullptr, nullptr, + atom->type, atom->size, LV2_ATOM_BODY_CONST(atom)), + &std::free); + qDebug("\n## UI => Plugin (%u bytes) ##\n%s\n", atom->size, str.get()); + } +#endif + + char buf[Lv2Proc::uiEventsBufsize()]; + Lv2UiControlChange* ev = (Lv2UiControlChange*)buf; + ev->index = port_index; + ev->protocol = protocol; + ev->size = buffer_size; + memcpy(ev + 1, buffer, buffer_size); + m_uiEvents.write(buf, sizeof(Lv2UiControlChange) + buffer_size); +} + + + + +const char* Lv2ViewProc::hostUiTypeUri() { return LV2_UI__Qt5UI; } + + + + +bool Lv2ViewProc::calculateIsResizable() const +{ + if (!m_ui) { return false; } + + Lv2Manager* mgr = Engine::getLv2Manager(); + + const LilvNode* s = lilv_ui_get_uri(m_ui); + AutoLilvNode p = mgr->uri(LV2_CORE__optionalFeature); + AutoLilvNode fs = mgr->uri(LV2_UI__fixedSize); + AutoLilvNode nrs = mgr->uri(LV2_UI__noUserResize); + + AutoLilvNodes fs_matches = mgr->findNodes(s, p.get(), fs.get()); + AutoLilvNodes nrs_matches = mgr->findNodes(s, p.get(), nrs.get()); + + return !fs_matches && !nrs_matches; +} + + + + +std::tuple Lv2ViewProc::selectPluginUi(LilvUIs* uis) const +{ +#ifdef LMMS_HAVE_SUIL + { + // Try to find an embeddable UI + AutoLilvNode hostUi = uri(hostUiTypeUri()); + qDebug() << "Searching for plugin UI matching Host UI" << hostUiTypeUri() << "..."; + + LILV_FOREACH (uis, u, uis) + { + const LilvUI* pluginUi = lilv_uis_get(uis, u); + const LilvNode* pluginUiType = nullptr; + const bool supported = lilv_ui_is_supported(pluginUi, suil_ui_supported, hostUi.get(), &pluginUiType); + if (supported) + { + if (pluginUiType) + { + qDebug() << "Found supported plugin UI" << lilv_node_as_uri(pluginUiType); + } + return std::make_tuple(pluginUi, pluginUiType); + } + else + { + qDebug() << "Skipping plugin UI" << lilv_node_as_uri(lilv_ui_get_uri(pluginUi)) << "- not supported"; + } + } + qDebug() << "... no matching plugin UI found. Defaulting to LMMS UI."; + } +#endif + return std::make_tuple(nullptr, nullptr); +} + + + + +Lv2ViewProc::Lv2ViewProc(QWidget* parent, Lv2Proc* proc, int colNum) : + LinkedModelGroupView (parent, proc, colNum), + m_uiEvents(Lv2Proc::uiMidiBufsize() * Lv2Proc::uiNBufferCycles()) +#ifdef LMMS_HAVE_LV2_1_17_2 + , m_requestValue { this, [] (LV2UI_Feature_Handle handle, + LV2_URID key, + LV2_URID type, + const LV2_Feature* const* features) + -> LV2UI_Request_Value_Status + { return static_cast(handle)-> + requestValue(key, type, features); } } +#endif +{ +#ifdef LMMS_HAVE_SUIL + if (Lv2Manager::wantUi()) + { + // User wants UI and we support it in general + // Select a suiting UI type. + // Note: It may be possible there is no suiting UI type. + std::tie(m_ui, m_uiType) = selectPluginUi(proc->getUis()); + } +#endif + +#ifdef LMMS_HAVE_SUIL + if (m_ui) + { + //LinkedModelGroupView::hide(); + + Lv2Manager* mgr = Engine::getLv2Manager(); + m_atomEventTransfer = mgr->uridMap().map(LV2_ATOM__eventTransfer); + m_uiHost = suil_host_new( + [](void* const h, uint32_t port, uint32_t bs, uint32_t pro, const void* buf) + { + static_cast(h)->writeToPlugin(port, bs, pro, buf); + }, + [](void* const c, const char* symbol) -> uint32_t + { + std::size_t portId = static_cast(c)->proc()->getIdOfPort(symbol); + return (portId == (std::size_t)-1 ? LV2UI_INVALID_PORT_INDEX : portId); + }, + nullptr, nullptr); + Q_ASSERT(m_uiHost); + suil_host_set_touch_func(m_uiHost, + [](void* controller, uint32_t port_index, bool grabbed) { - using PortVis = Lv2Ports::Vis; + static_cast(controller)->touch(port_index, grabbed); + }); + + const LV2_Feature parentFeature = {LV2_UI__parent, parent}; + const LV2_Feature instanceFeature = { + LV2_INSTANCE_ACCESS_URI, + lilv_instance_get_handle(proc->getInstanceForInstanceFeatureOnly())}; + const LV2_Feature dataFeature = {LV2_DATA_ACCESS_URI, + proc->extdataFeature()}; +#ifdef LMMS_HAVE_LV2_1_17_2 + const LV2_Feature requestValueFeature = {LV2_UI__requestValue, + &m_requestValue}; +#endif + + const LV2_Feature* uiFeatures[] = { + proc->mapFeature(), + proc->unmapFeature(), + &instanceFeature, + &dataFeature, + &parentFeature, + proc->optionsFeature(), +#ifdef LMMS_HAVE_LV2_1_17_2 + &requestValueFeature, +#endif + nullptr}; + + const char* bundleUri = lilv_node_as_uri(lilv_ui_get_bundle_uri(m_ui)); + const char* binaryUri = lilv_node_as_uri(lilv_ui_get_binary_uri(m_ui)); + auto bundlePath = AutoLilvPtr(lilv_file_uri_parse(bundleUri, NULL)); + auto binaryPath = AutoLilvPtr(lilv_file_uri_parse(binaryUri, NULL)); + + m_uiInstance = + suil_instance_new(m_uiHost, + this, + hostUiTypeUri(), + proc->pluginUri(), + lilv_node_as_uri(lilv_ui_get_uri(m_ui)), + lilv_node_as_uri(m_uiType), + bundlePath.get(), + binaryPath.get(), + uiFeatures); + if (!m_uiInstance) // TODO: fallback to default UI + { + qWarning() << "Failed to load plugin UI"; + } + else + { + qDebug() << "Created new plugin UI" << lilv_node_as_uri(lilv_ui_get_uri(m_ui)) << lilv_node_as_uri(m_uiType); + } - switch (port.m_vis) + m_uiInstanceWidget = static_cast(suil_instance_get_widget(m_uiInstance)); + assert(m_uiInstanceWidget); + //m_uiInstanceWidget->setParent(this); + //layout()->addWidget(m_uiInstanceWidget); + m_uiInstanceWidget->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding)); + layout()->addWidget(m_uiInstanceWidget); + layout()->setSizeConstraint(QLayout::SetNoConstraint); + + // connect UI ringbuffers + // about the order: Lv2Proc can reject sending plugin events as long as + // its UI reader is not connected + proc->connectToPluginEvents(m_pluginEventsReader); + proc->connectUiEventsReaderTo(m_uiEvents); + + // <--- This marks the entry point after which the UI can be used + + initUi(); // send all control values to UI once + + m_isResizable = calculateIsResizable(); + const QSizePolicy sizePolicyToSet = m_isResizable + ? QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding) + : QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + m_uiInstanceWidget->setSizePolicy(sizePolicyToSet); + setSizePolicy(sizePolicyToSet); + parent->setSizePolicy(sizePolicyToSet); // TODO: remove when Lv2 UI removes Lv2ViewBase + + if(!m_isResizable) + setFixedSize(m_uiInstanceWidget->size()); + + m_timer = new Timer(this); + m_timer->start(1000 / lv2UiRefreshRate()); + } + else +#endif // LMMS_HAVE_SUIL + { + class SetupTheWidget : public Lv2Ports::ConstVisitor + { + public: + QWidget* m_parent; // input + const LilvNode* m_commentUri; // input + Control* m_control = nullptr; // output + void visit(const Lv2Ports::Control& port) override + { + if (port.m_flow == Lv2Ports::Flow::Input) { - case PortVis::Generic: - m_control = new KnobControl(port.name(), m_parent); - break; - case PortVis::Integer: + using PortVis = Lv2Ports::Vis; + + switch (port.m_vis) { - sample_rate_t sr = Engine::audioEngine()->outputSampleRate(); - auto pMin = port.min(sr); - auto pMax = port.max(sr); - int numDigits = std::max(numDigitsAsInt(pMin), numDigitsAsInt(pMax)); - m_control = new LcdControl(numDigits, m_parent); - break; + case PortVis::Generic: + m_control = new KnobControl(port.name(), m_parent); + break; + case PortVis::Integer: + { + sample_rate_t sr = Engine::audioEngine()->outputSampleRate(); + auto pMin = port.min(sr); + auto pMax = port.max(sr); + int numDigits = std::max(numDigitsAsInt(pMin), numDigitsAsInt(pMax)); + m_control = new LcdControl(numDigits, m_parent); + break; + } + case PortVis::Enumeration: + m_control = new ComboControl(m_parent); + break; + case PortVis::Toggled: + m_control = new CheckControl(m_parent); + break; } - case PortVis::Enumeration: - m_control = new ComboControl(m_parent); - break; - case PortVis::Toggled: - m_control = new CheckControl(m_parent); + m_control->setText(port.name()); + + AutoLilvNodes props(lilv_port_get_value( + port.m_plugin, port.m_port, m_commentUri)); + LILV_FOREACH(nodes, itr, props.get()) + { + const LilvNode* nod = lilv_nodes_get(props.get(), itr); + m_control->topWidget()->setToolTip(lilv_node_as_string(nod)); break; + } } - m_control->setText(port.name()); - - AutoLilvNodes props(lilv_port_get_value( - port.m_plugin, port.m_port, m_commentUri)); - LILV_FOREACH(nodes, itr, props.get()) + } + }; + + AutoLilvNode commentUri = uri(LILV_NS_RDFS "comment"); + proc->foreach_port( + [this, &commentUri](const Lv2Ports::PortBase* port) + { + if (!lilv_port_has_property(port->m_plugin, port->m_port, + uri(LV2_PORT_PROPS__notOnGUI).get())) { - const LilvNode* nod = lilv_nodes_get(props.get(), itr); - m_control->topWidget()->setToolTip(lilv_node_as_string(nod)); - break; + SetupTheWidget setup; + setup.m_parent = this; + setup.m_commentUri = commentUri.get(); + port->accept(setup); + + if (setup.m_control) + { + addControl(setup.m_control, + lilv_node_as_string(lilv_port_get_symbol( + port->m_plugin, port->m_port)), + port->name().toUtf8().data(), + false); + } } - } + }); // proc->foreach_port + } // ! m_ui +} + + + + +Lv2ViewProc::~Lv2ViewProc() +{ + if (m_timer) { m_timer->stop(); } +} + + + + +QSize Lv2ViewProc::uiWidgetSize() const +{ +#ifdef LMMS_HAVE_SUIL + return m_uiInstanceWidget ? m_uiInstanceWidget->size() : QSize(); +#else + return QSize(); +#endif +} + + + + +void Lv2ViewProc::update() +{ + if (!m_pluginEventsReader) { return; } + + // Emit UI events + Lv2UiControlChange ev; + const size_t space = m_pluginEventsReader->read_space(); + + for (size_t i = 0; i + sizeof(ev) < space; i += sizeof(ev) + ev.size) + { + // Read event header to get the size + if (space - i < sizeof(ev)) { break; } + m_pluginEventsReader->read(sizeof(ev)).copy((char*)&ev, sizeof(ev)); + + // Resize read buffer if necessary + m_uiEventBuf.resize(ev.size); + void* const buf = m_uiEventBuf.data(); + + // Read event body + if (space - i < ev.size) + { + qWarning() << "error: Error reading from UI ring buffer"; + break; } - }; + m_pluginEventsReader->read(ev.size).copy((char*)buf, ev.size); - AutoLilvNode commentUri = uri(LILV_NS_RDFS "comment"); - proc->foreach_port( - [this, &commentUri](const Lv2Ports::PortBase* port) +#if defined(LMMS_HAVE_SERD) && defined(LMMS_HAVE_SRATOM) + if (lv2Dump && ev.protocol == m_atomEventTransfer) { - if(!lilv_port_has_property(port->m_plugin, port->m_port, - uri(LV2_PORT_PROPS__notOnGUI).get())) - { - SetupTheWidget setup; - setup.m_parent = this; - setup.m_commentUri = commentUri.get(); - port->accept(setup); + Lv2Manager* mgr = Engine::getLv2Manager(); + // Dump event in Turtle to the console + const LV2_Atom* atom = (LV2_Atom*)buf; + std::unique_ptr str( + sratom_to_turtle( + mgr->ui_sratom, mgr->uridMap().unmapFeature(), + "lmms:", nullptr, nullptr, + atom->type, atom->size, LV2_ATOM_BODY(atom)), + &std::free); + qDebug("\n## Plugin => UI (%u bytes) ##\n%s\n", atom->size, str.get()); + } +#endif - if (setup.m_control) - { - addControl(setup.m_control, - lilv_node_as_string(lilv_port_get_symbol( - port->m_plugin, port->m_port)), - port->name().toUtf8().data(), - false); - } - } - }); + uiPortEvent(ev.index, ev.size, ev.protocol, buf); + + if (lv2Dump && ev.protocol == 0) + { + qDebug() << proc()->portname(ev.index).toLocal8Bit().data() + << " = " << *static_cast(buf); + } + + } } -AutoLilvNode Lv2ViewProc::uri(const char *uriStr) + +AutoLilvNode Lv2ViewProc::uri(const char* uriStr) { return Engine::getLv2Manager()->uri(uriStr); } @@ -137,6 +519,29 @@ AutoLilvNode Lv2ViewProc::uri(const char *uriStr) +#ifdef LMMS_HAVE_LV2_1_17_2 +LV2UI_Request_Value_Status Lv2ViewProc::requestValue( + LV2_URID /*key*/, + LV2_URID /*type*/, + const LV2_Feature* const* /*features*/) +{ + // This requires the patch extension + // - which LMMS does not support at the moment + return LV2UI_REQUEST_VALUE_ERR_UNSUPPORTED; +} +#endif + + + + +void Lv2ViewProc::touch(uint32_t portIndex, bool grabbed) +{ + qDebug() << "touch:" << portIndex << grabbed; +} + + + + Lv2ViewBase::Lv2ViewBase(QWidget* meAsWidget, Lv2ControlBase *ctrlBase) : m_helpWindowEventFilter(this) { @@ -198,6 +603,8 @@ Lv2ViewBase::Lv2ViewBase(QWidget* meAsWidget, Lv2ControlBase *ctrlBase) : m_procView = new Lv2ViewProc(meAsWidget, ctrlBase->control(0), m_colNum); grid->addWidget(m_procView, Rows::ProcRow, 0); + if(m_procView->minimumSize() == m_procView->maximumSize()) + meAsWidget->setFixedSize(m_procView->size()); } @@ -276,7 +683,8 @@ HelpWindowEventFilter::HelpWindowEventFilter(Lv2ViewBase* viewBase) : bool HelpWindowEventFilter::eventFilter(QObject* , QEvent* event) { - if (event->type() == QEvent::Close) { + if (event->type() == QEvent::Close) + { m_viewBase->m_helpButton->setChecked(false); return true; } diff --git a/src/gui/SubWindow.cpp b/src/gui/SubWindow.cpp index a76f4055e99..52f494e0241 100644 --- a/src/gui/SubWindow.cpp +++ b/src/gui/SubWindow.cpp @@ -398,6 +398,14 @@ void SubWindow::focusChanged( QMdiSubWindow *subWindow ) */ void SubWindow::resizeEvent( QResizeEvent * event ) { + if (widget()) { + if (widget()->sizePolicy().horizontalPolicy() == QSizePolicy::Fixed && + widget()->sizePolicy().verticalPolicy() == QSizePolicy::Fixed) { + // No idea where these weird constants come from... + setFixedSize(widget()->size() + QSize(8, m_titleBarHeight + 6)); + } + } + // When the parent QMdiArea gets resized, maximized subwindows also gets resized, if any. // In that case, we should call QMdiSubWindow::resizeEvent first // to ensure we get the correct window state. diff --git a/src/gui/instrument/InstrumentTrackWindow.cpp b/src/gui/instrument/InstrumentTrackWindow.cpp index f6a0d69553f..1053ddd1ed6 100644 --- a/src/gui/instrument/InstrumentTrackWindow.cpp +++ b/src/gui/instrument/InstrumentTrackWindow.cpp @@ -306,15 +306,6 @@ InstrumentTrackWindow::InstrumentTrackWindow( InstrumentTrackView * _itv ) : subWin->hide(); } -void InstrumentTrackWindow::resizeEvent(QResizeEvent * event) { - /* m_instrumentView->resize(QSize(size().width()-1, maxHeight)); */ - adjustTabSize(m_instrumentView); - adjustTabSize(m_instrumentFunctionsView); - adjustTabSize(m_ssView); - adjustTabSize(m_effectView); - adjustTabSize(m_midiView); - adjustTabSize(m_tuningView); -} @@ -482,16 +473,21 @@ void InstrumentTrackWindow::updateInstrumentView() modelChanged(); // Get the instrument window to refresh m_track->dataChanged(); // Get the text on the trackButton to change - adjustTabSize(m_instrumentView); + m_pianoView->setVisible(m_track->m_instrument->hasNoteInput()); // adjust window size layout()->invalidate(); resize(sizeHint()); - if (parentWidget()) + + if(m_tabWidget->minimumSize() == m_tabWidget->maximumSize()) { - parentWidget()->resize(parentWidget()->sizeHint()); + setFixedSize(sizeHint()); + setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); } + else + resize(sizeHint()); + update(); m_instrumentView->update(); } diff --git a/src/gui/modals/SetupDialog.cpp b/src/gui/modals/SetupDialog.cpp index 4ea0d4dddf9..06308a6ca77 100644 --- a/src/gui/modals/SetupDialog.cpp +++ b/src/gui/modals/SetupDialog.cpp @@ -419,7 +419,7 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : QVBoxLayout * pluginsLayout = new QVBoxLayout(pluginsBox); m_vstEmbedLbl = new QLabel(pluginsBox); - m_vstEmbedLbl->setText(tr("VST plugins embedding:")); + m_vstEmbedLbl->setText(tr("External plugin embedding:")); pluginsLayout->addWidget(m_vstEmbedLbl); m_vstEmbedComboBox = new QComboBox(pluginsBox); diff --git a/src/gui/widgets/TabWidget.cpp b/src/gui/widgets/TabWidget.cpp index 81fae1c043a..5d1ef5755d3 100644 --- a/src/gui/widgets/TabWidget.cpp +++ b/src/gui/widgets/TabWidget.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include "DeprecationHelper.h" #include "embed.h" @@ -71,9 +72,9 @@ TabWidget::TabWidget(const QString& caption, QWidget* parent, bool usePixmap, void TabWidget::addTab(QWidget* w, const QString& name, const char* pixmap, int idx) { // Append tab when position is not given - if (idx < 0/* || m_widgets.contains(idx) == true*/) + if (idx < 0/* || m_widgets.contains(idx)*/) { - while(m_widgets.contains(++idx) == true) + while(m_widgets.contains(++idx)) { } } @@ -85,11 +86,42 @@ void TabWidget::addTab(QWidget* w, const QString& name, const char* pixmap, int widgetDesc d = {w, pixmap, name, tab_width}; m_widgets[idx] = d; - // Position tab's window + // Position tab's window and update size if (!m_resizable) { + // Tab widget is fixed -> child must be fixed w->setFixedSize(width() - 4, height() - m_tabbarHeight); } + else + { + // If the child has a maximum, we cannot grow larger + if(maximumHeight() > w->maximumHeight() + m_tabbarHeight) + { + setMaximumHeight(w->maximumHeight() + m_tabbarHeight); + } + if(maximumWidth() > w->maximumWidth() + 4) + { + setMaximumWidth(w->maximumWidth() + 4); + } + // If the child has a minimum, we cannot shrink smaller + if(minimumHeight() < w->minimumHeight() + m_tabbarHeight) + { + setMinimumHeight(w->minimumHeight() + m_tabbarHeight); + } + if(minimumWidth() < w->minimumWidth() + 4) + { + setMinimumWidth(w->minimumWidth() + 4); + } + qDebug() << "TAB min/max" << minimumSize() << maximumSize(); + + // Now that the size might have changed: resize all widgets + for (const auto& widget : m_widgets) + { + widget.w->resize(width() - 4, height() - m_tabbarHeight); + } + } + //if(w->minimumHeight() > minimumHeight() || w->minimumSizeHint().height() > minimumHeight()) { setMinimumHeight(w->minimumHeight()); } + //if(w->minimumWidth() > minimumWidth() || w->minimumSizeHint().width() > minimumWidth()) { setMinimumWidth(w->minimumWidth()); } w->move(2, m_tabbarHeight - 1); w->hide(); @@ -197,9 +229,32 @@ void TabWidget::mousePressEvent(QMouseEvent* me) -void TabWidget::resizeEvent(QResizeEvent*) +void TabWidget::resizeEvent(QResizeEvent* ev) { - if (!m_resizable) + if (m_resizable) + { + for (const auto& widget : m_widgets) + { + if(widget.w->minimumSize().height() > ev->size().height() - 4 || + widget.w->minimumSize().width() > ev->size().width() - m_tabbarHeight) + { + ev->ignore(); + return; + } + if(widget.w->maximumSize().height() < ev->size().height() - 4 || + widget.w->maximumSize().width() < ev->size().width() - m_tabbarHeight) + { + ev->ignore(); + return; + } + } + for (const auto& widget : m_widgets) + { + widget.w->resize(width() - 4, height() - m_tabbarHeight); + } + QWidget::resizeEvent(ev); + } + else { for (const auto& widget : m_widgets) { diff --git a/src/lmmsconfig.h.in b/src/lmmsconfig.h.in index e63ce1fa4d8..29e627ae7b3 100644 --- a/src/lmmsconfig.h.in +++ b/src/lmmsconfig.h.in @@ -21,7 +21,10 @@ #cmakedefine LMMS_HAVE_JACK_PRENAME #cmakedefine LMMS_HAVE_WEAKJACK #cmakedefine LMMS_HAVE_LV2 +#cmakedefine LMMS_HAVE_LV2_1_17_2 #cmakedefine LMMS_HAVE_SUIL +#cmakedefine LMMS_HAVE_SERD +#cmakedefine LMMS_HAVE_SRATOM #cmakedefine LMMS_HAVE_MP3LAME #cmakedefine LMMS_HAVE_SNDFILE_MP3 #cmakedefine LMMS_HAVE_OGGVORBIS