From 7be2482d486a357e31a7f6129ca69602d45d2728 Mon Sep 17 00:00:00 2001 From: Michael Dewberry Date: Thu, 11 Jan 2024 00:24:47 -0500 Subject: [PATCH 1/7] basic 14-bit CC support --- src/faderbank/FaderbankModule.cpp | 152 ++++++++++++++++++++++-------- src/faderbank/FaderbankModule.hpp | 26 ++++- src/faderbank/FaderbankWidget.cpp | 28 +++++- 3 files changed, 162 insertions(+), 44 deletions(-) diff --git a/src/faderbank/FaderbankModule.cpp b/src/faderbank/FaderbankModule.cpp index 318db61..f1b4815 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,51 +40,120 @@ 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 = 16; - 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 param = getParamQuantity(index); - if (param) + uint8_t index = iter->second; + if (index < NUM_FADERS) { - param->setScaledValue((msg.getValue() * 1.0) / 127.0); + records[index].highValue = msg.getValue(); + records[index].lastHighValue = msg.getValue(); + records[index].lastHighValueFrame = args.frame; + } + } + else if (use14bitCCs && ccNum >= 32 && ccNum < 64) + { + // 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()) + { + uint8_t index = iter->second; + if (index < NUM_FADERS) + { + 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 + 48 + NUM_FADERS)) { - uint8_t channel = msg.bytes[9 + 16 + i] - 1; - uint8_t ccNum = msg.bytes[9 + 48 + i]; - inputMap[(channel << 8) | ccNum] = i; + inputMap.clear(); + for (int i = 0; i < NUM_FADERS; i++) + { + uint8_t channel = msg.bytes[9 + 16 + i] - 1; + uint8_t ccNum = msg.bytes[9 + 48 + i]; + inputMap[(channel << 8) | ccNum] = i; + records[i].ccNum = ccNum; + } } } + break; + default: + break; + } + } + + for (int i = 0; i < NUM_FADERS; i++) + { + uint16_t value; + bool updateable = false; + bool expect14bit = use14bitCCs && 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; + } } - break; - default: - break; + 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)); + } + + records[i].highValue = 0xFF; + records[i].lowValue = 0xFF; + } } } @@ -99,6 +164,7 @@ void FaderbankModule::resetConfig() { // by default, assign CC faders starting with 32, all on channel 1 inputMap[32 + i] = i; + records[i].ccNum = 32 + i; } } @@ -145,8 +211,10 @@ json_t* FaderbankModule::dataToJson() json_object_set_new(rootJ, "faderRange", json_integer(faderRange)); json_object_set_new(rootJ, "faderSize", json_integer(faderSize)); json_object_set_new(rootJ, "polyphonicMode", json_boolean(polyphonicMode)); + json_object_set_new(rootJ, "use14bitCCs", json_boolean(use14bitCCs)); 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) @@ -174,10 +242,18 @@ void FaderbankModule::dataFromJson(json_t* rootJ) if (polyphonicModeJ) polyphonicMode = json_boolean_value(polyphonicModeJ); + json_t* use14bitCCsJ = json_object_get(rootJ, "use14bitCCs"); + if (use14bitCCsJ) + use14bitCCs = json_boolean_value(use14bitCCsJ); + json_t* midiJ = json_object_get(rootJ, "midi"); if (midiJ) midiInput.fromJson(midiJ); + json_t* midiOutputJ = json_object_get(rootJ, "midiOutput"); + if (midiOutputJ) + midiOutput.fromJson(midiOutputJ); + json_t* configJ = json_object_get(rootJ, "16n_config"); if (configJ) { @@ -187,7 +263,9 @@ void FaderbankModule::dataFromJson(json_t* rootJ) json_object_foreach(configJ, key, dataJ) { int16_t val = std::stoi(key); - inputMap[val] = json_integer_value(dataJ); + int8_t fader = json_integer_value(dataJ); + inputMap[val] = fader; + records[fader].ccNum = val & 0xFF; } } } diff --git a/src/faderbank/FaderbankModule.hpp b/src/faderbank/FaderbankModule.hpp index 692583b..a29fc53 100644 --- a/src/faderbank/FaderbankModule.hpp +++ b/src/faderbank/FaderbankModule.hpp @@ -5,6 +5,23 @@ #define NUM_FADERS 16 +struct ControllerRecord +{ + uint8_t highValue; + uint8_t lowValue; + uint8_t lastHighValue; + int64_t lastHighValueFrame; + int64_t lastLowValueFrame; + uint8_t ccNum; + + ControllerRecord() + { + highValue = 0xFF; + lowValue = 0xFF; + lastHighValue = 0; + } +}; + struct FaderbankModule : rack::Module { FaderbankModule(); @@ -12,7 +29,7 @@ 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 updateFaderRanges(); @@ -21,9 +38,10 @@ struct FaderbankModule : rack::Module // 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 { @@ -41,4 +59,8 @@ struct FaderbankModule : rack::Module FaderSize faderSize = FaderSize90mm; FaderRange faderRange = FaderRange10V; bool polyphonicMode = false; + bool use14bitCCs = false; + +protected: + ControllerRecord records[NUM_FADERS]; }; diff --git a/src/faderbank/FaderbankWidget.cpp b/src/faderbank/FaderbankWidget.cpp index 2c62974..d01ef33 100644 --- a/src/faderbank/FaderbankWidget.cpp +++ b/src/faderbank/FaderbankWidget.cpp @@ -200,7 +200,7 @@ void FaderbankWidget::appendContextMenu(Menu* menu) menu->addChild(new MenuSeparator()); - menu->addChild(createSubmenuItem("MIDI connection", fb->midiInput.getDeviceName(fb->midiInput.getDeviceId()), + menu->addChild(createSubmenuItem("MIDI input", fb->midiInput.getDeviceName(fb->midiInput.getDeviceId()), [=](Menu* childMenu) { appendMidiMenu(childMenu, &fb->midiInput); @@ -210,6 +210,27 @@ void FaderbankWidget::appendContextMenu(Menu* menu) delete last; })); + menu->addChild(createSubmenuItem("MIDI output", 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; + })); + + menu->addChild(createCheckMenuItem( + "Use 14-bit MIDI CCs", "", + [=]() + { + return fb->use14bitCCs; + }, + [=]() + { + fb->use14bitCCs = !fb->use14bitCCs; + })); + menu->addChild(createMenuItem("Autodetect 16n configuration", "", [=]() { @@ -220,9 +241,6 @@ void FaderbankWidget::appendContextMenu(Menu* menu) msg.setSize(6); msg.bytes = { 0xF0, 0x7d, 0x00, 0x00, 0x1F, 0xF7 }; - midi::Output output; - output.setDriverId(fb->midiInput.getDriverId()); - output.setDeviceId(fb->midiInput.getDeviceId()); - output.sendMessage(msg); + fb->midiOutput.sendMessage(msg); })); } \ No newline at end of file From 9453e39e4475bf122bc929052f10050e04c44596 Mon Sep 17 00:00:00 2001 From: Michael Dewberry Date: Sun, 17 Nov 2024 20:39:59 -0500 Subject: [PATCH 2/7] Per-fader 14-bit setting, better autodetect, fader settings menu --- src/faderbank/FaderbankModule.cpp | 183 ++++++++++++++++++++++++------ src/faderbank/FaderbankModule.hpp | 42 +++---- src/faderbank/FaderbankWidget.cpp | 174 +++++++++++++++++++++------- 3 files changed, 305 insertions(+), 94 deletions(-) diff --git a/src/faderbank/FaderbankModule.cpp b/src/faderbank/FaderbankModule.cpp index f1b4815..667f1d5 100644 --- a/src/faderbank/FaderbankModule.cpp +++ b/src/faderbank/FaderbankModule.cpp @@ -56,29 +56,35 @@ void FaderbankModule::processMIDIMessages(const ProcessArgs& args) // 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()) { - uint8_t index = iter->second; - if (index < NUM_FADERS) - { - records[index].highValue = msg.getValue(); - records[index].lastHighValue = msg.getValue(); - records[index].lastHighValueFrame = args.frame; + 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; + } } } - else if (use14bitCCs && ccNum >= 32 && ccNum < 64) + else if (ccNum >= 32 && ccNum < 64) { // 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()) { - uint8_t index = iter->second; - if (index < NUM_FADERS) + auto faderDestinations = iter->second; + for (auto index : faderDestinations) { - records[index].lowValue = msg.getValue(); - records[index].lastLowValueFrame = args.frame; + if (index < NUM_FADERS && records[index].faderMode == FaderMode14bitCC) + { + records[index].lowValue = msg.getValue(); + records[index].lastLowValueFrame = args.frame; + } } } } @@ -90,16 +96,21 @@ void FaderbankModule::processMIDIMessages(const ProcessArgs& args) msg.bytes[2] == 0x00 && msg.bytes[3] == 0x00 && msg.bytes[4] == 0x0F && // sysex config response ID - msg.bytes.size() > (9 + 48 + NUM_FADERS)) + msg.bytes.size() >= (9 + 80)) { - inputMap.clear(); for (int i = 0; i < NUM_FADERS; i++) { - uint8_t channel = msg.bytes[9 + 16 + i] - 1; - uint8_t ccNum = msg.bytes[9 + 48 + i]; - inputMap[(channel << 8) | ccNum] = 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; @@ -112,7 +123,7 @@ void FaderbankModule::processMIDIMessages(const ProcessArgs& args) { uint16_t value; bool updateable = false; - bool expect14bit = use14bitCCs && records[i].ccNum < 32; + bool expect14bit = records[i].faderMode == FaderMode14bitCC && records[i].ccNum < 32; if (records[i].highValue != 0xFF) { @@ -132,7 +143,7 @@ void FaderbankModule::processMIDIMessages(const ProcessArgs& args) } else { - value = records[i].highValue & 0x7F; + value = (records[i].highValue & 0x7F) << 7; updateable = true; } } @@ -148,7 +159,7 @@ void FaderbankModule::processMIDIMessages(const ProcessArgs& args) auto param = getParamQuantity(i); if (param) { - param->setScaledValue((value * 1.0f) / ((expect14bit ? 0x3FFF : 0x7F) * 1.0f)); + param->setScaledValue((value * 1.0f) / (0x3FFF * 1.0f)); } records[i].highValue = 0xFF; @@ -159,12 +170,30 @@ void FaderbankModule::processMIDIMessages(const ProcessArgs& args) 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); } } @@ -204,6 +233,47 @@ 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); + } +} + json_t* FaderbankModule::dataToJson() { json_t* rootJ = json_object(); @@ -211,17 +281,21 @@ json_t* FaderbankModule::dataToJson() json_object_set_new(rootJ, "faderRange", json_integer(faderRange)); json_object_set_new(rootJ, "faderSize", json_integer(faderSize)); json_object_set_new(rootJ, "polyphonicMode", json_boolean(polyphonicMode)); - json_object_set_new(rootJ, "use14bitCCs", json_boolean(use14bitCCs)); 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; } @@ -242,10 +316,6 @@ void FaderbankModule::dataFromJson(json_t* rootJ) if (polyphonicModeJ) polyphonicMode = json_boolean_value(polyphonicModeJ); - json_t* use14bitCCsJ = json_object_get(rootJ, "use14bitCCs"); - if (use14bitCCsJ) - use14bitCCs = json_boolean_value(use14bitCCsJ); - json_t* midiJ = json_object_get(rootJ, "midi"); if (midiJ) midiInput.fromJson(midiJ); @@ -254,20 +324,51 @@ void FaderbankModule::dataFromJson(json_t* rootJ) if (midiOutputJ) midiOutput.fromJson(midiOutputJ); - json_t* configJ = json_object_get(rootJ, "16n_config"); - if (configJ) + // 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); int8_t fader = json_integer_value(dataJ); - inputMap[val] = fader; - records[fader].ccNum = val & 0xFF; + 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) @@ -278,4 +379,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 a29fc53..e5ffa6f 100644 --- a/src/faderbank/FaderbankModule.hpp +++ b/src/faderbank/FaderbankModule.hpp @@ -5,22 +5,6 @@ #define NUM_FADERS 16 -struct ControllerRecord -{ - uint8_t highValue; - uint8_t lowValue; - uint8_t lastHighValue; - int64_t lastHighValueFrame; - int64_t lastLowValueFrame; - uint8_t ccNum; - - ControllerRecord() - { - highValue = 0xFF; - lowValue = 0xFF; - lastHighValue = 0; - } -}; struct FaderbankModule : rack::Module { @@ -31,14 +15,16 @@ struct FaderbankModule : rack::Module void processMIDIMessages(const ProcessArgs& args); void resetConfig(); + void updateInputMap(); void updateFaderRanges(); + void autodetectConfig(); 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; + std::map > inputMap; rack::midi::InputQueue midiInput; rack::midi::Output midiOutput; @@ -56,11 +42,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; - bool use14bitCCs = false; -protected: ControllerRecord records[NUM_FADERS]; }; diff --git a/src/faderbank/FaderbankWidget.cpp b/src/faderbank/FaderbankWidget.cpp index d01ef33..1d61eaf 100644 --- a/src/faderbank/FaderbankWidget.cpp +++ b/src/faderbank/FaderbankWidget.cpp @@ -107,7 +107,6 @@ struct FaderbankSliderYellow : LightSlider NUM_FADERS) + { + return; + } + + FaderbankModule::ControllerRecord record = fb->records[faderIndex]; + + 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::vector ccNames; + for (auto i = 0; i < 128; i++) + { + std::ostringstream ss; + ss << i; + ccNames.push_back(ss.str()); + } + + std::ostringstream faderName; + faderName << faderIndex + 1; + + 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); + } + + menu->addChild(createSubmenuItem(faderName.str(), faderDesc.str(), + [=](Menu* childMenu) + { + childMenu->addChild(createIndexSubmenuItem("Channel", channelNames, + [=]() + { + return fb->records[faderIndex].channel; + }, + [=](int index) + { + fb->records[faderIndex].channel = index & 0xF; + fb->updateInputMap(); + } + )); + + childMenu->addChild(createIndexSubmenuItem("Mode", modeNames, + [=]() + { + return fb->records[faderIndex].faderMode; + }, + [=](int index) + { + fb->records[faderIndex].faderMode = static_cast(index); + fb->updateInputMap(); + } + )); + + childMenu->addChild(createIndexSubmenuItem("CC Number", 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); @@ -200,47 +282,61 @@ void FaderbankWidget::appendContextMenu(Menu* menu) menu->addChild(new MenuSeparator()); - menu->addChild(createSubmenuItem("MIDI input", 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; - })); - - menu->addChild(createSubmenuItem("MIDI output", 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; - })); - - menu->addChild(createCheckMenuItem( - "Use 14-bit MIDI CCs", "", - [=]() - { - return fb->use14bitCCs; - }, + menu->addChild(createMenuItem("Autodetect 16n hardware", "", [=]() { - fb->use14bitCCs = !fb->use14bitCCs; + 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", "", + [=]() + { + // TODO + } + )); + } + )); +} - fb->midiOutput.sendMessage(msg); - })); -} \ No newline at end of file From dead422ff2bf050fae1ffc77e9e14c5c0eeae6c3 Mon Sep 17 00:00:00 2001 From: Michael Dewberry Date: Mon, 18 Nov 2024 00:03:10 -0500 Subject: [PATCH 3/7] Implement writing 16n sysex --- src/faderbank/FaderbankModule.cpp | 31 +++++++++++++++++++++++++++++++ src/faderbank/FaderbankModule.hpp | 1 + src/faderbank/FaderbankWidget.cpp | 2 +- 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/faderbank/FaderbankModule.cpp b/src/faderbank/FaderbankModule.cpp index 667f1d5..8c90e97 100644 --- a/src/faderbank/FaderbankModule.cpp +++ b/src/faderbank/FaderbankModule.cpp @@ -274,6 +274,37 @@ void FaderbankModule::autodetectConfig() } } +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(); diff --git a/src/faderbank/FaderbankModule.hpp b/src/faderbank/FaderbankModule.hpp index e5ffa6f..759d5dc 100644 --- a/src/faderbank/FaderbankModule.hpp +++ b/src/faderbank/FaderbankModule.hpp @@ -18,6 +18,7 @@ struct FaderbankModule : rack::Module void updateInputMap(); void updateFaderRanges(); void autodetectConfig(); + void writeConfigSysex(); json_t* dataToJson() override; void dataFromJson(json_t* rootJ) override; diff --git a/src/faderbank/FaderbankWidget.cpp b/src/faderbank/FaderbankWidget.cpp index 1d61eaf..b321d77 100644 --- a/src/faderbank/FaderbankWidget.cpp +++ b/src/faderbank/FaderbankWidget.cpp @@ -333,7 +333,7 @@ void FaderbankWidget::appendContextMenu(Menu* menu) configMenu->addChild(createMenuItem("Write configuration to 16n hardware", "", [=]() { - // TODO + fb->writeConfigSysex(); } )); } From 209e94150b83e1189fa1b783dab6d2b2dff69a11 Mon Sep 17 00:00:00 2001 From: Michael Dewberry Date: Mon, 18 Nov 2024 00:07:53 -0500 Subject: [PATCH 4/7] Fix 7-bit MIDI CC range --- src/faderbank/FaderbankModule.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/faderbank/FaderbankModule.cpp b/src/faderbank/FaderbankModule.cpp index 8c90e97..1b865e1 100644 --- a/src/faderbank/FaderbankModule.cpp +++ b/src/faderbank/FaderbankModule.cpp @@ -143,7 +143,7 @@ void FaderbankModule::processMIDIMessages(const ProcessArgs& args) } else { - value = (records[i].highValue & 0x7F) << 7; + value = records[i].highValue & 0x7F; updateable = true; } } @@ -159,7 +159,7 @@ void FaderbankModule::processMIDIMessages(const ProcessArgs& args) auto param = getParamQuantity(i); if (param) { - param->setScaledValue((value * 1.0f) / (0x3FFF * 1.0f)); + param->setScaledValue((value * 1.0f) / ((expect14bit ? 0x3FFF : 0x7F) * 1.0f)); } records[i].highValue = 0xFF; From 5a1d28bc677d21ee4dcba767f7c0eb009c47e9cb Mon Sep 17 00:00:00 2001 From: Michael Dewberry Date: Mon, 30 Dec 2024 01:02:29 -0500 Subject: [PATCH 5/7] Restrict 14-bit CCs to 0-31 --- src/faderbank/FaderbankWidget.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/faderbank/FaderbankWidget.cpp b/src/faderbank/FaderbankWidget.cpp index b321d77..b2475a1 100644 --- a/src/faderbank/FaderbankWidget.cpp +++ b/src/faderbank/FaderbankWidget.cpp @@ -156,6 +156,7 @@ void appendFaderConfigMenu(FaderbankModule* fb, ::Menu* menu, int faderIndex) FaderbankModule::ControllerRecord record = fb->records[faderIndex]; std::vector modeNames { "CC", "CC (14-bit)" }; + uint8_t ccMax = record.faderMode == FaderbankModule::FaderMode14bitCC ? 31 : 127; std::vector channelNames; for (auto i = 0; i < 16; i++) @@ -166,7 +167,7 @@ void appendFaderConfigMenu(FaderbankModule* fb, ::Menu* menu, int faderIndex) } std::vector ccNames; - for (auto i = 0; i < 128; i++) + for (auto i = 0; i < ccMax + 1; i++) { std::ostringstream ss; ss << i; From 6b919c7cbaddeb9e09124187a0679014eaab081c Mon Sep 17 00:00:00 2001 From: Michael Dewberry Date: Sun, 29 Dec 2024 23:36:23 -0500 Subject: [PATCH 6/7] fix second mapping of LSB CC shadowing 14-bit fader; make interval sample rate dependent --- src/faderbank/FaderbankModule.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/faderbank/FaderbankModule.cpp b/src/faderbank/FaderbankModule.cpp index 1b865e1..30d605d 100644 --- a/src/faderbank/FaderbankModule.cpp +++ b/src/faderbank/FaderbankModule.cpp @@ -42,7 +42,7 @@ void FaderbankModule::process(const ProcessArgs& args) void FaderbankModule::processMIDIMessages(const ProcessArgs& args) { - int min14bitInterval = 16; + int min14bitInterval = floor(args.sampleRate * 0.004); rack::midi::Message msg; while (midiInput.tryPop(&msg, args.frame)) @@ -70,7 +70,8 @@ void FaderbankModule::processMIDIMessages(const ProcessArgs& args) } } } - else if (ccNum >= 32 && ccNum < 64) + + if (ccNum >= 32 && ccNum < 64) { // look for potential LSB CC of 14-bit CC 0-31 key = (msg.getChannel() << 8) | (ccNum - 32); From 5fa36763cafeea55b5d866f2efbc79010bbfed1f Mon Sep 17 00:00:00 2001 From: Michael Dewberry Date: Mon, 30 Dec 2024 01:03:55 -0500 Subject: [PATCH 7/7] Leave menus up while editing fader config --- .vscode/settings.json | 12 +- src/common/widgets/CustomMenuTemplates.hpp | 151 +++++++++++++++++++++ src/faderbank/FaderbankWidget.cpp | 64 +++++---- 3 files changed, 198 insertions(+), 29 deletions(-) create mode 100644 src/common/widgets/CustomMenuTemplates.hpp 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/FaderbankWidget.cpp b/src/faderbank/FaderbankWidget.cpp index b2475a1..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; @@ -153,10 +154,7 @@ void appendFaderConfigMenu(FaderbankModule* fb, ::Menu* menu, int faderIndex) return; } - FaderbankModule::ControllerRecord record = fb->records[faderIndex]; - std::vector modeNames { "CC", "CC (14-bit)" }; - uint8_t ccMax = record.faderMode == FaderbankModule::FaderMode14bitCC ? 31 : 127; std::vector channelNames; for (auto i = 0; i < 16; i++) @@ -166,32 +164,28 @@ void appendFaderConfigMenu(FaderbankModule* fb, ::Menu* menu, int faderIndex) channelNames.push_back(ss.str()); } - std::vector ccNames; - for (auto i = 0; i < ccMax + 1; i++) - { - std::ostringstream ss; - ss << i; - ccNames.push_back(ss.str()); - } - std::ostringstream faderName; faderName << faderIndex + 1; - 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); - } - - menu->addChild(createSubmenuItem(faderName.str(), faderDesc.str(), + 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(createIndexSubmenuItem("Channel", channelNames, + childMenu->addChild(createUnconsumingIndexSubmenuItem("Channel", channelNames, [=]() { return fb->records[faderIndex].channel; @@ -203,7 +197,7 @@ void appendFaderConfigMenu(FaderbankModule* fb, ::Menu* menu, int faderIndex) } )); - childMenu->addChild(createIndexSubmenuItem("Mode", modeNames, + childMenu->addChild(createUnconsumingIndexSubmenuItem("Mode", modeNames, [=]() { return fb->records[faderIndex].faderMode; @@ -215,7 +209,21 @@ void appendFaderConfigMenu(FaderbankModule* fb, ::Menu* menu, int faderIndex) } )); - childMenu->addChild(createIndexSubmenuItem("CC Number", ccNames, + 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; @@ -240,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; }, @@ -256,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; },