From b91af832e126cff1d09bfdd409183d114646ae67 Mon Sep 17 00:00:00 2001 From: falkTX Date: Thu, 27 Jan 2022 22:12:06 +0000 Subject: [PATCH] Add implementation for MIDI-CC Signed-off-by: falkTX --- plugins/Cardinal/src/HostMIDI-CC.cpp | 358 ++++++++++++++++++++++++- plugins/Cardinal/src/HostMIDI-Gate.cpp | 19 +- 2 files changed, 360 insertions(+), 17 deletions(-) diff --git a/plugins/Cardinal/src/HostMIDI-CC.cpp b/plugins/Cardinal/src/HostMIDI-CC.cpp index 48aad06..c175f80 100644 --- a/plugins/Cardinal/src/HostMIDI-CC.cpp +++ b/plugins/Cardinal/src/HostMIDI-CC.cpp @@ -16,7 +16,7 @@ */ /** - * This file contains a substantial amount of code from VCVRack's core/....cpp and core/....cpp + * This file contains a substantial amount of code from VCVRack's core/CV_MIDICC.cpp and core/MIDICC_CV.cpp * Copyright (C) 2016-2021 VCV. * * This program is free software: you can redistribute it and/or @@ -38,9 +38,11 @@ struct HostMIDICC : Module { NUM_PARAMS }; enum InputIds { + ENUMS(CC_INPUTS, 16), NUM_INPUTS }; enum OutputIds { + ENUMS(CC_OUTPUT, 16), NUM_OUTPUTS }; enum LightIds { @@ -49,21 +51,261 @@ struct HostMIDICC : Module { CardinalPluginContext* const pcontext; + struct MidiInput { + // Cardinal specific + CardinalPluginContext* const pcontext; + const MidiEvent* midiEvents; + uint32_t midiEventsLeft; + uint32_t midiEventFrame; + int64_t lastBlockFrame; + uint8_t channel; + + // stuff from Rack + /** [cc][channel] */ + 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; + /** [cell][channel] */ + dsp::ExponentialFilter valueFilters[16][16]; + bool smooth; + bool mpeMode; + bool lsbMode; + + MidiInput(CardinalPluginContext* const pc) + : pcontext(pc) + { + for (int i = 0; i < 16; i++) { + for (int c = 0; c < 16; c++) { + valueFilters[i][c].setTau(1 / 30.f); + } + } + reset(); + } + + void reset() + { + midiEvents = nullptr; + midiEventsLeft = 0; + midiEventFrame = 0; + lastBlockFrame = -1; + channel = 0; + + 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++) { + msbValues[cc][c] = 0; + } + } + learningId = -1; + smooth = true; + mpeMode = false; + lsbMode = false; + } + + bool process(const ProcessArgs& args, std::vector& outputs, int learnedCcs[16]) + { + // Cardinal specific + const int64_t blockFrame = pcontext->engine->getBlockFrame(); + const bool blockFrameChanged = lastBlockFrame != blockFrame; + + if (blockFrameChanged) + { + lastBlockFrame = blockFrame; + + midiEvents = pcontext->midiEvents; + midiEventsLeft = pcontext->midiEventCount; + midiEventFrame = 0; + } + + while (midiEventsLeft != 0) + { + const MidiEvent& midiEvent(*midiEvents); + + if (midiEvent.frame > midiEventFrame) + break; + + ++midiEvents; + --midiEventsLeft; + + const uint8_t* const data = midiEvent.size > MidiEvent::kDataSize + ? midiEvent.dataExt + : midiEvent.data; + + if (channel != 0 && data[0] < 0xF0) + { + if ((data[0] & 0x0F) != (channel - 1)) + continue; + } + + // adapted from Rack + if ((data[0] & 0xF0) != 0xB0) + continue; + + uint8_t c = mpeMode ? (data[0] & 0x0F) : 0; + uint8_t cc = data[1]; + + // Allow CC to be negative if the 8th bit is set. + // The gamepad driver abuses this, for example. + // Cast uint8_t to int8_t + int8_t value = data[2]; + // Learn + if (learningId >= 0 && ccValues[cc][c] != value) { + learnedCcs[learningId] = cc; + learningId = -1; + } + + if (lsbMode && cc < 32) { + // Don't set MSB yet. Wait for LSB to be received. + msbValues[cc][c] = value; + } + else if (lsbMode && 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; + } + } + + ++midiEventFrame; + + // Rack stuff + const int channels = mpeMode ? 16 : 1; + + for (int i = 0; i < 16; i++) { + if (!outputs[CC_OUTPUT + i].isConnected()) + continue; + outputs[CC_OUTPUT + i].setChannels(channels); + + int cc = learnedCcs[i]; + + for (int c = 0; c < channels; c++) { + int16_t cellValue = int16_t(ccValues[cc][c]) * 128; + if (lsbMode && cc < 32) + cellValue += ccValues[cc + 32][c]; + // Maximum value for 14-bit CC should be MSB=127 LSB=0, not MSB=127 LSB=127, because this is the maximum value that 7-bit controllers can send. + float value = float(cellValue) / (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 (smooth && std::fabs(valueFilters[i][c].out - value) < 1.f) { + // Smooth value with filter + valueFilters[i][c].process(args.sampleTime, value); + } + else { + // Jump value + valueFilters[i][c].out = value; + } + outputs[CC_OUTPUT + i].setVoltage(valueFilters[i][c].out * 10.f, c); + } + } + + return blockFrameChanged; + } + + } midiInput; + + struct MidiOutput { + // cardinal specific + CardinalPluginContext* const pcontext; + uint8_t channel = 0; + + dsp::Timer rateLimiterTimer; + int lastValues[128]; + int64_t frame = 0; + + MidiOutput(CardinalPluginContext* const pc) + : pcontext(pc) + { + reset(); + } + + void reset() + { + for (int n = 0; n < 128; n++) + lastValues[n] = -1; + } + + void setValue(int value, int cc) + { + if (value == lastValues[cc]) + return; + lastValues[cc] = value; + // CC + midi::Message m; + m.setStatus(0xb); + m.setNote(cc); + m.setValue(value); + m.setFrame(frame); + sendMessage(m); + } + + void sendMessage(const midi::Message& message) + { + pcontext->writeMidiMessage(message, channel); + } + + } midiOutput; + + int learnedCcs[16]; + HostMIDICC() - : pcontext(static_cast(APP)) + : pcontext(static_cast(APP)), + midiInput(pcontext), + midiOutput(pcontext) { if (pcontext == nullptr) throw rack::Exception("Plugin context is null"); config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); + + for (int i = 0; i < 16; i++) + configInput(CC_INPUTS + i, string::f("Cell %d", i + 1)); + + for (int i = 0; i < 16; i++) + configOutput(CC_OUTPUT + i, string::f("Cell %d", i + 1)); + + onReset(); } void onReset() override { + for (int i = 0; i < 16; i++) { + learnedCcs[i] = i; + } + midiInput.reset(); + midiOutput.reset(); } void process(const ProcessArgs& args) override { + if (midiInput.process(args, outputs, learnedCcs)) + midiOutput.frame = 0; + else + ++midiOutput.frame; + + const float rateLimiterPeriod = 1 / 200.f; + bool rateLimiterTriggered = (midiOutput.rateLimiterTimer.process(args.sampleTime) >= rateLimiterPeriod); + if (rateLimiterTriggered) + midiOutput.rateLimiterTimer.time -= rateLimiterPeriod; + else + return; + + for (int i = 0; i < 16; i++) + { + int value = (int) std::round(inputs[CC_INPUTS + i].getVoltage() / 10.f * 127); + value = clamp(value, 0, 127); + midiOutput.setValue(value, learnedCcs[i]); + } } json_t* dataToJson() override @@ -71,11 +313,73 @@ struct HostMIDICC : Module { json_t* const rootJ = json_object(); DISTRHO_SAFE_ASSERT_RETURN(rootJ != nullptr, nullptr); + // input and output + if (json_t* const ccsJ = json_array()) + { + for (int i = 0; i < 16; i++) + json_array_append_new(ccsJ, json_integer(learnedCcs[i])); + json_object_set_new(rootJ, "ccs", ccsJ); + } + + // input only + if (json_t* const valuesJ = json_array()) + { + // Remember values so users don't have to touch MIDI controller knobs when restarting Rack + 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(midiInput.ccValues[i][0])); + json_object_set_new(rootJ, "values", valuesJ); + } + + json_object_set_new(rootJ, "smooth", json_boolean(midiInput.smooth)); + json_object_set_new(rootJ, "mpeMode", json_boolean(midiInput.mpeMode)); + json_object_set_new(rootJ, "lsbMode", json_boolean(midiInput.lsbMode)); + + // separate + json_object_set_new(rootJ, "inputChannel", json_integer(midiInput.channel)); + json_object_set_new(rootJ, "outputChannel", json_integer(midiOutput.channel)); + return rootJ; } - void dataFromJson(json_t* rootJ) override + void dataFromJson(json_t* const rootJ) override { + // input and output + if (json_t* const ccsJ = json_object_get(rootJ, "ccs")) + { + for (int i = 0; i < 16; i++) + { + if (json_t* const ccJ = json_array_get(ccsJ, i)) + learnedCcs[i] = json_integer_value(ccJ); + else + learnedCcs[i] = i; + } + } + + // input only + if (json_t* const valuesJ = json_object_get(rootJ, "values")) + { + for (int i = 0; i < 128; i++) { + if (json_t* const valueJ = json_array_get(valuesJ, i)) + midiInput.ccValues[i][0] = json_integer_value(valueJ); + } + } + + if (json_t* const smoothJ = json_object_get(rootJ, "smooth")) + midiInput.smooth = json_boolean_value(smoothJ); + + if (json_t* const mpeModeJ = json_object_get(rootJ, "mpeMode")) + midiInput.mpeMode = json_boolean_value(mpeModeJ); + + if (json_t* const lsbEnabledJ = json_object_get(rootJ, "lsbMode")) + midiInput.lsbMode = json_boolean_value(lsbEnabledJ); + + // separate + if (json_t* const inputChannelJ = json_object_get(rootJ, "inputChannel")) + midiInput.channel = json_integer_value(inputChannelJ); + + if (json_t* const outputChannelJ = json_object_get(rootJ, "outputChannel")) + midiOutput.channel = json_integer_value(outputChannelJ) & 0x0F; } }; @@ -115,6 +419,54 @@ struct HostMIDICCWidget : ModuleWidget { void appendContextMenu(Menu* const menu) override { + menu->addChild(new MenuSeparator); + menu->addChild(createMenuLabel("MIDI Input")); + + menu->addChild(createBoolPtrMenuItem("Smooth CC", "", &module->midiInput.smooth)); + menu->addChild(createBoolPtrMenuItem("MPE mode", "", &module->midiInput.mpeMode)); + menu->addChild(createBoolPtrMenuItem("14-bit CC 0-31 / 32-63", "", &module->midiInput.lsbMode)); + + struct InputChannelItem : MenuItem { + HostMIDICC* module; + Menu* createChildMenu() override { + Menu* menu = new Menu; + for (int c = 0; c <= 16; c++) { + menu->addChild(createCheckMenuItem((c == 0) ? "All" : string::f("%d", c), "", + [=]() {return module->midiInput.channel == c;}, + [=]() {module->midiInput.channel = c;} + )); + } + return menu; + } + }; + InputChannelItem* const inputChannelItem = new InputChannelItem; + inputChannelItem->text = "MIDI channel"; + inputChannelItem->rightText = (module->midiInput.channel ? string::f("%d", module->midiInput.channel) : "All") + + " " + RIGHT_ARROW; + inputChannelItem->module = module; + menu->addChild(inputChannelItem); + + menu->addChild(new MenuSeparator); + menu->addChild(createMenuLabel("MIDI Output")); + + struct OutputChannelItem : MenuItem { + HostMIDICC* module; + Menu* createChildMenu() override { + Menu* menu = new Menu; + for (uint8_t c = 0; c < 16; c++) { + menu->addChild(createCheckMenuItem(string::f("%d", c+1), "", + [=]() {return module->midiOutput.channel == c;}, + [=]() {module->midiOutput.channel = c;} + )); + } + return menu; + } + }; + OutputChannelItem* const outputChannelItem = new OutputChannelItem; + outputChannelItem->text = "MIDI channel"; + outputChannelItem->rightText = string::f("%d", module->midiOutput.channel+1) + " " + RIGHT_ARROW; + outputChannelItem->module = module; + menu->addChild(outputChannelItem); } }; diff --git a/plugins/Cardinal/src/HostMIDI-Gate.cpp b/plugins/Cardinal/src/HostMIDI-Gate.cpp index b9dfb46..9a980e3 100644 --- a/plugins/Cardinal/src/HostMIDI-Gate.cpp +++ b/plugins/Cardinal/src/HostMIDI-Gate.cpp @@ -54,7 +54,6 @@ struct HostMIDIGate : Module { struct MidiInput { // Cardinal specific CardinalPluginContext* const pcontext; - midi::Message converterMsg; const MidiEvent* midiEvents; uint32_t midiEventsLeft; uint32_t midiEventFrame; @@ -76,7 +75,6 @@ struct HostMIDIGate : Module { MidiInput(CardinalPluginContext* const pc) : pcontext(pc) { - converterMsg.bytes.resize(0xff); reset(); } @@ -92,7 +90,8 @@ struct HostMIDIGate : Module { panic(); } - void panic() { + void panic() + { for (int i = 0; i < 16; ++i) { for (int c = 0; c < 16; ++c) @@ -129,17 +128,9 @@ struct HostMIDIGate : Module { ++midiEvents; --midiEventsLeft; - const uint8_t* data; - - if (midiEvent.size > MidiEvent::kDataSize) - { - data = midiEvent.dataExt; - converterMsg.bytes.resize(midiEvent.size); - } - else - { - data = midiEvent.data; - } + const uint8_t* const data = midiEvent.size > MidiEvent::kDataSize + ? midiEvent.dataExt + : midiEvent.data; if (channel != 0 && data[0] < 0xF0) {