diff --git a/.vscode/settings.json b/.vscode/settings.json index cfe82f9..cd9e799 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -99,6 +99,16 @@ "__node_handle": "cpp", "mock_hardware_api_private.h": "c", "__functional_03": "cpp", - "mutex": "cpp" + "mutex": "cpp", + "__config": "cpp", + "__hash_table": "cpp", + "__split_buffer": "cpp", + "__threading_support": "cpp", + "__tree": "cpp", + "__verbose_abort": "cpp", + "cfenv": "cpp", + "charconv": "cpp", + "execution": "cpp", + "stack": "cpp" } } \ No newline at end of file diff --git a/src/common/widgets/CustomMenuTemplates.hpp b/src/common/widgets/CustomMenuTemplates.hpp new file mode 100644 index 0000000..a048f6f --- /dev/null +++ b/src/common/widgets/CustomMenuTemplates.hpp @@ -0,0 +1,151 @@ +#pragma once + +#include "rack.hpp" +using namespace rack; + +template +TMenuItem* createUnconsumingIndexSubmenuItem(std::string text, std::vector labels, std::function getter, std::function setter, bool disabled = false) +{ + struct IndexItem : ui::MenuItem + { + std::function getter; + std::function setter; + size_t index; + + void step() override + { + size_t currIndex = getter(); + this->rightText = CHECKMARK(currIndex == index); + MenuItem::step(); + } + void onAction(const event::Action& e) override + { + setter(index); + e.unconsume(); + } + }; + + struct Item : TMenuItem + { + std::function getter; + std::function setter; + std::vector labels; + + void step() override + { + size_t currIndex = getter(); + std::string label = (currIndex < labels.size()) ? labels[currIndex] : ""; + this->rightText = label + " " + RIGHT_ARROW; + TMenuItem::step(); + } + ui::Menu* createChildMenu() override + { + ui::Menu* menu = new ui::Menu; + for (size_t i = 0; i < labels.size(); i++) + { + IndexItem* item = createMenuItem(labels[i]); + item->getter = getter; + item->setter = setter; + item->index = i; + menu->addChild(item); + } + return menu; + } + }; + + Item* item = createMenuItem(text); + item->getter = getter; + item->setter = setter; + item->labels = labels; + item->disabled = disabled; + return item; +} + +template +TMenuItem* createUnconsumingIndexSubmenuItemWithDynamicLabels(std::string text, std::function()> getLabels, std::function getter, std::function setter, bool disabled = false) +{ + struct IndexItem : ui::MenuItem + { + std::function getter; + std::function setter; + size_t index; + + void step() override + { + size_t currIndex = getter(); + this->rightText = CHECKMARK(currIndex == index); + MenuItem::step(); + } + void onAction(const event::Action& e) override + { + setter(index); + e.unconsume(); + } + }; + + struct Item : TMenuItem + { + std::function()> getLabels; + std::function getter; + std::function setter; + + void step() override + { + size_t currIndex = getter(); + auto labels = getLabels(); + std::string label = (currIndex < labels.size()) ? labels[currIndex] : ""; + this->rightText = label + " " + RIGHT_ARROW; + TMenuItem::step(); + } + ui::Menu* createChildMenu() override + { + ui::Menu* menu = new ui::Menu; + auto labels = getLabels(); + for (size_t i = 0; i < labels.size(); i++) + { + IndexItem* item = createMenuItem(labels[i]); + item->getter = getter; + item->setter = setter; + item->index = i; + menu->addChild(item); + } + return menu; + } + }; + + Item* item = createMenuItem(text); + item->getter = getter; + item->setter = setter; + item->getLabels = getLabels; + item->disabled = disabled; + return item; +} + +template +TMenuItem* createSubmenuItemWithDynamicRightText(std::string text, std::function getRightText, std::function createMenu, bool disabled = false) +{ + struct Item : TMenuItem + { + std::function getRightText; + std::function createMenu; + + void step() override + { + this->rightText = getRightText() + " " + RIGHT_ARROW; + TMenuItem::step(); + } + + ui::Menu* createChildMenu() override + { + ui::Menu* menu = new ui::Menu; + createMenu(menu); + return menu; + } + }; + + Item* item = createMenuItem(text, getRightText() + " " + RIGHT_ARROW); + item->getRightText = getRightText; + item->createMenu = createMenu; + item->disabled = disabled; + return item; +} \ No newline at end of file diff --git a/src/faderbank/FaderbankModule.cpp b/src/faderbank/FaderbankModule.cpp index 318db61..30d605d 100644 --- a/src/faderbank/FaderbankModule.cpp +++ b/src/faderbank/FaderbankModule.cpp @@ -19,11 +19,7 @@ FaderbankModule::~FaderbankModule() void FaderbankModule::process(const ProcessArgs& args) { - rack::midi::Message msg; - while (midiInput.tryPop(&msg, args.frame)) - { - processMIDIMessage(msg); - } + processMIDIMessages(args); for (unsigned i = 0; i < NUM_FADERS; i++) { @@ -44,61 +40,161 @@ void FaderbankModule::process(const ProcessArgs& args) } } -void FaderbankModule::processMIDIMessage(const rack::midi::Message& msg) +void FaderbankModule::processMIDIMessages(const ProcessArgs& args) { - DEBUG("MIDI: %lld %s", msg.getFrame(), msg.toString().c_str()); + int min14bitInterval = floor(args.sampleRate * 0.004); - switch (msg.getStatus()) + rack::midi::Message msg; + while (midiInput.tryPop(&msg, args.frame)) { - case 0xb: // Continuous Controller - { - // Combine channel and CC number into a lookup key - uint16_t key = (msg.getChannel() << 8) | msg.getNote(); - auto iter = inputMap.find(key); - if (iter != inputMap.end()) + DEBUG("MIDI: %lld %s", msg.getFrame(), msg.toString().c_str()); + + switch (msg.getStatus()) + { + case 0xb: // Continuous Controller { - uint8_t index = iter->second; - if (index < NUM_FADERS) + // Combine channel and CC number into a lookup key + uint8_t ccNum = msg.getNote(); + uint16_t key = (msg.getChannel() << 8) | ccNum; + + auto iter = inputMap.find(key); + if (iter != inputMap.end()) + { + auto faderDestinations = iter->second; + for (auto index : faderDestinations) { + if (index < NUM_FADERS) + { + records[index].highValue = msg.getValue(); + records[index].lastHighValue = msg.getValue(); + records[index].lastHighValueFrame = args.frame; + } + } + } + + if (ccNum >= 32 && ccNum < 64) { - auto param = getParamQuantity(index); - if (param) + // look for potential LSB CC of 14-bit CC 0-31 + key = (msg.getChannel() << 8) | (ccNum - 32); + iter = inputMap.find(key); + if (iter != inputMap.end()) { - param->setScaledValue((msg.getValue() * 1.0) / 127.0); + auto faderDestinations = iter->second; + for (auto index : faderDestinations) + { + if (index < NUM_FADERS && records[index].faderMode == FaderMode14bitCC) + { + records[index].lowValue = msg.getValue(); + records[index].lastLowValueFrame = args.frame; + } + } } } } - } - break; - case 0xF: // System Exclusive - { - if (msg.bytes[1] == 0x7d && // 16n manufacturer ID - msg.bytes[2] == 0x00 && - msg.bytes[3] == 0x00 && - msg.bytes[4] == 0x0F && // sysex config response ID - msg.bytes.size() > (9 + 48 + NUM_FADERS)) + break; + case 0xF: // System Exclusive { - inputMap.clear(); - for (int i = 0; i < NUM_FADERS; i++) + if (msg.bytes[1] == 0x7d && // 16n manufacturer ID + msg.bytes[2] == 0x00 && + msg.bytes[3] == 0x00 && + msg.bytes[4] == 0x0F && // sysex config response ID + msg.bytes.size() >= (9 + 80)) { - uint8_t channel = msg.bytes[9 + 16 + i] - 1; - uint8_t ccNum = msg.bytes[9 + 48 + i]; - inputMap[(channel << 8) | ccNum] = i; + for (int i = 0; i < NUM_FADERS; i++) + { + uint8_t channel = ((msg.bytes[9 + 16 + i]) & 0xF) - 1; + uint8_t ccNum = msg.bytes[9 + 48 + i] & 0x7F; + records[i].ccNum = ccNum; + records[i].channel = channel; + if (msg.bytes.size() >= 9 + 82) + { + uint16_t ccMode = msg.bytes[9 + 80] << 8 | msg.bytes[9 + 81]; + records[i].faderMode = (ccMode & (1 << i)) == 0 ? FaderMode14bitCC : FaderModeCC; + } + } + updateInputMap(); } } + break; + default: + break; + } + } + + for (int i = 0; i < NUM_FADERS; i++) + { + uint16_t value; + bool updateable = false; + bool expect14bit = records[i].faderMode == FaderMode14bitCC && records[i].ccNum < 32; + + if (records[i].highValue != 0xFF) + { + if (expect14bit) + { + if (records[i].lowValue != 0xFF) + { + value = ((records[i].highValue & 0x7F) << 7) + (records[i].lowValue & 0x7F); + updateable = true; + } + else if ((args.frame - records[i].lastHighValueFrame) > min14bitInterval) + { + // give up waiting for a low value + value = (records[i].highValue & 0x7F) << 7; + updateable = true; + } + } + else + { + value = records[i].highValue & 0x7F; + updateable = true; + } + } + else if (expect14bit && records[i].lowValue != 0xFF && (args.frame - records[i].lastLowValueFrame) > min14bitInterval) + { + // give up waiting for a high value + value = ((records[i].lastHighValue & 0x7F) << 7) + (records[i].lowValue & 0x7F); + updateable = true; + } + + if (updateable) + { + auto param = getParamQuantity(i); + if (param) + { + param->setScaledValue((value * 1.0f) / ((expect14bit ? 0x3FFF : 0x7F) * 1.0f)); } - break; - default: - break; + + records[i].highValue = 0xFF; + records[i].lowValue = 0xFF; + } } } void FaderbankModule::resetConfig() { - inputMap.clear(); for (int i = 0; i < NUM_FADERS; i++) { // by default, assign CC faders starting with 32, all on channel 1 - inputMap[32 + i] = i; + records[i].ccNum = 32 + i; + records[i].channel = 0; + records[i].faderMode = FaderModeCC; + } + + updateInputMap(); +} + +void FaderbankModule::updateInputMap() +{ + inputMap.clear(); + + for (int i = 0; i < NUM_FADERS; i++) + { + uint16_t key = (records[i].channel << 8) | records[i].ccNum; + if (inputMap.find(key) == inputMap.end()) + { + inputMap.insert(make_pair(key, std::vector())); + } + + inputMap[key].push_back(i); } } @@ -138,6 +234,78 @@ void FaderbankModule::updateFaderRanges() } } +void FaderbankModule::autodetectConfig() +{ + resetConfig(); + + midiInput.setDriverId(rack::midi::getDriverIds()[0]); + if (midiInput.deviceId == -1) + { + for (int deviceId : midiInput.getDeviceIds()) + { + if (midiInput.getDeviceName(deviceId).substr(0, 3).find("16n") != std::string::npos) + { + midiInput.setDeviceId(deviceId); + break; + } + } + } + + midiOutput.setDriverId(rack::midi::getDriverIds()[0]); + if (midiOutput.deviceId == -1) + { + for (int deviceId : midiOutput.getDeviceIds()) + { + if (midiOutput.getDeviceName(deviceId).find("16n") != std::string::npos) + { + midiOutput.setDeviceId(deviceId); + break; + } + } + } + + // Send a sysex message to request device channel/CC config. + if (midiOutput.deviceId != -1) + { + rack::midi::Message msg; + msg.setSize(6); + msg.bytes = { 0xF0, 0x7d, 0x00, 0x00, 0x1F, 0xF7 }; + + midiOutput.sendMessage(msg); + } +} + +void FaderbankModule::writeConfigSysex() +{ + if (midiOutput.deviceId != -1) + { + rack::midi::Message msg; + msg.setSize(40); + + uint8_t header[] = { 0xF0, 0x7d, 0x00, 0x00, 0x0C }; + for (int i = 0; i < 5; i++) + { + msg.bytes[i] = header[i]; + } + + uint16_t modeBits = 0xFFFF; + for (int i = 0; i < NUM_FADERS; i++) + { + msg.bytes[5 + i] = (records[i].channel + 1) & 0x1F; + msg.bytes[21 + i] = records[i].ccNum & 0x7F; + if (records[i].faderMode == FaderMode14bitCC) + { + modeBits ^= 1 << i; + } + } + msg.bytes[37] = (modeBits >> 8) & 0x7F; + msg.bytes[38] = modeBits & 0x7F; + msg.bytes[39] = 0xF7; + + midiOutput.sendMessage(msg); + } +} + json_t* FaderbankModule::dataToJson() { json_t* rootJ = json_object(); @@ -147,13 +315,19 @@ json_t* FaderbankModule::dataToJson() json_object_set_new(rootJ, "polyphonicMode", json_boolean(polyphonicMode)); json_object_set_new(rootJ, "midi", midiInput.toJson()); + json_object_set_new(rootJ, "midiOutput", midiOutput.toJson()); - json_t* configJ = json_object(); - for (auto& entry : inputMap) + json_t* configJ = json_array(); + for (auto& entry : records) { - json_object_set_new(configJ, std::to_string(entry.first).c_str(), json_integer(entry.second)); + json_t* faderRecord = json_object(); + json_object_set_new(faderRecord, "channel", json_integer(entry.channel)); + json_object_set_new(faderRecord, "faderMode", json_integer(entry.faderMode)); + json_object_set_new(faderRecord, "ccNum", json_integer(entry.ccNum)); + + json_array_append(configJ, faderRecord); } - json_object_set_new(rootJ, "16n_config", configJ); + json_object_set_new(rootJ, "fader_config", configJ); return rootJ; } @@ -178,18 +352,55 @@ void FaderbankModule::dataFromJson(json_t* rootJ) if (midiJ) midiInput.fromJson(midiJ); - json_t* configJ = json_object_get(rootJ, "16n_config"); - if (configJ) + json_t* midiOutputJ = json_object_get(rootJ, "midiOutput"); + if (midiOutputJ) + midiOutput.fromJson(midiOutputJ); + + // backwards compatibility for patches with older config structure + json_t* oldConfigJ = json_object_get(rootJ, "16n_config"); + if (oldConfigJ) { - inputMap.clear(); json_t* dataJ; const char* key; - json_object_foreach(configJ, key, dataJ) + json_object_foreach(oldConfigJ, key, dataJ) { int16_t val = std::stoi(key); - inputMap[val] = json_integer_value(dataJ); + int8_t fader = json_integer_value(dataJ); + records[fader].ccNum = val & 0x7F; + records[fader].channel = val >> 8; + } + } + + // current format for config + json_t* configJ = json_object_get(rootJ, "fader_config"); + if (configJ) + { + json_t* dataJ; + size_t key; + json_array_foreach(configJ, key, dataJ) + { + if (key < NUM_FADERS) + { + json_t* channelJ = json_object_get(dataJ, "channel"); + if (channelJ) + { + records[key].channel = json_integer_value(channelJ) & 0xF; + } + json_t* modeJ = json_object_get(dataJ, "faderMode"); + if (modeJ) + { + records[key].faderMode = static_cast(json_integer_value(modeJ)); + } + json_t* ccJ = json_object_get(dataJ, "ccNum"); + if (ccJ) + { + records[key].ccNum = json_integer_value(ccJ) & 0x7F; + } + } } } + + updateInputMap(); } void FaderbankModule::fromJson(json_t* rootJ) @@ -200,4 +411,14 @@ void FaderbankModule::fromJson(json_t* rootJ) dataFromJson(dataJ); Module::fromJson(rootJ); +} + +FaderbankModule::ControllerRecord::ControllerRecord() +{ + highValue = 0xFF; + lowValue = 0xFF; + lastHighValue = 0; + channel = 0; + ccNum = 0; + faderMode = FaderModeCC; } \ No newline at end of file diff --git a/src/faderbank/FaderbankModule.hpp b/src/faderbank/FaderbankModule.hpp index 692583b..759d5dc 100644 --- a/src/faderbank/FaderbankModule.hpp +++ b/src/faderbank/FaderbankModule.hpp @@ -5,6 +5,7 @@ #define NUM_FADERS 16 + struct FaderbankModule : rack::Module { FaderbankModule(); @@ -12,18 +13,22 @@ struct FaderbankModule : rack::Module void process(const ProcessArgs& args) override; - void processMIDIMessage(const rack::midi::Message& msg); + void processMIDIMessages(const ProcessArgs& args); void resetConfig(); + void updateInputMap(); void updateFaderRanges(); + void autodetectConfig(); + void writeConfigSysex(); json_t* dataToJson() override; void dataFromJson(json_t* rootJ) override; // override fromJson to deserialize data before params void fromJson(json_t* rootJ) override; + std::map > inputMap; rack::midi::InputQueue midiInput; - std::map inputMap; + rack::midi::Output midiOutput; typedef enum { @@ -38,7 +43,29 @@ struct FaderbankModule : rack::Module FaderRangeBipolar } FaderRange; + typedef enum + { + FaderModeCC, + FaderMode14bitCC + } FaderMode; + + struct ControllerRecord + { + uint8_t highValue; + uint8_t lowValue; + uint8_t lastHighValue; + int64_t lastHighValueFrame; + int64_t lastLowValueFrame; + uint8_t ccNum; + uint8_t channel; + FaderMode faderMode; + + ControllerRecord(); + }; + FaderSize faderSize = FaderSize90mm; FaderRange faderRange = FaderRange10V; bool polyphonicMode = false; + + ControllerRecord records[NUM_FADERS]; }; diff --git a/src/faderbank/FaderbankWidget.cpp b/src/faderbank/FaderbankWidget.cpp index 2c62974..53d040a 100644 --- a/src/faderbank/FaderbankWidget.cpp +++ b/src/faderbank/FaderbankWidget.cpp @@ -1,5 +1,6 @@ #include "FaderbankWidget.hpp" #include "FaderbankModule.hpp" +#include "CustomMenuTemplates.hpp" extern rack::Plugin* pluginInstance; @@ -107,7 +108,6 @@ struct FaderbankSliderYellow : LightSlider NUM_FADERS) + { + return; + } + + std::vector modeNames { "CC", "CC (14-bit)" }; + + std::vector channelNames; + for (auto i = 0; i < 16; i++) + { + std::ostringstream ss; + ss << (i + 1); + channelNames.push_back(ss.str()); + } + + std::ostringstream faderName; + faderName << faderIndex + 1; + + menu->addChild(createSubmenuItemWithDynamicRightText(faderName.str(), + [=]() + { + FaderbankModule::ControllerRecord record = fb->records[faderIndex]; + std::ostringstream faderDesc; + faderDesc << "Ch " << (int)(record.channel + 1) << " " << modeNames[record.faderMode] << " "; + if (record.faderMode == FaderbankModule::FaderModeCC) + { + faderDesc << (int)record.ccNum; + } + else if (record.faderMode == FaderbankModule::FaderMode14bitCC) + { + faderDesc << (int)record.ccNum << "/" << (int)(record.ccNum + 32); + } + return faderDesc.str(); + }, + [=](Menu* childMenu) + { + childMenu->addChild(createUnconsumingIndexSubmenuItem("Channel", channelNames, + [=]() + { + return fb->records[faderIndex].channel; + }, + [=](int index) + { + fb->records[faderIndex].channel = index & 0xF; + fb->updateInputMap(); + } + )); + + childMenu->addChild(createUnconsumingIndexSubmenuItem("Mode", modeNames, + [=]() + { + return fb->records[faderIndex].faderMode; + }, + [=](int index) + { + fb->records[faderIndex].faderMode = static_cast(index); + fb->updateInputMap(); + } + )); + + childMenu->addChild(createUnconsumingIndexSubmenuItemWithDynamicLabels("CC Number", + [=]() + { + FaderbankModule::ControllerRecord record = fb->records[faderIndex]; + uint8_t ccMax = record.faderMode == FaderbankModule::FaderMode14bitCC ? 31 : 127; + + std::vector ccNames; + for (auto i = 0; i < ccMax + 1; i++) + { + std::ostringstream ss; + ss << i; + ccNames.push_back(ss.str()); + } + return ccNames; + }, + [=]() + { + return fb->records[faderIndex].ccNum; + }, + [=](int index) + { + fb->records[faderIndex].ccNum = index & 0x7F; + fb->updateInputMap(); + } + )); + } + )); +} + void FaderbankWidget::appendContextMenu(Menu* menu) { auto fb = dynamic_cast(module); @@ -157,7 +248,7 @@ void FaderbankWidget::appendContextMenu(Menu* menu) menu->addChild(new MenuSeparator()); - menu->addChild(createIndexSubmenuItem("Fader voltage range", { "0-10V", "0-5V", "+/-5V" }, + menu->addChild(createUnconsumingIndexSubmenuItem("Fader voltage range", { "0-10V", "0-5V", "+/-5V" }, [=]() { return fb->faderRange; }, @@ -173,7 +264,7 @@ void FaderbankWidget::appendContextMenu(Menu* menu) } })); - menu->addChild(createIndexSubmenuItem("Fader size", { "90mm", "60mm" }, + menu->addChild(createUnconsumingIndexSubmenuItem("Fader size", { "90mm", "60mm" }, [=]() { return fb->faderSize; }, @@ -200,29 +291,61 @@ void FaderbankWidget::appendContextMenu(Menu* menu) menu->addChild(new MenuSeparator()); - menu->addChild(createSubmenuItem("MIDI connection", fb->midiInput.getDeviceName(fb->midiInput.getDeviceId()), - [=](Menu* childMenu) + menu->addChild(createMenuItem("Autodetect 16n hardware", "", + [=]() { - appendMidiMenu(childMenu, &fb->midiInput); - // remove channel selection - auto last = childMenu->children.back(); - childMenu->removeChild(last); - delete last; + fb->autodetectConfig(); })); - menu->addChild(createMenuItem("Autodetect 16n configuration", "", - [=]() + menu->addChild(createSubmenuItem("MIDI Configuration", "", + [=](Menu* configMenu) { - fb->resetConfig(); - - // Send a sysex message to request device channel/CC config. - midi::Message msg; - msg.setSize(6); - msg.bytes = { 0xF0, 0x7d, 0x00, 0x00, 0x1F, 0xF7 }; + configMenu->addChild(createSubmenuItem("Input device", fb->midiInput.getDeviceName(fb->midiInput.getDeviceId()), + [=](Menu* childMenu) + { + appendMidiMenu(childMenu, &fb->midiInput); + // remove channel selection + auto last = childMenu->children.back(); + childMenu->removeChild(last); + delete last; + // and separator + last = childMenu->children.back(); + childMenu->removeChild(last); + delete last; + })); + + configMenu->addChild(createSubmenuItem("Output device", fb->midiOutput.getDeviceName(fb->midiOutput.getDeviceId()), + [=](Menu* childMenu) + { + appendMidiMenu(childMenu, &fb->midiOutput); + // remove channel selection + auto last = childMenu->children.back(); + childMenu->removeChild(last); + delete last; + // and separator + last = childMenu->children.back(); + childMenu->removeChild(last); + delete last; + })); + + configMenu->addChild(createSubmenuItem("Fader settings", "", + [=](Menu* childMenu) + { + for (int i = 0; i < NUM_FADERS; i++) + { + appendFaderConfigMenu(fb, childMenu, i); + } + })); + + configMenu->addChild(new MenuSeparator()); + + configMenu->addChild(createMenuItem("Write configuration to 16n hardware", "", + [=]() + { + fb->writeConfigSysex(); + } + )); + } + )); +} - midi::Output output; - output.setDriverId(fb->midiInput.getDriverId()); - output.setDeviceId(fb->midiInput.getDeviceId()); - output.sendMessage(msg); - })); -} \ No newline at end of file