1180 lines
32 KiB
C++
1180 lines
32 KiB
C++
/*
|
|
* 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.
|
|
*/
|
|
|
|
/**
|
|
* This file is an edited version of VCVRack's engine/Engine.cpp
|
|
* Copyright (C) 2016-2021 VCV.
|
|
*
|
|
* 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 (at your option) any later version.
|
|
*/
|
|
|
|
#include <algorithm>
|
|
#include <set>
|
|
#include <thread>
|
|
#include <condition_variable>
|
|
#include <mutex>
|
|
#include <atomic>
|
|
#include <tuple>
|
|
#include <pmmintrin.h>
|
|
|
|
#include <engine/Engine.hpp>
|
|
#include <engine/TerminalModule.hpp>
|
|
#include <settings.hpp>
|
|
#include <system.hpp>
|
|
#include <random.hpp>
|
|
#include <patch.hpp>
|
|
#include <plugin.hpp>
|
|
#include <mutex.hpp>
|
|
#include <helpers.hpp>
|
|
|
|
#ifdef NDEBUG
|
|
# undef DEBUG
|
|
#endif
|
|
|
|
#include "DistrhoUtils.hpp"
|
|
|
|
|
|
// known terminal modules
|
|
extern std::vector<rack::plugin::Model*> hostTerminalModels;
|
|
|
|
|
|
namespace rack {
|
|
namespace engine {
|
|
|
|
|
|
struct Engine::Internal {
|
|
std::vector<Module*> modules;
|
|
std::vector<TerminalModule*> terminalModules;
|
|
std::vector<Cable*> cables;
|
|
std::set<ParamHandle*> paramHandles;
|
|
|
|
// moduleId
|
|
std::map<int64_t, Module*> modulesCache;
|
|
// cableId
|
|
std::map<int64_t, Cable*> cablesCache;
|
|
// (moduleId, paramId)
|
|
std::map<std::tuple<int64_t, int>, ParamHandle*> paramHandlesCache;
|
|
|
|
float sampleRate = 0.f;
|
|
float sampleTime = 0.f;
|
|
int64_t block = 0;
|
|
int64_t frame = 0;
|
|
int64_t blockFrame = 0;
|
|
double blockTime = 0.0;
|
|
int blockFrames = 0;
|
|
|
|
#ifndef HEADLESS
|
|
// Meter
|
|
int meterCount = 0;
|
|
double meterTotal = 0.0;
|
|
double meterMax = 0.0;
|
|
double meterLastTime = -INFINITY;
|
|
double meterLastAverage = 0.0;
|
|
double meterLastMax = 0.0;
|
|
#endif
|
|
|
|
// Parameter smoothing
|
|
Module* smoothModule = NULL;
|
|
int smoothParamId = 0;
|
|
float smoothValue = 0.f;
|
|
|
|
/** Mutex that guards the Engine state, such as settings, Modules, and Cables.
|
|
Writers lock when mutating the engine's state or stepping the block.
|
|
Readers lock when using the engine's state.
|
|
*/
|
|
SharedMutex mutex;
|
|
};
|
|
|
|
|
|
static void Engine_updateExpander_NoLock(Engine* that, Module* module, bool side) {
|
|
Module::Expander& expander = side ? module->rightExpander : module->leftExpander;
|
|
Module* oldExpanderModule = expander.module;
|
|
|
|
if (expander.moduleId >= 0) {
|
|
if (!expander.module || expander.module->id != expander.moduleId) {
|
|
expander.module = that->getModule_NoLock(expander.moduleId);
|
|
}
|
|
}
|
|
else {
|
|
if (expander.module) {
|
|
expander.module = NULL;
|
|
}
|
|
}
|
|
|
|
if (expander.module != oldExpanderModule) {
|
|
// Dispatch ExpanderChangeEvent
|
|
Module::ExpanderChangeEvent e;
|
|
e.side = side;
|
|
module->onExpanderChange(e);
|
|
}
|
|
}
|
|
|
|
|
|
static void Cable_step(Cable* that) {
|
|
Output* output = &that->outputModule->outputs[that->outputId];
|
|
Input* input = &that->inputModule->inputs[that->inputId];
|
|
// Match number of polyphonic channels to output port
|
|
const int channels = output->channels;
|
|
// Copy all voltages from output to input
|
|
for (int c = 0; c < channels; c++) {
|
|
float v = output->voltages[c];
|
|
// Set 0V if infinite or NaN
|
|
if (!std::isfinite(v))
|
|
v = 0.f;
|
|
input->voltages[c] = v;
|
|
}
|
|
// Set higher channel voltages to 0
|
|
for (int c = channels; c < input->channels; c++) {
|
|
input->voltages[c] = 0.f;
|
|
}
|
|
input->channels = channels;
|
|
}
|
|
|
|
|
|
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) {
|
|
Engine::Internal* internal = that->internal;
|
|
|
|
// Param smoothing
|
|
Module* smoothModule = internal->smoothModule;
|
|
if (smoothModule) {
|
|
int smoothParamId = internal->smoothParamId;
|
|
float smoothValue = internal->smoothValue;
|
|
Param* smoothParam = &smoothModule->params[smoothParamId];
|
|
float value = smoothParam->value;
|
|
// Use decay rate of roughly 1 graphics frame
|
|
const float smoothLambda = 60.f;
|
|
float newValue = value + (smoothValue - value) * smoothLambda * internal->sampleTime;
|
|
if (value == newValue) {
|
|
// Snap to actual smooth value if the value doesn't change enough (due to the granularity of floats)
|
|
smoothParam->setValue(smoothValue);
|
|
internal->smoothModule = NULL;
|
|
internal->smoothParamId = 0;
|
|
}
|
|
else {
|
|
smoothParam->setValue(newValue);
|
|
}
|
|
}
|
|
|
|
// Flip messages for each module
|
|
for (Module* module : internal->modules) {
|
|
if (module->leftExpander.messageFlipRequested) {
|
|
std::swap(module->leftExpander.producerMessage, module->leftExpander.consumerMessage);
|
|
module->leftExpander.messageFlipRequested = false;
|
|
}
|
|
if (module->rightExpander.messageFlipRequested) {
|
|
std::swap(module->rightExpander.producerMessage, module->rightExpander.consumerMessage);
|
|
module->rightExpander.messageFlipRequested = false;
|
|
}
|
|
}
|
|
|
|
// Build ProcessArgs
|
|
Module::ProcessArgs processArgs;
|
|
processArgs.sampleRate = internal->sampleRate;
|
|
processArgs.sampleTime = internal->sampleTime;
|
|
processArgs.frame = internal->frame;
|
|
|
|
// 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;
|
|
}
|
|
|
|
|
|
static void Port_setDisconnected(Port* that) {
|
|
that->channels = 0;
|
|
for (int c = 0; c < PORT_MAX_CHANNELS; c++) {
|
|
that->voltages[c] = 0.f;
|
|
}
|
|
}
|
|
|
|
|
|
static void Port_setConnected(Port* that) {
|
|
if (that->channels > 0)
|
|
return;
|
|
that->channels = 1;
|
|
}
|
|
|
|
|
|
static void Engine_updateConnected(Engine* that) {
|
|
// Find disconnected ports
|
|
std::set<Input*> disconnectedInputs;
|
|
std::set<Output*> disconnectedOutputs;
|
|
for (Module* module : that->internal->modules) {
|
|
for (Input& input : module->inputs) {
|
|
disconnectedInputs.insert(&input);
|
|
}
|
|
for (Output& output : module->outputs) {
|
|
disconnectedOutputs.insert(&output);
|
|
}
|
|
}
|
|
for (TerminalModule* terminalModule : that->internal->terminalModules) {
|
|
for (Input& input : terminalModule->inputs) {
|
|
disconnectedInputs.insert(&input);
|
|
}
|
|
for (Output& output : terminalModule->outputs) {
|
|
disconnectedOutputs.insert(&output);
|
|
}
|
|
}
|
|
for (Cable* cable : that->internal->cables) {
|
|
// Connect input
|
|
Input& input = cable->inputModule->inputs[cable->inputId];
|
|
auto inputIt = disconnectedInputs.find(&input);
|
|
if (inputIt != disconnectedInputs.end())
|
|
disconnectedInputs.erase(inputIt);
|
|
Port_setConnected(&input);
|
|
// Connect output
|
|
Output& output = cable->outputModule->outputs[cable->outputId];
|
|
auto outputIt = disconnectedOutputs.find(&output);
|
|
if (outputIt != disconnectedOutputs.end())
|
|
disconnectedOutputs.erase(outputIt);
|
|
Port_setConnected(&output);
|
|
}
|
|
// Disconnect ports that have no cable
|
|
for (Input* input : disconnectedInputs) {
|
|
Port_setDisconnected(input);
|
|
}
|
|
for (Output* output : disconnectedOutputs) {
|
|
Port_setDisconnected(output);
|
|
DISTRHO_SAFE_ASSERT(output->cables.empty());
|
|
}
|
|
}
|
|
|
|
|
|
static void Engine_refreshParamHandleCache(Engine* that) {
|
|
// Clear cache
|
|
that->internal->paramHandlesCache.clear();
|
|
// Add active ParamHandles to cache
|
|
for (ParamHandle* paramHandle : that->internal->paramHandles) {
|
|
if (paramHandle->moduleId >= 0) {
|
|
that->internal->paramHandlesCache[std::make_tuple(paramHandle->moduleId, paramHandle->paramId)] = paramHandle;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
Engine::Engine() {
|
|
internal = new Internal;
|
|
}
|
|
|
|
|
|
Engine::~Engine() {
|
|
// Clear modules, cables, etc
|
|
clear();
|
|
|
|
// Make sure there are no cables or modules in the rack on destruction.
|
|
// 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());
|
|
DISTRHO_SAFE_ASSERT(internal->cablesCache.empty());
|
|
DISTRHO_SAFE_ASSERT(internal->paramHandlesCache.empty());
|
|
|
|
delete internal;
|
|
}
|
|
|
|
|
|
void Engine::clear() {
|
|
std::lock_guard<SharedMutex> lock(internal->mutex);
|
|
clear_NoLock();
|
|
}
|
|
|
|
|
|
void Engine::clear_NoLock() {
|
|
// Copy lists because we'll be removing while iterating
|
|
std::set<ParamHandle*> paramHandles = internal->paramHandles;
|
|
for (ParamHandle* paramHandle : paramHandles) {
|
|
removeParamHandle_NoLock(paramHandle);
|
|
// Don't delete paramHandle because they're normally owned by Module subclasses
|
|
}
|
|
std::vector<Cable*> cables = internal->cables;
|
|
for (Cable* cable : cables) {
|
|
removeCable_NoLock(cable);
|
|
delete cable;
|
|
}
|
|
std::vector<Module*> modules = internal->modules;
|
|
for (Module* module : modules) {
|
|
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
|
|
|
|
SharedLock<SharedMutex> lock(internal->mutex);
|
|
// Configure thread
|
|
random::init();
|
|
|
|
internal->blockFrame = internal->frame;
|
|
internal->blockTime = system::getTime();
|
|
internal->blockFrames = frames;
|
|
|
|
// Update expander pointers
|
|
for (Module* module : internal->modules) {
|
|
Engine_updateExpander_NoLock(this, module, false);
|
|
Engine_updateExpander_NoLock(this, module, true);
|
|
}
|
|
|
|
// Step individual frames
|
|
for (int i = 0; i < frames; i++) {
|
|
Engine_stepFrame(this);
|
|
}
|
|
|
|
internal->block++;
|
|
|
|
#ifndef HEADLESS
|
|
// Stop timer
|
|
double endTime = system::getTime();
|
|
double meter = (endTime - startTime) / (frames * internal->sampleTime);
|
|
internal->meterTotal += meter;
|
|
internal->meterMax = std::fmax(internal->meterMax, meter);
|
|
internal->meterCount++;
|
|
|
|
// Update meter values
|
|
const double meterUpdateDuration = 1.0;
|
|
if (startTime - internal->meterLastTime >= meterUpdateDuration) {
|
|
internal->meterLastAverage = internal->meterTotal / internal->meterCount;
|
|
internal->meterLastMax = internal->meterMax;
|
|
internal->meterLastTime = startTime;
|
|
internal->meterCount = 0;
|
|
internal->meterTotal = 0.0;
|
|
internal->meterMax = 0.0;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
|
|
void Engine::setMasterModule(Module* module) {
|
|
}
|
|
|
|
|
|
void Engine::setMasterModule_NoLock(Module* module) {
|
|
}
|
|
|
|
|
|
Module* Engine::getMasterModule() {
|
|
return nullptr;
|
|
}
|
|
|
|
|
|
float Engine::getSampleRate() {
|
|
return internal->sampleRate;
|
|
}
|
|
|
|
|
|
void Engine::setSampleRate(float sampleRate) {
|
|
if (sampleRate == internal->sampleRate)
|
|
return;
|
|
std::lock_guard<SharedMutex> lock(internal->mutex);
|
|
|
|
internal->sampleRate = sampleRate;
|
|
internal->sampleTime = 1.f / sampleRate;
|
|
// Dispatch SampleRateChangeEvent
|
|
Module::SampleRateChangeEvent e;
|
|
e.sampleRate = internal->sampleRate;
|
|
e.sampleTime = internal->sampleTime;
|
|
for (Module* module : internal->modules) {
|
|
module->onSampleRateChange(e);
|
|
}
|
|
for (TerminalModule* terminalModule : internal->terminalModules) {
|
|
terminalModule->onSampleRateChange(e);
|
|
}
|
|
}
|
|
|
|
|
|
void Engine::setSuggestedSampleRate(float suggestedSampleRate) {
|
|
}
|
|
|
|
|
|
float Engine::getSampleTime() {
|
|
return internal->sampleTime;
|
|
}
|
|
|
|
|
|
void Engine::yieldWorkers() {
|
|
}
|
|
|
|
|
|
int64_t Engine::getBlock() {
|
|
return internal->block;
|
|
}
|
|
|
|
|
|
int64_t Engine::getFrame() {
|
|
return internal->frame;
|
|
}
|
|
|
|
|
|
void Engine::setFrame(int64_t frame) {
|
|
internal->frame = frame;
|
|
}
|
|
|
|
|
|
int64_t Engine::getBlockFrame() {
|
|
return internal->blockFrame;
|
|
}
|
|
|
|
|
|
double Engine::getBlockTime() {
|
|
return internal->blockTime;
|
|
}
|
|
|
|
|
|
int Engine::getBlockFrames() {
|
|
return internal->blockFrames;
|
|
}
|
|
|
|
|
|
double Engine::getBlockDuration() {
|
|
return internal->blockFrames * internal->sampleTime;
|
|
}
|
|
|
|
|
|
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() + internal->terminalModules.size();
|
|
}
|
|
|
|
|
|
size_t Engine::getModuleIds(int64_t* moduleIds, size_t len) {
|
|
SharedLock<SharedMutex> lock(internal->mutex);
|
|
size_t i = 0;
|
|
for (Module* m : internal->modules) {
|
|
if (i >= len)
|
|
break;
|
|
moduleIds[i++] = m->id;
|
|
}
|
|
for (TerminalModule* m : internal->terminalModules) {
|
|
if (i >= len)
|
|
break;
|
|
moduleIds[i++] = m->id;
|
|
}
|
|
return i;
|
|
}
|
|
|
|
|
|
std::vector<int64_t> Engine::getModuleIds() {
|
|
SharedLock<SharedMutex> lock(internal->mutex);
|
|
std::vector<int64_t> moduleIds;
|
|
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 (std::find(hostTerminalModels.begin(), hostTerminalModels.end(), model) != hostTerminalModels.end())
|
|
return static_cast<TerminalModule*>(module);
|
|
return nullptr;
|
|
}
|
|
|
|
|
|
void Engine::addModule(Module* module) {
|
|
std::lock_guard<SharedMutex> lock(internal->mutex);
|
|
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
|
|
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;
|
|
module->onAdd(eAdd);
|
|
// Dispatch SampleRateChangeEvent
|
|
Module::SampleRateChangeEvent eSrc;
|
|
eSrc.sampleRate = internal->sampleRate;
|
|
eSrc.sampleTime = internal->sampleTime;
|
|
module->onSampleRateChange(eSrc);
|
|
// Update ParamHandles' module pointers
|
|
for (ParamHandle* paramHandle : internal->paramHandles) {
|
|
if (paramHandle->moduleId == module->id)
|
|
paramHandle->module = module;
|
|
}
|
|
}
|
|
|
|
|
|
void Engine::removeModule(Module* module) {
|
|
std::lock_guard<SharedMutex> lock(internal->mutex);
|
|
removeModule_NoLock(module);
|
|
}
|
|
|
|
|
|
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,);
|
|
helper->removeCachedModuleWidget(module);
|
|
// Dispatch RemoveEvent
|
|
Module::RemoveEvent eRemove;
|
|
module->onRemove(eRemove);
|
|
// Update ParamHandles' module pointers
|
|
for (ParamHandle* paramHandle : internal->paramHandles) {
|
|
if (paramHandle->moduleId == module->id)
|
|
paramHandle->module = NULL;
|
|
}
|
|
// If a param is being smoothed on this module, stop smoothing it immediately
|
|
if (module == internal->smoothModule) {
|
|
internal->smoothModule = NULL;
|
|
}
|
|
// Check that all cables are disconnected
|
|
for (Cable* cable : internal->cables) {
|
|
DISTRHO_SAFE_ASSERT(cable->inputModule != module);
|
|
DISTRHO_SAFE_ASSERT(cable->outputModule != module);
|
|
}
|
|
// Update expanders of other modules
|
|
for (Module* m : internal->modules) {
|
|
if (m->leftExpander.module == module) {
|
|
m->leftExpander.moduleId = -1;
|
|
m->leftExpander.module = NULL;
|
|
}
|
|
if (m->rightExpander.module == module) {
|
|
m->rightExpander.moduleId = -1;
|
|
m->rightExpander.module = NULL;
|
|
}
|
|
}
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
|
|
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);
|
|
auto tit = std::find(internal->terminalModules.begin(), internal->terminalModules.end(), module);
|
|
return it != internal->modules.end() && tit != internal->terminalModules.end();
|
|
}
|
|
|
|
|
|
Module* Engine::getModule(int64_t moduleId) {
|
|
SharedLock<SharedMutex> lock(internal->mutex);
|
|
return getModule_NoLock(moduleId);
|
|
}
|
|
|
|
|
|
Module* Engine::getModule_NoLock(int64_t moduleId) {
|
|
auto it = internal->modulesCache.find(moduleId);
|
|
if (it == internal->modulesCache.end())
|
|
return NULL;
|
|
return it->second;
|
|
}
|
|
|
|
|
|
void Engine::resetModule(Module* module) {
|
|
std::lock_guard<SharedMutex> lock(internal->mutex);
|
|
DISTRHO_SAFE_ASSERT_RETURN(module,);
|
|
|
|
Module::ResetEvent eReset;
|
|
module->onReset(eReset);
|
|
}
|
|
|
|
|
|
void Engine::randomizeModule(Module* module) {
|
|
std::lock_guard<SharedMutex> lock(internal->mutex);
|
|
DISTRHO_SAFE_ASSERT_RETURN(module,);
|
|
|
|
Module::RandomizeEvent eRandomize;
|
|
module->onRandomize(eRandomize);
|
|
}
|
|
|
|
|
|
void Engine::bypassModule(Module* module, bool bypassed) {
|
|
DISTRHO_SAFE_ASSERT_RETURN(module,);
|
|
if (module->isBypassed() == bypassed)
|
|
return;
|
|
|
|
std::lock_guard<SharedMutex> lock(internal->mutex);
|
|
|
|
// Clear outputs and set to 1 channel
|
|
for (Output& output : module->outputs) {
|
|
// This zeros all voltages, but the channel is set to 1 if connected
|
|
output.setChannels(0);
|
|
}
|
|
// Set bypassed state
|
|
module->setBypassed(bypassed);
|
|
if (bypassed) {
|
|
// Dispatch BypassEvent
|
|
Module::BypassEvent eBypass;
|
|
module->onBypass(eBypass);
|
|
}
|
|
else {
|
|
// Dispatch UnBypassEvent
|
|
Module::UnBypassEvent eUnBypass;
|
|
module->onUnBypass(eUnBypass);
|
|
}
|
|
}
|
|
|
|
|
|
json_t* Engine::moduleToJson(Module* module) {
|
|
SharedLock<SharedMutex> lock(internal->mutex);
|
|
return module->toJson();
|
|
}
|
|
|
|
|
|
void Engine::moduleFromJson(Module* module, json_t* rootJ) {
|
|
std::lock_guard<SharedMutex> lock(internal->mutex);
|
|
module->fromJson(rootJ);
|
|
}
|
|
|
|
|
|
void Engine::prepareSaveModule(Module* module) {
|
|
SharedLock<SharedMutex> lock(internal->mutex);
|
|
Module::SaveEvent e;
|
|
module->onSave(e);
|
|
}
|
|
|
|
|
|
void Engine::prepareSave() {
|
|
SharedLock<SharedMutex> lock(internal->mutex);
|
|
for (Module* module : internal->modules) {
|
|
Module::SaveEvent e;
|
|
module->onSave(e);
|
|
}
|
|
for (TerminalModule* terminalModule : internal->terminalModules) {
|
|
Module::SaveEvent e;
|
|
terminalModule->onSave(e);
|
|
}
|
|
}
|
|
|
|
|
|
size_t Engine::getNumCables() {
|
|
return internal->cables.size();
|
|
}
|
|
|
|
|
|
size_t Engine::getCableIds(int64_t* cableIds, size_t len) {
|
|
SharedLock<SharedMutex> lock(internal->mutex);
|
|
size_t i = 0;
|
|
for (Cable* c : internal->cables) {
|
|
if (i >= len)
|
|
break;
|
|
cableIds[i] = c->id;
|
|
i++;
|
|
}
|
|
return i;
|
|
}
|
|
|
|
|
|
std::vector<int64_t> Engine::getCableIds() {
|
|
SharedLock<SharedMutex> lock(internal->mutex);
|
|
std::vector<int64_t> cableIds;
|
|
cableIds.reserve(internal->cables.size());
|
|
for (Cable* c : internal->cables) {
|
|
cableIds.push_back(c->id);
|
|
}
|
|
return cableIds;
|
|
}
|
|
|
|
|
|
void Engine::addCable(Cable* cable) {
|
|
std::lock_guard<SharedMutex> lock(internal->mutex);
|
|
DISTRHO_SAFE_ASSERT_RETURN(cable,);
|
|
// Check cable properties
|
|
DISTRHO_SAFE_ASSERT_RETURN(cable->inputModule,);
|
|
DISTRHO_SAFE_ASSERT_RETURN(cable->outputModule,);
|
|
bool outputWasConnected = false;
|
|
for (Cable* cable2 : internal->cables) {
|
|
// Check that the cable is not already added
|
|
DISTRHO_SAFE_ASSERT_RETURN(cable2 != cable,);
|
|
// Check that the input is not already used by another cable
|
|
DISTRHO_SAFE_ASSERT_RETURN(!(cable2->inputModule == cable->inputModule && cable2->inputId == cable->inputId),);
|
|
// 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)
|
|
outputWasConnected = true;
|
|
}
|
|
// Set ID if unset or collides with an existing ID
|
|
while (cable->id < 0 || internal->cablesCache.find(cable->id) != internal->cablesCache.end()) {
|
|
// Randomly generate ID
|
|
cable->id = random::u64() % (1ull << 53);
|
|
}
|
|
// 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
|
|
{
|
|
Module::PortChangeEvent e;
|
|
e.connecting = true;
|
|
e.type = Port::INPUT;
|
|
e.portId = cable->inputId;
|
|
cable->inputModule->onPortChange(e);
|
|
}
|
|
// Dispatch output port event if its state went from disconnected to connected.
|
|
if (!outputWasConnected) {
|
|
Module::PortChangeEvent e;
|
|
e.connecting = true;
|
|
e.type = Port::OUTPUT;
|
|
e.portId = cable->outputId;
|
|
cable->outputModule->onPortChange(e);
|
|
}
|
|
}
|
|
|
|
|
|
void Engine::removeCable(Cable* cable) {
|
|
std::lock_guard<SharedMutex> lock(internal->mutex);
|
|
removeCable_NoLock(cable);
|
|
}
|
|
|
|
|
|
void Engine::removeCable_NoLock(Cable* cable) {
|
|
DISTRHO_SAFE_ASSERT_RETURN(cable,);
|
|
// Check that the cable is already added
|
|
auto it = std::find(internal->cables.begin(), internal->cables.end(), cable);
|
|
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);
|
|
Engine_updateConnected(this);
|
|
bool outputIsConnected = false;
|
|
for (Cable* cable2 : internal->cables) {
|
|
// 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)
|
|
outputIsConnected = true;
|
|
}
|
|
// Dispatch input port event
|
|
{
|
|
Module::PortChangeEvent e;
|
|
e.connecting = false;
|
|
e.type = Port::INPUT;
|
|
e.portId = cable->inputId;
|
|
cable->inputModule->onPortChange(e);
|
|
}
|
|
// Dispatch output port event if its state went from connected to disconnected.
|
|
if (!outputIsConnected) {
|
|
Module::PortChangeEvent e;
|
|
e.connecting = false;
|
|
e.type = Port::OUTPUT;
|
|
e.portId = cable->outputId;
|
|
cable->outputModule->onPortChange(e);
|
|
}
|
|
}
|
|
|
|
|
|
bool Engine::hasCable(Cable* cable) {
|
|
SharedLock<SharedMutex> lock(internal->mutex);
|
|
// TODO Performance could be improved by searching cablesCache, but more testing would be needed to make sure it's always valid.
|
|
auto it = std::find(internal->cables.begin(), internal->cables.end(), cable);
|
|
return it != internal->cables.end();
|
|
}
|
|
|
|
|
|
Cable* Engine::getCable(int64_t cableId) {
|
|
SharedLock<SharedMutex> lock(internal->mutex);
|
|
auto it = internal->cablesCache.find(cableId);
|
|
if (it == internal->cablesCache.end())
|
|
return NULL;
|
|
return it->second;
|
|
}
|
|
|
|
|
|
void Engine::setParamValue(Module* module, int paramId, float value) {
|
|
// If param is being smoothed, cancel smoothing.
|
|
if (internal->smoothModule == module && internal->smoothParamId == paramId) {
|
|
internal->smoothModule = NULL;
|
|
internal->smoothParamId = 0;
|
|
}
|
|
module->params[paramId].value = value;
|
|
}
|
|
|
|
|
|
float Engine::getParamValue(Module* module, int paramId) {
|
|
return module->params[paramId].value;
|
|
}
|
|
|
|
|
|
void Engine::setParamSmoothValue(Module* module, int paramId, float value) {
|
|
// If another param is being smoothed, jump value
|
|
if (internal->smoothModule && !(internal->smoothModule == module && internal->smoothParamId == paramId)) {
|
|
internal->smoothModule->params[internal->smoothParamId].value = internal->smoothValue;
|
|
}
|
|
internal->smoothParamId = paramId;
|
|
internal->smoothValue = value;
|
|
// Set this last so the above values are valid as soon as it is set
|
|
internal->smoothModule = module;
|
|
}
|
|
|
|
|
|
float Engine::getParamSmoothValue(Module* module, int paramId) {
|
|
if (internal->smoothModule == module && internal->smoothParamId == paramId)
|
|
return internal->smoothValue;
|
|
return module->params[paramId].value;
|
|
}
|
|
|
|
|
|
void Engine::addParamHandle(ParamHandle* paramHandle) {
|
|
std::lock_guard<SharedMutex> lock(internal->mutex);
|
|
// New ParamHandles must be blank.
|
|
// This means we don't have to refresh the cache.
|
|
DISTRHO_SAFE_ASSERT_RETURN(paramHandle->moduleId < 0,);
|
|
|
|
// Check that the ParamHandle is not already added
|
|
auto it = internal->paramHandles.find(paramHandle);
|
|
DISTRHO_SAFE_ASSERT_RETURN(it == internal->paramHandles.end(),);
|
|
|
|
// Add it
|
|
internal->paramHandles.insert(paramHandle);
|
|
// No need to refresh the cache because the moduleId is not set.
|
|
}
|
|
|
|
|
|
void Engine::removeParamHandle(ParamHandle* paramHandle) {
|
|
std::lock_guard<SharedMutex> lock(internal->mutex);
|
|
removeParamHandle_NoLock(paramHandle);
|
|
}
|
|
|
|
|
|
void Engine::removeParamHandle_NoLock(ParamHandle* paramHandle) {
|
|
// Check that the ParamHandle is already added
|
|
auto it = internal->paramHandles.find(paramHandle);
|
|
DISTRHO_SAFE_ASSERT_RETURN(it != internal->paramHandles.end(),);
|
|
|
|
// Remove it
|
|
paramHandle->module = NULL;
|
|
internal->paramHandles.erase(it);
|
|
Engine_refreshParamHandleCache(this);
|
|
}
|
|
|
|
|
|
ParamHandle* Engine::getParamHandle(int64_t moduleId, int paramId) {
|
|
SharedLock<SharedMutex> lock(internal->mutex);
|
|
return getParamHandle_NoLock(moduleId, paramId);
|
|
}
|
|
|
|
|
|
ParamHandle* Engine::getParamHandle_NoLock(int64_t moduleId, int paramId) {
|
|
auto it = internal->paramHandlesCache.find(std::make_tuple(moduleId, paramId));
|
|
if (it == internal->paramHandlesCache.end())
|
|
return NULL;
|
|
return it->second;
|
|
}
|
|
|
|
|
|
ParamHandle* Engine::getParamHandle(Module* module, int paramId) {
|
|
return getParamHandle(module->id, paramId);
|
|
}
|
|
|
|
|
|
void Engine::updateParamHandle(ParamHandle* paramHandle, int64_t moduleId, int paramId, bool overwrite) {
|
|
std::lock_guard<SharedMutex> lock(internal->mutex);
|
|
updateParamHandle_NoLock(paramHandle, moduleId, paramId, overwrite);
|
|
}
|
|
|
|
|
|
void Engine::updateParamHandle_NoLock(ParamHandle* paramHandle, int64_t moduleId, int paramId, bool overwrite) {
|
|
// Check that it exists
|
|
auto it = internal->paramHandles.find(paramHandle);
|
|
DISTRHO_SAFE_ASSERT_RETURN(it != internal->paramHandles.end(),);
|
|
|
|
// Set IDs
|
|
paramHandle->moduleId = moduleId;
|
|
paramHandle->paramId = paramId;
|
|
paramHandle->module = NULL;
|
|
// At this point, the ParamHandle cache might be invalid.
|
|
|
|
if (paramHandle->moduleId >= 0) {
|
|
// Replace old ParamHandle, or reset the current ParamHandle
|
|
ParamHandle* oldParamHandle = getParamHandle_NoLock(moduleId, paramId);
|
|
if (oldParamHandle) {
|
|
if (overwrite) {
|
|
oldParamHandle->moduleId = -1;
|
|
oldParamHandle->paramId = 0;
|
|
oldParamHandle->module = NULL;
|
|
}
|
|
else {
|
|
paramHandle->moduleId = -1;
|
|
paramHandle->paramId = 0;
|
|
paramHandle->module = NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set module pointer if the above block didn't reset it
|
|
if (paramHandle->moduleId >= 0) {
|
|
paramHandle->module = getModule_NoLock(paramHandle->moduleId);
|
|
}
|
|
|
|
Engine_refreshParamHandleCache(this);
|
|
}
|
|
|
|
|
|
json_t* Engine::toJson() {
|
|
SharedLock<SharedMutex> lock(internal->mutex);
|
|
json_t* rootJ = json_object();
|
|
|
|
// modules
|
|
json_t* modulesJ = json_array();
|
|
for (Module* module : internal->modules) {
|
|
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
|
|
json_t* cablesJ = json_array();
|
|
for (Cable* cable : internal->cables) {
|
|
json_t* cableJ = cable->toJson();
|
|
json_array_append_new(cablesJ, cableJ);
|
|
}
|
|
json_object_set_new(rootJ, "cables", cablesJ);
|
|
|
|
return rootJ;
|
|
}
|
|
|
|
|
|
void Engine::fromJson(json_t* rootJ) {
|
|
// Don't write-lock the entire method because most of it doesn't need it.
|
|
|
|
// Write-locks
|
|
clear();
|
|
// modules
|
|
json_t* modulesJ = json_object_get(rootJ, "modules");
|
|
if (!modulesJ)
|
|
return;
|
|
size_t moduleIndex;
|
|
json_t* moduleJ;
|
|
json_array_foreach(modulesJ, moduleIndex, moduleJ) {
|
|
// Get model
|
|
plugin::Model* model;
|
|
try {
|
|
model = plugin::modelFromJson(moduleJ);
|
|
}
|
|
catch (Exception& e) {
|
|
WARN("Cannot load model: %s", e.what());
|
|
// APP->patch->log(e.what());
|
|
continue;
|
|
}
|
|
|
|
// Create module
|
|
Module* const module = model->createModule();
|
|
DISTRHO_SAFE_ASSERT_CONTINUE(module != nullptr);
|
|
|
|
// Create the widget too, needed by a few modules
|
|
CardinalPluginModelHelper* const helper = dynamic_cast<CardinalPluginModelHelper*>(model);
|
|
DISTRHO_SAFE_ASSERT_CONTINUE(helper != nullptr);
|
|
|
|
app::ModuleWidget* const moduleWidget = helper->createModuleWidgetFromEngineLoad(module);
|
|
DISTRHO_SAFE_ASSERT_CONTINUE(moduleWidget != nullptr);
|
|
|
|
try {
|
|
// This doesn't need a lock because the Module is not added to the Engine yet.
|
|
module->fromJson(moduleJ);
|
|
|
|
// Before 1.0, the module ID was the index in the "modules" array
|
|
if (module->id < 0) {
|
|
module->id = moduleIndex;
|
|
}
|
|
|
|
// Write-locks
|
|
addModule(module);
|
|
}
|
|
catch (Exception& e) {
|
|
WARN("Cannot load module: %s", e.what());
|
|
// APP->patch->log(e.what());
|
|
helper->removeCachedModuleWidget(module);
|
|
delete module;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// cables
|
|
json_t* cablesJ = json_object_get(rootJ, "cables");
|
|
// Before 1.0, cables were called wires
|
|
if (!cablesJ)
|
|
cablesJ = json_object_get(rootJ, "wires");
|
|
if (!cablesJ)
|
|
return;
|
|
size_t cableIndex;
|
|
json_t* cableJ;
|
|
json_array_foreach(cablesJ, cableIndex, cableJ) {
|
|
// cable
|
|
Cable* cable = new Cable;
|
|
|
|
try {
|
|
cable->fromJson(cableJ);
|
|
|
|
// Before 1.0, the cable ID was the index in the "cables" array
|
|
if (cable->id < 0) {
|
|
cable->id = cableIndex;
|
|
}
|
|
|
|
// Write-locks
|
|
addCable(cable);
|
|
}
|
|
catch (Exception& e) {
|
|
WARN("Cannot load cable: %s", e.what());
|
|
delete cable;
|
|
// Don't log exceptions because missing modules create unnecessary complaining when cables try to connect to them.
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void Engine::startFallbackThread() {
|
|
}
|
|
|
|
|
|
} // namespace engine
|
|
} // namespace rack
|