@@ -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 |