Enable Lv2 Atom ports

All Atom ports are now being created and connected. Currently only MIDI
input ports are supplied with data.

Major contribution to #562 ("lv2 support").
This commit is contained in:
Johannes Lorenz
2019-06-02 19:23:31 +02:00
committed by Johannes Lorenz
parent acd0e4d430
commit 8b2902c27a
13 changed files with 686 additions and 32 deletions

View File

@@ -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

View File

@@ -26,6 +26,7 @@
#ifdef LMMS_HAVE_LV2
#include <algorithm>
#include <QtGlobal>
#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

193
src/core/lv2/Lv2Evbuf.cpp Normal file
View File

@@ -0,0 +1,193 @@
/*
* lv2_evbuf.cpp - Lv2 event buffer implementation
*
* Copyright (c) 2019-2020 Johannes Lorenz <jlsf2013$users.sourceforge.net, $=@>
*
* 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 <http://drobilla.net>
*/
#include "Lv2Evbuf.h"
#ifdef LMMS_HAVE_LV2
#include <cassert>
#include <cstdlib>
#include <cstring>
#include <lv2/lv2plug.in/ns/ext/atom/atom.h>
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

View File

@@ -27,9 +27,12 @@
#ifdef LMMS_HAVE_LV2
#include <lv2/lv2plug.in/ns/ext/atom/atom.h>
#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<PluginIssue> 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() {}

View File

@@ -27,6 +27,11 @@
#ifdef LMMS_HAVE_LV2
#include <cmath>
#include <lv2/lv2plug.in/ns/ext/midi/midi.h>
#include <lv2/lv2plug.in/ns/ext/atom/atom.h>
#include <lv2/lv2plug.in/ns/ext/resize-port/resize-port.h>
#include <QDebug>
#include <QtGlobal>
#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<PluginIssue>& 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);
}
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;
for (const std::unique_ptr<Lv2Ports::PortBase>& port : m_ports) {
port->accept(copy); }
// feed each input port with the respective data from the LMMS core
for (const std::unique_ptr<Lv2Ports::PortBase>& 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)
{
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;
// fetch data from each output port and bring it to the LMMS core
for (const std::unique_ptr<Lv2Ports::PortBase>& 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<MidiEvent + time info>` 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<uint32_t>(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<uint32_t>(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
{