| @@ -22,12 +22,17 @@ struct MIDI_CC : Module { | |||
| midi::InputQueue midiInput; | |||
| /** [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 learnedCcs[16]; | |||
| /** [cell][channel] */ | |||
| dsp::ExponentialFilter valueFilters[16][16]; | |||
| bool mpeMode; | |||
| bool lsbEnabled; | |||
| MIDI_CC() { | |||
| config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); | |||
| @@ -43,17 +48,23 @@ struct MIDI_CC : Module { | |||
| } | |||
| 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++) { | |||
| values[i][c] = 0; | |||
| msbValues[cc][c] = 0; | |||
| } | |||
| } | |||
| learningId = -1; | |||
| for (int i = 0; i < 16; i++) { | |||
| learnedCcs[i] = i; | |||
| } | |||
| learningId = -1; | |||
| midiInput.reset(); | |||
| mpeMode = false; | |||
| lsbEnabled = false; | |||
| } | |||
| void process(const ProcessArgs& args) override { | |||
| @@ -77,7 +88,13 @@ struct MIDI_CC : Module { | |||
| int cc = learnedCcs[i]; | |||
| 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. | |||
| 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()) { | |||
| // cc | |||
| 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 cc = msg.getNote(); | |||
| if (msg.bytes.size() < 2) | |||
| return; | |||
| // Allow CC to be negative if the 8th bit is set. | |||
| // The gamepad driver abuses this, for example. | |||
| // Cast uint8_t to int8_t | |||
| if (msg.bytes.size() < 2) | |||
| return; | |||
| int8_t value = msg.bytes[2]; | |||
| value = clamp(value, -127, 127); | |||
| // Learn | |||
| if (learningId >= 0 && values[cc][c] != value) { | |||
| if (learningId >= 0 && ccValues[cc][c] != value) { | |||
| learnedCcs[learningId] = cc; | |||
| 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 { | |||
| @@ -134,13 +162,14 @@ struct MIDI_CC : Module { | |||
| json_t* valuesJ = json_array(); | |||
| 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. | |||
| 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, "midi", midiInput.toJson()); | |||
| json_object_set_new(rootJ, "mpeMode", json_boolean(mpeMode)); | |||
| json_object_set_new(rootJ, "lsbEnabled", json_boolean(lsbEnabled)); | |||
| return rootJ; | |||
| } | |||
| @@ -159,7 +188,7 @@ struct MIDI_CC : Module { | |||
| for (int i = 0; i < 128; i++) { | |||
| json_t* valueJ = json_array_get(valuesJ, i); | |||
| 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"); | |||
| if (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); | |||
| 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->rightText = CHECKMARK(module->mpeMode); | |||
| mpeModeItem->module = module; | |||
| 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); | |||
| } | |||
| }; | |||