| @@ -22,12 +22,17 @@ struct MIDI_CC : Module { | |||||
| midi::InputQueue midiInput; | midi::InputQueue midiInput; | ||||
| /** [cc][channel] */ | /** [cc][channel] */ | ||||
| int8_t values[128][16]; | |||||
| int8_t ccValues[128][16]; | |||||
| /** When LSB is enabled for CC 0-31, the MSB is stored here until the LSB is received. | |||||
| [cc][channel] | |||||
| */ | |||||
| int8_t msbValues[32][16]; | |||||
| int learningId; | int learningId; | ||||
| int learnedCcs[16]; | int learnedCcs[16]; | ||||
| /** [cell][channel] */ | /** [cell][channel] */ | ||||
| dsp::ExponentialFilter valueFilters[16][16]; | dsp::ExponentialFilter valueFilters[16][16]; | ||||
| bool mpeMode; | bool mpeMode; | ||||
| bool lsbEnabled; | |||||
| MIDI_CC() { | MIDI_CC() { | ||||
| config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); | config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); | ||||
| @@ -43,17 +48,23 @@ struct MIDI_CC : Module { | |||||
| } | } | ||||
| void onReset() override { | void onReset() override { | ||||
| for (int i = 0; i < 128; i++) { | |||||
| for (int cc = 0; cc < 128; cc++) { | |||||
| for (int c = 0; c < 16; c++) { | |||||
| ccValues[cc][c] = 0; | |||||
| } | |||||
| } | |||||
| for (int cc = 0; cc < 32; cc++) { | |||||
| for (int c = 0; c < 16; c++) { | for (int c = 0; c < 16; c++) { | ||||
| values[i][c] = 0; | |||||
| msbValues[cc][c] = 0; | |||||
| } | } | ||||
| } | } | ||||
| learningId = -1; | |||||
| for (int i = 0; i < 16; i++) { | for (int i = 0; i < 16; i++) { | ||||
| learnedCcs[i] = i; | learnedCcs[i] = i; | ||||
| } | } | ||||
| learningId = -1; | |||||
| midiInput.reset(); | midiInput.reset(); | ||||
| mpeMode = false; | mpeMode = false; | ||||
| lsbEnabled = false; | |||||
| } | } | ||||
| void process(const ProcessArgs& args) override { | void process(const ProcessArgs& args) override { | ||||
| @@ -77,7 +88,13 @@ struct MIDI_CC : Module { | |||||
| int cc = learnedCcs[i]; | int cc = learnedCcs[i]; | ||||
| for (int c = 0; c < channels; c++) { | for (int c = 0; c < channels; c++) { | ||||
| float value = values[cc][c] / 127.f; | |||||
| int16_t cellValue = ccValues[cc][c] * 128; | |||||
| if (lsbEnabled && cc < 32) | |||||
| cellValue += ccValues[cc + 32][c]; | |||||
| // Maximum value for 14-bit CC should be MSB=127 LSB=0, not MSB=127 LSB=127. | |||||
| float value = cellValue / float(128 * 127); | |||||
| // Support negative values because the gamepad MIDI driver generates nonstandard 8-bit CC values. | |||||
| value = clamp(value, -1.f, 1.f); | |||||
| // Detect behavior from MIDI buttons. | // Detect behavior from MIDI buttons. | ||||
| if (std::fabs(valueFilters[i][c].out - value) >= 1.f) { | if (std::fabs(valueFilters[i][c].out - value) >= 1.f) { | ||||
| @@ -93,7 +110,7 @@ struct MIDI_CC : Module { | |||||
| } | } | ||||
| } | } | ||||
| void processMessage(const midi::Message &msg) { | |||||
| void processMessage(const midi::Message& msg) { | |||||
| switch (msg.getStatus()) { | switch (msg.getStatus()) { | ||||
| // cc | // cc | ||||
| case 0xb: { | case 0xb: { | ||||
| @@ -103,22 +120,33 @@ struct MIDI_CC : Module { | |||||
| } | } | ||||
| } | } | ||||
| void processCC(const midi::Message &msg) { | |||||
| void processCC(const midi::Message& msg) { | |||||
| uint8_t c = mpeMode ? msg.getChannel() : 0; | uint8_t c = mpeMode ? msg.getChannel() : 0; | ||||
| uint8_t cc = msg.getNote(); | uint8_t cc = msg.getNote(); | ||||
| if (msg.bytes.size() < 2) | |||||
| return; | |||||
| // Allow CC to be negative if the 8th bit is set. | // Allow CC to be negative if the 8th bit is set. | ||||
| // The gamepad driver abuses this, for example. | // The gamepad driver abuses this, for example. | ||||
| // Cast uint8_t to int8_t | // Cast uint8_t to int8_t | ||||
| if (msg.bytes.size() < 2) | |||||
| return; | |||||
| int8_t value = msg.bytes[2]; | int8_t value = msg.bytes[2]; | ||||
| value = clamp(value, -127, 127); | |||||
| // Learn | // Learn | ||||
| if (learningId >= 0 && values[cc][c] != value) { | |||||
| if (learningId >= 0 && ccValues[cc][c] != value) { | |||||
| learnedCcs[learningId] = cc; | learnedCcs[learningId] = cc; | ||||
| learningId = -1; | learningId = -1; | ||||
| } | } | ||||
| values[cc][c] = value; | |||||
| if (lsbEnabled && cc < 32) { | |||||
| // Don't set MSB yet. Wait for LSB to be received. | |||||
| msbValues[cc][c] = value; | |||||
| } | |||||
| else if (lsbEnabled && 32 <= cc && cc < 64) { | |||||
| // Apply MSB when LSB is received | |||||
| ccValues[cc - 32][c] = msbValues[cc - 32][c]; | |||||
| ccValues[cc][c] = value; | |||||
| } | |||||
| else { | |||||
| ccValues[cc][c] = value; | |||||
| } | |||||
| } | } | ||||
| json_t* dataToJson() override { | json_t* dataToJson() override { | ||||
| @@ -134,13 +162,14 @@ struct MIDI_CC : Module { | |||||
| json_t* valuesJ = json_array(); | json_t* valuesJ = json_array(); | ||||
| for (int i = 0; i < 128; i++) { | for (int i = 0; i < 128; i++) { | ||||
| // Note: Only save channel 0. Since MPE mode won't be commonly used, it's pointless to save all 16 channels. | // Note: Only save channel 0. Since MPE mode won't be commonly used, it's pointless to save all 16 channels. | ||||
| json_array_append_new(valuesJ, json_integer(values[i][0])); | |||||
| json_array_append_new(valuesJ, json_integer(ccValues[i][0])); | |||||
| } | } | ||||
| json_object_set_new(rootJ, "values", valuesJ); | json_object_set_new(rootJ, "values", valuesJ); | ||||
| json_object_set_new(rootJ, "midi", midiInput.toJson()); | json_object_set_new(rootJ, "midi", midiInput.toJson()); | ||||
| json_object_set_new(rootJ, "mpeMode", json_boolean(mpeMode)); | json_object_set_new(rootJ, "mpeMode", json_boolean(mpeMode)); | ||||
| json_object_set_new(rootJ, "lsbEnabled", json_boolean(lsbEnabled)); | |||||
| return rootJ; | return rootJ; | ||||
| } | } | ||||
| @@ -159,7 +188,7 @@ struct MIDI_CC : Module { | |||||
| for (int i = 0; i < 128; i++) { | for (int i = 0; i < 128; i++) { | ||||
| json_t* valueJ = json_array_get(valuesJ, i); | json_t* valueJ = json_array_get(valuesJ, i); | ||||
| if (valueJ) { | if (valueJ) { | ||||
| values[i][0] = json_integer_value(valueJ); | |||||
| ccValues[i][0] = json_integer_value(valueJ); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -171,14 +200,10 @@ struct MIDI_CC : Module { | |||||
| json_t* mpeModeJ = json_object_get(rootJ, "mpeMode"); | json_t* mpeModeJ = json_object_get(rootJ, "mpeMode"); | ||||
| if (mpeModeJ) | if (mpeModeJ) | ||||
| mpeMode = json_boolean_value(mpeModeJ); | mpeMode = json_boolean_value(mpeModeJ); | ||||
| } | |||||
| }; | |||||
| struct MIDI_CCMpeModeItem : MenuItem { | |||||
| MIDI_CC* module; | |||||
| void onAction(const event::Action& e) override { | |||||
| module->mpeMode ^= true; | |||||
| json_t* lsbEnabledJ = json_object_get(rootJ, "lsbEnabled"); | |||||
| if (lsbEnabledJ) | |||||
| lsbEnabled = json_boolean_value(lsbEnabledJ); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -223,11 +248,31 @@ struct MIDI_CCWidget : ModuleWidget { | |||||
| menu->addChild(new MenuSeparator); | menu->addChild(new MenuSeparator); | ||||
| MIDI_CCMpeModeItem* mpeModeItem = new MIDI_CCMpeModeItem; | |||||
| struct MpeModeItem : MenuItem { | |||||
| MIDI_CC* module; | |||||
| void onAction(const event::Action& e) override { | |||||
| module->mpeMode ^= true; | |||||
| } | |||||
| }; | |||||
| MpeModeItem* mpeModeItem = new MpeModeItem; | |||||
| mpeModeItem->text = "MPE mode"; | mpeModeItem->text = "MPE mode"; | ||||
| mpeModeItem->rightText = CHECKMARK(module->mpeMode); | mpeModeItem->rightText = CHECKMARK(module->mpeMode); | ||||
| mpeModeItem->module = module; | mpeModeItem->module = module; | ||||
| menu->addChild(mpeModeItem); | menu->addChild(mpeModeItem); | ||||
| struct LSBItem : MenuItem { | |||||
| MIDI_CC* module; | |||||
| void onAction(const event::Action& e) override { | |||||
| module->lsbEnabled ^= true; | |||||
| } | |||||
| }; | |||||
| LSBItem* highResolutionItem = new LSBItem; | |||||
| highResolutionItem->text = "CC 0-31 controls are 14-bit"; | |||||
| highResolutionItem->rightText = CHECKMARK(module->lsbEnabled); | |||||
| highResolutionItem->module = module; | |||||
| menu->addChild(highResolutionItem); | |||||
| } | } | ||||
| }; | }; | ||||