diff --git a/include/Lv2ControlBase.h b/include/Lv2ControlBase.h index d6591a50d7b..d5d83f37800 100644 --- a/include/Lv2ControlBase.h +++ b/include/Lv2ControlBase.h @@ -100,8 +100,12 @@ class Lv2ControlBase : public LinkedModelGroups /* utils for the run thread */ - //! Copy values from all connected models into the respective ports + //! Copy values from the LMMS core (connected models, MIDI events, ...) into + //! the respective ports void copyModelsFromLmms(); + //! Bring values from all ports to the LMMS core + void copyModelsToLmms() const; + //! Copy buffer passed by LMMS into our ports void copyBuffersFromLmms(const sampleFrame *buf, fpp_t frames); //! Copy our ports into buffers passed by LMMS @@ -123,6 +127,9 @@ class Lv2ControlBase : public LinkedModelGroups */ std::size_t controlCount() const; QString nodeName() const { return "lv2controls"; } + bool hasNoteInput() const; + void handleMidiInputEvent(const class MidiEvent &event, + const class MidiTime &time, f_cnt_t offset); private: //! Return the DataFile settings type diff --git a/include/Lv2Evbuf.h b/include/Lv2Evbuf.h new file mode 100644 index 00000000000..9cf6a6801fe --- /dev/null +++ b/include/Lv2Evbuf.h @@ -0,0 +1,149 @@ +/* + * lv2_evbuf.h - Lv2 event buffer definitions + * + * Copyright (c) 2019-2020 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +/* + * The original code was written by David Robillard + * Original version: 6f22ee0 from https://github.com/drobilla/jalv.git + * Minor changes have been done, but no functional changes. + * Considering this as an "external library", the identifiers do not need to + * match the LMMS coding conventions. + */ + +#ifndef LV2_EVBUF_H +#define LV2_EVBUF_H + +#include "lmmsconfig.h" + +#ifdef LMMS_HAVE_LV2 + +#include + +/** + An abstract/opaque LV2 event buffer. +*/ +typedef struct LV2_Evbuf_Impl LV2_Evbuf; + +/** + An iterator over an LV2_Evbuf. +*/ +typedef struct { + LV2_Evbuf* evbuf; + uint32_t offset; +} LV2_Evbuf_Iterator; + +/** + Allocate a new, empty event buffer. + URIDs for atom:Chunk and atom:Sequence must be passed for LV2_EVBUF_ATOM. +*/ +LV2_Evbuf* +lv2_evbuf_new(uint32_t capacity, uint32_t atom_Chunk, uint32_t atom_Sequence); + +/** + Free an event buffer allocated with lv2_evbuf_new. +*/ +void +lv2_evbuf_free(LV2_Evbuf* evbuf); + +/** + Clear and initialize an existing event buffer. + The contents of buf are ignored entirely and overwritten, except capacity + which is unmodified. + If input is false and this is an atom buffer, the buffer will be prepared + for writing by the plugin. This MUST be called before every run cycle. +*/ +void +lv2_evbuf_reset(LV2_Evbuf* evbuf, bool input); + +/** + Return the total padded size of the events stored in the buffer. +*/ +uint32_t +lv2_evbuf_get_size(LV2_Evbuf* evbuf); + +/** + Return the actual buffer implementation. + The format of the buffer returned depends on the buffer type. +*/ +void* +lv2_evbuf_get_buffer(LV2_Evbuf* evbuf); + +/** + Return an iterator to the start of `evbuf`. +*/ +LV2_Evbuf_Iterator +lv2_evbuf_begin(LV2_Evbuf* evbuf); + +/** + Return an iterator to the end of `evbuf`. +*/ +LV2_Evbuf_Iterator +lv2_evbuf_end(LV2_Evbuf* evbuf); + +/** + Check if `iter` is valid. + @return True if `iter` is valid, otherwise false (past end of buffer) +*/ +bool +lv2_evbuf_is_valid(LV2_Evbuf_Iterator iter); + +/** + Advance `iter` forward one event. + `iter` must be valid. + @return True if `iter` is valid, otherwise false (reached end of buffer) +*/ +LV2_Evbuf_Iterator +lv2_evbuf_next(LV2_Evbuf_Iterator iter); + +/** + Dereference an event iterator (i.e. get the event currently pointed to). + `iter` must be valid. + `type` Set to the type of the event. + `size` Set to the size of the event. + `data` Set to the contents of the event. + @return True on success. +*/ +bool +lv2_evbuf_get( LV2_Evbuf_Iterator iter, + uint32_t* frames, + uint32_t* type, + uint32_t* size, + uint8_t** data); + +/** + Write an event at `iter`. + The event (if any) pointed to by `iter` will be overwritten, and `iter` + incremented to point to the following event (i.e. several calls to this + function can be done in sequence without twiddling iter in-between). + @return True if event was written, otherwise false (buffer is full). +*/ +bool +lv2_evbuf_write( LV2_Evbuf_Iterator* iter, + uint32_t frames, + uint32_t type, + uint32_t size, + const uint8_t* data); + +#endif // LMMS_HAVE_LV2 + +#endif // LV2_EVBUF_H diff --git a/include/Lv2Ports.h b/include/Lv2Ports.h index 567e1ddb585..8241b3e8af4 100644 --- a/include/Lv2Ports.h +++ b/include/Lv2Ports.h @@ -37,6 +37,7 @@ #include "PluginIssue.h" struct ConnectPortVisitor; +typedef struct LV2_Evbuf_Impl LV2_Evbuf; namespace Lv2Ports { @@ -53,7 +54,7 @@ enum class Type { Unknown, Control, Audio, - Event, //!< TODO: unused, describe + AtomSeq, Cv //!< TODO: unused, describe }; @@ -74,6 +75,7 @@ struct ControlPortBase; struct Control; struct Audio; struct Cv; +struct AtomSeq; struct Unknown; struct ConstVisitor @@ -82,6 +84,7 @@ struct ConstVisitor virtual void visit(const Lv2Ports::Control& ) {} virtual void visit(const Lv2Ports::Audio& ) {} virtual void visit(const Lv2Ports::Cv& ) {} + virtual void visit(const Lv2Ports::AtomSeq& ) {} virtual void visit(const Lv2Ports::Unknown& ) {} virtual ~ConstVisitor(); @@ -93,6 +96,7 @@ struct Visitor virtual void visit(Lv2Ports::Control& ) {} virtual void visit(Lv2Ports::Audio& ) {} virtual void visit(Lv2Ports::Cv& ) {} + virtual void visit(Lv2Ports::AtomSeq& ) {} virtual void visit(Lv2Ports::Unknown& ) {} virtual ~Visitor(); @@ -192,6 +196,23 @@ struct Audio : public VisitablePort friend struct ::ConnectPortVisitor; }; +struct AtomSeq : public VisitablePort +{ + enum FlagType + { + None = 0, + Midi = 1 + }; + unsigned flags = FlagType::None; + + struct Lv2EvbufDeleter + { + void operator()(LV2_Evbuf* n); + }; + using AutoLv2Evbuf = std::unique_ptr; + AutoLv2Evbuf m_buf; +}; + struct Unknown : public VisitablePort { }; diff --git a/include/Lv2Proc.h b/include/Lv2Proc.h index 1c1cc11d8ac..81e25f785f5 100644 --- a/include/Lv2Proc.h +++ b/include/Lv2Proc.h @@ -36,15 +36,18 @@ #include "Lv2Basics.h" #include "Lv2Features.h" #include "LinkedModelGroups.h" +#include "MidiEvent.h" +#include "MidiTime.h" #include "Plugin.h" #include "PluginIssue.h" - +#include "../src/3rdparty/ringbuffer/include/ringbuffer/ringbuffer.h" // forward declare port structs/enums namespace Lv2Ports { struct Audio; struct PortBase; + struct AtomSeq; enum class Type; enum class Flow; @@ -106,8 +109,11 @@ class Lv2Proc : public LinkedModelGroup /* utils for the run thread */ - //! Copy values from all connected models into the respective ports + //! 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(); /** * Copy buffer passed by the core into our ports * @param buf buffer of sample frames, each sample frame is something like @@ -137,11 +143,15 @@ class Lv2Proc : public LinkedModelGroup //! Run the Lv2 plugin instance for @param frames frames void run(fpp_t frames); + void handleMidiInputEvent(const class MidiEvent &event, + const MidiTime &time, f_cnt_t offset); + /* misc */ class AutomatableModel *modelAtPort(const QString &uri); // unused currently std::size_t controlCount() const { return LinkedModelGroup::modelNum(); } + bool hasNoteInput() const; protected: /* @@ -159,8 +169,25 @@ class Lv2Proc : public LinkedModelGroup LilvInstance* m_instance; Lv2Features m_features; + // full list of ports std::vector> m_ports; + // quick reference to specific, unique ports StereoPortRef m_inPorts, m_outPorts; + Lv2Ports::AtomSeq *m_midiIn = nullptr, *m_midiOut = nullptr; + + // MIDI + // many things here may be moved into the `Instrument` class + constexpr const static std::size_t m_maxMidiInputEvents = 1024; + //! spinlock for the MIDI ringbuffer (for MIDI events going to the plugin) + std::atomic_flag m_ringLock = ATOMIC_FLAG_INIT; + + //! MIDI ringbuffer (for MIDI events going to the plugin) + ringbuffer_t m_midiInputBuf; + //! MIDI ringbuffer reader + ringbuffer_reader_t m_midiInputReader; + + // other + static std::size_t minimumEvbufSize() { return 1 << 15; /* ardour uses this*/ } //! models for the controls, sorted by port symbols std::map m_connectedModels; diff --git a/include/Lv2UridCache.h b/include/Lv2UridCache.h index b4cfa59f33d..81dd1346cf7 100644 --- a/include/Lv2UridCache.h +++ b/include/Lv2UridCache.h @@ -37,7 +37,7 @@ class Lv2UridCache public: enum class Id //!< ID for m_uridCache array { - midi_MidiEvent, //!< just an example, unused yet + midi_MidiEvent, size }; //! Return URID for a cache ID diff --git a/plugins/Lv2Effect/Lv2Effect.cpp b/plugins/Lv2Effect/Lv2Effect.cpp index 36e9df46ba7..4f84104bbe9 100644 --- a/plugins/Lv2Effect/Lv2Effect.cpp +++ b/plugins/Lv2Effect/Lv2Effect.cpp @@ -74,6 +74,7 @@ bool Lv2Effect::processAudioBuffer(sampleFrame *buf, const fpp_t frames) m_controls.run(frames); // m_pluginMutex.unlock(); + m_controls.copyModelsToLmms(); m_controls.copyBuffersToLmms(m_tmpOutputSmps.data(), frames); double outSum = .0; diff --git a/plugins/Lv2Instrument/Lv2Instrument.cpp b/plugins/Lv2Instrument/Lv2Instrument.cpp index 76d9d017f9d..ca918d2ed04 100644 --- a/plugins/Lv2Instrument/Lv2Instrument.cpp +++ b/plugins/Lv2Instrument/Lv2Instrument.cpp @@ -131,11 +131,9 @@ void Lv2Instrument::loadFile(const QString &file) bool Lv2Instrument::handleMidiEvent( const MidiEvent &event, const MidiTime &time, f_cnt_t offset) { - // this function can be called from GUI threads while the plugin is running, - // so this requires caching, e.g. in ringbuffers - (void)time; - (void)offset; - (void)event; + // this function can be called from GUI threads while the plugin is running + // handleMidiInputEvent will use a thread-safe ringbuffer + handleMidiInputEvent(event, time, offset); return true; } #endif @@ -161,6 +159,7 @@ void Lv2Instrument::play(sampleFrame *buf) run(fpp); + copyModelsToLmms(); copyBuffersToLmms(buf, fpp); instrumentTrack()->processAudioBuffer(buf, fpp, nullptr); diff --git a/plugins/Lv2Instrument/Lv2Instrument.h b/plugins/Lv2Instrument/Lv2Instrument.h index 4fb2e1e34c3..a35830952f9 100644 --- a/plugins/Lv2Instrument/Lv2Instrument.h +++ b/plugins/Lv2Instrument/Lv2Instrument.h @@ -63,7 +63,7 @@ class Lv2Instrument : public Instrument, public Lv2ControlBase /* realtime funcs */ - bool hasNoteInput() const override { return false; /* not supported yet */ } + bool hasNoteInput() const override { return Lv2ControlBase::hasNoteInput(); } #ifdef LV2_INSTRUMENT_USE_MIDI bool handleMidiEvent(const MidiEvent &event, const MidiTime &time = MidiTime(), f_cnt_t offset = 0) override; diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 8dbee31d31b..22bb1195103 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -93,6 +93,7 @@ set(LMMS_SRCS core/lv2/Lv2Basics.cpp core/lv2/Lv2ControlBase.cpp + core/lv2/Lv2Evbuf.cpp core/lv2/Lv2Features.cpp core/lv2/Lv2Ports.cpp core/lv2/Lv2Proc.cpp diff --git a/src/core/lv2/Lv2ControlBase.cpp b/src/core/lv2/Lv2ControlBase.cpp index cc986dafed0..00326eeb88c 100644 --- a/src/core/lv2/Lv2ControlBase.cpp +++ b/src/core/lv2/Lv2ControlBase.cpp @@ -26,6 +26,7 @@ #ifdef LMMS_HAVE_LV2 +#include #include #include "Engine.h" @@ -113,6 +114,14 @@ void Lv2ControlBase::copyModelsFromLmms() { +void Lv2ControlBase::copyModelsToLmms() const +{ + for (auto& c : m_procs) { c->copyModelsToCore(); } +} + + + + void Lv2ControlBase::copyBuffersFromLmms(const sampleFrame *buf, fpp_t frames) { unsigned firstChan = 0; // tell the procs which channels they shall read from for (auto& c : m_procs) { @@ -187,4 +196,22 @@ std::size_t Lv2ControlBase::controlCount() const { +bool Lv2ControlBase::hasNoteInput() const +{ + return std::any_of(m_procs.begin(), m_procs.end(), + [](const auto& c) { return c->hasNoteInput(); }); +} + + + + +void Lv2ControlBase::handleMidiInputEvent(const MidiEvent &event, + const MidiTime &time, f_cnt_t offset) +{ + for (auto& c : m_procs) { c->handleMidiInputEvent(event, time, offset); } +} + + + + #endif // LMMS_HAVE_LV2 diff --git a/src/core/lv2/Lv2Evbuf.cpp b/src/core/lv2/Lv2Evbuf.cpp new file mode 100644 index 00000000000..335dfd36224 --- /dev/null +++ b/src/core/lv2/Lv2Evbuf.cpp @@ -0,0 +1,193 @@ +/* + * lv2_evbuf.cpp - Lv2 event buffer implementation + * + * Copyright (c) 2019-2020 Johannes Lorenz + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +/* + * The original code was written by David Robillard + */ + +#include "Lv2Evbuf.h" + +#ifdef LMMS_HAVE_LV2 + +#include +#include +#include + +#include + +struct LV2_Evbuf_Impl { + uint32_t capacity; + uint32_t atom_Chunk; + uint32_t atom_Sequence; + LV2_Atom_Sequence buf; +}; + +static inline uint32_t +lv2_evbuf_pad_size(uint32_t size) +{ + return (size + 7) & (~7); +} + +LV2_Evbuf* +lv2_evbuf_new(uint32_t capacity, uint32_t atom_Chunk, uint32_t atom_Sequence) +{ + // FIXME: memory must be 64-bit aligned + LV2_Evbuf* evbuf = (LV2_Evbuf*)malloc( + sizeof(LV2_Evbuf) + sizeof(LV2_Atom_Sequence) + capacity); + evbuf->capacity = capacity; + evbuf->atom_Chunk = atom_Chunk; + evbuf->atom_Sequence = atom_Sequence; + lv2_evbuf_reset(evbuf, true); + return evbuf; +} + +void +lv2_evbuf_free(LV2_Evbuf* evbuf) +{ + free(evbuf); +} + +void +lv2_evbuf_reset(LV2_Evbuf* evbuf, bool input) +{ + if (input) { + evbuf->buf.atom.size = sizeof(LV2_Atom_Sequence_Body); + evbuf->buf.atom.type = evbuf->atom_Sequence; + } else { + evbuf->buf.atom.size = evbuf->capacity; + evbuf->buf.atom.type = evbuf->atom_Chunk; + } +} + +uint32_t +lv2_evbuf_get_size(LV2_Evbuf* evbuf) +{ + assert(evbuf->buf.atom.type != evbuf->atom_Sequence + || evbuf->buf.atom.size >= sizeof(LV2_Atom_Sequence_Body)); + return evbuf->buf.atom.type == evbuf->atom_Sequence + ? evbuf->buf.atom.size - sizeof(LV2_Atom_Sequence_Body) + : 0; +} + +void* +lv2_evbuf_get_buffer(LV2_Evbuf* evbuf) +{ + return &evbuf->buf; +} + +LV2_Evbuf_Iterator +lv2_evbuf_begin(LV2_Evbuf* evbuf) +{ + LV2_Evbuf_Iterator iter = { evbuf, 0 }; + return iter; +} + +LV2_Evbuf_Iterator +lv2_evbuf_end(LV2_Evbuf* evbuf) +{ + const uint32_t size = lv2_evbuf_get_size(evbuf); + const LV2_Evbuf_Iterator iter = { evbuf, lv2_evbuf_pad_size(size) }; + return iter; +} + +bool +lv2_evbuf_is_valid(LV2_Evbuf_Iterator iter) +{ + return iter.offset < lv2_evbuf_get_size(iter.evbuf); +} + +LV2_Evbuf_Iterator +lv2_evbuf_next(LV2_Evbuf_Iterator iter) +{ + if (!lv2_evbuf_is_valid(iter)) { + return iter; + } + + LV2_Evbuf* evbuf = iter.evbuf; + uint32_t offset = iter.offset; + uint32_t size; + size = ((LV2_Atom_Event*) + ((char*)LV2_ATOM_CONTENTS(LV2_Atom_Sequence, &evbuf->buf.atom) + + offset))->body.size; + offset += lv2_evbuf_pad_size(sizeof(LV2_Atom_Event) + size); + + LV2_Evbuf_Iterator next = { evbuf, offset }; + return next; +} + +bool +lv2_evbuf_get(LV2_Evbuf_Iterator iter, + uint32_t* frames, + uint32_t* type, + uint32_t* size, + uint8_t** data) +{ + *frames = *type = *size = 0; + *data = NULL; + + if (!lv2_evbuf_is_valid(iter)) { + return false; + } + + LV2_Atom_Sequence* aseq = &iter.evbuf->buf; + LV2_Atom_Event* aev = (LV2_Atom_Event*)( + (char*)LV2_ATOM_CONTENTS(LV2_Atom_Sequence, aseq) + iter.offset); + + *frames = aev->time.frames; + *type = aev->body.type; + *size = aev->body.size; + *data = (uint8_t*)LV2_ATOM_BODY(&aev->body); + + return true; +} + +bool +lv2_evbuf_write(LV2_Evbuf_Iterator* iter, + uint32_t frames, + uint32_t type, + uint32_t size, + const uint8_t* data) +{ + LV2_Atom_Sequence* aseq = &iter->evbuf->buf; + if (iter->evbuf->capacity - sizeof(LV2_Atom) - aseq->atom.size < + sizeof(LV2_Atom_Event) + size) { + return false; + } + + LV2_Atom_Event* aev = (LV2_Atom_Event*)( + (char*)LV2_ATOM_CONTENTS(LV2_Atom_Sequence, aseq) + iter->offset); + + aev->time.frames = frames; + aev->body.type = type; + aev->body.size = size; + memcpy(LV2_ATOM_BODY(&aev->body), data, size); + + size = lv2_evbuf_pad_size(sizeof(LV2_Atom_Event) + size); + aseq->atom.size += size; + iter->offset += size; + + return true; +} + +#endif // LMMS_HAVE_LV2 diff --git a/src/core/lv2/Lv2Ports.cpp b/src/core/lv2/Lv2Ports.cpp index f2fac5744d0..cc8ecf6ca43 100644 --- a/src/core/lv2/Lv2Ports.cpp +++ b/src/core/lv2/Lv2Ports.cpp @@ -27,9 +27,12 @@ #ifdef LMMS_HAVE_LV2 +#include + #include "Engine.h" #include "Lv2Basics.h" #include "Lv2Manager.h" +#include "Lv2Evbuf.h" namespace Lv2Ports { @@ -57,7 +60,7 @@ const char *toStr(Type pt) case Type::Unknown: return "unknown"; case Type::Control: return "control"; case Type::Audio: return "audio"; - case Type::Event: return "event"; + case Type::AtomSeq: return "atom-sequence"; case Type::Cv: return "cv"; } return ""; @@ -168,6 +171,23 @@ std::vector Meta::get(const LilvPlugin *plugin, issue(badPortType, "cvPort"); m_type = Type::Cv; } + else if (isA(LV2_ATOM__AtomPort)) + { + AutoLilvNode uriAtomSequence(Engine::getLv2Manager()->uri(LV2_ATOM__Sequence)); + AutoLilvNode uriAtomBufferType(Engine::getLv2Manager()->uri(LV2_ATOM__bufferType)); + AutoLilvNodes bufferTypes(lilv_port_get_value(plugin, lilvPort, uriAtomBufferType.get())); + + if (lilv_nodes_contains(bufferTypes.get(), uriAtomSequence.get())) + { + // we accept all kinds of atom sequence ports, even if they take or + // offer atom types that we do not support: + // * atom input ports only say what *can* be input, but not what is + // required as input + // * atom output ports only say what *can* be output, but not what must + // be evaluated + m_type = Type::AtomSeq; + } + } if(m_type == Type::Unknown) { @@ -245,6 +265,11 @@ void Audio::copyBuffersToCore(sampleFrame *lmmsBuf, +void AtomSeq::Lv2EvbufDeleter::operator()(LV2_Evbuf *n) { lv2_evbuf_free(n); } + + + + // make the compiler happy, give each class with virtuals // a function (the destructor here) which is in a cpp file PortBase::~PortBase() {} diff --git a/src/core/lv2/Lv2Proc.cpp b/src/core/lv2/Lv2Proc.cpp index 26593428b2c..0922ac242ed 100644 --- a/src/core/lv2/Lv2Proc.cpp +++ b/src/core/lv2/Lv2Proc.cpp @@ -27,6 +27,11 @@ #ifdef LMMS_HAVE_LV2 #include +#include +#include +#include +#include +#include #include "AutomatableModel.h" #include "ComboBoxModel.h" @@ -34,17 +39,31 @@ #include "Lv2Features.h" #include "Lv2Manager.h" #include "Lv2Ports.h" +#include "Lv2Evbuf.h" +#include "MidiEventToByteSeq.h" #include "Mixer.h" +// container for everything required to store MIDI events going to the plugin +struct MidiInputEvent +{ + MidiEvent ev; + MidiTime time; + f_cnt_t offset; +}; + + + + Plugin::PluginTypes Lv2Proc::check(const LilvPlugin *plugin, std::vector& issues, bool printIssues) { unsigned maxPorts = lilv_plugin_get_num_ports(plugin); enum { inCount, outCount, maxCount }; - unsigned audioChannels[maxCount] = { 0, 0 }; // input and output count + unsigned audioChannels[maxCount] = { 0, 0 }; // audio input and output count + unsigned midiChannels[maxCount] = { 0, 0 }; // MIDI input and output count for (unsigned portNum = 0; portNum < maxPorts; ++portNum) { @@ -58,8 +77,15 @@ Plugin::PluginTypes Lv2Proc::check(const LilvPlugin *plugin, lilv_plugin_get_port_by_index(plugin, portNum)) && !meta.m_optional; if (meta.m_type == Lv2Ports::Type::Audio && portMustBeUsed) + { ++audioChannels[meta.m_flow == Lv2Ports::Flow::Output ? outCount : inCount]; + } + else if(meta.m_type == Lv2Ports::Type::AtomSeq && portMustBeUsed) + { + ++midiChannels[meta.m_flow == Lv2Ports::Flow::Output + ? outCount : inCount]; + } } if (audioChannels[inCount] > 2) @@ -71,6 +97,13 @@ Plugin::PluginTypes Lv2Proc::check(const LilvPlugin *plugin, issues.emplace_back(tooManyOutputChannels, std::to_string(audioChannels[outCount])); + if (midiChannels[inCount] > 1) + issues.emplace_back(tooManyMidiInputChannels, + std::to_string(midiChannels[inCount])); + if (midiChannels[outCount] > 1) + issues.emplace_back(tooManyMidiOutputChannels, + std::to_string(midiChannels[outCount])); + AutoLilvNodes reqFeats(lilv_plugin_get_required_features(plugin)); LILV_FOREACH (nodes, itr, reqFeats.get()) { @@ -104,7 +137,9 @@ Plugin::PluginTypes Lv2Proc::check(const LilvPlugin *plugin, Lv2Proc::Lv2Proc(const LilvPlugin *plugin, Model* parent) : LinkedModelGroup(parent), - m_plugin(plugin) + m_plugin(plugin), + m_midiInputBuf(m_maxMidiInputEvents), + m_midiInputReader(m_midiInputBuf) { initPlugin(); } @@ -149,29 +184,79 @@ void Lv2Proc::copyModelsFromCore() { void visit(Lv2Ports::Control& ctrl) override { - if (ctrl.m_flow == Lv2Ports::Flow::Input) - { - FloatFromModelVisitor ffm; - ffm.m_scalePointMap = &ctrl.m_scalePointMap; - ctrl.m_connectedModel->accept(ffm); - ctrl.m_val = ffm.m_res; - } + FloatFromModelVisitor ffm; + ffm.m_scalePointMap = &ctrl.m_scalePointMap; + ctrl.m_connectedModel->accept(ffm); + ctrl.m_val = ffm.m_res; } void visit(Lv2Ports::Cv& cv) override { - if (cv.m_flow == Lv2Ports::Flow::Input) + FloatFromModelVisitor ffm; + ffm.m_scalePointMap = &cv.m_scalePointMap; + cv.m_connectedModel->accept(ffm); + // dirty fix, needs better interpolation + std::fill(cv.m_buffer.begin(), cv.m_buffer.end(), ffm.m_res); + } + void visit(Lv2Ports::AtomSeq& atomPort) override + { + lv2_evbuf_reset(atomPort.m_buf.get(), true); + } + } copy; + + // feed each input port with the respective data from the LMMS core + for (const std::unique_ptr& port : m_ports) + { + if (port->m_flow == Lv2Ports::Flow::Input) + { + port->accept(copy); + } + } + + // send pending MIDI events to atom port + if(m_midiIn) + { + LV2_Evbuf_Iterator iter = lv2_evbuf_begin(m_midiIn->m_buf.get()); + // MIDI events waiting to go to the plugin? + while(m_midiInputReader.read_space() > 0) + { + const MidiInputEvent ev = m_midiInputReader.read(1)[0]; + uint32_t atomStamp = + ev.time.frames(Engine::framesPerTick()) + ev.offset; + uint32_t type = Engine::getLv2Manager()-> + uridCache()[Lv2UridCache::Id::midi_MidiEvent]; + uint8_t buf[4]; + std::size_t bufsize = writeToByteSeq(ev.ev, buf, sizeof(buf)); + if(bufsize) { - FloatFromModelVisitor ffm; - ffm.m_scalePointMap = &cv.m_scalePointMap; - cv.m_connectedModel->accept(ffm); - // dirty fix, needs better interpolation - std::fill(cv.m_buffer.begin(), cv.m_buffer.end(), ffm.m_res); + lv2_evbuf_write(&iter, atomStamp, type, bufsize, buf); } } + } +} + + + + +void Lv2Proc::copyModelsToCore() +{ + struct Copy : public Lv2Ports::Visitor + { + void visit(Lv2Ports::AtomSeq& atomPort) override + { + // we currently don't copy anything, but we need to clear the buffer + // for the plugin to write again + lv2_evbuf_reset(atomPort.m_buf.get(), false); + } } copy; - for (const std::unique_ptr& port : m_ports) { - port->accept(copy); } + // fetch data from each output port and bring it to the LMMS core + for (const std::unique_ptr& port : m_ports) + { + if (port->m_flow == Lv2Ports::Flow::Output) + { + port->accept(copy); + } + } } @@ -229,6 +314,41 @@ void Lv2Proc::run(fpp_t frames) +// in case there will be a PR which removes this callback and instead adds a +// `ringbuffer_t` to `class Instrument`, this +// function (and the ringbuffer and its reader in `Lv2Proc`) will simply vanish +void Lv2Proc::handleMidiInputEvent(const MidiEvent &event, const MidiTime &time, f_cnt_t offset) +{ + if(m_midiIn) + { + // ringbuffer allows only one writer at a time + // however, this function can be called by multiple threads + // (different RT and non-RT!) at the same time + // for now, a spinlock looks like the most safe/easy compromise + + // source: https://en.cppreference.com/w/cpp/atomic/atomic_flag + while (m_ringLock.test_and_set(std::memory_order_acquire)) // acquire lock + ; // spin + + MidiInputEvent ev { event, time, offset }; + std::size_t written = m_midiInputBuf.write(&ev, 1); + if(written != 1) + { + qWarning("MIDI ringbuffer is too small! Discarding MIDI event."); + } + + m_ringLock.clear(std::memory_order_release); + } + else + { + qWarning() << "Warning: Caught MIDI event for an Lv2 instrument" + << "that can not hande MIDI... Ignoring"; + } +} + + + + AutomatableModel *Lv2Proc::modelAtPort(const QString &uri) { // unused currently @@ -284,6 +404,18 @@ void Lv2Proc::shutdownPlugin() +bool Lv2Proc::hasNoteInput() const +{ + return m_midiIn; + // we could additionally check for + // http://lv2plug.in/ns/lv2core#InstrumentPlugin + // however, jalv does not do that, too + // so, if there's any MIDI input, we just assume we can send notes there +} + + + + void Lv2Proc::initPluginSpecificFeatures() { // nothing yet @@ -385,6 +517,46 @@ void Lv2Proc::createPort(std::size_t portNum) port = audio; break; } + case Lv2Ports::Type::AtomSeq: + { + Lv2Ports::AtomSeq* atomPort = new Lv2Ports::AtomSeq; + + { + AutoLilvNode uriAtomSupports(Engine::getLv2Manager()->uri(LV2_ATOM__supports)); + AutoLilvNodes atomSupports(lilv_port_get_value(m_plugin, lilvPort, uriAtomSupports.get())); + AutoLilvNode uriMidiEvent(Engine::getLv2Manager()->uri(LV2_MIDI__MidiEvent)); + + LILV_FOREACH (nodes, itr, atomSupports.get()) + { + if(lilv_node_equals(lilv_nodes_get(atomSupports.get(), itr), uriMidiEvent.get())) + { + atomPort->flags |= Lv2Ports::AtomSeq::FlagType::Midi; + } + } + } + + int minimumSize = minimumEvbufSize(); + + Lv2Manager* mgr = Engine::getLv2Manager(); + + // check for alternative minimum size + { + AutoLilvNode rszMinimumSize = mgr->uri(LV2_RESIZE_PORT__minimumSize); + AutoLilvNodes minSizeV(lilv_port_get_value(m_plugin, lilvPort, rszMinimumSize.get())); + LilvNode* minSize = minSizeV ? lilv_nodes_get_first(minSizeV.get()) : nullptr; + if (minSize && lilv_node_is_int(minSize)) { + minimumSize = std::max(minimumSize, lilv_node_as_int(minSize)); + } + } + + atomPort->m_buf.reset( + lv2_evbuf_new(static_cast(minimumSize), + mgr->uridMap().map(LV2_ATOM__Chunk), + mgr->uridMap().map(LV2_ATOM__Sequence))); + + port = atomPort; + break; + } default: port = new Lv2Ports::Unknown; } @@ -445,6 +617,33 @@ void Lv2Proc::createPorts() else if (!portRef->m_right) { portRef->m_right = &audio; } } } + + void visit(Lv2Ports::AtomSeq& atomPort) override + { + if(atomPort.m_flow == Lv2Ports::Flow::Input) + { + if(atomPort.flags & Lv2Ports::AtomSeq::FlagType::Midi) + { + // take any MIDI input, prefer mandatory MIDI input + // (Lv2Proc::check() assures there are <=1 mandatory MIDI + // input ports) + if(!m_proc->m_midiIn || !atomPort.m_optional) + m_proc->m_midiIn = &atomPort; + } + } + else if(atomPort.m_flow == Lv2Ports::Flow::Output) + { + if(atomPort.flags & Lv2Ports::AtomSeq::FlagType::Midi) + { + // take any MIDI output, prefer mandatory MIDI output + // (Lv2Proc::check() assures there are <=1 mandatory MIDI + // output ports) + if(!m_proc->m_midiOut || !atomPort.m_optional) + m_proc->m_midiOut = &atomPort; + } + } + else { Q_ASSERT(false); } + } }; std::size_t maxPorts = lilv_plugin_get_num_ports(m_plugin); @@ -472,10 +671,15 @@ struct ConnectPortVisitor : public Lv2Ports::Visitor { std::size_t m_num; LilvInstance* m_instance; - void connectPort(void* location) { + void connectPort(void* location) + { lilv_instance_connect_port(m_instance, static_cast(m_num), location); } + void visit(Lv2Ports::AtomSeq& atomSeq) override + { + connectPort(lv2_evbuf_get_buffer(atomSeq.m_buf.get())); + } void visit(Lv2Ports::Control& ctrl) override { connectPort(&ctrl.m_val); } void visit(Lv2Ports::Audio& audio) override {