diff --git a/plugins/Cardinal/plugin.json b/plugins/Cardinal/plugin.json index f6ffe6e..3d1fc33 100644 --- a/plugins/Cardinal/plugin.json +++ b/plugins/Cardinal/plugin.json @@ -50,7 +50,7 @@ { "slug": "HostMIDICC", "name": "Host MIDI CC", - "description": "Exposes host-provided MIDI CC in a module", + "description": "Exposes host-provided MIDI CC, channel pressure and pitchbend in a module", "tags": [ "External", "MIDI" diff --git a/plugins/Cardinal/src/HostMIDI-CC.cpp b/plugins/Cardinal/src/HostMIDI-CC.cpp index a9a7f3a..560245e 100644 --- a/plugins/Cardinal/src/HostMIDI-CC.cpp +++ b/plugins/Cardinal/src/HostMIDI-CC.cpp @@ -39,13 +39,13 @@ struct HostMIDICC : Module { }; enum InputIds { ENUMS(CC_INPUTS, 16), - CC_INPUT_CHANNEL_PRESSURE, + CC_INPUT_CH_PRESSURE, CC_INPUT_PITCHBEND, NUM_INPUTS }; enum OutputIds { ENUMS(CC_OUTPUT, 16), - CC_OUTPUT_CHANNEL_PRESSURE, + CC_OUTPUT_CH_PRESSURE, CC_OUTPUT_PITCHBEND, NUM_OUTPUTS }; @@ -64,16 +64,19 @@ struct HostMIDICC : Module { int64_t lastBlockFrame; uint8_t channel; + uint8_t chPressure[16]; + uint16_t pitchbend[16]; + // stuff from Rack /** [cc][channel] */ - int8_t ccValues[128][16]; + uint8_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]; + uint8_t msbValues[32][16]; int learningId; /** [cell][channel] */ - dsp::ExponentialFilter valueFilters[16][16]; + dsp::ExponentialFilter valueFilters[NUM_OUTPUTS][16]; bool smooth; bool mpeMode; bool lsbMode; @@ -81,7 +84,7 @@ struct HostMIDICC : Module { MidiInput(CardinalPluginContext* const pc) : pcontext(pc) { - for (int i = 0; i < 16; i++) { + for (int i = 0; i < NUM_OUTPUTS; i++) { for (int c = 0; c < 16; c++) { valueFilters[i][c].setTau(1 / 30.f); } @@ -107,6 +110,10 @@ struct HostMIDICC : Module { msbValues[cc][c] = 0; } } + for (int c = 0; c < 16; c++) { + chPressure[c] = 0; + pitchbend[c] = 8192; + } learningId = -1; smooth = true; mpeMode = false; @@ -148,33 +155,47 @@ struct HostMIDICC : Module { continue; } - // adapted from Rack - if ((data[0] & 0xF0) != 0xB0) + const uint8_t status = data[0] & 0xF0; + const uint8_t chan = data[0] & 0x0F; + + /**/ if (status == 0xD0) + { + chPressure[chan] = data[1]; + } + else if (status == 0xE0) + { + pitchbend[chan] = (data[2] << 7) | data[1]; + } + else if (status != 0xB0) + { continue; + } - uint8_t c = mpeMode ? (data[0] & 0x0F) : 0; - uint8_t cc = data[1]; + // adapted from Rack + const uint8_t c = mpeMode ? chan : 0; + const uint8_t cc = data[1]; + const uint8_t value = data[2]; - // 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) { + if (learningId >= 0 && ccValues[cc][c] != value) + { learnedCcs[learningId] = cc; learningId = -1; } - if (lsbMode && cc < 32) { + 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) { + 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 { + else + { ccValues[cc][c] = value; } } @@ -184,35 +205,87 @@ struct HostMIDICC : Module { // Rack stuff const int channels = mpeMode ? 16 : 1; - for (int i = 0; i < 16; i++) { + 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++) { + 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); + const float value = static_cast(cellValue) / (128.0f * 127.0f); // Detect behavior from MIDI buttons. - if (smooth && std::fabs(valueFilters[i][c].out - value) < 1.f) { + if (smooth && std::fabs(valueFilters[i][c].out - value) < 1.f) + { // Smooth value with filter valueFilters[i][c].process(args.sampleTime, value); } - else { + else + { // Jump value valueFilters[i][c].out = value; } + outputs[CC_OUTPUT + i].setVoltage(valueFilters[i][c].out * 10.f, c); } } + if (outputs[CC_OUTPUT_CH_PRESSURE].isConnected()) + { + outputs[CC_OUTPUT_CH_PRESSURE].setChannels(channels); + + for (int c = 0; c < channels; c++) + { + const float value = static_cast(chPressure[c]) / 128.0f; + + // Detect behavior from MIDI buttons. + if (smooth && std::fabs(valueFilters[CC_OUTPUT_CH_PRESSURE][c].out - value) < 1.f) + { + // Smooth value with filter + valueFilters[CC_OUTPUT_CH_PRESSURE][c].process(args.sampleTime, value); + } + else + { + // Jump value + valueFilters[CC_OUTPUT_CH_PRESSURE][c].out = value; + } + + outputs[CC_OUTPUT_CH_PRESSURE].setVoltage(valueFilters[CC_OUTPUT_CH_PRESSURE][c].out * 10.f, c); + } + } + + if (outputs[CC_OUTPUT_PITCHBEND].isConnected()) + { + outputs[CC_OUTPUT_PITCHBEND].setChannels(channels); + + for (int c = 0; c < channels; c++) + { + const float value = static_cast(pitchbend[c]) / 16384.0f; + + // Detect behavior from MIDI buttons. + if (smooth && std::fabs(valueFilters[CC_OUTPUT_PITCHBEND][c].out - value) < 1.f) + { + // Smooth value with filter + valueFilters[CC_OUTPUT_PITCHBEND][c].process(args.sampleTime, value); + } + else + { + // Jump value + valueFilters[CC_OUTPUT_PITCHBEND][c].out = value; + } + + outputs[CC_OUTPUT_CH_PRESSURE].setVoltage(valueFilters[CC_OUTPUT_PITCHBEND][c].out * 10.f, c); + } + } + return blockFrameChanged; } @@ -224,8 +297,7 @@ struct HostMIDICC : Module { uint8_t channel = 0; // from Rack - dsp::Timer rateLimiterTimer; - int lastValues[128]; + int lastValues[130]; int64_t frame = 0; MidiOutput(CardinalPluginContext* const pc) @@ -236,16 +308,15 @@ struct HostMIDICC : Module { void reset() { - for (int n = 0; n < 128; n++) + for (int n = 0; n < 130; ++n) lastValues[n] = -1; } - void setValue(int value, int cc) + void sendCC(const int cc, const int value) { - if (value == lastValues[cc]) + if (lastValues[cc] == value) return; lastValues[cc] = value; - // CC midi::Message m; m.setStatus(0xb); m.setNote(cc); @@ -254,6 +325,31 @@ struct HostMIDICC : Module { sendMessage(m); } + void sendChanPressure(const int pressure) + { + if (lastValues[128] == pressure) + return; + lastValues[128] = pressure; + midi::Message m; + m.setStatus(0xd); + m.setNote(pressure); + m.setFrame(frame); + sendMessage(m); + } + + void sendPitchbend(const int pitchbend) + { + if (lastValues[129] == pitchbend) + return; + lastValues[129] = pitchbend; + midi::Message m; + m.setStatus(0xe); + m.setNote(pitchbend & 0x7F); + m.setValue(pitchbend >> 7); + m.setFrame(frame); + sendMessage(m); + } + void sendMessage(const midi::Message& message) { pcontext->writeMidiMessage(message, channel); @@ -276,13 +372,13 @@ struct HostMIDICC : Module { for (int i = 0; i < 16; i++) configInput(CC_INPUTS + i, string::f("Cell %d", i + 1)); - configInput(CC_INPUT_CHANNEL_PRESSURE, "Channel pressure"); + configInput(CC_INPUT_CH_PRESSURE, "Channel pressure"); configInput(CC_INPUT_PITCHBEND, "Pitchbend"); for (int i = 0; i < 16; i++) configOutput(CC_OUTPUT + i, string::f("Cell %d", i + 1)); - configOutput(CC_OUTPUT_CHANNEL_PRESSURE, "Channel pressure"); + configOutput(CC_OUTPUT_CH_PRESSURE, "Channel pressure"); configOutput(CC_OUTPUT_PITCHBEND, "Pitchbend"); onReset(); @@ -304,18 +400,23 @@ struct HostMIDICC : Module { 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]); + midiOutput.sendCC(learnedCcs[i], value); + } + + { + int value = (int) std::round(inputs[CC_INPUT_CH_PRESSURE].getVoltage() / 10.f * 127); + value = clamp(value, 0, 127); + midiOutput.sendChanPressure(value); + } + + { + int value = (int) std::round(inputs[CC_INPUT_PITCHBEND].getVoltage() / 10.f * 16383); + value = clamp(value, 0, 16383); + midiOutput.sendPitchbend(value); } } diff --git a/plugins/Cardinal/src/HostMIDI-Map.cpp b/plugins/Cardinal/src/HostMIDI-Map.cpp index 25bbe46..ef9606b 100644 --- a/plugins/Cardinal/src/HostMIDI-Map.cpp +++ b/plugins/Cardinal/src/HostMIDI-Map.cpp @@ -708,7 +708,7 @@ struct HostMIDIMapWidget : ModuleWidget { for (int c = 0; c <= 16; c++) { menu->addChild(createCheckMenuItem((c == 0) ? "All" : string::f("%d", c), "", [=]() {return module->channel == c;}, - [=]() {module->channel = c;} + [=]() {module->setChannel(c);} )); } return menu; diff --git a/plugins/Cardinal/src/HostMIDI.cpp b/plugins/Cardinal/src/HostMIDI.cpp index 300a881..458973a 100644 --- a/plugins/Cardinal/src/HostMIDI.cpp +++ b/plugins/Cardinal/src/HostMIDI.cpp @@ -519,7 +519,6 @@ struct HostMIDI : Module { struct MidiOutput : dsp::MidiGenerator { CardinalPluginContext* const pcontext; uint8_t channel = 0; - dsp::Timer rateLimiterTimer; MidiOutput(CardinalPluginContext* const pc) : pcontext(pc) {} @@ -579,15 +578,8 @@ struct HostMIDI : Module { else ++midiOutput.frame; - // MIDI baud rate is 31250 b/s, or 3125 B/s. - // CC messages are 3 bytes, so we can send a maximum of 1041 CC messages per second. - // Since multiple CCs can be generated, play it safe and limit the CC rate to 200 Hz. - static constexpr const float rateLimiterPeriod = 1 / 200.f; - bool rateLimiterTriggered = (midiOutput.rateLimiterTimer.process(args.sampleTime) >= rateLimiterPeriod); - if (rateLimiterTriggered) - midiOutput.rateLimiterTimer.time -= rateLimiterPeriod; - - for (int c = 0; c < inputs[PITCH_INPUT].getChannels(); c++) { + for (int c = 0; c < inputs[PITCH_INPUT].getChannels(); ++c) + { int vel = (int) std::round(inputs[VELOCITY_INPUT].getNormalPolyVoltage(10.f * 100 / 127, c) / 10.f * 127); vel = clamp(vel, 0, 127); midiOutput.setVelocity(vel, c); @@ -602,30 +594,13 @@ struct HostMIDI : Module { midiOutput.setKeyPressure(aft, c); } - if (rateLimiterTriggered) { - int pw = (int) std::round((inputs[PITCHBEND_INPUT].getVoltage() + 5.f) / 10.f * 0x4000); - pw = clamp(pw, 0, 0x3fff); - midiOutput.setPitchWheel(pw); + int pw = (int) std::round((inputs[PITCHBEND_INPUT].getVoltage() + 5.f) / 10.f * 16383); + pw = clamp(pw, 0, 16383); + midiOutput.setPitchWheel(pw); - int mw = (int) std::round(inputs[MODWHEEL_INPUT].getVoltage() / 10.f * 127); - mw = clamp(mw, 0, 127); - midiOutput.setModWheel(mw); - - /* unused - int vol = (int) std::round(inputs[VOL_INPUT].getNormalVoltage(10.f) / 10.f * 127); - vol = clamp(vol, 0, 127); - midiOutput.setVolume(vol); - - int pan = (int) std::round((inputs[PAN_INPUT].getVoltage() + 5.f) / 10.f * 127); - pan = clamp(pan, 0, 127); - midiOutput.setPan(pan); - */ - } - - /* unused - bool clk = inputs[CLK_INPUT].getVoltage() >= 1.f; - midiOutput.setClock(clk); - */ + int mw = (int) std::round(inputs[MODWHEEL_INPUT].getVoltage() / 10.f * 127); + mw = clamp(mw, 0, 127); + midiOutput.setModWheel(mw); bool start = inputs[START_INPUT].getVoltage() >= 1.f; midiOutput.setStart(start);