diff --git a/include/app/ModuleLightWidget.hpp b/include/app/ModuleLightWidget.hpp index ff5f76aa..75e505e0 100644 --- a/include/app/ModuleLightWidget.hpp +++ b/include/app/ModuleLightWidget.hpp @@ -1,6 +1,7 @@ #pragma once #include "app/common.hpp" #include "app/MultiLightWidget.hpp" +#include "engine/Module.hpp" namespace rack { diff --git a/include/app/ModuleWidget.hpp b/include/app/ModuleWidget.hpp index 2a6f9fb8..40d369c5 100644 --- a/include/app/ModuleWidget.hpp +++ b/include/app/ModuleWidget.hpp @@ -6,7 +6,7 @@ #include "app/Port.hpp" #include "app/ParamWidget.hpp" #include "plugin.hpp" -#include "engine.hpp" +#include "engine/Module.hpp" namespace rack { diff --git a/include/app/ParamQuantity.hpp b/include/app/ParamQuantity.hpp index 8dfc0c74..d4844249 100644 --- a/include/app/ParamQuantity.hpp +++ b/include/app/ParamQuantity.hpp @@ -1,6 +1,6 @@ #pragma once #include "ui/Quantity.hpp" -#include "engine.hpp" +#include "engine/Module.hpp" namespace rack { diff --git a/include/app/ParamWidget.hpp b/include/app/ParamWidget.hpp index 00546bc2..122551b2 100644 --- a/include/app/ParamWidget.hpp +++ b/include/app/ParamWidget.hpp @@ -2,7 +2,6 @@ #include "widgets/OpaqueWidget.hpp" #include "app/ParamQuantity.hpp" #include "app/common.hpp" -#include "engine.hpp" namespace rack { diff --git a/include/app/RackWidget.hpp b/include/app/RackWidget.hpp index bf3319d3..81fe5abd 100644 --- a/include/app/RackWidget.hpp +++ b/include/app/RackWidget.hpp @@ -2,7 +2,6 @@ #include "app/common.hpp" #include "app/WireWidget.hpp" #include "app/WireContainer.hpp" -#include "engine.hpp" namespace rack { diff --git a/include/app/SVGButton.hpp b/include/app/SVGButton.hpp index 28439e61..55b63f1d 100644 --- a/include/app/SVGButton.hpp +++ b/include/app/SVGButton.hpp @@ -1,6 +1,5 @@ #pragma once #include "app/common.hpp" -#include "engine.hpp" namespace rack { diff --git a/include/app/WireContainer.hpp b/include/app/WireContainer.hpp index bb81e8f9..25097f18 100644 --- a/include/app/WireContainer.hpp +++ b/include/app/WireContainer.hpp @@ -1,7 +1,6 @@ #pragma once #include "app/common.hpp" #include "app/WireWidget.hpp" -#include "engine.hpp" namespace rack { diff --git a/include/app/WireWidget.hpp b/include/app/WireWidget.hpp index c182ff02..14fa3f49 100644 --- a/include/app/WireWidget.hpp +++ b/include/app/WireWidget.hpp @@ -1,5 +1,6 @@ #pragma once #include "app/common.hpp" +#include "engine/Wire.hpp" namespace rack { diff --git a/include/common.hpp b/include/common.hpp index 50a4dadc..ee2e717f 100644 --- a/include/common.hpp +++ b/include/common.hpp @@ -1,6 +1,6 @@ #pragma once -// Include some of the C++ standard library for convenience +// Include most of the C stdlib for convenience #include #include #include @@ -9,6 +9,8 @@ #include #include #include + +// Include some of the C++ stdlib for convenience #include diff --git a/include/engine.hpp b/include/engine.hpp deleted file mode 100644 index b0bbd7df..00000000 --- a/include/engine.hpp +++ /dev/null @@ -1,171 +0,0 @@ -#pragma once -#include -#include "common.hpp" -#include - - -namespace rack { - - -struct Param { - float value = 0.f; - float minValue = 0.f; - float maxValue = 1.f; - float defaultValue = 0.f; - - // For formatting/displaying the value - /** Set to 0 for linear, nonzero for exponential */ - float displayBase = 0.f; - float displayMultiplier = 1.f; - int displayPrecision = 2; - std::string label; - std::string unit; - - // TODO Change this horrible method name - void setup(float minValue, float maxValue, float defaultValue, std::string label = "", std::string unit = "", int displayPrecision = 2) { - this->value = defaultValue; - this->minValue = minValue; - this->maxValue = maxValue; - this->defaultValue = defaultValue; - this->label = label; - this->unit = unit; - this->displayPrecision = displayPrecision; - } - - json_t *toJson(); - void fromJson(json_t *rootJ); -}; - - -struct Light { - /** The square of the brightness value */ - float value = 0.0; - float getBrightness(); - void setBrightness(float brightness) { - value = (brightness > 0.f) ? brightness * brightness : 0.f; - } - /** Emulates slow fall (but immediate rise) of LED brightness. - `frames` rescales the timestep. For example, if your module calls this method every 16 frames, use 16.0. - */ - void setBrightnessSmooth(float brightness, float frames = 1.f); -}; - - -struct Input { - /** Voltage of the port, zero if not plugged in. Read-only by Module */ - float value = 0.0; - /** Whether a wire is plugged in */ - bool active = false; - Light plugLights[2]; - /** Returns the value if a wire is plugged in, otherwise returns the given default value */ - float normalize(float normalValue) { - return active ? value : normalValue; - } -}; - - -struct Output { - /** Voltage of the port. Write-only by Module */ - float value = 0.0; - /** Whether a wire is plugged in */ - bool active = false; - Light plugLights[2]; -}; - - -struct Module { - std::vector params; - std::vector inputs; - std::vector outputs; - std::vector lights; - /** For CPU usage meter */ - float cpuTime = 0.0; - - /** Constructs a Module with no params, inputs, outputs, and lights */ - Module() {} - /** Constructs a Module with a fixed number of params, inputs, outputs, and lights */ - Module(int numParams, int numInputs, int numOutputs, int numLights = 0) { - params.resize(numParams); - inputs.resize(numInputs); - outputs.resize(numOutputs); - lights.resize(numLights); - } - virtual ~Module() {} - - /** Advances the module by 1 audio frame with duration 1.0 / gSampleRate - Override this method to read inputs and params, and to write outputs and lights. - */ - virtual void step() {} - - /** Called when the engine sample rate is changed - */ - virtual void onSampleRateChange() {} - /** Deprecated */ - virtual void onCreate() {} - /** Deprecated */ - virtual void onDelete() {} - /** Called when user clicks Initialize in the module context menu */ - virtual void onReset() { - // Call deprecated method - reset(); - } - /** Called when user clicks Randomize in the module context menu */ - virtual void onRandomize() { - // Call deprecated method - randomize(); - } - - json_t *toJson(); - void fromJson(json_t *rootJ); - - /** Override these to store extra internal data in the "data" property of the module's JSON object */ - virtual json_t *dataToJson() { return NULL; } - virtual void dataFromJson(json_t *root) {} - - /** Deprecated */ - virtual void reset() {} - /** Deprecated */ - virtual void randomize() {} -}; - - -struct Wire { - Module *outputModule = NULL; - int outputId; - Module *inputModule = NULL; - int inputId; - void step(); -}; - - -void engineInit(); -void engineDestroy(); -/** Launches engine thread */ -void engineStart(); -void engineStop(); -/** Does not transfer pointer ownership */ -void engineAddModule(Module *module); -void engineRemoveModule(Module *module); -void engineResetModule(Module *module); -void engineRandomizeModule(Module *module); -/** Does not transfer pointer ownership */ -void engineAddWire(Wire *wire); -void engineRemoveWire(Wire *wire); -void engineSetParam(Module *module, int paramId, float value); -void engineSetParamSmooth(Module *module, int paramId, float value); -void engineSetSampleRate(float sampleRate); -float engineGetSampleRate(); -/** Returns the inverse of the current sample rate */ -float engineGetSampleTime(); - - -extern bool gPaused; -/** Plugins should not manipulate other modules or wires unless that is the entire purpose of the module. -Your plugin needs to have a clear purpose for manipulating other modules and wires and must be done with a good UX. -*/ -extern std::vector gModules; -extern std::vector gWires; -extern bool gPowerMeter; - - -} // namespace rack diff --git a/include/engine/Engine.hpp b/include/engine/Engine.hpp new file mode 100644 index 00000000..9ecf88ae --- /dev/null +++ b/include/engine/Engine.hpp @@ -0,0 +1,51 @@ +#pragma once +#include +#include "common.hpp" +#include "engine/Module.hpp" +#include "engine/Wire.hpp" + + +namespace rack { + + +struct Engine { + /** Plugins should not manipulate other modules or wires unless that is the entire purpose of the module. + Your plugin needs to have a clear purpose for manipulating other modules and wires and must be done with a good UX. + */ + std::vector modules; + std::vector wires; + bool paused = false; + bool powerMeter = false; + + struct Internal; + Internal *internal; + + Engine(); + ~Engine(); + /** Starts engine thread */ + void start(); + /** Stops engine thread */ + void stop(); + /** Does not transfer pointer ownership */ + void addModule(Module *module); + void removeModule(Module *module); + void resetModule(Module *module); + void randomizeModule(Module *module); + /** Does not transfer pointer ownership */ + void addWire(Wire *wire); + void removeWire(Wire *wire); + void setParam(Module *module, int paramId, float value); + void setParamSmooth(Module *module, int paramId, float value); + + void setSampleRate(float sampleRate); + float getSampleRate(); + /** Returns the inverse of the current sample rate */ + float getSampleTime(); +}; + + +// TODO Move to global state header +extern Engine *gEngine; + + +} // namespace rack diff --git a/include/engine/Input.hpp b/include/engine/Input.hpp new file mode 100644 index 00000000..9a908b25 --- /dev/null +++ b/include/engine/Input.hpp @@ -0,0 +1,23 @@ +#pragma once +#include "common.hpp" +#include "engine/Light.hpp" + + +namespace rack { + + +struct Input { + /** Voltage of the port, zero if not plugged in. Read-only by Module */ + float value = 0.f; + /** Whether a wire is plugged in */ + bool active = false; + Light plugLights[2]; + + /** Returns the value if a wire is plugged in, otherwise returns the given default value */ + float normalize(float normalValue) { + return active ? value : normalValue; + } +}; + + +} // namespace rack diff --git a/include/engine/Light.hpp b/include/engine/Light.hpp new file mode 100644 index 00000000..7abd206a --- /dev/null +++ b/include/engine/Light.hpp @@ -0,0 +1,21 @@ +#pragma once +#include "common.hpp" + + +namespace rack { + + +struct Light { + float value = 0.f; + float getBrightness(); + void setBrightness(float brightness) { + value = (brightness > 0.f) ? std::pow(brightness, 2) : 0.f; + } + /** Emulates slow fall (but immediate rise) of LED brightness. + `frames` rescales the timestep. For example, if your module calls this method every 16 frames, use 16.f. + */ + void setBrightnessSmooth(float brightness, float frames = 1.f); +}; + + +} // namespace rack diff --git a/include/engine/Module.hpp b/include/engine/Module.hpp new file mode 100644 index 00000000..a2c1890c --- /dev/null +++ b/include/engine/Module.hpp @@ -0,0 +1,58 @@ +#pragma once +#include +#include "common.hpp" +#include "engine/Param.hpp" +#include "engine/Input.hpp" +#include "engine/Output.hpp" +#include "engine/Light.hpp" +#include + + +namespace rack { + + +struct Module { + std::vector params; + std::vector inputs; + std::vector outputs; + std::vector lights; + /** For CPU usage meter */ + float cpuTime = 0.f; + + /** Constructs a Module with no params, inputs, outputs, and lights */ + Module() {} + /** Constructs a Module with a fixed number of params, inputs, outputs, and lights */ + Module(int numParams, int numInputs, int numOutputs, int numLights = 0) { + setup(numParams, numInputs, numOutputs, numLights); + } + virtual ~Module() {} + + void setup(int numParams, int numInputs, int numOutputs, int numLights = 0) { + params.resize(numParams); + inputs.resize(numInputs); + outputs.resize(numOutputs); + lights.resize(numLights); + } + + /** Advances the module by 1 audio frame with duration 1.0 / gSampleRate + Override this method to read inputs and params, and to write outputs and lights. + */ + virtual void step() {} + + /** Called when the engine sample rate is changed */ + virtual void onSampleRateChange() {} + /** Called when user clicks Initialize in the module context menu */ + virtual void onReset() {} + /** Called when user clicks Randomize in the module context menu */ + virtual void onRandomize() {} + + json_t *toJson(); + void fromJson(json_t *rootJ); + + /** Override these to store extra internal data in the "data" property of the module's JSON object */ + virtual json_t *dataToJson() { return NULL; } + virtual void dataFromJson(json_t *root) {} +}; + + +} // namespace rack diff --git a/include/engine/Output.hpp b/include/engine/Output.hpp new file mode 100644 index 00000000..b3084e40 --- /dev/null +++ b/include/engine/Output.hpp @@ -0,0 +1,18 @@ +#pragma once +#include "common.hpp" +#include "engine/Light.hpp" + + +namespace rack { + + +struct Output { + /** Voltage of the port. Write-only by Module */ + float value = 0.f; + /** Whether a wire is plugged in */ + bool active = false; + Light plugLights[2]; +}; + + +} // namespace rack diff --git a/include/engine/Param.hpp b/include/engine/Param.hpp new file mode 100644 index 00000000..6a1c66aa --- /dev/null +++ b/include/engine/Param.hpp @@ -0,0 +1,38 @@ +#pragma once +#include "common.hpp" +#include + + +namespace rack { + + +struct Param { + float value = 0.f; + float minValue = 0.f; + float maxValue = 1.f; + float defaultValue = 0.f; + + // For formatting/displaying the value + /** Set to 0 for linear, nonzero for exponential */ + float displayBase = 0.f; + float displayMultiplier = 1.f; + int displayPrecision = 2; + std::string label; + std::string unit; + + void setup(float minValue, float maxValue, float defaultValue, std::string label = "", std::string unit = "", int displayPrecision = 2) { + this->value = defaultValue; + this->minValue = minValue; + this->maxValue = maxValue; + this->defaultValue = defaultValue; + this->label = label; + this->unit = unit; + this->displayPrecision = displayPrecision; + } + + json_t *toJson(); + void fromJson(json_t *rootJ); +}; + + +} // namespace rack diff --git a/include/engine/Wire.hpp b/include/engine/Wire.hpp new file mode 100644 index 00000000..82b99caa --- /dev/null +++ b/include/engine/Wire.hpp @@ -0,0 +1,18 @@ +#pragma once +#include "common.hpp" +#include "engine/Module.hpp" + + +namespace rack { + + +struct Wire { + Module *outputModule = NULL; + int outputId; + Module *inputModule = NULL; + int inputId; + void step(); +}; + + +} // namespace rack diff --git a/include/helpers.hpp b/include/helpers.hpp index 12a86b42..86451358 100644 --- a/include/helpers.hpp +++ b/include/helpers.hpp @@ -1,7 +1,7 @@ #include "plugin.hpp" -#include "engine.hpp" #include "app.hpp" #include "event.hpp" +#include "engine/Module.hpp" namespace rack { diff --git a/include/rack.hpp b/include/rack.hpp index 7b2676a3..928ff270 100644 --- a/include/rack.hpp +++ b/include/rack.hpp @@ -8,7 +8,7 @@ #include "network.hpp" #include "asset.hpp" #include "plugin.hpp" -#include "engine.hpp" +#include "engine/Engine.hpp" #include "window.hpp" #include "widgets.hpp" #include "app.hpp" diff --git a/src/Core/AudioInterface.cpp b/src/Core/AudioInterface.cpp index fde51b70..21e27966 100644 --- a/src/Core/AudioInterface.cpp +++ b/src/Core/AudioInterface.cpp @@ -144,7 +144,7 @@ struct AudioInterface : Module { void AudioInterface::step() { // Update SRC states - int sampleRate = (int) engineGetSampleRate(); + int sampleRate = (int) gEngine->getSampleRate(); inputSrc.setRates(audioIO.sampleRate, sampleRate); outputSrc.setRates(sampleRate, audioIO.sampleRate); diff --git a/src/Core/MIDICCToCVInterface.cpp b/src/Core/MIDICCToCVInterface.cpp index 9603519d..1b966190 100644 --- a/src/Core/MIDICCToCVInterface.cpp +++ b/src/Core/MIDICCToCVInterface.cpp @@ -47,7 +47,7 @@ struct MIDICCToCVInterface : Module { processMessage(msg); } - float lambda = 100.f * engineGetSampleTime(); + float lambda = 100.f * gEngine->getSampleTime(); for (int i = 0; i < 16; i++) { int learnedCc = learnedCcs[i]; float value = rescale(clamp(ccs[learnedCc], -127, 127), 0, 127, 0.f, 10.f); diff --git a/src/Core/MIDIToCVInterface.cpp b/src/Core/MIDIToCVInterface.cpp index 3a6b7d50..390409bc 100644 --- a/src/Core/MIDIToCVInterface.cpp +++ b/src/Core/MIDIToCVInterface.cpp @@ -144,7 +144,7 @@ struct MIDIToCVInterface : Module { while (midiInput.shift(&msg)) { processMessage(msg); } - float deltaTime = engineGetSampleTime(); + float deltaTime = gEngine->getSampleTime(); outputs[CV_OUTPUT].value = (lastNote - 60) / 12.f; outputs[GATE_OUTPUT].value = gate ? 10.f : 0.f; diff --git a/src/Core/MIDITriggerToCVInterface.cpp b/src/Core/MIDITriggerToCVInterface.cpp index 04fc8b6f..b7aaeb9f 100644 --- a/src/Core/MIDITriggerToCVInterface.cpp +++ b/src/Core/MIDITriggerToCVInterface.cpp @@ -1,5 +1,6 @@ #include "Core.hpp" #include "midi.hpp" +#include "engine/Engine.hpp" #include "event.hpp" @@ -70,7 +71,7 @@ struct MIDITriggerToCVInterface : Module { while (midiInput.shift(&msg)) { processMessage(msg); } - float deltaTime = engineGetSampleTime(); + float deltaTime = gEngine->getSampleTime(); for (int i = 0; i < 16; i++) { if (gateTimes[i] > 0.f) { diff --git a/src/app/ModuleLightWidget.cpp b/src/app/ModuleLightWidget.cpp index 27da8a66..ff79f8c0 100644 --- a/src/app/ModuleLightWidget.cpp +++ b/src/app/ModuleLightWidget.cpp @@ -1,5 +1,4 @@ #include "app.hpp" -#include "engine.hpp" namespace rack { diff --git a/src/app/ModuleWidget.cpp b/src/app/ModuleWidget.cpp index 611a6ca4..8f36ca08 100644 --- a/src/app/ModuleWidget.cpp +++ b/src/app/ModuleWidget.cpp @@ -1,4 +1,5 @@ #include "osdialog.h" +#include "engine/Engine.hpp" #include "rack.hpp" @@ -7,7 +8,7 @@ namespace rack { ModuleWidget::ModuleWidget(Module *module) { if (module) { - engineAddModule(module); + gEngine->addModule(module); } this->module = module; } @@ -17,7 +18,7 @@ ModuleWidget::~ModuleWidget() { disconnect(); // Remove and delete the Module instance if (module) { - engineRemoveModule(module); + gEngine->removeModule(module); delete module; module = NULL; } @@ -229,7 +230,7 @@ void ModuleWidget::reset() { param->reset(); } if (module) { - engineResetModule(module); + gEngine->resetModule(module); } } @@ -238,7 +239,7 @@ void ModuleWidget::randomize() { param->randomize(); } if (module) { - engineRandomizeModule(module); + gEngine->randomizeModule(module); } } @@ -247,7 +248,7 @@ void ModuleWidget::draw(NVGcontext *vg) { Widget::draw(vg); // Power meter - if (module && gPowerMeter) { + if (module && gEngine->powerMeter) { nvgBeginPath(vg); nvgRect(vg, 0, box.size.y - 20, diff --git a/src/app/ParamWidget.cpp b/src/app/ParamWidget.cpp index f0bfc2fe..8eda1c3b 100644 --- a/src/app/ParamWidget.cpp +++ b/src/app/ParamWidget.cpp @@ -1,5 +1,4 @@ #include "app/ParamWidget.hpp" -#include "engine.hpp" #include "random.hpp" diff --git a/src/app/Port.cpp b/src/app/Port.cpp index 4ade46bb..2a9e3fff 100644 --- a/src/app/Port.cpp +++ b/src/app/Port.cpp @@ -1,6 +1,5 @@ #include "app.hpp" #include "window.hpp" -#include "engine.hpp" #include "componentlibrary.hpp" diff --git a/src/app/Toolbar.cpp b/src/app/Toolbar.cpp index d7014250..fbd08878 100644 --- a/src/app/Toolbar.cpp +++ b/src/app/Toolbar.cpp @@ -1,6 +1,6 @@ #include "app.hpp" #include "window.hpp" -#include "engine.hpp" +#include "engine/Engine.hpp" #include "asset.hpp" #include "helpers.hpp" @@ -96,21 +96,21 @@ struct PowerMeterButton : TooltipIconButton { } std::string getTooltipText() override {return "Toggle power meter (see manual for explanation)";} void onAction(event::Action &e) override { - gPowerMeter ^= true; + gEngine->powerMeter ^= true; } }; struct EnginePauseItem : MenuItem { void onAction(event::Action &e) override { - gPaused ^= true; + gEngine->paused ^= true; } }; struct SampleRateItem : MenuItem { float sampleRate; void onAction(event::Action &e) override { - engineSetSampleRate(sampleRate); - gPaused = false; + gEngine->setSampleRate(sampleRate); + gEngine->paused = false; } }; @@ -127,14 +127,14 @@ struct SampleRateButton : TooltipIconButton { menu->addChild(createMenuLabel("Engine sample rate")); EnginePauseItem *pauseItem = new EnginePauseItem; - pauseItem->text = gPaused ? "Resume engine" : "Pause engine"; + pauseItem->text = gEngine->paused ? "Resume engine" : "Pause engine"; menu->addChild(pauseItem); std::vector sampleRates = {44100, 48000, 88200, 96000, 176400, 192000}; for (float sampleRate : sampleRates) { SampleRateItem *item = new SampleRateItem; item->text = string::f("%.0f Hz", sampleRate); - item->rightText = CHECKMARK(engineGetSampleRate() == sampleRate); + item->rightText = CHECKMARK(gEngine->getSampleRate() == sampleRate); item->sampleRate = sampleRate; menu->addChild(item); } diff --git a/src/app/WireWidget.cpp b/src/app/WireWidget.cpp index 7dc14aab..c78a3690 100644 --- a/src/app/WireWidget.cpp +++ b/src/app/WireWidget.cpp @@ -1,5 +1,5 @@ #include "app.hpp" -#include "engine.hpp" +#include "engine/Engine.hpp" #include "componentlibrary.hpp" #include "window.hpp" #include "event.hpp" @@ -107,12 +107,12 @@ void WireWidget::updateWire() { wire->outputId = outputPort->portId; wire->inputModule = inputPort->module; wire->inputId = inputPort->portId; - engineAddWire(wire); + gEngine->addWire(wire); } } else { if (wire) { - engineRemoveWire(wire); + gEngine->removeWire(wire); delete wire; wire = NULL; } diff --git a/src/engine.cpp b/src/engine.cpp deleted file mode 100644 index 9ab5ddb8..00000000 --- a/src/engine.cpp +++ /dev/null @@ -1,411 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include -#include - -#include "rack.hpp" -#include "engine.hpp" - - -namespace rack { - -bool gPaused = false; -std::vector gModules; -std::vector gWires; -bool gPowerMeter = false; - -static bool running = false; -static float sampleRate = 44100.f; -static float sampleTime = 1.f / sampleRate; -static float sampleRateRequested = sampleRate; - -static Module *resetModule = NULL; -static Module *randomizeModule = NULL; - - -/** Threads which obtain a VIPLock will cause wait() to block for other less important threads. -This does not provide the VIPs with an exclusive lock. That should be left up to another mutex shared between the less important thread. -*/ -struct VIPMutex { - int count = 0; - std::condition_variable cv; - std::mutex countMutex; - - /** Blocks until there are no remaining VIPLocks */ - void wait() { - std::unique_lock lock(countMutex); - while (count > 0) - cv.wait(lock); - } -}; - -struct VIPLock { - VIPMutex &m; - VIPLock(VIPMutex &m) : m(m) { - std::unique_lock lock(m.countMutex); - m.count++; - } - ~VIPLock() { - std::unique_lock lock(m.countMutex); - m.count--; - lock.unlock(); - m.cv.notify_all(); - } -}; - - -static std::mutex mutex; -static std::thread thread; -static VIPMutex vipMutex; - -// Parameter interpolation -static Module *smoothModule = NULL; -static int smoothParamId; -static float smoothValue; - - -json_t *Param::toJson() { - json_t *rootJ = json_object(); - - // Infinite params should serialize to 0 - float v = (std::isfinite(minValue) && std::isfinite(maxValue)) ? value : 0.f; - json_object_set_new(rootJ, "value", json_real(v)); - - return rootJ; -} - -void Param::fromJson(json_t *rootJ) { - json_t *valueJ = json_object_get(rootJ, "value"); - if (valueJ) - value = json_number_value(valueJ); -} - - -float Light::getBrightness() { - // LEDs are diodes, so don't allow reverse current. - // For some reason, instead of the RMS, the sqrt of RMS looks better - return std::pow(std::fmaxf(0.f, value), 0.25f); -} - -void Light::setBrightnessSmooth(float brightness, float frames) { - float v = (brightness > 0.f) ? brightness * brightness : 0.f; - if (v < value) { - // Fade out light with lambda = framerate - value += (v - value) * sampleTime * frames * 60.f; - } - else { - // Immediately illuminate light - value = v; - } -} - - -json_t *Module::toJson() { - json_t *rootJ = json_object(); - - // params - json_t *paramsJ = json_array(); - for (Param ¶m : params) { - json_t *paramJ = param.toJson(); - json_array_append_new(paramsJ, paramJ); - } - json_object_set_new(rootJ, "params", paramsJ); - - // data - json_t *dataJ = dataToJson(); - if (dataJ) { - json_object_set_new(rootJ, "data", dataJ); - } - - return rootJ; -} - -void Module::fromJson(json_t *rootJ) { - // params - json_t *paramsJ = json_object_get(rootJ, "params"); - size_t i; - json_t *paramJ; - json_array_foreach(paramsJ, i, paramJ) { - uint32_t paramId = i; - // Get paramId - json_t *paramIdJ = json_object_get(paramJ, "paramId"); - if (paramIdJ) { - // Legacy v0.6.0 to outputs[outputId].value; - inputModule->inputs[inputId].value = value; -} - - -void engineInit() { -} - -void engineDestroy() { - // Make sure there are no wires or modules in the rack on destruction. This suggests that a module failed to remove itself before the RackWidget was destroyed. - assert(gWires.empty()); - assert(gModules.empty()); -} - -static void engineStep() { - // Sample rate - if (sampleRateRequested != sampleRate) { - sampleRate = sampleRateRequested; - sampleTime = 1.f / sampleRate; - for (Module *module : gModules) { - module->onSampleRateChange(); - } - } - - // Events - if (resetModule) { - resetModule->onReset(); - resetModule = NULL; - } - if (randomizeModule) { - randomizeModule->onRandomize(); - randomizeModule = NULL; - } - - // Param smoothing - { - Module *localSmoothModule = smoothModule; - int localSmoothParamId = smoothParamId; - float localSmoothValue = smoothValue; - if (localSmoothModule) { - float value = localSmoothModule->params[localSmoothParamId].value; - const float lambda = 60.0; // decay rate is 1 graphics frame - float delta = localSmoothValue - value; - float newValue = value + delta * lambda * sampleTime; - if (value == newValue) { - // Snap to actual smooth value if the value doesn't change enough (due to the granularity of floats) - localSmoothModule->params[localSmoothParamId].value = localSmoothValue; - smoothModule = NULL; - } - else { - localSmoothModule->params[localSmoothParamId].value = newValue; - } - } - } - - // Step modules - for (Module *module : gModules) { - std::chrono::high_resolution_clock::time_point startTime; - if (gPowerMeter) { - startTime = std::chrono::high_resolution_clock::now(); - - module->step(); - - auto stopTime = std::chrono::high_resolution_clock::now(); - float cpuTime = std::chrono::duration(stopTime - startTime).count() * sampleRate; - module->cpuTime += (cpuTime - module->cpuTime) * sampleTime / 0.5f; - } - else { - module->step(); - } - - // Step ports - for (Input &input : module->inputs) { - if (input.active) { - float value = input.value / 5.f; - input.plugLights[0].setBrightnessSmooth(value); - input.plugLights[1].setBrightnessSmooth(-value); - } - } - for (Output &output : module->outputs) { - if (output.active) { - float value = output.value / 5.f; - output.plugLights[0].setBrightnessSmooth(value); - output.plugLights[1].setBrightnessSmooth(-value); - } - } - } - - // Step cables by moving their output values to inputs - for (Wire *wire : gWires) { - wire->step(); - } -} - -static void engineRun() { - // Set CPU to flush-to-zero (FTZ) and denormals-are-zero (DAZ) mode - // https://software.intel.com/en-us/node/682949 - _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON); - _MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON); - - // Every time the engine waits and locks a mutex, it steps this many frames - const int mutexSteps = 64; - // Time in seconds that the engine is rushing ahead of the estimated clock time - double ahead = 0.0; - auto lastTime = std::chrono::high_resolution_clock::now(); - - while (running) { - vipMutex.wait(); - - if (!gPaused) { - std::lock_guard lock(mutex); - for (int i = 0; i < mutexSteps; i++) { - engineStep(); - } - } - - double stepTime = mutexSteps * sampleTime; - ahead += stepTime; - auto currTime = std::chrono::high_resolution_clock::now(); - const double aheadFactor = 2.0; - ahead -= aheadFactor * std::chrono::duration(currTime - lastTime).count(); - lastTime = currTime; - ahead = fmaxf(ahead, 0.0); - - // Avoid pegging the CPU at 100% when there are no "blocking" modules like AudioInterface, but still step audio at a reasonable rate - // The number of steps to wait before possibly sleeping - const double aheadMax = 1.0; // seconds - if (ahead > aheadMax) { - std::this_thread::sleep_for(std::chrono::duration(stepTime)); - } - } -} - -void engineStart() { - running = true; - thread = std::thread(engineRun); -} - -void engineStop() { - running = false; - thread.join(); -} - -void engineAddModule(Module *module) { - assert(module); - VIPLock vipLock(vipMutex); - std::lock_guard lock(mutex); - // Check that the module is not already added - auto it = std::find(gModules.begin(), gModules.end(), module); - assert(it == gModules.end()); - gModules.push_back(module); -} - -void engineRemoveModule(Module *module) { - assert(module); - VIPLock vipLock(vipMutex); - std::lock_guard lock(mutex); - // If a param is being smoothed on this module, stop smoothing it immediately - if (module == smoothModule) { - smoothModule = NULL; - } - // Check that all wires are disconnected - for (Wire *wire : gWires) { - assert(wire->outputModule != module); - assert(wire->inputModule != module); - } - // Check that the module actually exists - auto it = std::find(gModules.begin(), gModules.end(), module); - assert(it != gModules.end()); - // Remove it - gModules.erase(it); -} - -void engineResetModule(Module *module) { - resetModule = module; -} - -void engineRandomizeModule(Module *module) { - randomizeModule = module; -} - -static void updateActive() { - // Set everything to inactive - for (Module *module : gModules) { - for (Input &input : module->inputs) { - input.active = false; - } - for (Output &output : module->outputs) { - output.active = false; - } - } - // Set inputs/outputs to active - for (Wire *wire : gWires) { - wire->outputModule->outputs[wire->outputId].active = true; - wire->inputModule->inputs[wire->inputId].active = true; - } -} - -void engineAddWire(Wire *wire) { - assert(wire); - VIPLock vipLock(vipMutex); - std::lock_guard lock(mutex); - // Check wire properties - assert(wire->outputModule); - assert(wire->inputModule); - // Check that the wire is not already added, and that the input is not already used by another cable - for (Wire *wire2 : gWires) { - assert(wire2 != wire); - assert(!(wire2->inputModule == wire->inputModule && wire2->inputId == wire->inputId)); - } - // Add the wire - gWires.push_back(wire); - updateActive(); -} - -void engineRemoveWire(Wire *wire) { - assert(wire); - VIPLock vipLock(vipMutex); - std::lock_guard lock(mutex); - // Check that the wire is already added - auto it = std::find(gWires.begin(), gWires.end(), wire); - assert(it != gWires.end()); - // Set input to 0V - wire->inputModule->inputs[wire->inputId].value = 0.0; - // Remove the wire - gWires.erase(it); - updateActive(); -} - -void engineSetParam(Module *module, int paramId, float value) { - module->params[paramId].value = value; -} - -void engineSetParamSmooth(Module *module, int paramId, float value) { - // If another param is being smoothed, jump value - if (smoothModule && !(smoothModule == module && smoothParamId == paramId)) { - smoothModule->params[smoothParamId].value = smoothValue; - } - smoothParamId = paramId; - smoothValue = value; - smoothModule = module; -} - -void engineSetSampleRate(float newSampleRate) { - sampleRateRequested = newSampleRate; -} - -float engineGetSampleRate() { - return sampleRate; -} - -float engineGetSampleTime() { - return sampleTime; -} - -} // namespace rack diff --git a/src/engine/Engine.cpp b/src/engine/Engine.cpp new file mode 100644 index 00000000..a2ebab3c --- /dev/null +++ b/src/engine/Engine.cpp @@ -0,0 +1,332 @@ +#include +#include +#include +#include +#include + +#include +#include + +#include "engine/Engine.hpp" + + +namespace rack { + + +/** Threads which obtain a VIPLock will cause wait() to block for other less important threads. +This does not provide the VIPs with an exclusive lock. That should be left up to another mutex shared between the less important thread. +*/ +struct VIPMutex { + int count = 0; + std::condition_variable cv; + std::mutex countMutex; + + /** Blocks until there are no remaining VIPLocks */ + void wait() { + std::unique_lock lock(countMutex); + while (count > 0) + cv.wait(lock); + } +}; + +struct VIPLock { + VIPMutex &m; + VIPLock(VIPMutex &m) : m(m) { + std::unique_lock lock(m.countMutex); + m.count++; + } + ~VIPLock() { + std::unique_lock lock(m.countMutex); + m.count--; + lock.unlock(); + m.cv.notify_all(); + } +}; + + +struct Engine::Internal { + bool running = false; + float sampleRate; + float sampleTime; + float sampleRateRequested; + + Module *resetModule = NULL; + Module *randomizeModule = NULL; + + // Parameter smoothing + Module *smoothModule = NULL; + int smoothParamId; + float smoothValue; + + std::mutex mutex; + std::thread thread; + VIPMutex vipMutex; +}; + + +Engine::Engine() { + internal = new Internal; + + float sampleRate = 44100.f; + internal->sampleRate = sampleRate; + internal->sampleTime = 1 / sampleRate; + internal->sampleRateRequested = sampleRate; +} + +Engine::~Engine() { + // Make sure there are no wires or modules in the rack on destruction. This suggests that a module failed to remove itself before the RackWidget was destroyed. + assert(wires.empty()); + assert(modules.empty()); + + delete internal; +} + +static void Engine_step(Engine *engine) { + // Sample rate + if (engine->internal->sampleRateRequested != engine->internal->sampleRate) { + engine->internal->sampleRate = engine->internal->sampleRateRequested; + engine->internal->sampleTime = 1 / engine->internal->sampleRate; + for (Module *module : engine->modules) { + module->onSampleRateChange(); + } + } + + // Events + if (engine->internal->resetModule) { + engine->internal->resetModule->onReset(); + engine->internal->resetModule = NULL; + } + if (engine->internal->randomizeModule) { + engine->internal->randomizeModule->onRandomize(); + engine->internal->randomizeModule = NULL; + } + + // Param smoothing + { + // Store in local variables for thread safety + Module *localSmoothModule = engine->internal->smoothModule; + int localSmoothParamId = engine->internal->smoothParamId; + float localSmoothValue = engine->internal->smoothValue; + if (localSmoothModule) { + float value = localSmoothModule->params[localSmoothParamId].value; + // decay rate is 1 graphics frame + const float lambda = 60.f; + float delta = localSmoothValue - value; + float newValue = value + delta * lambda * engine->internal->sampleTime; + if (value == newValue) { + // Snap to actual smooth value if the value doesn't change enough (due to the granularity of floats) + localSmoothModule->params[localSmoothParamId].value = localSmoothValue; + engine->internal->smoothModule = NULL; + } + else { + localSmoothModule->params[localSmoothParamId].value = newValue; + } + } + } + + // Step modules + for (Module *module : engine->modules) { + if (engine->powerMeter) { + auto startTime = std::chrono::high_resolution_clock::now(); + + module->step(); + + auto stopTime = std::chrono::high_resolution_clock::now(); + float cpuTime = std::chrono::duration(stopTime - startTime).count() * engine->internal->sampleRate; + // Smooth cpu time + module->cpuTime += (cpuTime - module->cpuTime) * engine->internal->sampleTime / 0.5f; + } + else { + module->step(); + } + + // Step ports + for (Input &input : module->inputs) { + if (input.active) { + float value = input.value / 5.f; + input.plugLights[0].setBrightnessSmooth(value); + input.plugLights[1].setBrightnessSmooth(-value); + } + } + for (Output &output : module->outputs) { + if (output.active) { + float value = output.value / 5.f; + output.plugLights[0].setBrightnessSmooth(value); + output.plugLights[1].setBrightnessSmooth(-value); + } + } + } + + // Step cables by moving their output values to inputs + for (Wire *wire : engine->wires) { + wire->step(); + } +} + +static void Engine_run(Engine *engine) { + // Set CPU to flush-to-zero (FTZ) and denormals-are-zero (DAZ) mode + // https://software.intel.com/en-us/node/682949 + _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON); + _MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON); + + // Every time the engine waits and locks a mutex, it steps this many frames + const int mutexSteps = 64; + // Time in seconds that the engine is rushing ahead of the estimated clock time + double ahead = 0.0; + auto lastTime = std::chrono::high_resolution_clock::now(); + + while (engine->internal->running) { + engine->internal->vipMutex.wait(); + + if (!engine->paused) { + std::lock_guard lock(engine->internal->mutex); + for (int i = 0; i < mutexSteps; i++) { + Engine_step(engine); + } + } + + double stepTime = mutexSteps * engine->internal->sampleTime; + ahead += stepTime; + auto currTime = std::chrono::high_resolution_clock::now(); + const double aheadFactor = 2.0; + ahead -= aheadFactor * std::chrono::duration(currTime - lastTime).count(); + lastTime = currTime; + ahead = std::fmax(ahead, 0.0); + + // Avoid pegging the CPU at 100% when there are no "blocking" modules like AudioInterface, but still step audio at a reasonable rate + // The number of steps to wait before possibly sleeping + const double aheadMax = 1.0; // seconds + if (ahead > aheadMax) { + std::this_thread::sleep_for(std::chrono::duration(stepTime)); + } + } +} + +void Engine::start() { + internal->running = true; + internal->thread = std::thread(Engine_run, this); +} + +void Engine::stop() { + internal->running = false; + internal->thread.join(); +} + +void Engine::addModule(Module *module) { + assert(module); + VIPLock vipLock(internal->vipMutex); + std::lock_guard lock(internal->mutex); + // Check that the module is not already added + auto it = std::find(modules.begin(), modules.end(), module); + assert(it == modules.end()); + modules.push_back(module); +} + +void Engine::removeModule(Module *module) { + assert(module); + VIPLock vipLock(internal->vipMutex); + std::lock_guard lock(internal->mutex); + // If a param is being smoothed on this module, stop smoothing it immediately + if (module == internal->smoothModule) { + internal->smoothModule = NULL; + } + // Check that all wires are disconnected + for (Wire *wire : wires) { + assert(wire->outputModule != module); + assert(wire->inputModule != module); + } + // Check that the module actually exists + auto it = std::find(modules.begin(), modules.end(), module); + assert(it != modules.end()); + // Remove it + modules.erase(it); +} + +void Engine::resetModule(Module *module) { + internal->resetModule = module; +} + +void Engine::randomizeModule(Module *module) { + internal->randomizeModule = module; +} + +static void Engine_updateActive(Engine *engine) { + // Set everything to inactive + for (Module *module : engine->modules) { + for (Input &input : module->inputs) { + input.active = false; + } + for (Output &output : module->outputs) { + output.active = false; + } + } + // Set inputs/outputs to active + for (Wire *wire : engine->wires) { + wire->outputModule->outputs[wire->outputId].active = true; + wire->inputModule->inputs[wire->inputId].active = true; + } +} + +void Engine::addWire(Wire *wire) { + assert(wire); + VIPLock vipLock(internal->vipMutex); + std::lock_guard lock(internal->mutex); + // Check wire properties + assert(wire->outputModule); + assert(wire->inputModule); + // Check that the wire is not already added, and that the input is not already used by another cable + for (Wire *wire2 : wires) { + assert(wire2 != wire); + assert(!(wire2->inputModule == wire->inputModule && wire2->inputId == wire->inputId)); + } + // Add the wire + wires.push_back(wire); + Engine_updateActive(this); +} + +void Engine::removeWire(Wire *wire) { + assert(wire); + VIPLock vipLock(internal->vipMutex); + std::lock_guard lock(internal->mutex); + // Check that the wire is already added + auto it = std::find(wires.begin(), wires.end(), wire); + assert(it != wires.end()); + // Set input to 0V + wire->inputModule->inputs[wire->inputId].value = 0.f; + // Remove the wire + wires.erase(it); + Engine_updateActive(this); +} + +void Engine::setParam(Module *module, int paramId, float value) { + // TODO Make thread safe + module->params[paramId].value = value; +} + +void Engine::setParamSmooth(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; + internal->smoothModule = module; +} + +void Engine::setSampleRate(float newSampleRate) { + internal->sampleRateRequested = newSampleRate; +} + +float Engine::getSampleRate() { + return internal->sampleRate; +} + +float Engine::getSampleTime() { + return internal->sampleTime; +} + + +Engine *gEngine = NULL; + + +} // namespace rack diff --git a/src/engine/Light.cpp b/src/engine/Light.cpp new file mode 100644 index 00000000..47b3f05a --- /dev/null +++ b/src/engine/Light.cpp @@ -0,0 +1,27 @@ +#include "engine/Light.hpp" +#include "engine/Engine.hpp" + + +namespace rack { + + +float Light::getBrightness() { + // LEDs are diodes, so don't allow reverse current. + // For some reason, instead of the RMS, the sqrt of RMS looks better + return std::pow(std::fmax(0.f, value), 0.25f); +} + +void Light::setBrightnessSmooth(float brightness, float frames) { + float v = (brightness > 0.f) ? std::pow(brightness, 2) : 0.f; + if (v < value) { + // Fade out light with lambda = framerate + value += (v - value) * gEngine->getSampleTime() * frames * 60.f; + } + else { + // Immediately illuminate light + value = v; + } +} + + +} // namespace rack diff --git a/src/engine/Module.cpp b/src/engine/Module.cpp new file mode 100644 index 00000000..cd9cba03 --- /dev/null +++ b/src/engine/Module.cpp @@ -0,0 +1,54 @@ +#include "engine/Module.hpp" + + +namespace rack { + + +json_t *Module::toJson() { + json_t *rootJ = json_object(); + + // params + json_t *paramsJ = json_array(); + for (Param ¶m : params) { + json_t *paramJ = param.toJson(); + json_array_append_new(paramsJ, paramJ); + } + json_object_set_new(rootJ, "params", paramsJ); + + // data + json_t *dataJ = dataToJson(); + if (dataJ) { + json_object_set_new(rootJ, "data", dataJ); + } + + return rootJ; +} + +void Module::fromJson(json_t *rootJ) { + // params + json_t *paramsJ = json_object_get(rootJ, "params"); + size_t i; + json_t *paramJ; + json_array_foreach(paramsJ, i, paramJ) { + uint32_t paramId = i; + // Get paramId + json_t *paramIdJ = json_object_get(paramJ, "paramId"); + if (paramIdJ) { + // Legacy v0.6.0 to outputs[outputId].value; + inputModule->inputs[inputId].value = value; +} + + +} // namespace rack diff --git a/src/main.cpp b/src/main.cpp index 327d26ab..6233b998 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,6 +6,7 @@ #include "gamepad.hpp" #include "bridge.hpp" #include "settings.hpp" +#include "engine/Engine.hpp" #ifdef ARCH_WIN #include @@ -64,7 +65,7 @@ int main(int argc, char* argv[]) { // Initialize app pluginInit(devMode); - engineInit(); + gEngine = new Engine; rtmidiInit(); bridgeInit(); keyboard::init(); @@ -95,9 +96,9 @@ int main(int argc, char* argv[]) { gRackWidget->lastPath = patchFile; } - engineStart(); + gEngine->start(); windowRun(); - engineStop(); + gEngine->stop(); // Destroy namespaces gRackWidget->save(asset::local("autosave.vcv")); @@ -105,7 +106,7 @@ int main(int argc, char* argv[]) { appDestroy(); windowDestroy(); bridgeDestroy(); - engineDestroy(); + delete gEngine; midiDestroy(); pluginDestroy(); logger::destroy(); diff --git a/src/settings.cpp b/src/settings.cpp index 37c92748..652fddb6 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -1,6 +1,10 @@ -#include -#include "rack.hpp" #include "settings.hpp" +#include "logger.hpp" +#include "window.hpp" +#include "plugin.hpp" +#include "app.hpp" +#include "engine/Engine.hpp" +#include namespace rack { @@ -50,7 +54,7 @@ static json_t *settingsToJson() { json_object_set_new(rootJ, "allowCursorLock", allowCursorLockJ); // sampleRate - json_t *sampleRateJ = json_real(engineGetSampleRate()); + json_t *sampleRateJ = json_real(gEngine->getSampleRate()); json_object_set_new(rootJ, "sampleRate", sampleRateJ); // lastPath @@ -66,7 +70,7 @@ static json_t *settingsToJson() { json_object_set_new(rootJ, "moduleBrowser", appModuleBrowserToJson()); // powerMeter - json_object_set_new(rootJ, "powerMeter", json_boolean(gPowerMeter)); + json_object_set_new(rootJ, "powerMeter", json_boolean(gEngine->powerMeter)); // checkVersion json_object_set_new(rootJ, "checkVersion", json_boolean(gCheckVersion)); @@ -121,7 +125,7 @@ static void settingsFromJson(json_t *rootJ) { json_t *sampleRateJ = json_object_get(rootJ, "sampleRate"); if (sampleRateJ) { float sampleRate = json_number_value(sampleRateJ); - engineSetSampleRate(sampleRate); + gEngine->setSampleRate(sampleRate); } // lastPath @@ -142,7 +146,7 @@ static void settingsFromJson(json_t *rootJ) { // powerMeter json_t *powerMeterJ = json_object_get(rootJ, "powerMeter"); if (powerMeterJ) - gPowerMeter = json_boolean_value(powerMeterJ); + gEngine->powerMeter = json_boolean_value(powerMeterJ); // checkVersion json_t *checkVersionJ = json_object_get(rootJ, "checkVersion");