Alternative approach to zero-latency cables; Update patches
Signed-off-by: falkTX <falktx@falktx.com>
This commit is contained in:
parent
ff1b592257
commit
0000456cf5
12 changed files with 662 additions and 192 deletions
|
@ -46,27 +46,12 @@ struct Cable;
|
|||
|
||||
struct Port {
|
||||
/** Voltage of the port. */
|
||||
/** NOTE Purposefully renamed in Cardinal as a way to catch plugins using it directly. */
|
||||
union {
|
||||
/** Unstable API. Use getVoltage() and setVoltage() instead. */
|
||||
float cvoltages[PORT_MAX_CHANNELS] = {};
|
||||
float voltages[PORT_MAX_CHANNELS] = {};
|
||||
/** DEPRECATED. Unstable API. Use getVoltage() and setVoltage() instead. */
|
||||
float cvalue;
|
||||
float value;
|
||||
};
|
||||
|
||||
/** Special trickery for backwards compatibility with plugins using DEPRECATED APIs */
|
||||
struct BackwardsCompatPortValue {
|
||||
Port* const port;
|
||||
BackwardsCompatPortValue(Port* p) : port(p) {}
|
||||
void operator=(float value) { port->setVoltage(value); }
|
||||
void operator-=(float value) { port->setVoltage(port->cvalue - value); }
|
||||
void operator+=(float value) { port->setVoltage(port->cvalue + value); }
|
||||
void operator*=(float value) { port->setVoltage(port->cvalue * value); }
|
||||
void operator/=(float value) { port->setVoltage(port->cvalue / value); }
|
||||
operator float() const { return port->cvalue; }
|
||||
} value;
|
||||
Port() : value(this) {}
|
||||
|
||||
union {
|
||||
/** Number of polyphonic channels.
|
||||
DEPRECATED. Unstable API. Use set/getChannels() instead.
|
||||
|
@ -87,24 +72,19 @@ struct Port {
|
|||
OUTPUT,
|
||||
};
|
||||
|
||||
/** Cables connected to this output port. */
|
||||
/** List of cables connected to this port (if output type). */
|
||||
std::list<Cable*> cables;
|
||||
|
||||
/** Step-through the cables.
|
||||
Called whenever voltage changes, required for zero latency operation. */
|
||||
void stepCables();
|
||||
|
||||
/** Sets the voltage of the given channel. */
|
||||
void setVoltage(float voltage, int channel = 0) {
|
||||
cvoltages[channel] = voltage;
|
||||
stepCables();
|
||||
voltages[channel] = voltage;
|
||||
}
|
||||
|
||||
/** Returns the voltage of the given channel.
|
||||
Because of proper bookkeeping, all channels higher than the input port's number of channels should be 0V.
|
||||
*/
|
||||
float getVoltage(int channel = 0) {
|
||||
return cvoltages[channel];
|
||||
return voltages[channel];
|
||||
}
|
||||
|
||||
/** Returns the given channel's voltage if the port is polyphonic, otherwise returns the first voltage (channel 0). */
|
||||
|
@ -124,15 +104,14 @@ struct Port {
|
|||
/** Returns a pointer to the array of voltages beginning with firstChannel.
|
||||
The pointer can be used for reading and writing.
|
||||
*/
|
||||
// TODO convert to const float* for zero-latency cable stuff and fix all plugins after
|
||||
float* getVoltages(int firstChannel = 0) {
|
||||
return &cvoltages[firstChannel];
|
||||
return &voltages[firstChannel];
|
||||
}
|
||||
|
||||
/** Copies the port's voltages to an array of size at least `channels`. */
|
||||
void readVoltages(float* v) {
|
||||
for (int c = 0; c < channels; c++) {
|
||||
v[c] = cvoltages[c];
|
||||
v[c] = voltages[c];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -141,24 +120,22 @@ struct Port {
|
|||
*/
|
||||
void writeVoltages(const float* v) {
|
||||
for (int c = 0; c < channels; c++) {
|
||||
cvoltages[c] = v[c];
|
||||
voltages[c] = v[c];
|
||||
}
|
||||
stepCables();
|
||||
}
|
||||
|
||||
/** Sets all voltages to 0. */
|
||||
void clearVoltages() {
|
||||
for (int c = 0; c < channels; c++) {
|
||||
cvoltages[c] = 0.f;
|
||||
voltages[c] = 0.f;
|
||||
}
|
||||
stepCables();
|
||||
}
|
||||
|
||||
/** Returns the sum of all voltages. */
|
||||
float getVoltageSum() {
|
||||
float sum = 0.f;
|
||||
for (int c = 0; c < channels; c++) {
|
||||
sum += cvoltages[c];
|
||||
sum += voltages[c];
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
@ -171,12 +148,12 @@ struct Port {
|
|||
return 0.f;
|
||||
}
|
||||
else if (channels == 1) {
|
||||
return std::fabs(cvoltages[0]);
|
||||
return std::fabs(voltages[0]);
|
||||
}
|
||||
else {
|
||||
float sum = 0.f;
|
||||
for (int c = 0; c < channels; c++) {
|
||||
sum += std::pow(cvoltages[c], 2);
|
||||
sum += std::pow(voltages[c], 2);
|
||||
}
|
||||
return std::sqrt(sum);
|
||||
}
|
||||
|
@ -184,7 +161,7 @@ struct Port {
|
|||
|
||||
template <typename T>
|
||||
T getVoltageSimd(int firstChannel) {
|
||||
return T::load(&cvoltages[firstChannel]);
|
||||
return T::load(&voltages[firstChannel]);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
|
@ -204,8 +181,7 @@ struct Port {
|
|||
|
||||
template <typename T>
|
||||
void setVoltageSimd(T voltage, int firstChannel) {
|
||||
voltage.store(&cvoltages[firstChannel]);
|
||||
stepCables();
|
||||
voltage.store(&voltages[firstChannel]);
|
||||
}
|
||||
|
||||
/** Sets the number of polyphony channels.
|
||||
|
@ -220,7 +196,7 @@ struct Port {
|
|||
}
|
||||
// Set higher channel voltages to 0
|
||||
for (int c = channels; c < this->channels; c++) {
|
||||
cvoltages[c] = 0.f;
|
||||
voltages[c] = 0.f;
|
||||
}
|
||||
// Don't allow caller to set port as disconnected
|
||||
if (channels == 0) {
|
||||
|
|
31
include/engine/TerminalModule.hpp
Normal file
31
include/engine/TerminalModule.hpp
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* DISTRHO Cardinal Plugin
|
||||
* Copyright (C) 2021-2022 Filipe Coelho <falktx@falktx.com>
|
||||
*
|
||||
* 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 3 of
|
||||
* the License, or 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.
|
||||
*
|
||||
* For a full copy of the GNU General Public License see the LICENSE file.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <engine/Module.hpp>
|
||||
|
||||
namespace rack {
|
||||
namespace engine {
|
||||
|
||||
struct TerminalModule : Module {
|
||||
virtual void processTerminalInput(const ProcessArgs& args) = 0;
|
||||
virtual void processTerminalOutput(const ProcessArgs& args) = 0;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
|
@ -1 +1 @@
|
|||
Subproject commit 988c2372a95d163b71d04b217080e612b767c539
|
||||
Subproject commit e55fcd2e1d7c0fef69d4919baac6f791172c89ca
|
|
@ -24,7 +24,7 @@
|
|||
USE_NAMESPACE_DISTRHO;
|
||||
|
||||
template<int numIO>
|
||||
struct HostAudio : Module {
|
||||
struct HostAudio : TerminalModule {
|
||||
CardinalPluginContext* const pcontext;
|
||||
const int numParams;
|
||||
const int numInputs;
|
||||
|
@ -74,45 +74,65 @@ struct HostAudio : Module {
|
|||
dcFilters[i].setCutoffFreq(10.f * e.sampleTime);
|
||||
}
|
||||
|
||||
void process(const ProcessArgs&) override
|
||||
void processTerminalInput(const ProcessArgs&) override
|
||||
{
|
||||
const float* const* const dataIns = pcontext->dataIns;
|
||||
float** const dataOuts = pcontext->dataOuts;
|
||||
|
||||
const int blockFrames = pcontext->engine->getBlockFrames();
|
||||
const int64_t blockFrame = pcontext->engine->getBlockFrame();
|
||||
|
||||
// only checked on input
|
||||
if (lastBlockFrame != blockFrame)
|
||||
{
|
||||
dataFrame = 0;
|
||||
lastBlockFrame = blockFrame;
|
||||
}
|
||||
|
||||
// only incremented on output
|
||||
const int k = dataFrame;
|
||||
DISTRHO_SAFE_ASSERT_INT2_RETURN(k < blockFrames, k, blockFrames,);
|
||||
|
||||
// from host into cardinal, shows as output plug
|
||||
if (isBypassed())
|
||||
{
|
||||
for (int i=0; i<numOutputs; ++i)
|
||||
outputs[i].setVoltage(0.0f);
|
||||
}
|
||||
else if (dataIns != nullptr)
|
||||
{
|
||||
for (int i=0; i<numOutputs; ++i)
|
||||
outputs[i].setVoltage(dataIns[i][k] * 10.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void processTerminalOutput(const ProcessArgs&) override
|
||||
{
|
||||
float** const dataOuts = pcontext->dataOuts;
|
||||
|
||||
const int blockFrames = pcontext->engine->getBlockFrames();
|
||||
|
||||
// only incremented on output
|
||||
const int k = dataFrame++;
|
||||
DISTRHO_SAFE_ASSERT_INT2_RETURN(k < blockFrames, k, blockFrames,);
|
||||
|
||||
const float gain = numParams != 0 ? std::pow(params[0].getValue(), 2.f) : 1.0f;
|
||||
|
||||
// from host into cardinal, shows as output plug
|
||||
if (dataIns != nullptr)
|
||||
{
|
||||
for (int i=0; i<numOutputs; ++i)
|
||||
outputs[i].setVoltage(dataIns[i][k] * 10.0f);
|
||||
}
|
||||
|
||||
// from cardinal into host, shows as input plug
|
||||
for (int i=0; i<numInputs; ++i)
|
||||
if (! isBypassed())
|
||||
{
|
||||
float v = inputs[i].getVoltageSum() * 0.1f;
|
||||
|
||||
// Apply DC filter
|
||||
if (dcFilterEnabled)
|
||||
for (int i=0; i<numInputs; ++i)
|
||||
{
|
||||
dcFilters[i].process(v);
|
||||
v = dcFilters[i].highpass();
|
||||
}
|
||||
float v = inputs[i].getVoltageSum() * 0.1f;
|
||||
|
||||
dataOuts[i][k] += clamp(v * gain, -1.0f, 1.0f);
|
||||
// Apply DC filter
|
||||
if (dcFilterEnabled)
|
||||
{
|
||||
dcFilters[i].process(v);
|
||||
v = dcFilters[i].highpass();
|
||||
}
|
||||
|
||||
dataOuts[i][k] += clamp(v * gain, -1.0f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
if (numInputs == 2)
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
#pragma once
|
||||
|
||||
#include "rack.hpp"
|
||||
#include "engine/TerminalModule.hpp"
|
||||
|
||||
#ifdef NDEBUG
|
||||
# undef DEBUG
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
#include <pmmintrin.h>
|
||||
|
||||
#include <engine/Engine.hpp>
|
||||
#include <engine/TerminalModule.hpp>
|
||||
#include <settings.hpp>
|
||||
#include <system.hpp>
|
||||
#include <random.hpp>
|
||||
|
@ -49,12 +50,19 @@
|
|||
|
||||
#include "DistrhoUtils.hpp"
|
||||
|
||||
|
||||
// known terminal modules
|
||||
extern rack::plugin::Model* modelHostAudio2;
|
||||
extern rack::plugin::Model* modelHostAudio8;
|
||||
|
||||
|
||||
namespace rack {
|
||||
namespace engine {
|
||||
|
||||
|
||||
struct Engine::Internal {
|
||||
std::vector<Module*> modules;
|
||||
std::vector<TerminalModule*> terminalModules;
|
||||
std::vector<Cable*> cables;
|
||||
std::set<ParamHandle*> paramHandles;
|
||||
|
||||
|
@ -127,24 +135,64 @@ static void Cable_step(Cable* that) {
|
|||
const int channels = output->channels;
|
||||
// Copy all voltages from output to input
|
||||
for (int c = 0; c < channels; c++) {
|
||||
float v = output->cvoltages[c];
|
||||
float v = output->voltages[c];
|
||||
// Set 0V if infinite or NaN
|
||||
if (!std::isfinite(v))
|
||||
v = 0.f;
|
||||
input->cvoltages[c] = v;
|
||||
input->voltages[c] = v;
|
||||
}
|
||||
// Set higher channel voltages to 0
|
||||
for (int c = channels; c < input->channels; c++) {
|
||||
input->cvoltages[c] = 0.f;
|
||||
input->voltages[c] = 0.f;
|
||||
}
|
||||
input->channels = channels;
|
||||
}
|
||||
|
||||
|
||||
void Port::stepCables()
|
||||
{
|
||||
for (Cable* cable : cables)
|
||||
Cable_step(cable);
|
||||
static void Port_step(Port* that, float deltaTime) {
|
||||
// Set plug lights
|
||||
if (that->channels == 0) {
|
||||
that->plugLights[0].setBrightness(0.f);
|
||||
that->plugLights[1].setBrightness(0.f);
|
||||
that->plugLights[2].setBrightness(0.f);
|
||||
}
|
||||
else if (that->channels == 1) {
|
||||
float v = that->getVoltage() / 10.f;
|
||||
that->plugLights[0].setSmoothBrightness(-v, deltaTime);
|
||||
that->plugLights[1].setSmoothBrightness(v, deltaTime);
|
||||
that->plugLights[2].setBrightness(0.f);
|
||||
}
|
||||
else {
|
||||
float v = that->getVoltageRMS() / 10.f;
|
||||
that->plugLights[0].setBrightness(0.f);
|
||||
that->plugLights[1].setBrightness(0.f);
|
||||
that->plugLights[2].setSmoothBrightness(v, deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void TerminalModule__doProcess(TerminalModule* terminalModule, const Module::ProcessArgs& args, bool input) {
|
||||
// Step module
|
||||
if (input) {
|
||||
terminalModule->processTerminalInput(args);
|
||||
for (Output& output : terminalModule->outputs) {
|
||||
for (Cable* cable : output.cables)
|
||||
Cable_step(cable);
|
||||
}
|
||||
} else {
|
||||
terminalModule->processTerminalOutput(args);
|
||||
}
|
||||
|
||||
// Iterate ports to step plug lights
|
||||
if (args.frame % 7 /* PORT_DIVIDER */ == 0) {
|
||||
float portTime = args.sampleTime * 7 /* PORT_DIVIDER */;
|
||||
for (Input& input : terminalModule->inputs) {
|
||||
Port_step(&input, portTime);
|
||||
}
|
||||
for (Output& output : terminalModule->outputs) {
|
||||
Port_step(&output, portTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -174,14 +222,6 @@ static void Engine_stepFrame(Engine* that) {
|
|||
}
|
||||
}
|
||||
|
||||
/* NOTE this is likely not needed in Cardinal, but needs testing.
|
||||
* Leaving it as comment in case we need it bring it back
|
||||
// Step cables
|
||||
for (Cable* cable : internal->cables) {
|
||||
Cable_step(cable);
|
||||
}
|
||||
*/
|
||||
|
||||
// Flip messages for each module
|
||||
for (Module* module : internal->modules) {
|
||||
if (module->leftExpander.messageFlipRequested) {
|
||||
|
@ -200,16 +240,25 @@ static void Engine_stepFrame(Engine* that) {
|
|||
processArgs.sampleTime = internal->sampleTime;
|
||||
processArgs.frame = internal->frame;
|
||||
|
||||
// Step each module
|
||||
// Process terminal inputs first
|
||||
for (TerminalModule* terminalModule : internal->terminalModules) {
|
||||
TerminalModule__doProcess(terminalModule, processArgs, true);
|
||||
}
|
||||
|
||||
// Step each module and cables
|
||||
for (Module* module : internal->modules) {
|
||||
module->doProcess(processArgs);
|
||||
// FIXME remove this section below after all modules can use zero-latency cable stuff
|
||||
for (Output& output : module->outputs) {
|
||||
for (Cable* cable : output.cables)
|
||||
Cable_step(cable);
|
||||
}
|
||||
}
|
||||
|
||||
// Process terminal outputs last
|
||||
for (TerminalModule* terminalModule : internal->terminalModules) {
|
||||
TerminalModule__doProcess(terminalModule, processArgs, false);
|
||||
}
|
||||
|
||||
++internal->frame;
|
||||
}
|
||||
|
||||
|
@ -217,7 +266,7 @@ static void Engine_stepFrame(Engine* that) {
|
|||
static void Port_setDisconnected(Port* that) {
|
||||
that->channels = 0;
|
||||
for (int c = 0; c < PORT_MAX_CHANNELS; c++) {
|
||||
that->cvoltages[c] = 0.f;
|
||||
that->voltages[c] = 0.f;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -240,6 +289,14 @@ static void Engine_updateConnected(Engine* that) {
|
|||
disconnectedPorts.insert(&output);
|
||||
}
|
||||
}
|
||||
for (TerminalModule* terminalModule : that->internal->terminalModules) {
|
||||
for (Input& input : terminalModule->inputs) {
|
||||
disconnectedPorts.insert(&input);
|
||||
}
|
||||
for (Output& output : terminalModule->outputs) {
|
||||
disconnectedPorts.insert(&output);
|
||||
}
|
||||
}
|
||||
for (Cable* cable : that->internal->cables) {
|
||||
// Connect input
|
||||
Input& input = cable->inputModule->inputs[cable->inputId];
|
||||
|
@ -287,6 +344,7 @@ Engine::~Engine() {
|
|||
// If this happens, a module must have failed to remove itself before the RackWidget was destroyed.
|
||||
DISTRHO_SAFE_ASSERT(internal->cables.empty());
|
||||
DISTRHO_SAFE_ASSERT(internal->modules.empty());
|
||||
DISTRHO_SAFE_ASSERT(internal->terminalModules.empty());
|
||||
DISTRHO_SAFE_ASSERT(internal->paramHandles.empty());
|
||||
|
||||
DISTRHO_SAFE_ASSERT(internal->modulesCache.empty());
|
||||
|
@ -320,6 +378,11 @@ void Engine::clear_NoLock() {
|
|||
removeModule_NoLock(module);
|
||||
delete module;
|
||||
}
|
||||
std::vector<TerminalModule*> terminalModules = internal->terminalModules;
|
||||
for (TerminalModule* terminalModule : terminalModules) {
|
||||
removeModule_NoLock(terminalModule);
|
||||
delete terminalModule;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -404,6 +467,9 @@ void Engine::setSampleRate(float sampleRate) {
|
|||
for (Module* module : internal->modules) {
|
||||
module->onSampleRateChange(e);
|
||||
}
|
||||
for (TerminalModule* terminalModule : internal->terminalModules) {
|
||||
terminalModule->onSampleRateChange(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -474,7 +540,7 @@ double Engine::getMeterMax() {
|
|||
|
||||
|
||||
size_t Engine::getNumModules() {
|
||||
return internal->modules.size();
|
||||
return internal->modules.size() + internal->terminalModules.size();
|
||||
}
|
||||
|
||||
|
||||
|
@ -484,8 +550,12 @@ size_t Engine::getModuleIds(int64_t* moduleIds, size_t len) {
|
|||
for (Module* m : internal->modules) {
|
||||
if (i >= len)
|
||||
break;
|
||||
moduleIds[i] = m->id;
|
||||
i++;
|
||||
moduleIds[i++] = m->id;
|
||||
}
|
||||
for (TerminalModule* m : internal->terminalModules) {
|
||||
if (i >= len)
|
||||
break;
|
||||
moduleIds[i++] = m->id;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
@ -494,27 +564,43 @@ size_t Engine::getModuleIds(int64_t* moduleIds, size_t len) {
|
|||
std::vector<int64_t> Engine::getModuleIds() {
|
||||
SharedLock<SharedMutex> lock(internal->mutex);
|
||||
std::vector<int64_t> moduleIds;
|
||||
moduleIds.reserve(internal->modules.size());
|
||||
moduleIds.reserve(getNumModules());
|
||||
for (Module* m : internal->modules) {
|
||||
moduleIds.push_back(m->id);
|
||||
}
|
||||
for (TerminalModule* tm : internal->terminalModules) {
|
||||
moduleIds.push_back(tm->id);
|
||||
}
|
||||
return moduleIds;
|
||||
}
|
||||
|
||||
|
||||
static TerminalModule* asTerminalModule(Module* const module) {
|
||||
const plugin::Model* const model = module->model;
|
||||
if (model == modelHostAudio2 || model == modelHostAudio8)
|
||||
return static_cast<TerminalModule*>(module);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
void Engine::addModule(Module* module) {
|
||||
std::lock_guard<SharedMutex> lock(internal->mutex);
|
||||
DISTRHO_SAFE_ASSERT_RETURN(module,);
|
||||
DISTRHO_SAFE_ASSERT_RETURN(module != nullptr,);
|
||||
// Check that the module is not already added
|
||||
auto it = std::find(internal->modules.begin(), internal->modules.end(), module);
|
||||
DISTRHO_SAFE_ASSERT_RETURN(it == internal->modules.end(),);
|
||||
auto tit = std::find(internal->terminalModules.begin(), internal->terminalModules.end(), module);
|
||||
DISTRHO_SAFE_ASSERT_RETURN(tit == internal->terminalModules.end(),);
|
||||
// Set ID if unset or collides with an existing ID
|
||||
while (module->id < 0 || internal->modulesCache.find(module->id) != internal->modulesCache.end()) {
|
||||
// Randomly generate ID
|
||||
module->id = random::u64() % (1ull << 53);
|
||||
}
|
||||
// Add module
|
||||
internal->modules.push_back(module);
|
||||
if (TerminalModule* const terminalModule = asTerminalModule(module))
|
||||
internal->terminalModules.push_back(terminalModule);
|
||||
else
|
||||
internal->modules.push_back(module);
|
||||
internal->modulesCache[module->id] = module;
|
||||
// Dispatch AddEvent
|
||||
Module::AddEvent eAdd;
|
||||
|
@ -538,11 +624,7 @@ void Engine::removeModule(Module* module) {
|
|||
}
|
||||
|
||||
|
||||
void Engine::removeModule_NoLock(Module* module) {
|
||||
DISTRHO_SAFE_ASSERT_RETURN(module,);
|
||||
// Check that the module actually exists
|
||||
auto it = std::find(internal->modules.begin(), internal->modules.end(), module);
|
||||
DISTRHO_SAFE_ASSERT_RETURN(it != internal->modules.end(),);
|
||||
static void removeModule_NoLock_common(Engine::Internal* internal, Module* module) {
|
||||
// Remove from widgets cache
|
||||
CardinalPluginModelHelper* const helper = dynamic_cast<CardinalPluginModelHelper*>(module->model);
|
||||
DISTRHO_SAFE_ASSERT_RETURN(helper != nullptr,);
|
||||
|
@ -575,14 +657,31 @@ void Engine::removeModule_NoLock(Module* module) {
|
|||
m->rightExpander.module = NULL;
|
||||
}
|
||||
}
|
||||
// Remove module
|
||||
internal->modulesCache.erase(module->id);
|
||||
internal->modules.erase(it);
|
||||
// Reset expanders
|
||||
module->leftExpander.moduleId = -1;
|
||||
module->leftExpander.module = NULL;
|
||||
module->rightExpander.moduleId = -1;
|
||||
module->rightExpander.module = NULL;
|
||||
// Remove module
|
||||
internal->modulesCache.erase(module->id);
|
||||
}
|
||||
|
||||
|
||||
void Engine::removeModule_NoLock(Module* module) {
|
||||
DISTRHO_SAFE_ASSERT_RETURN(module,);
|
||||
// Check that the module actually exists
|
||||
if (TerminalModule* const terminalModule = asTerminalModule(module)) {
|
||||
auto tit = std::find(internal->terminalModules.begin(), internal->terminalModules.end(), terminalModule);
|
||||
DISTRHO_SAFE_ASSERT_RETURN(tit != internal->terminalModules.end(),);
|
||||
removeModule_NoLock_common(internal, module);
|
||||
internal->terminalModules.erase(tit);
|
||||
}
|
||||
else {
|
||||
auto it = std::find(internal->modules.begin(), internal->modules.end(), module);
|
||||
DISTRHO_SAFE_ASSERT_RETURN(it != internal->modules.end(),);
|
||||
removeModule_NoLock_common(internal, module);
|
||||
internal->modules.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -590,7 +689,8 @@ bool Engine::hasModule(Module* module) {
|
|||
SharedLock<SharedMutex> lock(internal->mutex);
|
||||
// TODO Performance could be improved by searching modulesCache, but more testing would be needed to make sure it's always valid.
|
||||
auto it = std::find(internal->modules.begin(), internal->modules.end(), module);
|
||||
return it != internal->modules.end();
|
||||
auto tit = std::find(internal->terminalModules.begin(), internal->terminalModules.end(), module);
|
||||
return it != internal->modules.end() && tit != internal->terminalModules.end();
|
||||
}
|
||||
|
||||
|
||||
|
@ -678,6 +778,10 @@ void Engine::prepareSave() {
|
|||
Module::SaveEvent e;
|
||||
module->onSave(e);
|
||||
}
|
||||
for (TerminalModule* terminalModule : internal->terminalModules) {
|
||||
Module::SaveEvent e;
|
||||
terminalModule->onSave(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -957,6 +1061,10 @@ json_t* Engine::toJson() {
|
|||
json_t* moduleJ = module->toJson();
|
||||
json_array_append_new(modulesJ, moduleJ);
|
||||
}
|
||||
for (TerminalModule* terminalModule : internal->terminalModules) {
|
||||
json_t* terminalModuleJ = terminalModule->toJson();
|
||||
json_array_append_new(modulesJ, terminalModuleJ);
|
||||
}
|
||||
json_object_set_new(rootJ, "modules", modulesJ);
|
||||
|
||||
// cables
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
--- ../Rack/src/engine/Engine.cpp 2022-01-15 14:44:46.395281005 +0000
|
||||
+++ Engine.cpp 2022-01-23 17:13:03.200930905 +0000
|
||||
--- ../Rack/src/engine/Engine.cpp 2022-02-05 22:30:09.253393116 +0000
|
||||
+++ Engine.cpp 2022-02-08 02:48:26.045085405 +0000
|
||||
@@ -1,3 +1,30 @@
|
||||
+/*
|
||||
+ * DISTRHO Cardinal Plugin
|
||||
|
@ -31,7 +31,11 @@
|
|||
#include <algorithm>
|
||||
#include <set>
|
||||
#include <thread>
|
||||
@@ -11,178 +38,25 @@
|
||||
@@ -8,181 +35,36 @@
|
||||
#include <pmmintrin.h>
|
||||
|
||||
#include <engine/Engine.hpp>
|
||||
+#include <engine/TerminalModule.hpp>
|
||||
#include <settings.hpp>
|
||||
#include <system.hpp>
|
||||
#include <random.hpp>
|
||||
|
@ -40,17 +44,15 @@
|
|||
#include <plugin.hpp>
|
||||
#include <mutex.hpp>
|
||||
+#include <helpers.hpp>
|
||||
+
|
||||
|
||||
+#ifdef NDEBUG
|
||||
+# undef DEBUG
|
||||
+#endif
|
||||
|
||||
+#include "DistrhoUtils.hpp"
|
||||
|
||||
namespace rack {
|
||||
namespace engine {
|
||||
|
||||
|
||||
-namespace rack {
|
||||
-namespace engine {
|
||||
-
|
||||
-
|
||||
-static void initMXCSR() {
|
||||
- // Set CPU to flush-to-zero (FTZ) and denormals-are-zero (DAZ) mode
|
||||
- // https://software.intel.com/en-us/node/682949
|
||||
|
@ -107,7 +109,8 @@
|
|||
- void setThreads(int threads) {
|
||||
- this->threads = threads;
|
||||
- }
|
||||
-
|
||||
+#include "DistrhoUtils.hpp"
|
||||
|
||||
- void wait() {
|
||||
- uint8_t s = step;
|
||||
- if (count.fetch_add(1, std::memory_order_acquire) + 1 >= threads) {
|
||||
|
@ -126,8 +129,11 @@
|
|||
- }
|
||||
- }
|
||||
-};
|
||||
-
|
||||
-
|
||||
|
||||
+// known terminal modules
|
||||
+extern rack::plugin::Model* modelHostAudio2;
|
||||
+extern rack::plugin::Model* modelHostAudio8;
|
||||
|
||||
-/** Barrier that spin-locks until yield() is called, and then all threads switch to a mutex.
|
||||
-yield() should be called if it is likely that all threads will block for a while and continuing to spin-lock is unnecessary.
|
||||
-Saves CPU power after yield is called.
|
||||
|
@ -164,7 +170,7 @@
|
|||
- }
|
||||
- return;
|
||||
- }
|
||||
-
|
||||
|
||||
- // Spin until the last thread begins waiting
|
||||
- while (!yielded.load(std::memory_order_relaxed)) {
|
||||
- if (step.load(std::memory_order_relaxed) != s)
|
||||
|
@ -206,17 +212,36 @@
|
|||
-
|
||||
- void run();
|
||||
-};
|
||||
-
|
||||
-
|
||||
+namespace rack {
|
||||
+namespace engine {
|
||||
|
||||
|
||||
struct Engine::Internal {
|
||||
std::vector<Module*> modules;
|
||||
+ std::vector<TerminalModule*> terminalModules;
|
||||
std::vector<Cable*> cables;
|
||||
std::set<ParamHandle*> paramHandles;
|
||||
- Module* masterModule = NULL;
|
||||
|
||||
// moduleId
|
||||
std::map<int64_t, Module*> modulesCache;
|
||||
@@ -217,22 +91,6 @@
|
||||
@@ -199,6 +81,7 @@
|
||||
double blockTime = 0.0;
|
||||
int blockFrames = 0;
|
||||
|
||||
+#ifndef HEADLESS
|
||||
// Meter
|
||||
int meterCount = 0;
|
||||
double meterTotal = 0.0;
|
||||
@@ -206,6 +89,7 @@
|
||||
double meterLastTime = -INFINITY;
|
||||
double meterLastAverage = 0.0;
|
||||
double meterLastMax = 0.0;
|
||||
+#endif
|
||||
|
||||
// Parameter smoothing
|
||||
Module* smoothModule = NULL;
|
||||
@@ -217,22 +101,6 @@
|
||||
Readers lock when using the engine's state.
|
||||
*/
|
||||
SharedMutex mutex;
|
||||
|
@ -239,7 +264,7 @@
|
|||
};
|
||||
|
||||
|
||||
@@ -260,71 +118,6 @@
|
||||
@@ -260,76 +128,11 @@
|
||||
}
|
||||
|
||||
|
||||
|
@ -311,22 +336,82 @@
|
|||
static void Cable_step(Cable* that) {
|
||||
Output* output = &that->outputModule->outputs[that->outputId];
|
||||
Input* input = &that->inputModule->inputs[that->inputId];
|
||||
@@ -373,12 +166,12 @@
|
||||
// Match number of polyphonic channels to output port
|
||||
- int channels = output->channels;
|
||||
+ const int channels = output->channels;
|
||||
// Copy all voltages from output to input
|
||||
for (int c = 0; c < channels; c++) {
|
||||
float v = output->voltages[c];
|
||||
@@ -346,6 +149,53 @@
|
||||
}
|
||||
|
||||
|
||||
+static void Port_step(Port* that, float deltaTime) {
|
||||
+ // Set plug lights
|
||||
+ if (that->channels == 0) {
|
||||
+ that->plugLights[0].setBrightness(0.f);
|
||||
+ that->plugLights[1].setBrightness(0.f);
|
||||
+ that->plugLights[2].setBrightness(0.f);
|
||||
+ }
|
||||
+ else if (that->channels == 1) {
|
||||
+ float v = that->getVoltage() / 10.f;
|
||||
+ that->plugLights[0].setSmoothBrightness(-v, deltaTime);
|
||||
+ that->plugLights[1].setSmoothBrightness(v, deltaTime);
|
||||
+ that->plugLights[2].setBrightness(0.f);
|
||||
+ }
|
||||
+ else {
|
||||
+ float v = that->getVoltageRMS() / 10.f;
|
||||
+ that->plugLights[0].setBrightness(0.f);
|
||||
+ that->plugLights[1].setBrightness(0.f);
|
||||
+ that->plugLights[2].setSmoothBrightness(v, deltaTime);
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+
|
||||
+static void TerminalModule__doProcess(TerminalModule* terminalModule, const Module::ProcessArgs& args, bool input) {
|
||||
+ // Step module
|
||||
+ if (input) {
|
||||
+ terminalModule->processTerminalInput(args);
|
||||
+ for (Output& output : terminalModule->outputs) {
|
||||
+ for (Cable* cable : output.cables)
|
||||
+ Cable_step(cable);
|
||||
+ }
|
||||
+ } else {
|
||||
+ terminalModule->processTerminalOutput(args);
|
||||
+ }
|
||||
+
|
||||
+ // Iterate ports to step plug lights
|
||||
+ if (args.frame % 7 /* PORT_DIVIDER */ == 0) {
|
||||
+ float portTime = args.sampleTime * 7 /* PORT_DIVIDER */;
|
||||
+ for (Input& input : terminalModule->inputs) {
|
||||
+ Port_step(&input, portTime);
|
||||
+ }
|
||||
+ for (Output& output : terminalModule->outputs) {
|
||||
+ Port_step(&output, portTime);
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+
|
||||
/** Steps a single frame
|
||||
*/
|
||||
static void Engine_stepFrame(Engine* that) {
|
||||
@@ -372,13 +222,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Step cables
|
||||
- // Step cables
|
||||
- for (Cable* cable : that->internal->cables) {
|
||||
+ for (Cable* cable : internal->cables) {
|
||||
Cable_step(cable);
|
||||
}
|
||||
|
||||
- Cable_step(cable);
|
||||
- }
|
||||
-
|
||||
// Flip messages for each module
|
||||
- for (Module* module : that->internal->modules) {
|
||||
+ for (Module* module : internal->modules) {
|
||||
if (module->leftExpander.messageFlipRequested) {
|
||||
std::swap(module->leftExpander.producerMessage, module->leftExpander.consumerMessage);
|
||||
module->leftExpander.messageFlipRequested = false;
|
||||
@@ -389,13 +182,18 @@
|
||||
@@ -389,13 +234,32 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -341,9 +426,23 @@
|
|||
+ processArgs.sampleTime = internal->sampleTime;
|
||||
+ processArgs.frame = internal->frame;
|
||||
+
|
||||
+ // Step each module
|
||||
+ // Process terminal inputs first
|
||||
+ for (TerminalModule* terminalModule : internal->terminalModules) {
|
||||
+ TerminalModule__doProcess(terminalModule, processArgs, true);
|
||||
+ }
|
||||
+
|
||||
+ // Step each module and cables
|
||||
+ for (Module* module : internal->modules) {
|
||||
+ module->doProcess(processArgs);
|
||||
+ for (Output& output : module->outputs) {
|
||||
+ for (Cable* cable : output.cables)
|
||||
+ Cable_step(cable);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ // Process terminal outputs last
|
||||
+ for (TerminalModule* terminalModule : internal->terminalModules) {
|
||||
+ TerminalModule__doProcess(terminalModule, processArgs, false);
|
||||
+ }
|
||||
|
||||
- internal->frame++;
|
||||
|
@ -351,7 +450,30 @@
|
|||
}
|
||||
|
||||
|
||||
@@ -460,37 +258,22 @@
|
||||
@@ -425,6 +289,14 @@
|
||||
disconnectedPorts.insert(&output);
|
||||
}
|
||||
}
|
||||
+ for (TerminalModule* terminalModule : that->internal->terminalModules) {
|
||||
+ for (Input& input : terminalModule->inputs) {
|
||||
+ disconnectedPorts.insert(&input);
|
||||
+ }
|
||||
+ for (Output& output : terminalModule->outputs) {
|
||||
+ disconnectedPorts.insert(&output);
|
||||
+ }
|
||||
+ }
|
||||
for (Cable* cable : that->internal->cables) {
|
||||
// Connect input
|
||||
Input& input = cable->inputModule->inputs[cable->inputId];
|
||||
@@ -442,6 +314,7 @@
|
||||
// Disconnect ports that have no cable
|
||||
for (Port* port : disconnectedPorts) {
|
||||
Port_setDisconnected(port);
|
||||
+ DISTRHO_SAFE_ASSERT(port->cables.empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,37 +333,23 @@
|
||||
|
||||
Engine::Engine() {
|
||||
internal = new Internal;
|
||||
|
@ -388,6 +510,7 @@
|
|||
- assert(internal->paramHandlesCache.empty());
|
||||
+ DISTRHO_SAFE_ASSERT(internal->cables.empty());
|
||||
+ DISTRHO_SAFE_ASSERT(internal->modules.empty());
|
||||
+ DISTRHO_SAFE_ASSERT(internal->terminalModules.empty());
|
||||
+ DISTRHO_SAFE_ASSERT(internal->paramHandles.empty());
|
||||
+
|
||||
+ DISTRHO_SAFE_ASSERT(internal->modulesCache.empty());
|
||||
|
@ -396,9 +519,23 @@
|
|||
|
||||
delete internal;
|
||||
}
|
||||
@@ -526,11 +309,8 @@
|
||||
@@ -519,18 +378,22 @@
|
||||
removeModule_NoLock(module);
|
||||
delete module;
|
||||
}
|
||||
+ std::vector<TerminalModule*> terminalModules = internal->terminalModules;
|
||||
+ for (TerminalModule* terminalModule : terminalModules) {
|
||||
+ removeModule_NoLock(terminalModule);
|
||||
+ delete terminalModule;
|
||||
+ }
|
||||
}
|
||||
|
||||
|
||||
void Engine::stepBlock(int frames) {
|
||||
+#ifndef HEADLESS
|
||||
// Start timer before locking
|
||||
double startTime = system::getTime();
|
||||
+#endif
|
||||
|
||||
- std::lock_guard<std::mutex> stepLock(internal->blockMutex);
|
||||
SharedLock<SharedMutex> lock(internal->mutex);
|
||||
|
@ -408,7 +545,7 @@
|
|||
random::init();
|
||||
|
||||
internal->blockFrame = internal->frame;
|
||||
@@ -543,16 +323,11 @@
|
||||
@@ -543,18 +406,14 @@
|
||||
Engine_updateExpander_NoLock(this, module, true);
|
||||
}
|
||||
|
||||
|
@ -424,14 +561,18 @@
|
|||
-
|
||||
internal->block++;
|
||||
|
||||
+#ifndef HEADLESS
|
||||
// Stop timer
|
||||
@@ -572,47 +347,19 @@
|
||||
double endTime = system::getTime();
|
||||
double meter = (endTime - startTime) / (frames * internal->sampleTime);
|
||||
@@ -572,47 +431,20 @@
|
||||
internal->meterTotal = 0.0;
|
||||
internal->meterMax = 0.0;
|
||||
}
|
||||
-
|
||||
- // Reset MXCSR back to original value
|
||||
- _mm_setcsr(csr);
|
||||
+#endif
|
||||
}
|
||||
|
||||
|
||||
|
@ -474,7 +615,14 @@
|
|||
}
|
||||
|
||||
|
||||
@@ -639,16 +386,6 @@
|
||||
@@ -635,20 +467,13 @@
|
||||
for (Module* module : internal->modules) {
|
||||
module->onSampleRateChange(e);
|
||||
}
|
||||
+ for (TerminalModule* terminalModule : internal->terminalModules) {
|
||||
+ terminalModule->onSampleRateChange(e);
|
||||
+ }
|
||||
}
|
||||
|
||||
|
||||
void Engine::setSuggestedSampleRate(float suggestedSampleRate) {
|
||||
|
@ -491,7 +639,7 @@
|
|||
}
|
||||
|
||||
|
||||
@@ -658,7 +395,6 @@
|
||||
@@ -658,7 +483,6 @@
|
||||
|
||||
|
||||
void Engine::yieldWorkers() {
|
||||
|
@ -499,29 +647,106 @@
|
|||
}
|
||||
|
||||
|
||||
@@ -738,10 +474,10 @@
|
||||
@@ -698,17 +522,25 @@
|
||||
|
||||
|
||||
double Engine::getMeterAverage() {
|
||||
+#ifndef HEADLESS
|
||||
return internal->meterLastAverage;
|
||||
+#else
|
||||
+ return 0.0;
|
||||
+#endif
|
||||
}
|
||||
|
||||
|
||||
double Engine::getMeterMax() {
|
||||
+#ifndef HEADLESS
|
||||
return internal->meterLastMax;
|
||||
+#else
|
||||
+ return 0.0;
|
||||
+#endif
|
||||
}
|
||||
|
||||
|
||||
size_t Engine::getNumModules() {
|
||||
- return internal->modules.size();
|
||||
+ return internal->modules.size() + internal->terminalModules.size();
|
||||
}
|
||||
|
||||
|
||||
@@ -718,8 +550,12 @@
|
||||
for (Module* m : internal->modules) {
|
||||
if (i >= len)
|
||||
break;
|
||||
- moduleIds[i] = m->id;
|
||||
- i++;
|
||||
+ moduleIds[i++] = m->id;
|
||||
+ }
|
||||
+ for (TerminalModule* m : internal->terminalModules) {
|
||||
+ if (i >= len)
|
||||
+ break;
|
||||
+ moduleIds[i++] = m->id;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
@@ -728,27 +564,43 @@
|
||||
std::vector<int64_t> Engine::getModuleIds() {
|
||||
SharedLock<SharedMutex> lock(internal->mutex);
|
||||
std::vector<int64_t> moduleIds;
|
||||
- moduleIds.reserve(internal->modules.size());
|
||||
+ moduleIds.reserve(getNumModules());
|
||||
for (Module* m : internal->modules) {
|
||||
moduleIds.push_back(m->id);
|
||||
}
|
||||
+ for (TerminalModule* tm : internal->terminalModules) {
|
||||
+ moduleIds.push_back(tm->id);
|
||||
+ }
|
||||
return moduleIds;
|
||||
}
|
||||
|
||||
|
||||
+static TerminalModule* asTerminalModule(Module* const module) {
|
||||
+ const plugin::Model* const model = module->model;
|
||||
+ if (model == modelHostAudio2 || model == modelHostAudio8)
|
||||
+ return static_cast<TerminalModule*>(module);
|
||||
+ return nullptr;
|
||||
+}
|
||||
+
|
||||
+
|
||||
void Engine::addModule(Module* module) {
|
||||
std::lock_guard<SharedMutex> lock(internal->mutex);
|
||||
- assert(module);
|
||||
+ DISTRHO_SAFE_ASSERT_RETURN(module,);
|
||||
+ DISTRHO_SAFE_ASSERT_RETURN(module != nullptr,);
|
||||
// Check that the module is not already added
|
||||
auto it = std::find(internal->modules.begin(), internal->modules.end(), module);
|
||||
- assert(it == internal->modules.end());
|
||||
+ DISTRHO_SAFE_ASSERT_RETURN(it == internal->modules.end(),);
|
||||
+ auto tit = std::find(internal->terminalModules.begin(), internal->terminalModules.end(), module);
|
||||
+ DISTRHO_SAFE_ASSERT_RETURN(tit == internal->terminalModules.end(),);
|
||||
// Set ID if unset or collides with an existing ID
|
||||
while (module->id < 0 || internal->modulesCache.find(module->id) != internal->modulesCache.end()) {
|
||||
// Randomly generate ID
|
||||
@@ -773,10 +509,14 @@
|
||||
module->id = random::u64() % (1ull << 53);
|
||||
}
|
||||
// Add module
|
||||
- internal->modules.push_back(module);
|
||||
+ if (TerminalModule* const terminalModule = asTerminalModule(module))
|
||||
+ internal->terminalModules.push_back(terminalModule);
|
||||
+ else
|
||||
+ internal->modules.push_back(module);
|
||||
internal->modulesCache[module->id] = module;
|
||||
// Dispatch AddEvent
|
||||
Module::AddEvent eAdd;
|
||||
@@ -772,11 +624,11 @@
|
||||
}
|
||||
|
||||
|
||||
void Engine::removeModule_NoLock(Module* module) {
|
||||
-void Engine::removeModule_NoLock(Module* module) {
|
||||
- assert(module);
|
||||
+ DISTRHO_SAFE_ASSERT_RETURN(module,);
|
||||
// Check that the module actually exists
|
||||
auto it = std::find(internal->modules.begin(), internal->modules.end(), module);
|
||||
- // Check that the module actually exists
|
||||
- auto it = std::find(internal->modules.begin(), internal->modules.end(), module);
|
||||
- assert(it != internal->modules.end());
|
||||
+ DISTRHO_SAFE_ASSERT_RETURN(it != internal->modules.end(),);
|
||||
+static void removeModule_NoLock_common(Engine::Internal* internal, Module* module) {
|
||||
+ // Remove from widgets cache
|
||||
+ CardinalPluginModelHelper* const helper = dynamic_cast<CardinalPluginModelHelper*>(module->model);
|
||||
+ DISTRHO_SAFE_ASSERT_RETURN(helper != nullptr,);
|
||||
|
@ -529,7 +754,7 @@
|
|||
// Dispatch RemoveEvent
|
||||
Module::RemoveEvent eRemove;
|
||||
module->onRemove(eRemove);
|
||||
@@ -785,18 +525,14 @@
|
||||
@@ -785,18 +637,14 @@
|
||||
if (paramHandle->moduleId == module->id)
|
||||
paramHandle->module = NULL;
|
||||
}
|
||||
|
@ -550,7 +775,52 @@
|
|||
}
|
||||
// Update expanders of other modules
|
||||
for (Module* m : internal->modules) {
|
||||
@@ -844,7 +580,7 @@
|
||||
@@ -809,14 +657,31 @@
|
||||
m->rightExpander.module = NULL;
|
||||
}
|
||||
}
|
||||
- // Remove module
|
||||
- internal->modulesCache.erase(module->id);
|
||||
- internal->modules.erase(it);
|
||||
// Reset expanders
|
||||
module->leftExpander.moduleId = -1;
|
||||
module->leftExpander.module = NULL;
|
||||
module->rightExpander.moduleId = -1;
|
||||
module->rightExpander.module = NULL;
|
||||
+ // Remove module
|
||||
+ internal->modulesCache.erase(module->id);
|
||||
+}
|
||||
+
|
||||
+
|
||||
+void Engine::removeModule_NoLock(Module* module) {
|
||||
+ DISTRHO_SAFE_ASSERT_RETURN(module,);
|
||||
+ // Check that the module actually exists
|
||||
+ if (TerminalModule* const terminalModule = asTerminalModule(module)) {
|
||||
+ auto tit = std::find(internal->terminalModules.begin(), internal->terminalModules.end(), terminalModule);
|
||||
+ DISTRHO_SAFE_ASSERT_RETURN(tit != internal->terminalModules.end(),);
|
||||
+ removeModule_NoLock_common(internal, module);
|
||||
+ internal->terminalModules.erase(tit);
|
||||
+ }
|
||||
+ else {
|
||||
+ auto it = std::find(internal->modules.begin(), internal->modules.end(), module);
|
||||
+ DISTRHO_SAFE_ASSERT_RETURN(it != internal->modules.end(),);
|
||||
+ removeModule_NoLock_common(internal, module);
|
||||
+ internal->modules.erase(it);
|
||||
+ }
|
||||
}
|
||||
|
||||
|
||||
@@ -824,7 +689,8 @@
|
||||
SharedLock<SharedMutex> lock(internal->mutex);
|
||||
// TODO Performance could be improved by searching modulesCache, but more testing would be needed to make sure it's always valid.
|
||||
auto it = std::find(internal->modules.begin(), internal->modules.end(), module);
|
||||
- return it != internal->modules.end();
|
||||
+ auto tit = std::find(internal->terminalModules.begin(), internal->terminalModules.end(), module);
|
||||
+ return it != internal->modules.end() && tit != internal->terminalModules.end();
|
||||
}
|
||||
|
||||
|
||||
@@ -844,7 +710,7 @@
|
||||
|
||||
void Engine::resetModule(Module* module) {
|
||||
std::lock_guard<SharedMutex> lock(internal->mutex);
|
||||
|
@ -559,7 +829,7 @@
|
|||
|
||||
Module::ResetEvent eReset;
|
||||
module->onReset(eReset);
|
||||
@@ -853,7 +589,7 @@
|
||||
@@ -853,7 +719,7 @@
|
||||
|
||||
void Engine::randomizeModule(Module* module) {
|
||||
std::lock_guard<SharedMutex> lock(internal->mutex);
|
||||
|
@ -568,7 +838,7 @@
|
|||
|
||||
Module::RandomizeEvent eRandomize;
|
||||
module->onRandomize(eRandomize);
|
||||
@@ -861,7 +597,7 @@
|
||||
@@ -861,7 +727,7 @@
|
||||
|
||||
|
||||
void Engine::bypassModule(Module* module, bool bypassed) {
|
||||
|
@ -577,7 +847,18 @@
|
|||
if (module->isBypassed() == bypassed)
|
||||
return;
|
||||
|
||||
@@ -946,16 +682,16 @@
|
||||
@@ -912,6 +778,10 @@
|
||||
Module::SaveEvent e;
|
||||
module->onSave(e);
|
||||
}
|
||||
+ for (TerminalModule* terminalModule : internal->terminalModules) {
|
||||
+ Module::SaveEvent e;
|
||||
+ terminalModule->onSave(e);
|
||||
+ }
|
||||
}
|
||||
|
||||
|
||||
@@ -946,16 +816,16 @@
|
||||
|
||||
void Engine::addCable(Cable* cable) {
|
||||
std::lock_guard<SharedMutex> lock(internal->mutex);
|
||||
|
@ -599,7 +880,16 @@
|
|||
// Get connected status of output, to decide whether we need to call a PortChangeEvent.
|
||||
// It's best to not trust `cable->outputModule->outputs[cable->outputId]->isConnected()`
|
||||
if (cable2->outputModule == cable->outputModule && cable2->outputId == cable->outputId)
|
||||
@@ -996,10 +732,10 @@
|
||||
@@ -969,6 +839,8 @@
|
||||
// Add the cable
|
||||
internal->cables.push_back(cable);
|
||||
internal->cablesCache[cable->id] = cable;
|
||||
+ // Add the cable's zero-latency shortcut
|
||||
+ cable->outputModule->outputs[cable->outputId].cables.push_back(cable);
|
||||
Engine_updateConnected(this);
|
||||
// Dispatch input port event
|
||||
{
|
||||
@@ -996,10 +868,12 @@
|
||||
|
||||
|
||||
void Engine::removeCable_NoLock(Cable* cable) {
|
||||
|
@ -609,10 +899,12 @@
|
|||
auto it = std::find(internal->cables.begin(), internal->cables.end(), cable);
|
||||
- assert(it != internal->cables.end());
|
||||
+ DISTRHO_SAFE_ASSERT_RETURN(it != internal->cables.end(),);
|
||||
+ // Remove the cable's zero-latency shortcut
|
||||
+ cable->outputModule->outputs[cable->outputId].cables.remove(cable);
|
||||
// Remove the cable
|
||||
internal->cablesCache.erase(cable->id);
|
||||
internal->cables.erase(it);
|
||||
@@ -1085,11 +821,11 @@
|
||||
@@ -1085,11 +959,11 @@
|
||||
std::lock_guard<SharedMutex> lock(internal->mutex);
|
||||
// New ParamHandles must be blank.
|
||||
// This means we don't have to refresh the cache.
|
||||
|
@ -626,7 +918,7 @@
|
|||
|
||||
// Add it
|
||||
internal->paramHandles.insert(paramHandle);
|
||||
@@ -1106,7 +842,7 @@
|
||||
@@ -1106,7 +980,7 @@
|
||||
void Engine::removeParamHandle_NoLock(ParamHandle* paramHandle) {
|
||||
// Check that the ParamHandle is already added
|
||||
auto it = internal->paramHandles.find(paramHandle);
|
||||
|
@ -635,7 +927,7 @@
|
|||
|
||||
// Remove it
|
||||
paramHandle->module = NULL;
|
||||
@@ -1143,7 +879,7 @@
|
||||
@@ -1143,7 +1017,7 @@
|
||||
void Engine::updateParamHandle_NoLock(ParamHandle* paramHandle, int64_t moduleId, int paramId, bool overwrite) {
|
||||
// Check that it exists
|
||||
auto it = internal->paramHandles.find(paramHandle);
|
||||
|
@ -644,7 +936,18 @@
|
|||
|
||||
// Set IDs
|
||||
paramHandle->moduleId = moduleId;
|
||||
@@ -1197,11 +933,6 @@
|
||||
@@ -1187,6 +1061,10 @@
|
||||
json_t* moduleJ = module->toJson();
|
||||
json_array_append_new(modulesJ, moduleJ);
|
||||
}
|
||||
+ for (TerminalModule* terminalModule : internal->terminalModules) {
|
||||
+ json_t* terminalModuleJ = terminalModule->toJson();
|
||||
+ json_array_append_new(modulesJ, terminalModuleJ);
|
||||
+ }
|
||||
json_object_set_new(rootJ, "modules", modulesJ);
|
||||
|
||||
// cables
|
||||
@@ -1197,11 +1075,6 @@
|
||||
}
|
||||
json_object_set_new(rootJ, "cables", cablesJ);
|
||||
|
||||
|
@ -656,7 +959,7 @@
|
|||
return rootJ;
|
||||
}
|
||||
|
||||
@@ -1225,14 +956,20 @@
|
||||
@@ -1225,14 +1098,20 @@
|
||||
}
|
||||
catch (Exception& e) {
|
||||
WARN("Cannot load model: %s", e.what());
|
||||
|
@ -681,7 +984,7 @@
|
|||
|
||||
try {
|
||||
// This doesn't need a lock because the Module is not added to the Engine yet.
|
||||
@@ -1248,7 +985,8 @@
|
||||
@@ -1248,7 +1127,8 @@
|
||||
}
|
||||
catch (Exception& e) {
|
||||
WARN("Cannot load module: %s", e.what());
|
||||
|
@ -691,7 +994,7 @@
|
|||
delete module;
|
||||
continue;
|
||||
}
|
||||
@@ -1285,67 +1023,10 @@
|
||||
@@ -1285,67 +1165,10 @@
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
--- ../Rack/src/app/MenuBar.cpp 2022-01-15 14:44:46.391280963 +0000
|
||||
+++ MenuBar.cpp 2022-01-24 11:25:15.507061204 +0000
|
||||
--- ../Rack/src/app/MenuBar.cpp 2022-02-05 22:30:09.233392896 +0000
|
||||
+++ MenuBar.cpp 2022-02-05 18:08:00.272028714 +0000
|
||||
@@ -1,8 +1,33 @@
|
||||
+/*
|
||||
+ * DISTRHO Cardinal Plugin
|
||||
|
@ -48,7 +48,7 @@
|
|||
|
||||
namespace rack {
|
||||
namespace app {
|
||||
@@ -48,79 +78,75 @@
|
||||
@@ -48,79 +78,79 @@
|
||||
};
|
||||
|
||||
|
||||
|
@ -103,25 +103,34 @@
|
|||
-
|
||||
menu->addChild(createMenuItem("Save", RACK_MOD_CTRL_NAME "+S", []() {
|
||||
- APP->patch->saveDialog();
|
||||
- }));
|
||||
+ // NOTE: will do nothing if path is empty, intentionally
|
||||
+ patchUtils::saveDialog(APP->patch->path);
|
||||
+ }, APP->patch->path.empty()));
|
||||
+
|
||||
|
||||
- menu->addChild(createMenuItem("Save as", RACK_MOD_CTRL_NAME "+Shift+S", []() {
|
||||
- APP->patch->saveAsDialog();
|
||||
+ menu->addChild(createMenuItem("Save as / Export...", RACK_MOD_CTRL_NAME "+Shift+S", []() {
|
||||
+ patchUtils::saveAsDialog();
|
||||
}));
|
||||
|
||||
- menu->addChild(createMenuItem("Save as", RACK_MOD_CTRL_NAME "+Shift+S", []() {
|
||||
- APP->patch->saveAsDialog();
|
||||
- }));
|
||||
- menu->addChild(createMenuItem("Save a copy", "", []() {
|
||||
- APP->patch->saveAsDialog(false);
|
||||
+ menu->addChild(createMenuItem("Export uncompressed json...", "", []() {
|
||||
+ patchUtils::saveAsDialogUncompressed();
|
||||
}));
|
||||
|
||||
- menu->addChild(createMenuItem("Revert", RACK_MOD_CTRL_NAME "+" RACK_MOD_SHIFT_NAME "+O", []() {
|
||||
- APP->patch->revertDialog();
|
||||
- }, APP->patch->path == ""));
|
||||
+#ifdef HAVE_LIBLO
|
||||
+ if (patchUtils::isRemoteConnected()) {
|
||||
+ menu->addChild(createMenuItem("Deploy to MOD", "F7", []() {
|
||||
+ patchUtils::deployToRemote();
|
||||
+ }));
|
||||
|
||||
- menu->addChild(createMenuItem("Save a copy", "", []() {
|
||||
- APP->patch->saveAsDialog(false);
|
||||
- menu->addChild(createMenuItem("Overwrite template", "", []() {
|
||||
- APP->patch->saveTemplateDialog();
|
||||
- }));
|
||||
+ const bool autoDeploy = patchUtils::isRemoteAutoDeployed();
|
||||
+ menu->addChild(createCheckMenuItem("Auto deploy to MOD", "",
|
||||
|
@ -129,19 +138,13 @@
|
|||
+ [=]() {patchUtils::setRemoteAutoDeploy(!autoDeploy);}
|
||||
+ ));
|
||||
+ } else {
|
||||
+ menu->addChild(createMenuItem("Connect to MOD", "", [this]() {
|
||||
+ menu->addChild(createMenuItem("Connect to MOD", "", []() {
|
||||
+ patchUtils::connectToRemote();
|
||||
+ }));
|
||||
+ }
|
||||
+#endif
|
||||
|
||||
menu->addChild(createMenuItem("Revert", RACK_MOD_CTRL_NAME "+" RACK_MOD_SHIFT_NAME "+O", []() {
|
||||
- APP->patch->revertDialog();
|
||||
- }, APP->patch->path == ""));
|
||||
-
|
||||
- menu->addChild(createMenuItem("Overwrite template", "", []() {
|
||||
- APP->patch->saveTemplateDialog();
|
||||
- }));
|
||||
+
|
||||
+ menu->addChild(createMenuItem("Revert", RACK_MOD_CTRL_NAME "+" RACK_MOD_SHIFT_NAME "+O", []() {
|
||||
+ patchUtils::revertDialog();
|
||||
+ }, APP->patch->path.empty()));
|
||||
|
||||
|
@ -167,7 +170,7 @@
|
|||
}
|
||||
};
|
||||
|
||||
@@ -166,7 +192,7 @@
|
||||
@@ -166,7 +196,7 @@
|
||||
|
||||
menu->addChild(new ui::MenuSeparator);
|
||||
|
||||
|
@ -176,7 +179,7 @@
|
|||
}
|
||||
};
|
||||
|
||||
@@ -256,7 +282,7 @@
|
||||
@@ -256,7 +286,7 @@
|
||||
return settings::cableTension;
|
||||
}
|
||||
float getDefaultValue() override {
|
||||
|
@ -185,7 +188,7 @@
|
|||
}
|
||||
float getDisplayValue() override {
|
||||
return getValue() * 100;
|
||||
@@ -421,28 +447,9 @@
|
||||
@@ -421,28 +451,9 @@
|
||||
haloBrightnessSlider->box.size.x = 250.0;
|
||||
menu->addChild(haloBrightnessSlider);
|
||||
|
||||
|
@ -215,7 +218,7 @@
|
|||
|
||||
static const std::vector<std::string> knobModeLabels = {
|
||||
"Linear",
|
||||
@@ -467,6 +474,21 @@
|
||||
@@ -467,6 +478,21 @@
|
||||
menu->addChild(knobScrollSensitivitySlider);
|
||||
|
||||
menu->addChild(createBoolPtrMenuItem("Lock module positions", "", &settings::lockModules));
|
||||
|
@ -237,7 +240,7 @@
|
|||
}
|
||||
};
|
||||
|
||||
@@ -476,47 +498,6 @@
|
||||
@@ -476,47 +502,6 @@
|
||||
////////////////////
|
||||
|
||||
|
||||
|
@ -285,7 +288,7 @@
|
|||
struct EngineButton : MenuButton {
|
||||
void onAction(const ActionEvent& e) override {
|
||||
ui::Menu* menu = createMenu();
|
||||
@@ -529,269 +510,6 @@
|
||||
@@ -529,269 +514,6 @@
|
||||
menu->addChild(createMenuItem("Performance meters", cpuMeterText, [=]() {
|
||||
settings::cpuMeter ^= true;
|
||||
}));
|
||||
|
@ -555,7 +558,7 @@
|
|||
}
|
||||
};
|
||||
|
||||
@@ -802,63 +520,24 @@
|
||||
@@ -802,63 +524,23 @@
|
||||
|
||||
|
||||
struct HelpButton : MenuButton {
|
||||
|
@ -614,17 +617,17 @@
|
|||
|
||||
- menu->addChild(createMenuLabel(APP_NAME + " " + APP_EDITION_NAME + " " + APP_VERSION));
|
||||
- }
|
||||
+ menu->addChild(createMenuLabel(APP_EDITION + " " + APP_EDITION_NAME));
|
||||
|
||||
-
|
||||
- void step() override {
|
||||
- notification->box.pos = math::Vec(0, 0);
|
||||
- notification->visible = library::isAppUpdateAvailable();
|
||||
- MenuButton::step();
|
||||
+ menu->addChild(createMenuLabel("Cardinal " + APP_EDITION + " " + CARDINAL_VERSION));
|
||||
+ menu->addChild(createMenuLabel("Rack " + APP_VERSION + " Compatible"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -908,7 +587,9 @@
|
||||
@@ -908,7 +590,9 @@
|
||||
struct MenuBar : widget::OpaqueWidget {
|
||||
MeterLabel* meterLabel;
|
||||
|
||||
|
@ -635,7 +638,7 @@
|
|||
const float margin = 5;
|
||||
box.size.y = BND_WIDGET_HEIGHT + 2 * margin;
|
||||
|
||||
@@ -917,7 +598,7 @@
|
||||
@@ -917,7 +601,7 @@
|
||||
layout->spacing = math::Vec(0, 0);
|
||||
addChild(layout);
|
||||
|
||||
|
@ -644,7 +647,7 @@
|
|||
fileButton->text = "File";
|
||||
layout->addChild(fileButton);
|
||||
|
||||
@@ -933,10 +614,6 @@
|
||||
@@ -933,10 +617,6 @@
|
||||
engineButton->text = "Engine";
|
||||
layout->addChild(engineButton);
|
||||
|
||||
|
@ -655,7 +658,7 @@
|
|||
HelpButton* helpButton = new HelpButton;
|
||||
helpButton->text = "Help";
|
||||
layout->addChild(helpButton);
|
||||
@@ -971,7 +648,11 @@
|
||||
@@ -971,7 +651,11 @@
|
||||
|
||||
|
||||
widget::Widget* createMenuBar() {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
--- ../Rack/src/app/Scene.cpp 2021-12-14 21:35:44.414568198 +0000
|
||||
+++ Scene.cpp 2022-01-26 18:47:48.006168325 +0000
|
||||
+++ Scene.cpp 2022-02-06 14:11:59.259830276 +0000
|
||||
@@ -1,3 +1,30 @@
|
||||
+/*
|
||||
+ * DISTRHO Cardinal Plugin
|
||||
|
@ -108,13 +108,13 @@
|
|||
+
|
||||
+ void onEnter(const EnterEvent& e) override {
|
||||
+ glfwSetCursor(nullptr, (GLFWcursor*)0x1);
|
||||
+ }
|
||||
+
|
||||
+ void onLeave(const LeaveEvent& e) override {
|
||||
+ glfwSetCursor(nullptr, nullptr);
|
||||
}
|
||||
|
||||
- void onDragStart(const DragStartEvent& e) override {
|
||||
+ void onLeave(const LeaveEvent& e) override {
|
||||
+ glfwSetCursor(nullptr, nullptr);
|
||||
+ }
|
||||
+
|
||||
+ void onDragStart(const DragStartEvent&) override {
|
||||
size = APP->window->getSize();
|
||||
}
|
||||
|
@ -260,7 +260,23 @@
|
|||
e.consume(this);
|
||||
}
|
||||
if (e.keyName == "z" && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) {
|
||||
@@ -232,10 +326,8 @@
|
||||
@@ -220,10 +314,14 @@
|
||||
APP->scene->rackScroll->setZoom(std::pow(2.f, zoom));
|
||||
e.consume(this);
|
||||
}
|
||||
- if ((e.keyName == "0") && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) {
|
||||
+ if ((e.keyName == "0" || e.keyName == "1") && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) {
|
||||
APP->scene->rackScroll->setZoom(1.f);
|
||||
e.consume(this);
|
||||
}
|
||||
+ if (e.keyName == "2" && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) {
|
||||
+ APP->scene->rackScroll->setZoom(2.f);
|
||||
+ e.consume(this);
|
||||
+ }
|
||||
if (e.key == GLFW_KEY_F1 && (e.mods & RACK_MOD_MASK) == 0) {
|
||||
system::openBrowser("https://vcvrack.com/manual/");
|
||||
e.consume(this);
|
||||
@@ -232,10 +330,8 @@
|
||||
settings::cpuMeter ^= true;
|
||||
e.consume(this);
|
||||
}
|
||||
|
@ -273,7 +289,7 @@
|
|||
e.consume(this);
|
||||
}
|
||||
|
||||
@@ -326,13 +418,6 @@
|
||||
@@ -326,13 +422,6 @@
|
||||
|
||||
// Key commands that can be overridden by children
|
||||
if (e.action == GLFW_PRESS || e.action == GLFW_REPEAT) {
|
||||
|
@ -287,7 +303,7 @@
|
|||
if (e.keyName == "v" && (e.mods & RACK_MOD_MASK) == RACK_MOD_CTRL) {
|
||||
rack->pasteClipboardAction();
|
||||
e.consume(this);
|
||||
@@ -351,7 +436,7 @@
|
||||
@@ -351,7 +440,7 @@
|
||||
std::string extension = system::getExtension(path);
|
||||
|
||||
if (extension == ".vcv") {
|
||||
|
@ -296,7 +312,7 @@
|
|||
e.consume(this);
|
||||
return;
|
||||
}
|
||||
@@ -368,3 +453,73 @@
|
||||
@@ -368,3 +457,77 @@
|
||||
|
||||
} // namespace app
|
||||
} // namespace rack
|
||||
|
@ -306,6 +322,7 @@
|
|||
+
|
||||
+
|
||||
+bool connectToRemote() {
|
||||
+#ifdef HAVE_LIBLO
|
||||
+ rack::app::Scene::Internal* const internal = APP->scene->internal;
|
||||
+
|
||||
+ if (internal->oscServer == nullptr) {
|
||||
|
@ -321,6 +338,9 @@
|
|||
+ lo_address_free(addr);
|
||||
+
|
||||
+ return true;
|
||||
+#else
|
||||
+ return false;
|
||||
+#endif
|
||||
+}
|
||||
+
|
||||
+
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
--- ../Rack/src/common.cpp 2021-11-23 19:57:23.719015894 +0000
|
||||
+++ common.cpp 2022-01-23 17:13:08.824652617 +0000
|
||||
+++ common.cpp 2022-01-31 13:24:14.558807713 +0000
|
||||
@@ -1,6 +1,38 @@
|
||||
+/*
|
||||
+ * DISTRHO Cardinal Plugin
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
--- ../Rack/src/context.cpp 2022-01-15 14:44:46.391280963 +0000
|
||||
--- ../Rack/src/context.cpp 2022-02-05 22:30:09.253393116 +0000
|
||||
+++ context.cpp 2022-01-23 17:13:11.652514338 +0000
|
||||
@@ -1,3 +1,30 @@
|
||||
+/*
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
--- ../Rack/src/plugin.cpp 2022-01-15 14:44:46.395281005 +0000
|
||||
+++ plugin.cpp 2022-01-24 20:38:11.436099651 +0000
|
||||
--- ../Rack/src/plugin.cpp 2022-02-05 22:30:09.265393248 +0000
|
||||
+++ plugin.cpp 2022-01-30 00:24:49.375329910 +0000
|
||||
@@ -1,308 +1,40 @@
|
||||
-#include <thread>
|
||||
-#include <map>
|
||||
|
@ -337,7 +337,7 @@
|
|||
/** Given slug => fallback slug.
|
||||
Correctly handles bidirectional fallbacks.
|
||||
To request fallback slugs to be added to this list, open a GitHub issue.
|
||||
@@ -352,6 +84,12 @@
|
||||
@@ -352,8 +84,19 @@
|
||||
*/
|
||||
using PluginModuleSlug = std::tuple<std::string, std::string>;
|
||||
static const std::map<PluginModuleSlug, PluginModuleSlug> moduleSlugFallbacks = {
|
||||
|
@ -345,12 +345,20 @@
|
|||
+ {{"Core", "AudioInterface"}, {"Cardinal", "HostAudio8"}},
|
||||
+ {{"Core", "AudioInterface16"}, {"Cardinal", "HostAudio8"}},
|
||||
+ {{"Core", "MIDIToCVInterface"}, {"Cardinal", "HostMIDI"}},
|
||||
+ {{"Core", "MIDICCToCVInterface"}, {"Cardinal", "HostMIDICC"}},
|
||||
+ {{"Core", "MIDITriggerToCVInterface"}, {"Cardinal", "HostMIDIGate"}},
|
||||
+ {{"Core", "CV-MIDI"}, {"Cardinal", "HostMIDI"}},
|
||||
+ {{"Core", "CV-CC"}, {"Cardinal", "HostMIDICC"}},
|
||||
+ {{"Core", "CV-Gate"}, {"Cardinal", "HostMIDIGate"}},
|
||||
+ {{"Core", "MIDI-Map"}, {"Cardinal", "HostMIDIMap"}},
|
||||
+ {{"Core", "Notes"}, {"Cardinal", "TextEditor"}},
|
||||
+ {{"Core", "Blank"}, {"Cardinal", "Blank"}},
|
||||
{{"MindMeld-ShapeMasterPro", "ShapeMasterPro"}, {"MindMeldModular", "ShapeMaster"}},
|
||||
{{"MindMeldModular", "ShapeMaster"}, {"MindMeld-ShapeMasterPro", "ShapeMasterPro"}},
|
||||
- {{"MindMeldModular", "ShapeMaster"}, {"MindMeld-ShapeMasterPro", "ShapeMasterPro"}},
|
||||
// {{"", ""}, {"", ""}},
|
||||
@@ -441,7 +179,6 @@
|
||||
};
|
||||
|
||||
@@ -441,7 +184,6 @@
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue