Implement Lv2 Worker (#6484)

This commit is contained in:
Johannes Lorenz
2023-09-22 23:27:02 +02:00
committed by Johannes Lorenz
parent 83777dc1f7
commit 33d1baddc0
10 changed files with 580 additions and 4 deletions

View File

@@ -197,6 +197,7 @@ public:
// audio-device-stuff
bool renderOnly() const { return m_renderOnly; }
// Returns the current audio device's name. This is not necessarily
// the user's preferred audio device, in case you were thinking that.
inline const QString & audioDevName() const

93
include/LmmsSemaphore.h Normal file
View File

@@ -0,0 +1,93 @@
/*
* Semaphore.h - Semaphore declaration
*
* Copyright (c) 2022-2022 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.
*
*/
/*
* This code has been copied and adapted from https://github.com/drobilla/jalv
* File src/zix/sem.h
*/
#ifndef LMMS_SEMAPHORE_H
#define LMMS_SEMAPHORE_H
#include "lmmsconfig.h"
#ifdef LMMS_BUILD_APPLE
# include <mach/mach.h>
#elif defined(LMMS_BUILD_WIN32)
# include <windows.h>
#else
# include <semaphore.h>
#endif
#include <system_error>
namespace lmms {
/**
A counting semaphore.
This is an integer that is always positive, and has two main operations:
increment (post) and decrement (wait). If a decrement can not be performed
(i.e. the value is 0) the caller will be blocked until another thread posts
and the operation can succeed.
Semaphores can be created with any starting value, but typically this will
be 0 so the semaphore can be used as a simple signal where each post
corresponds to one wait.
Semaphores are very efficient (much moreso than a mutex/cond pair). In
particular, at least on Linux, post is async-signal-safe, which means it
does not block and will not be interrupted. If you need to signal from
a realtime thread, this is the most appropriate primitive to use.
@note Likely outdated with C++20's std::counting_semaphore
(though we have to check that this will be RT conforming on all platforms)
*/
class Semaphore
{
public:
Semaphore(unsigned initial);
Semaphore(const Semaphore&) = delete;
Semaphore& operator=(const Semaphore&) = delete;
Semaphore(Semaphore&&) = delete;
Semaphore& operator=(Semaphore&&) = delete;
~Semaphore();
void post();
void wait();
bool tryWait();
private:
#ifdef LMMS_BUILD_APPLE
semaphore_t m_sem;
#elif defined(LMMS_BUILD_WIN32)
HANDLE m_sem;
#else
sem_t m_sem;
#endif
};
} // namespace lmms
#endif // LMMS_SEMAPHORE_H

View File

@@ -51,13 +51,14 @@ public:
std::size_t capacity() const {return m_buffer.maximum_eventual_write_space();}
std::size_t free() const {return m_buffer.write_space();}
void wakeAll() {m_notifier.wakeAll();}
std::size_t write(const sampleFrame *src, std::size_t cnt, bool notify = false)
std::size_t write(const T *src, std::size_t cnt, bool notify = false)
{
std::size_t written = LocklessRingBuffer<T>::m_buffer.write(src, cnt);
// Let all waiting readers know new data are available.
if (notify) {LocklessRingBuffer<T>::m_notifier.wakeAll();}
return written;
}
void mlock() { m_buffer.mlock(); }
protected:
ringbuffer_t<T> m_buffer;

View File

@@ -1,7 +1,7 @@
/*
* Lv2Proc.h - Lv2 processor class
*
* Copyright (c) 2019-2020 Johannes Lorenz <jlsf2013$users.sourceforge.net, $=@>
* Copyright (c) 2019-2022 Johannes Lorenz <jlsf2013$users.sourceforge.net, $=@>
*
* This file is part of LMMS - https://lmms.io
*
@@ -31,11 +31,14 @@
#include <lilv/lilv.h>
#include <memory>
#include <optional>
#include "LinkedModelGroups.h"
#include "LmmsSemaphore.h"
#include "Lv2Basics.h"
#include "Lv2Features.h"
#include "Lv2Options.h"
#include "LinkedModelGroups.h"
#include "Lv2Worker.h"
#include "Plugin.h"
#include "../src/3rdparty/ringbuffer/include/ringbuffer/ringbuffer.h"
#include "TimePos.h"
@@ -174,8 +177,14 @@ private:
const LilvPlugin* m_plugin;
LilvInstance* m_instance;
Lv2Features m_features;
// options
Lv2Options m_options;
// worker
std::optional<Lv2Worker> m_worker;
Semaphore m_workLock; // this must be shared by different workers
// full list of ports
std::vector<std::unique_ptr<Lv2Ports::PortBase>> m_ports;
// quick reference to specific, unique ports

93
include/Lv2Worker.h Normal file
View File

@@ -0,0 +1,93 @@
/*
* Lv2Worker.h - Lv2Worker class
*
* Copyright (c) 2022-2022 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.
*
*/
#ifndef LV2WORKER_H
#define LV2WORKER_H
#include "lmmsconfig.h"
#ifdef LMMS_HAVE_LV2
#include <lilv/lilv.h>
#include <lv2/lv2plug.in/ns/ext/worker/worker.h>
#include <thread>
#include <vector>
#include "LocklessRingBuffer.h"
#include "LmmsSemaphore.h"
namespace lmms
{
/**
Worker container
*/
class Lv2Worker
{
public:
// CTOR/DTOR/feature access
Lv2Worker(const LV2_Worker_Interface* iface, Semaphore* common_work_lock, bool threaded);
~Lv2Worker();
void setHandle(LV2_Handle handle) { m_handle = handle; }
LV2_Worker_Schedule* feature() { return &m_scheduleFeature; }
// public API
void emitResponses();
void notifyPluginThatRunFinished()
{
if(m_iface->end_run) { m_iface->end_run(m_scheduleFeature.handle); }
}
// to be called only by static functions
LV2_Worker_Status scheduleWork(uint32_t size, const void* data);
LV2_Worker_Status respond(uint32_t size, const void* data);
private:
// functions
void workerFunc();
std::size_t bufferSize() const; //!< size of internal buffers
// parameters
const LV2_Worker_Interface* m_iface;
bool m_threaded;
LV2_Handle m_handle;
LV2_Worker_Schedule m_scheduleFeature;
// threading/synchronization
std::thread m_thread;
std::vector<char> m_response; //!< buffer where single requests from m_requests are unpacked
LocklessRingBuffer<char> m_requests, m_responses; //!< ringbuffer to queue multiple requests
LocklessRingBufferReader<char> m_requestsReader, m_responsesReader;
std::atomic<bool> m_exit = false; //!< Whether the worker function should keep looping
Semaphore m_sem;
Semaphore* m_workLock;
};
} // namespace lmms
#endif // LMMS_HAVE_LV2
#endif // LV2WORKER_H

View File

@@ -70,6 +70,7 @@ set(LMMS_SRCS
core/SamplePlayHandle.cpp
core/SampleRecordHandle.cpp
core/Scale.cpp
core/LmmsSemaphore.cpp
core/SerializingObject.cpp
core/Song.cpp
core/TempoSyncKnobModel.cpp
@@ -112,6 +113,7 @@ set(LMMS_SRCS
core/lv2/Lv2SubPluginFeatures.cpp
core/lv2/Lv2UridCache.cpp
core/lv2/Lv2UridMap.cpp
core/lv2/Lv2Worker.cpp
core/midi/MidiAlsaRaw.cpp
core/midi/MidiAlsaSeq.cpp

143
src/core/LmmsSemaphore.cpp Normal file
View File

@@ -0,0 +1,143 @@
/*
* Semaphore.cpp - Semaphore implementation
*
* Copyright (c) 2022-2022 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.
*
*/
/*
* This code has been copied and adapted from https://github.com/drobilla/jalv
* File src/zix/sem.h
*/
#include "LmmsSemaphore.h"
#if defined(LMMS_BUILD_WIN32)
# include <limits.h>
#else
# include <errno.h>
#endif
#include <system_error>
namespace lmms {
#ifdef LMMS_BUILD_APPLE
Semaphore::Semaphore(unsigned val)
{
kern_return_t rval = semaphore_create(mach_task_self(), &m_sem, SYNC_POLICY_FIFO, val);
if(rval != 0) {
throw std::system_error(rval, std::system_category(), "Could not create semaphore");
}
}
Semaphore::~Semaphore()
{
semaphore_destroy(mach_task_self(), m_sem);
}
void Semaphore::post()
{
semaphore_signal(m_sem);
}
void Semaphore::wait()
{
kern_return_t rval = semaphore_wait(m_sem);
if (rval != KERN_SUCCESS) {
throw std::system_error(rval, std::system_category(), "Waiting for semaphore failed");
}
}
bool Semaphore::tryWait()
{
const mach_timespec_t zero = { 0, 0 };
return semaphore_timedwait(m_sem, zero) == KERN_SUCCESS;
}
#elif defined(LMMS_BUILD_WIN32)
Semaphore::Semaphore(unsigned initial)
{
if(CreateSemaphore(nullptr, initial, LONG_MAX, nullptr) == nullptr) {
throw std::system_error(GetLastError(), std::system_category(), "Could not create semaphore");
}
}
Semaphore::~Semaphore()
{
CloseHandle(m_sem);
}
void Semaphore::post()
{
ReleaseSemaphore(m_sem, 1, nullptr);
}
void Semaphore::wait()
{
if (WaitForSingleObject(m_sem, INFINITE) != WAIT_OBJECT_0) {
throw std::system_error(GetLastError(), std::system_category(), "Waiting for semaphore failed");
}
}
bool Semaphore::tryWait()
{
return WaitForSingleObject(m_sem, 0) == WAIT_OBJECT_0;
}
#else /* !defined(LMMS_BUILD_APPLE) && !defined(LMMS_BUILD_WIN32) */
Semaphore::Semaphore(unsigned initial)
{
if(sem_init(&m_sem, 0, initial) != 0) {
throw std::system_error(errno, std::generic_category(), "Could not create semaphore");
}
}
Semaphore::~Semaphore()
{
sem_destroy(&m_sem);
}
void Semaphore::post()
{
sem_post(&m_sem);
}
void Semaphore::wait()
{
while (sem_wait(&m_sem) != 0) {
if (errno != EINTR) {
throw std::system_error(errno, std::generic_category(), "Waiting for semaphore failed");
}
/* Otherwise, interrupted, so try again. */
}
}
bool Semaphore::tryWait()
{
return (sem_trywait(&m_sem) == 0);
}
#endif
} // namespace lmms

View File

@@ -31,6 +31,7 @@
#include <lilv/lilv.h>
#include <lv2/lv2plug.in/ns/ext/buf-size/buf-size.h>
#include <lv2/lv2plug.in/ns/ext/options/options.h>
#include <lv2/lv2plug.in/ns/ext/worker/worker.h>
#include <QDebug>
#include <QElapsedTimer>
@@ -172,6 +173,7 @@ Lv2Manager::Lv2Manager() :
m_supportedFeatureURIs.insert(LV2_URID__map);
m_supportedFeatureURIs.insert(LV2_URID__unmap);
m_supportedFeatureURIs.insert(LV2_OPTIONS__options);
m_supportedFeatureURIs.insert(LV2_WORKER__schedule);
// min/max is always passed in the options
m_supportedFeatureURIs.insert(LV2_BUF_SIZE__boundedBlockLength);
// block length is only changed initially in AudioEngine CTOR

View File

@@ -1,7 +1,7 @@
/*
* Lv2Proc.cpp - Lv2 processor class
*
* Copyright (c) 2019-2020 Johannes Lorenz <jlsf2013$users.sourceforge.net, $=@>
* Copyright (c) 2019-2022 Johannes Lorenz <jlsf2013$users.sourceforge.net, $=@>
*
* This file is part of LMMS - https://lmms.io
*
@@ -30,6 +30,7 @@
#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 <lv2/lv2plug.in/ns/ext/worker/worker.h>
#include <QDebug>
#include <QDomDocument>
#include <QtGlobal>
@@ -170,6 +171,7 @@ Plugin::Type Lv2Proc::check(const LilvPlugin *plugin,
Lv2Proc::Lv2Proc(const LilvPlugin *plugin, Model* parent) :
LinkedModelGroup(parent),
m_plugin(plugin),
m_workLock(1),
m_midiInputBuf(m_maxMidiInputEvents),
m_midiInputReader(m_midiInputBuf)
{
@@ -360,7 +362,19 @@ void Lv2Proc::copyBuffersToCore(sampleFrame* buf,
void Lv2Proc::run(fpp_t frames)
{
if (m_worker)
{
// Process any worker replies
m_worker->emitResponses();
}
lilv_instance_run(m_instance, static_cast<uint32_t>(frames));
if (m_worker)
{
// Notify the plugin the run() cycle is finished
m_worker->notifyPluginThatRunFinished();
}
}
@@ -428,6 +442,9 @@ void Lv2Proc::initPlugin()
if (m_instance)
{
if(m_worker) {
m_worker->setHandle(lilv_instance_get_handle(m_instance));
}
for (std::size_t portNum = 0; portNum < m_ports.size(); ++portNum)
connectPort(portNum);
lilv_instance_activate(m_instance);
@@ -504,8 +521,20 @@ void Lv2Proc::initMOptions()
void Lv2Proc::initPluginSpecificFeatures()
{
// options
initMOptions();
m_features[LV2_OPTIONS__options] = const_cast<LV2_Options_Option*>(m_options.feature());
// worker (if plugin has worker extension)
Lv2Manager* mgr = Engine::getLv2Manager();
if (lilv_plugin_has_extension_data(m_plugin, mgr->uri(LV2_WORKER__interface).get())) {
const auto iface = static_cast<const LV2_Worker_Interface*>(
lilv_instance_get_extension_data(m_instance, LV2_WORKER__interface));
bool threaded = !Engine::audioEngine()->renderOnly();
m_worker.emplace(iface, &m_workLock, threaded);
m_features[LV2_WORKER__schedule] = m_worker->feature();
// Note: m_worker::setHandle will still need to be called later
}
}

203
src/core/lv2/Lv2Worker.cpp Normal file
View File

@@ -0,0 +1,203 @@
/*
* Lv2Worker.cpp - Lv2Worker implementation
*
* Copyright (c) 2022-2022 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.
*
*/
#include "Lv2Worker.h"
#include <cassert>
#include <QDebug>
#ifdef LMMS_HAVE_LV2
#include "Engine.h"
namespace lmms
{
// static wrappers
static LV2_Worker_Status
staticWorkerRespond(LV2_Worker_Respond_Handle handle,
uint32_t size, const void* data)
{
Lv2Worker* worker = static_cast<Lv2Worker*>(handle);
return worker->respond(size, data);
}
std::size_t Lv2Worker::bufferSize() const
{
// ardour uses this fixed size for ALSA:
return 8192 * 4;
// for jack, they use 4 * jack_port_type_get_buffer_size (..., JACK_DEFAULT_MIDI_TYPE)
// (possible extension for AudioDevice)
}
Lv2Worker::Lv2Worker(const LV2_Worker_Interface* iface,
Semaphore* common_work_lock,
bool threaded) :
m_iface(iface),
m_threaded(threaded),
m_response(bufferSize()),
m_requests(bufferSize()),
m_responses(bufferSize()),
m_requestsReader(m_requests),
m_responsesReader(m_responses),
m_sem(0),
m_workLock(common_work_lock)
{
assert(iface);
m_scheduleFeature.handle = static_cast<LV2_Worker_Schedule_Handle>(this);
m_scheduleFeature.schedule_work = [](LV2_Worker_Schedule_Handle handle,
uint32_t size, const void* data) -> LV2_Worker_Status
{
Lv2Worker* worker = static_cast<Lv2Worker*>(handle);
return worker->scheduleWork(size, data);
};
if (threaded) { m_thread = std::thread(&Lv2Worker::workerFunc, this); }
m_requests.mlock();
m_responses.mlock();
}
Lv2Worker::~Lv2Worker()
{
m_exit = true;
if(m_threaded) {
m_sem.post();
m_thread.join();
}
}
// Let the worker send responses to the audio thread
LV2_Worker_Status Lv2Worker::respond(uint32_t size, const void* data)
{
if(m_threaded)
{
if(m_responses.free() < sizeof(size) + size)
{
return LV2_WORKER_ERR_NO_SPACE;
}
else
{
m_responses.write((const char*)&size, sizeof(size));
if(size && data) { m_responses.write((const char*)data, size); }
}
}
else
{
m_iface->work_response(m_handle, size, data);
}
return LV2_WORKER_SUCCESS;
}
// Let the worker receive work from the audio thread and "work" on it
void Lv2Worker::workerFunc()
{
std::vector<char> buf;
uint32_t size;
while (true) {
m_sem.wait();
if (m_exit) { break; }
const std::size_t readSpace = m_requestsReader.read_space();
if (readSpace <= sizeof(size)) { continue; } // (should not happen)
m_requestsReader.read(sizeof(size)).copy((char*)&size, sizeof(size));
assert(size <= readSpace - sizeof(size));
if(size > buf.size()) { buf.resize(size); }
if(size) { m_requestsReader.read(size).copy(buf.data(), size); }
m_workLock->wait();
m_iface->work(m_handle, staticWorkerRespond, this, size, buf.data());
m_workLock->post();
}
}
// Let the audio thread schedule work for the worker
LV2_Worker_Status Lv2Worker::scheduleWork(uint32_t size, const void *data)
{
if (m_threaded)
{
if(m_requests.free() < sizeof(size) + size)
{
return LV2_WORKER_ERR_NO_SPACE;
}
else
{
// Schedule a request to be executed by the worker thread
m_requests.write((const char*)&size, sizeof(size));
if(size && data) { m_requests.write((const char*)data, size); }
m_sem.post();
}
}
else
{
// Execute work immediately in this thread
m_workLock->wait();
m_iface->work(m_handle, staticWorkerRespond, this, size, data);
m_workLock->post();
}
return LV2_WORKER_SUCCESS;
}
// Let the audio thread read incoming worker responses, and process it
void Lv2Worker::emitResponses()
{
std::size_t read_space = m_responsesReader.read_space();
uint32_t size;
while (read_space > sizeof(size)) {
m_responsesReader.read(sizeof(size)).copy((char*)&size, sizeof(size));
if(size) { m_responsesReader.read(size).copy(m_response.data(), size); }
m_iface->work_response(m_handle, size, m_response.data());
read_space -= sizeof(size) + size;
}
}
} // namespace lmms
#endif // LMMS_HAVE_LV2