| @@ -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; | |||
| } | |||
| @@ -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); | |||
| }; | |||
| @@ -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 | |||
| @@ -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); | |||
| } | |||
| @@ -1,20 +0,0 @@ | |||
| #pragma once | |||
| #include "common.hpp" | |||
| #include <jansson.h> | |||
| namespace rack { | |||
| namespace engine { | |||
| struct ParamMap { | |||
| int moduleId = -1; | |||
| int paramId = -1; | |||
| json_t *toJson(); | |||
| void fromJson(json_t *rootJ); | |||
| }; | |||
| } // namespace engine | |||
| } // namespace rack | |||
| @@ -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" | |||
| @@ -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 { | |||
| @@ -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; | |||
| } | |||
| }; | |||
| @@ -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(); | |||
| @@ -127,6 +127,7 @@ struct EngineWorker { | |||
| struct Engine::Internal { | |||
| std::vector<Module*> modules; | |||
| std::vector<Cable*> cables; | |||
| std::vector<ModuleHandle*> moduleHandles; | |||
| bool paused = false; | |||
| bool running = false; | |||
| @@ -149,6 +150,9 @@ struct Engine::Internal { | |||
| std::vector<EngineWorker> 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<std::recursive_mutex> 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<std::recursive_mutex> 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(); | |||
| @@ -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 | |||