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

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