diff --git a/include/app/RackWidget.hpp b/include/app/RackWidget.hpp index 702ef609..9208bf01 100644 --- a/include/app/RackWidget.hpp +++ b/include/app/RackWidget.hpp @@ -127,6 +127,7 @@ struct RackWidget : widget::OpaqueWidget { /** Returns the most recently added complete cable connected to the given Port, i.e. the top of the stack. */ CableWidget* getTopCable(PortWidget* port); CableWidget* getCable(int64_t cableId); + CableWidget* getCable(PortWidget* outputPort, PortWidget* inputPort); std::vector getCompleteCables(); /** Returns all cables attached to port, complete or not. */ std::vector getCablesOnPort(PortWidget* port); diff --git a/src/app/PortWidget.cpp b/src/app/PortWidget.cpp index f87de97e..62c5e53a 100644 --- a/src/app/PortWidget.cpp +++ b/src/app/PortWidget.cpp @@ -250,7 +250,7 @@ void PortWidget::createContextMenu() { !topCw )); - if (type == engine::Port::INPUT) { + { PortCloneCableItem* item = createMenuItem("Duplicate top cable", RACK_MOD_CTRL_NAME "+drag"); item->disabled = !topCw; item->pw = this; @@ -261,11 +261,9 @@ void PortWidget::createContextMenu() { menu->addChild(new ui::MenuSeparator); // New cable items - bool createCableDisabled = (type == engine::Port::INPUT) && topCw; for (NVGcolor color : settings::cableColors) { // Include extra leading spaces for the color circle PortCreateCableItem* item = createMenuItem("New cable", "Click+drag"); - item->disabled = createCableDisabled; item->pw = this; item->color = color; menu->addChild(item); @@ -366,35 +364,31 @@ void PortWidget::onDragStart(const DragStartEvent& e) { }); CableWidget* cw = NULL; - if (internal->overrideCreateCable) { + int mods = APP->window->getMods(); + if (internal->overrideCreateCable || (mods & RACK_MOD_MASK) == RACK_MOD_CTRL) { + // Create cable with Ctrl+drag or PortCreateCableItem // Keep cable NULL. Will be created below } - else if (internal->overrideCloneCw || (APP->window->getMods() & RACK_MOD_MASK) == RACK_MOD_CTRL) { - if (type == engine::Port::OUTPUT) { - // Ctrl-clicking an output creates a new cable. - // Keep cable NULL. Will be created below - } - else { - // Ctrl-clicking an input clones the cable already patched to it. - CableWidget* cloneCw; - if (internal->overrideCloneCw) - cloneCw = internal->overrideCloneCw; + else if (internal->overrideCloneCw || (mods & RACK_MOD_MASK) == (RACK_MOD_CTRL | GLFW_MOD_SHIFT)) { + // Clone top cable with Ctrl+shift+drag or PortCloneCableItem + CableWidget* cloneCw = internal->overrideCloneCw; + if (!cloneCw) + cloneCw = APP->scene->rack->getTopCable(this); + + if (cloneCw) { + cw = new CableWidget; + cw->color = cloneCw->color; + if (type == engine::Port::OUTPUT) + cw->inputPort = cloneCw->inputPort; else - cloneCw = APP->scene->rack->getTopCable(this); - - if (cloneCw) { - cw = new CableWidget; - cw->color = cloneCw->color; cw->outputPort = cloneCw->outputPort; - cw->updateCable(); - } + cw->updateCable(); } } else { // Grab cable on top of stack - if (internal->overrideCw) - cw = internal->overrideCw; - else + cw = internal->overrideCw; + if (!cw) cw = APP->scene->rack->getTopCable(this); if (cw) { @@ -414,13 +408,6 @@ void PortWidget::onDragStart(const DragStartEvent& e) { } if (!cw) { - // Check that inputs don't already have a cable - if (type == engine::Port::INPUT) { - CableWidget* topCw = APP->scene->rack->getTopCable(this); - if (topCw) - return; - } - // Create a new cable cw = new CableWidget; @@ -465,52 +452,44 @@ void PortWidget::onDragEnd(const DragEndEvent& e) { void PortWidget::onDragDrop(const DragDropEvent& e) { - // HACK: Only delete tooltip if we're not (normal) dragging it. - if (e.origin == this) - createTooltip(); - if (e.button != GLFW_MOUSE_BUTTON_LEFT) return; - // Reject ports if this is an input port and something is already plugged into it - if (type == engine::Port::INPUT) { - if (APP->scene->rack->getTopCable(this)) - return; - } + // HACK: Only delete tooltip if we're not (normal) dragging it. + if (e.origin == this) + createTooltip(); CableWidget* cw = APP->scene->rack->getIncompleteCable(); if (cw) { cw->hoveredOutputPort = cw->hoveredInputPort = NULL; - if (type == engine::Port::OUTPUT) + if (type == engine::Port::OUTPUT && cw->inputPort && !APP->scene->rack->getCable(this, cw->inputPort)) { cw->outputPort = this; - else + } + if (type == engine::Port::INPUT && cw->outputPort && !APP->scene->rack->getCable(cw->outputPort, this)) { cw->inputPort = this; + } cw->updateCable(); } } void PortWidget::onDragEnter(const DragEnterEvent& e) { - PortWidget* pw = dynamic_cast(e.origin); - if (pw) { - createTooltip(); - } - if (e.button != GLFW_MOUSE_BUTTON_LEFT) return; - // Reject ports if this is an input port and something is already plugged into it - if (type == engine::Port::INPUT) { - if (APP->scene->rack->getTopCable(this)) - return; + PortWidget* pw = dynamic_cast(e.origin); + if (pw) { + createTooltip(); } CableWidget* cw = APP->scene->rack->getIncompleteCable(); if (cw) { - if (type == engine::Port::OUTPUT) + if (type == engine::Port::OUTPUT && cw->inputPort && !APP->scene->rack->getCable(this, cw->inputPort)) { cw->hoveredOutputPort = this; - else + } + if (type == engine::Port::INPUT && cw->outputPort && !APP->scene->rack->getCable(cw->outputPort, this)) { cw->hoveredInputPort = this; + } } } @@ -529,7 +508,7 @@ void PortWidget::onDragLeave(const DragLeaveEvent& e) { if (cw) { if (type == engine::Port::OUTPUT) cw->hoveredOutputPort = NULL; - else + if (type == engine::Port::INPUT) cw->hoveredInputPort = NULL; } } diff --git a/src/app/RackWidget.cpp b/src/app/RackWidget.cpp index 7b60bb4b..7bea8023 100644 --- a/src/app/RackWidget.cpp +++ b/src/app/RackWidget.cpp @@ -1467,6 +1467,16 @@ CableWidget* RackWidget::getCable(int64_t cableId) { return NULL; } +CableWidget* RackWidget::getCable(PortWidget* outputPort, PortWidget* inputPort) { + for (widget::Widget* w : internal->cableContainer->children) { + CableWidget* cw = dynamic_cast(w); + assert(cw); + if (cw->outputPort == outputPort && cw->inputPort == inputPort) + return cw; + } + return NULL; +} + std::vector RackWidget::getCompleteCables() { std::vector cws; cws.reserve(internal->cableContainer->children.size()); diff --git a/src/engine/Engine.cpp b/src/engine/Engine.cpp index a1455671..876b581c 100644 --- a/src/engine/Engine.cpp +++ b/src/engine/Engine.cpp @@ -184,6 +184,10 @@ struct Engine::Internal { std::map cablesCache; // (moduleId, paramId) std::map, ParamHandle*> paramHandlesCache; + /** Cache of cables connected to each input + Only connected inputs are allowed. + */ + std::map> inputCablesCache; float sampleRate = 0.f; float sampleTime = 0.f; @@ -319,27 +323,6 @@ static void Engine_stepWorker(Engine* that, int threadId) { } -static void Cable_step(Cable* that) { - Output* output = &that->outputModule->outputs[that->outputId]; - Input* input = &that->inputModule->inputs[that->inputId]; - // Match number of polyphonic channels to output port - int channels = output->channels; - // Copy all voltages from output to input - for (int c = 0; c < channels; c++) { - float v = output->voltages[c]; - // Set 0V if infinite or NaN - if (!std::isfinite(v)) - v = 0.f; - input->voltages[c] = v; - } - // Set higher channel voltages to 0 - for (int c = channels; c < input->channels; c++) { - input->voltages[c] = 0.f; - } - input->channels = channels; -} - - /** Steps a single frame */ static void Engine_stepFrame(Engine* that) { @@ -372,9 +355,43 @@ static void Engine_stepFrame(Engine* that) { Engine_stepWorker(that, 0); internal->workerBarrier.wait(); - // Step cables - for (Cable* cable : that->internal->cables) { - Cable_step(cable); + // Step cables for each input + for (const auto& pair : internal->inputCablesCache) { + Input* input = pair.first; + const std::vector& cables = pair.second; + // Clear input voltages up to old number of input channels + for (int c = 0; c < input->channels; c++) { + input->voltages[c] = 0.f; + } + // Find max number of channels + uint8_t channels = 1; + for (Cable* cable : cables) { + Output* output = &cable->outputModule->outputs[cable->outputId]; + channels = std::max(channels, output->channels); + } + input->channels = channels; + // Sum all outputs to input value + for (Cable* cable : cables) { + Output* output = &cable->outputModule->outputs[cable->outputId]; + + auto finitize = [](float x) { + return std::isfinite(x) ? x : 0.f; + }; + + // Sum monophonic value to all input channels + if (output->channels == 1) { + float value = finitize(output->voltages[0]); + for (int c = 0; c < channels; c++) { + input->voltages[c] += value; + } + } + // Sum polyphonic values to each input channel + else { + for (int c = 0; c < output->channels; c++) { + input->voltages[c] += finitize(output->voltages[c]); + } + } + } } // Flip messages for each module @@ -898,34 +915,41 @@ void Engine::addCable_NoLock(Cable* cable) { // Check cable properties assert(cable->inputModule); assert(cable->outputModule); + Input& input = cable->inputModule->inputs[cable->inputId]; + Output& output = cable->outputModule->outputs[cable->outputId]; + bool inputWasConnected = false; bool outputWasConnected = false; for (Cable* cable2 : internal->cables) { // Check that the cable is not already added assert(cable2 != cable); - // Check that the input is not already used by another cable - assert(!(cable2->inputModule == cable->inputModule && cable2->inputId == cable->inputId)); + // Check that cable isn't similar to another cable + // assert(!(cable2->inputModule == cable->inputModule && cable2->inputId == cable->inputId && cable2->outputModule == cable->outputModule && cable2->outputId == cable->outputId)); + // Check if input is already connected to a cable + if (cable2->inputModule == cable->inputModule && cable2->inputId == cable->inputId) + inputWasConnected = true; // Check if output is already connected to a cable if (cable2->outputModule == cable->outputModule && cable2->outputId == cable->outputId) outputWasConnected = true; } // Set ID if unset or collides with an existing ID while (cable->id < 0 || internal->cablesCache.find(cable->id) != internal->cablesCache.end()) { - // Randomly generate ID + // Generate random 52-bit ID cable->id = random::u64() % (1ull << 53); } // Add the cable internal->cables.push_back(cable); - internal->cablesCache[cable->id] = cable; - // Set input as connected - Input& input = cable->inputModule->inputs[cable->inputId]; - input.channels = 1; - // Set output as connected, which might already be connected - Output& output = cable->outputModule->outputs[cable->outputId]; - if (output.channels == 0) { + // Set default number of input/output channels + if (!inputWasConnected) { + input.channels = 1; + } + if (!outputWasConnected) { output.channels = 1; } + // Add caches + internal->cablesCache[cable->id] = cable; + internal->inputCablesCache[&input].push_back(cable); // Dispatch input port event - { + if (!inputWasConnected) { Module::PortChangeEvent e; e.connecting = true; e.type = Port::INPUT; @@ -951,42 +975,58 @@ void Engine::removeCable(Cable* cable) { void Engine::removeCable_NoLock(Cable* cable) { assert(cable); + Input& input = cable->inputModule->inputs[cable->inputId]; + Output& output = cable->outputModule->outputs[cable->outputId]; // Check that the cable is already added auto it = std::find(internal->cables.begin(), internal->cables.end(), cable); assert(it != internal->cables.end()); - // Remove the cable + // Remove cable caches + { + auto& v = internal->inputCablesCache[&input]; + auto it = std::find(v.begin(), v.end(), cable); + assert(it != v.end()); + v.erase(it); + // Remove input from cache if no cables are connected + if (v.empty()) { + internal->inputCablesCache.erase(&input); + } + } internal->cablesCache.erase(cable->id); + // Remove cable internal->cables.erase(it); - // Set input as disconnected - Input& input = cable->inputModule->inputs[cable->inputId]; - input.channels = 0; - // Clear input values - for (uint8_t c = 0; c < PORT_MAX_CHANNELS; c++) { - input.setVoltage(0.f, c); - } - // Check if output is still connected to a cable + // Check if input/output is still connected to a cable + bool inputIsConnected = false; bool outputIsConnected = false; for (Cable* cable2 : internal->cables) { + if (cable2->inputModule == cable->inputModule && cable2->inputId == cable->inputId) { + inputIsConnected = true; + } if (cable2->outputModule == cable->outputModule && cable2->outputId == cable->outputId) { outputIsConnected = true; - break; + } + } + // Set input as disconnected if disconnected from all cables + if (!inputIsConnected) { + input.channels = 0; + // Clear input values + for (uint8_t c = 0; c < PORT_MAX_CHANNELS; c++) { + input.setVoltage(0.f, c); } } // Set output as disconnected if disconnected from all cables if (!outputIsConnected) { - Output& output = cable->outputModule->outputs[cable->outputId]; output.channels = 0; // Don't clear output values } // Dispatch input port event - { + if (!inputIsConnected) { Module::PortChangeEvent e; e.connecting = false; e.type = Port::INPUT; e.portId = cable->inputId; cable->inputModule->onPortChange(e); } - // Dispatch output port event if its state went from connected to disconnected. + // Dispatch output port event if (!outputIsConnected) { Module::PortChangeEvent e; e.connecting = false; @@ -1249,7 +1289,6 @@ void Engine::fromJson(json_t* rootJ) { catch (Exception& e) { WARN("Cannot load cable: %s", e.what()); delete cable; - // Don't log exceptions because missing modules create unnecessary complaining when cables try to connect to them. continue; } }