From 7be2482d486a357e31a7f6129ca69602d45d2728 Mon Sep 17 00:00:00 2001 From: Michael Dewberry Date: Thu, 11 Jan 2024 00:24:47 -0500 Subject: [PATCH] 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