From 7458b5d709552d576570e1d11858aebd6adcc8d5 Mon Sep 17 00:00:00 2001 From: Andrew Belt Date: Fri, 8 Feb 2019 16:25:38 -0500 Subject: [PATCH] Add param touch to engine. Add param learning to MIDI-Map. --- include/dsp/filter.hpp | 23 +++-- include/engine/Engine.hpp | 18 +++- include/engine/Module.hpp | 7 ++ include/engine/Param.hpp | 3 + include/engine/ParamMap.hpp | 20 ---- include/rack.hpp | 1 - src/Core/MIDI_CC.cpp | 2 +- src/Core/MIDI_Map.cpp | 199 ++++++++++++++++++++++++++++-------- src/app/ParamWidget.cpp | 8 ++ src/engine/Engine.cpp | 60 +++++++++++ src/engine/ParamMap.cpp | 27 ----- 11 files changed, 268 insertions(+), 100 deletions(-) delete mode 100644 include/engine/ParamMap.hpp delete mode 100644 src/engine/ParamMap.cpp diff --git a/include/dsp/filter.hpp b/include/dsp/filter.hpp index 218e03ac..030ba11b 100644 --- a/include/dsp/filter.hpp +++ b/include/dsp/filter.hpp @@ -87,20 +87,29 @@ struct ExponentialSlewLimiter { \f$ \frac{dy}{dt} = x \lambda \f$. */ struct ExponentialFilter { - float out = 0.f; + float out; float lambda = 0.f; + ExponentialFilter() { + reset(); + } + void reset() { - out = 0.f; + out = NAN; } float process(float deltaTime, float in) { - float y = out + (in - out) * lambda * deltaTime; - // If no change was detected, assume float granularity is too small and snap output to input - if (out == y) + if (std::isnan(out)) { out = in; - else - out = y; + } + else { + float y = out + (in - out) * lambda * deltaTime; + // If no change was detected, assume float granularity is too small and snap output to input + if (out == y) + out = in; + else + out = y; + } return out; } diff --git a/include/engine/Engine.hpp b/include/engine/Engine.hpp index ee196b44..afca096b 100644 --- a/include/engine/Engine.hpp +++ b/include/engine/Engine.hpp @@ -29,7 +29,11 @@ struct Engine { float getSampleTime(); // Modules - /** Does not transfer pointer ownership. */ + /** Adds a module to the rack engine. + The module ID must not be taken by another module. + If the module ID is -1, an ID is automatically assigned. + Does not transfer pointer ownership. + */ void addModule(Module *module); void removeModule(Module *module); Module *getModule(int moduleId); @@ -38,7 +42,11 @@ struct Engine { void bypassModule(Module *module, bool bypass); // Cables - /** Does not transfer pointer ownership. */ + /** Adds a cable to the rack engine. + The cable ID must not be taken by another cable. + If the cable ID is -1, an ID is automatically assigned. + Does not transfer pointer ownership. + */ void addCable(Cable *cable); void removeCable(Cable *cable); @@ -47,6 +55,12 @@ struct Engine { float getParam(Module *module, int paramId); void setSmoothParam(Module *module, int paramId, float value); float getSmoothParam(Module *module, int paramId); + void setTouchedParam(Module *module, int paramId); + void getTouchedParam(Module *&module, int ¶mId); + + // ModuleHandles + void addModuleHandle(ModuleHandle *moduleHandle); + void removeModuleHandle(ModuleHandle *moduleHandle); }; diff --git a/include/engine/Module.hpp b/include/engine/Module.hpp index 905a975b..ad912cdc 100644 --- a/include/engine/Module.hpp +++ b/include/engine/Module.hpp @@ -56,5 +56,12 @@ struct Module { }; +struct ModuleHandle { + int id = -1; + /** Automatically set when added to the Engine. */ + Module *module = NULL; +}; + + } // namespace engine } // namespace rack diff --git a/include/engine/Param.hpp b/include/engine/Param.hpp index 10e19b6a..2790531f 100644 --- a/include/engine/Param.hpp +++ b/include/engine/Param.hpp @@ -81,6 +81,9 @@ struct Param { return value; } + /* Clamps and set the value. + Don't call this directly from Modules. Use `APP->engine->setParam()`. + */ void setValue(float value) { this->value = math::clamp(value, minValue, maxValue); } diff --git a/include/engine/ParamMap.hpp b/include/engine/ParamMap.hpp deleted file mode 100644 index 10bb43b1..00000000 --- a/include/engine/ParamMap.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once -#include "common.hpp" -#include - - -namespace rack { -namespace engine { - - -struct ParamMap { - int moduleId = -1; - int paramId = -1; - - json_t *toJson(); - void fromJson(json_t *rootJ); -}; - - -} // namespace engine -} // namespace rack diff --git a/include/rack.hpp b/include/rack.hpp index b5233346..3d9a0ab7 100644 --- a/include/rack.hpp +++ b/include/rack.hpp @@ -72,7 +72,6 @@ #include "engine/Module.hpp" #include "engine/Param.hpp" #include "engine/Cable.hpp" -#include "engine/ParamMap.hpp" #include "plugin/Plugin.hpp" #include "plugin/Model.hpp" diff --git a/src/Core/MIDI_CC.cpp b/src/Core/MIDI_CC.cpp index fc6f86cf..41ca4fa7 100644 --- a/src/Core/MIDI_CC.cpp +++ b/src/Core/MIDI_CC.cpp @@ -91,7 +91,7 @@ struct MIDI_CC : Module { } // Allow CC to be negative if the 8th bit is set. // The gamepad driver abuses this, for example. - values[cc] = msg.data2; + values[cc] = clamp(msg.data2, -127, 127); } json_t *dataToJson() override { diff --git a/src/Core/MIDI_Map.cpp b/src/Core/MIDI_Map.cpp index d4021357..b67d0b7a 100644 --- a/src/Core/MIDI_Map.cpp +++ b/src/Core/MIDI_Map.cpp @@ -16,26 +16,38 @@ struct MIDI_Map : Module { }; midi::InputQueue midiInput; - int8_t values[128]; int learningId; - int lastLearnedCc; int learnedCcs[8]; + ModuleHandle learnedModuleHandles[8]; + int learnedParamIds[8]; + int8_t values[128]; dsp::ExponentialFilter valueFilters[8]; - ParamMap paramMaps[8]; MIDI_Map() { config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); for (int i = 0; i < 8; i++) { - valueFilters[i].lambda = 40.f; + valueFilters[i].lambda = 60.f; } onReset(); } + ~MIDI_Map() { + for (int i = 0; i < 8; i++) { + unloadModuleHandle(i); + } + } + void onReset() override { learningId = -1; - lastLearnedCc = -1; for (int i = 0; i < 8; i++) { learnedCcs[i] = -1; + unloadModuleHandle(i); + learnedModuleHandles[i].id = -1; + learnedParamIds[i] = 0; + valueFilters[i].reset(); + } + for (int i = 0; i < 128; i++) { + values[i] = -1; } midiInput.reset(); } @@ -48,23 +60,43 @@ struct MIDI_Map : Module { float deltaTime = APP->engine->getSampleTime(); + // Check touched params when learning + if (learningId >= 0) { + Module *module; + int paramId; + APP->engine->getTouchedParam(module, paramId); + APP->engine->setTouchedParam(NULL, 0); + if (module) { + unloadModuleHandle(learningId); + learnedModuleHandles[learningId].id = module->id; + loadModuleHandle(learningId); + learnedParamIds[learningId] = paramId; + commitLearn(); + } + } + + // Step channels for (int i = 0; i < 8; i++) { - // Get module - int moduleId = paramMaps[i].moduleId; - if (moduleId < 0) + int cc = learnedCcs[i]; + if (cc < 0) continue; - Module *module = APP->engine->getModule(moduleId); + // Check if CC value has been set + if (values[cc] < 0) + continue; + // Get module + Module *module = learnedModuleHandles[i].module; if (!module) continue; // Get param - int paramId = paramMaps[i].paramId; + int paramId = learnedParamIds[i]; Param *param = &module->params[paramId]; if (!param->isBounded()) continue; // Set param - float v = rescale(values[i], 0, 127, param->minValue, param->maxValue); + float v = rescale(values[cc], 0, 127, 0.f, 1.f); v = valueFilters[i].process(deltaTime, v); - module->params[paramId].setValue(v); + v = rescale(v, 0.f, 1.f, param->minValue, param->maxValue); + APP->engine->setParam(module, paramId, v); } } @@ -81,17 +113,44 @@ struct MIDI_Map : Module { void processCC(midi::Message msg) { uint8_t cc = msg.getNote(); // Learn - if (learningId >= 0 && values[cc] != msg.data2) { - if (lastLearnedCc != cc) { - learnedCcs[learningId] = cc; - lastLearnedCc = cc; - if (++learningId >= 8) - learningId = -1; - } + if (learningId >= 0 && values[cc] != msg.getValue()) { + learnedCcs[learningId] = cc; + commitLearn(); } values[cc] = msg.getValue(); } + void loadModuleHandle(int i) { + if (learnedModuleHandles[i].id >= 0) { + APP->engine->addModuleHandle(&learnedModuleHandles[i]); + } + } + + void unloadModuleHandle(int i) { + if (learnedModuleHandles[i].id >= 0) { + APP->engine->removeModuleHandle(&learnedModuleHandles[i]); + } + } + + void commitLearn() { + if (learningId < 0) + return; + if (learnedModuleHandles[learningId].id < 0) + return; + if (learnedCcs[learningId] < 0) + return; + learningId++; + if (learningId >= 8) + learningId = -1; + } + + void clearLearn(int id) { + learnedCcs[id] = -1; + unloadModuleHandle(id); + learnedModuleHandles[id].id = -1; + loadModuleHandle(id); + } + json_t *dataToJson() override { json_t *rootJ = json_object(); @@ -101,11 +160,17 @@ struct MIDI_Map : Module { } json_object_set_new(rootJ, "ccs", ccsJ); - json_t *paramMapsJ = json_array(); + json_t *moduleIdsJ = json_array(); for (int i = 0; i < 8; i++) { - json_array_append_new(paramMapsJ, paramMaps[i].toJson()); + json_array_append_new(moduleIdsJ, json_integer(learnedModuleHandles[i].id)); } - json_object_set_new(rootJ, "paramMaps", paramMapsJ); + json_object_set_new(rootJ, "moduleIds", moduleIdsJ); + + json_t *paramIdsJ = json_array(); + for (int i = 0; i < 8; i++) { + json_array_append_new(paramIdsJ, json_integer(learnedParamIds[i])); + } + json_object_set_new(rootJ, "paramIds", paramIdsJ); json_object_set_new(rootJ, "midi", midiInput.toJson()); return rootJ; @@ -121,12 +186,23 @@ struct MIDI_Map : Module { } } - json_t *paramMapsJ = json_object_get(rootJ, "paramMaps"); - if (paramMapsJ) { + json_t *moduleIdsJ = json_object_get(rootJ, "moduleIds"); + if (moduleIdsJ) { for (int i = 0; i < 8; i++) { - json_t *paramMapJ = json_array_get(paramMapsJ, i); - if (paramMapJ) - paramMaps[i].fromJson(paramMapJ); + json_t *moduleIdJ = json_array_get(moduleIdsJ, i); + unloadModuleHandle(i); + if (moduleIdJ) + learnedModuleHandles[i].id = json_integer_value(moduleIdJ); + loadModuleHandle(i); + } + } + + json_t *paramIdsJ = json_object_get(rootJ, "paramIds"); + if (paramIdsJ) { + for (int i = 0; i < 8; i++) { + json_t *paramIdJ = json_array_get(paramIdsJ, i); + if (paramIdJ) + learnedParamIds[i] = json_integer_value(paramIdJ); } } @@ -145,10 +221,17 @@ struct MIDI_MapChoice : LedDisplayChoice { this->module = module; } - void onAction(const event::Action &e) override { - if (!module) - return; - module->lastLearnedCc = -1; + void onButton(const event::Button &e) override { + if (e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_LEFT) { + APP->engine->setTouchedParam(NULL, 0); + e.consume(this); + } + + if (e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_RIGHT) { + if (module) { + module->clearLearn(id); + } + } } void onSelect(const event::Select &e) override { @@ -170,8 +253,6 @@ struct MIDI_MapChoice : LedDisplayChoice { if (!module) return; if (module->learningId == id) { - text = "Mapping..."; - color.a = 1.0; bgColor = color; bgColor.a = 0.15; @@ -180,22 +261,56 @@ struct MIDI_MapChoice : LedDisplayChoice { APP->event->setSelected(this); } else { - if (module->learnedCcs[id] >= 0) { - text = string::f("CC%d", module->learnedCcs[id]); - color.a = 1.0; - bgColor = nvgRGBA(0, 0, 0, 0); + bgColor = nvgRGBA(0, 0, 0, 0); + + // HACK + if (APP->event->selectedWidget == this) + APP->event->setSelected(NULL); + } + + text = ""; + color.a = 1.0; + if (module->learnedCcs[id] >= 0) { + text += string::f("CC%d ", module->learnedCcs[id]); + } + if (module->learnedModuleHandles[id].id >= 0) { + text += getParamName(); + } + if (!(module->learnedCcs[id] >= 0) && !(module->learnedModuleHandles[id].id >= 0)) { + if (module->learningId == id) { + text = "Mapping..."; } else { text = "Unmapped"; color.a = 0.5; - bgColor = nvgRGBA(0, 0, 0, 0); } - - // HACK - if (APP->event->selectedWidget == this) - APP->event->setSelected(NULL); } } + + std::string getParamName() { + if (!module) + return ""; + ModuleHandle *moduleHandle = &module->learnedModuleHandles[id]; + if (moduleHandle->id < 0) + return ""; + ModuleWidget *mw = APP->scene->rackWidget->getModule(moduleHandle->id); + if (!mw) + return ""; + // Get the Module from the ModuleWidget instead of the ModuleHandle. + // I think this is more elegant since this method is called in the app world instead of the engine world. + Module *m = mw->module; + if (!m) + return ""; + int paramId = module->learnedParamIds[id]; + if (paramId >= (int) m->params.size()) + return ""; + Param *param = &m->params[paramId]; + std::string s; + s += mw->model->name; + s += " "; + s += param->label; + return s; + } }; diff --git a/src/app/ParamWidget.cpp b/src/app/ParamWidget.cpp index b83438e4..7871bc4d 100644 --- a/src/app/ParamWidget.cpp +++ b/src/app/ParamWidget.cpp @@ -4,6 +4,7 @@ #include "app/Scene.hpp" #include "app/ParamQuantity.hpp" #include "app.hpp" +#include "engine/Engine.hpp" #include "settings.hpp" #include "random.hpp" #include "history.hpp" @@ -143,6 +144,13 @@ void ParamWidget::draw(const widget::DrawContext &ctx) { } void ParamWidget::onButton(const event::Button &e) { + // Touch parameter + if (e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_LEFT && (e.mods & WINDOW_MOD_MASK) == 0) { + if (paramQuantity) { + APP->engine->setTouchedParam(paramQuantity->module, paramQuantity->paramId); + } + } + // Right click to open context menu if (e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_RIGHT && (e.mods & WINDOW_MOD_MASK) == 0) { createContextMenu(); diff --git a/src/engine/Engine.cpp b/src/engine/Engine.cpp index b87a5c5a..498bcd54 100644 --- a/src/engine/Engine.cpp +++ b/src/engine/Engine.cpp @@ -127,6 +127,7 @@ struct EngineWorker { struct Engine::Internal { std::vector modules; std::vector cables; + std::vector moduleHandles; bool paused = false; bool running = false; @@ -149,6 +150,9 @@ struct Engine::Internal { std::vector workers; SpinBarrier engineBarrier; SpinBarrier workerBarrier; + + Module *touchedModule = NULL; + int touchedParamId = 0; }; @@ -173,6 +177,7 @@ Engine::~Engine() { // If this happens, a module must have failed to remove itself before the RackWidget was destroyed. assert(internal->cables.empty()); assert(internal->modules.empty()); + assert(internal->moduleHandles.empty()); delete internal; } @@ -235,6 +240,7 @@ static void Engine_step(Engine *engine) { // Snap to actual smooth value if the value doesn't change enough (due to the granularity of floats), or if newValue is out of bounds param->setValue(smoothValue); internal->smoothModule = NULL; + internal->smoothParamId = 0; } else { param->value = newValue; @@ -400,6 +406,11 @@ void Engine::addModule(Module *module) { internal->nextModuleId = module->id + 1; } } + // Update ModuleHandle + for (ModuleHandle *moduleHandle : internal->moduleHandles) { + if (moduleHandle->id == module->id) + moduleHandle->module = module; + } // Add module internal->modules.push_back(module); } @@ -417,6 +428,16 @@ void Engine::removeModule(Module *module) { assert(cable->outputModule != module); assert(cable->inputModule != module); } + // Remove touched param + if (internal->touchedModule == module) { + internal->touchedModule = NULL; + internal->touchedParamId = 0; + } + // Update ModuleHandle + for (ModuleHandle *moduleHandle : internal->moduleHandles) { + if (moduleHandle->id == module->id) + moduleHandle->module = NULL; + } // Check that the module actually exists auto it = std::find(internal->modules.begin(), internal->modules.end(), module); assert(it != internal->modules.end()); @@ -537,6 +558,11 @@ void Engine::removeCable(Cable *cable) { void Engine::setParam(Module *module, int paramId, float value) { // TODO Does this need to be thread-safe? + // If being smoothed, cancel smoothing + if (internal->smoothModule == module && internal->smoothParamId == paramId) { + internal->smoothModule = NULL; + internal->smoothParamId = 0; + } module->params[paramId].value = value; } @@ -551,6 +577,7 @@ void Engine::setSmoothParam(Module *module, int paramId, float value) { } internal->smoothParamId = paramId; internal->smoothValue = value; + // Set this last so the above values are valid as soon as it is set internal->smoothModule = module; } @@ -560,6 +587,39 @@ float Engine::getSmoothParam(Module *module, int paramId) { return getParam(module, paramId); } +void Engine::setTouchedParam(Module *module, int paramId) { + internal->touchedModule = module; + internal->touchedParamId = paramId; +} + +void Engine::getTouchedParam(Module *&module, int ¶mId) { + module = internal->touchedModule; + paramId = internal->touchedParamId; +} + +void Engine::addModuleHandle(ModuleHandle *moduleHandle) { + VIPLock vipLock(internal->vipMutex); + std::lock_guard lock(internal->mutex); + + // Check that the ModuleHandle is not already added + auto it = std::find(internal->moduleHandles.begin(), internal->moduleHandles.end(), moduleHandle); + assert(it == internal->moduleHandles.end()); + + moduleHandle->module = getModule(moduleHandle->id); + internal->moduleHandles.push_back(moduleHandle); +} + +void Engine::removeModuleHandle(ModuleHandle *moduleHandle) { + VIPLock vipLock(internal->vipMutex); + std::lock_guard lock(internal->mutex); + + moduleHandle->module = NULL; + // Check that the ModuleHandle is already added + auto it = std::find(internal->moduleHandles.begin(), internal->moduleHandles.end(), moduleHandle); + assert(it != internal->moduleHandles.end()); + internal->moduleHandles.erase(it); +} + void EngineWorker::step() { engine->internal->engineBarrier.wait(); diff --git a/src/engine/ParamMap.cpp b/src/engine/ParamMap.cpp deleted file mode 100644 index 734d7886..00000000 --- a/src/engine/ParamMap.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#include "engine/ParamMap.hpp" - - -namespace rack { -namespace engine { - - -json_t *ParamMap::toJson() { - json_t *rootJ = json_object(); - json_object_set_new(rootJ, "moduleId", json_integer(moduleId)); - json_object_set_new(rootJ, "paramId", json_integer(paramId)); - return rootJ; -} - -void ParamMap::fromJson(json_t *rootJ) { - json_t *moduleIdJ = json_object_get(rootJ, "moduleId"); - if (moduleIdJ) - moduleId = json_integer_value(moduleIdJ); - - json_t *paramIdJ = json_object_get(rootJ, "paramId"); - if (paramIdJ) - paramId = json_integer_value(paramIdJ); -} - - -} // namespace engine -} // namespace rack