1
Fork 0
cardinal/plugins/Cardinal/src/HostMIDI-CC.cpp

837 lines
26 KiB
C++

/*
* DISTRHO Cardinal Plugin
* Copyright (C) 2021-2022 Filipe Coelho <falktx@falktx.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 3 of
* the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* For a full copy of the GNU General Public License see the LICENSE file.
*/
/**
* 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
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 3 of
* the License, or (at your option) any later version.
*/
#include "plugincontext.hpp"
#include "Widgets.hpp"
#include <algorithm>
// -----------------------------------------------------------------------------------------------------------
USE_NAMESPACE_DISTRHO;
struct HostMIDICC : TerminalModule {
enum ParamIds {
NUM_PARAMS
};
enum InputIds {
ENUMS(CC_INPUTS, 16),
CC_INPUT_CH_PRESSURE,
CC_INPUT_PITCHBEND,
NUM_INPUTS
};
enum OutputIds {
ENUMS(CC_OUTPUT, 16),
CC_OUTPUT_CH_PRESSURE,
CC_OUTPUT_PITCHBEND,
NUM_OUTPUTS
};
enum LightIds {
NUM_LIGHTS
};
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;
uint8_t chPressure[16];
uint16_t pitchbend[16];
// stuff from Rack
/** [cc][channel] */
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]
*/
uint8_t msbValues[32][16];
int learningId;
/** [cell][channel] */
dsp::ExponentialFilter valueFilters[NUM_OUTPUTS][16];
bool smooth;
bool mpeMode;
bool lsbMode;
MidiInput(CardinalPluginContext* const pc)
: pcontext(pc)
{
for (int i = 0; i < NUM_OUTPUTS; 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 (uint8_t cc = 0; cc < 128; cc++) {
for (int c = 0; c < 16; c++) {
ccValues[cc][c] = 0;
}
}
for (uint8_t cc = 0; cc < 32; cc++) {
for (int c = 0; c < 16; c++) {
msbValues[cc][c] = 0;
}
}
for (int c = 0; c < 16; c++) {
chPressure[c] = 0;
pitchbend[c] = 8192;
}
learningId = -1;
smooth = true;
mpeMode = false;
lsbMode = false;
}
bool process(const ProcessArgs& args, std::vector<rack::engine::Output>& outputs, int8_t learnedCcs[16],
const bool isBypassed)
{
// 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;
}
if (isBypassed)
{
++midiEventFrame;
return false;
}
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;
}
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;
}
// adapted from Rack
const uint8_t c = mpeMode ? chan : 0;
const int8_t cc = data[1];
const uint8_t value = data[2];
// Learn
if (learningId >= 0 && ccValues[cc][c] != value)
{
// NOTE: does the same as `setLearnedCc`
if (cc >= 0)
{
for (int id = 0; id < 16; ++id)
{
if (learnedCcs[id] == cc)
learnedCcs[id] = -1;
}
}
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);
const int8_t cc = learnedCcs[i];
if (cc < 0)
continue;
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.
const float value = static_cast<float>(cellValue) / (128.0f * 127.0f);
// 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);
}
}
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<float>(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<float>(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;
}
} midiInput;
struct MidiOutput {
// cardinal specific
CardinalPluginContext* const pcontext;
uint8_t channel = 0;
// from Rack
int lastValues[130];
int64_t frame = 0;
MidiOutput(CardinalPluginContext* const pc)
: pcontext(pc)
{
reset();
}
void reset()
{
for (int n = 0; n < 130; ++n)
lastValues[n] = -1;
}
void sendCC(const int cc, const int value)
{
if (lastValues[cc] == value)
return;
lastValues[cc] = value;
midi::Message m;
m.setStatus(0xb);
m.setNote(cc);
m.setValue(value);
m.setFrame(frame);
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);
}
} midiOutput;
int8_t learnedCcs[16];
HostMIDICC()
: pcontext(static_cast<CardinalPluginContext*>(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 id = 0; id < 16; ++id)
configInput(CC_INPUTS + id, string::f("Cell %d", id + 1));
configInput(CC_INPUT_CH_PRESSURE, "Channel pressure");
configInput(CC_INPUT_PITCHBEND, "Pitchbend");
for (int id = 0; id < 16; ++id)
configOutput(CC_OUTPUT + id, string::f("Cell %d", id + 1));
configOutput(CC_OUTPUT_CH_PRESSURE, "Channel pressure");
configOutput(CC_OUTPUT_PITCHBEND, "Pitchbend");
onReset();
}
void onReset() override
{
for (int id = 0; id < 16; ++id)
learnedCcs[id] = id + 1;
midiInput.reset();
midiOutput.reset();
}
void processTerminalInput(const ProcessArgs& args) override
{
if (midiInput.process(args, outputs, learnedCcs, isBypassed()))
midiOutput.frame = 0;
else
++midiOutput.frame;
}
void processTerminalOutput(const ProcessArgs&) override
{
if (isBypassed())
return;
for (int i = 0; i < 16; i++)
{
if (learnedCcs[id] < 0)
continue;
uint8_t value = (uint8_t) clamp(std::round(inputs[CC_INPUTS + id].getVoltage() / 10.f * 127), 0.f, 127.f);
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);
}
}
void setLearnedCc(const int id, const int8_t cc)
{
// Unset IDs of similar CCs
if (cc >= 0)
{
for (int idx = 0; idx < 16; ++idx)
{
if (learnedCcs[idx] == cc)
learnedCcs[idx] = -1;
}
}
learnedCcs[id] = cc;
}
json_t* dataToJson() override
{
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 id = 0; id < 16; ++id)
json_array_append_new(ccsJ, json_integer(learnedCcs[id]));
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* const rootJ) override
{
// input and output
if (json_t* const ccsJ = json_object_get(rootJ, "ccs"))
{
for (int id = 0; id < 16; ++id)
{
if (json_t* const ccJ = json_array_get(ccsJ, id))
setLearnedCc(id, json_integer_value(ccJ));
else
learnedCcs[id] = -1;
}
}
// 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;
}
};
// --------------------------------------------------------------------------------------------------------------------
#ifndef HEADLESS
/**
* Based on VCVRack's CcChoice as defined in src/core/plugin.hpp
* Copyright (C) 2016-2021 VCV.
*
* This program is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 3 of
* the License, or (at your option) any later version.
*/
struct CardinalCcChoice : CardinalLedDisplayChoice {
HostMIDICC* const module;
const int id;
int8_t focusCc = -1;
CardinalCcChoice(HostMIDICC* const m, const int i)
: CardinalLedDisplayChoice(),
module(m),
id(i)
{
// Module browser setup
if (m == nullptr)
{
text = string::f("%d", i+1);
}
}
void step() override
{
int8_t cc;
if (module == nullptr)
{
cc = id;
}
else if (module->midiInput.learningId == id)
{
cc = focusCc;
color.a = 0.5f;
}
else
{
cc = module->learnedCcs[id];
color.a = 1.0f;
// Cancel focus if no longer learning
if (APP->event->getSelectedWidget() == this)
APP->event->setSelectedWidget(NULL);
}
// Set text
if (cc < 0)
text = "--";
else
text = string::f("%d", cc);
}
void onSelect(const SelectEvent& e) override
{
DISTRHO_SAFE_ASSERT_RETURN(module != nullptr,);
module->midiInput.learningId = id;
focusCc = -1;
e.consume(this);
}
void onDeselect(const DeselectEvent&) override
{
DISTRHO_SAFE_ASSERT_RETURN(module != nullptr,);
if (module->midiInput.learningId == id)
{
if (focusCc >= 0)
module->setLearnedCc(id, focusCc);
module->midiInput.learningId = -1;
}
}
void onSelectText(const SelectTextEvent& e) override
{
int c = e.codepoint;
if ('0' <= c && c <= '9')
{
if (focusCc < 0)
focusCc = 0;
focusCc = focusCc * 10 + (c - '0');
}
if (focusCc < 0)
focusCc = -1;
e.consume(this);
}
void onSelectKey(const SelectKeyEvent& e) override
{
if (e.key != GLFW_KEY_ENTER && e.key != GLFW_KEY_KP_ENTER)
return;
if (e.action != GLFW_PRESS)
return;
if (e.mods & RACK_MOD_MASK)
return;
DeselectEvent eDeselect;
onDeselect(eDeselect);
APP->event->selectedWidget = NULL;
e.consume(this);
}
};
struct CCGridDisplay : Widget {
void draw(const DrawArgs& args) override
{
nvgBeginPath(args.vg);
nvgRoundedRect(args.vg, 0, 0, box.size.x, box.size.y, 4);
nvgFillColor(args.vg, nvgRGB(0, 0, 0));
nvgFill(args.vg);
Widget::draw(args);
}
void setModule(HostMIDICC* const module)
{
LedDisplaySeparator* hSeparators[6];
LedDisplaySeparator* vSeparators[3];
LedDisplayChoice* choices[3][6];
// Add vSeparators
for (int x = 0; x < 3; ++x)
{
vSeparators[x] = new LedDisplaySeparator;
vSeparators[x]->box.pos = Vec(box.size.x / 3 * (x+1), 0.0f);
vSeparators[x]->box.size = Vec(1.0f, box.size.y);
addChild(vSeparators[x]);
}
// Add hSeparators and choice widgets
for (int y = 0; y < 6; ++y)
{
hSeparators[y] = new LedDisplaySeparator;
hSeparators[y]->box.pos = Vec(0.0f, box.size.y / 6 * (y+1));
hSeparators[y]->box.size = Vec(box.size.x, 1.0f);
addChild(hSeparators[y]);
for (int x = 0; x < 3; ++x)
{
const int id = 6 * x + y;
switch (id)
{
case 16:
choices[x][y] = new CardinalLedDisplayChoice("Ch.press");
break;
case 17:
choices[x][y] = new CardinalLedDisplayChoice("Pbend");
break;
default:
choices[x][y] = new CardinalCcChoice(module, id);
break;
}
choices[x][y]->box.pos = Vec(box.size.x / 3 * x, box.size.y / 6 * y);
choices[x][y]->box.size = Vec(box.size.x / 3, box.size.y / 6);
addChild(choices[x][y]);
}
}
}
};
struct HostMIDICCWidget : ModuleWidget {
static constexpr const float startX_In = 14.0f;
static constexpr const float startX_Out = 115.0f;
static constexpr const float startY = 190.0f;
static constexpr const float padding = 29.0f;
HostMIDICC* const module;
HostMIDICCWidget(HostMIDICC* const m)
: module(m)
{
setModule(m);
setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/HostMIDICC.svg")));
addChild(createWidget<ScrewBlack>(Vec(RACK_GRID_WIDTH, 0)));
addChild(createWidget<ScrewBlack>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0)));
addChild(createWidget<ScrewBlack>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
addChild(createWidget<ScrewBlack>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
for (int i=0; i<18; ++i)
{
const float x = startX_In + int(i / 6) * padding;
const float y = startY + int(i % 6) * padding;
addInput(createInput<PJ301MPort>(Vec(x, y), module, i));
}
for (int i=0; i<18; ++i)
{
const float x = startX_Out + int(i / 6) * padding;
const float y = startY + int(i % 6) * padding;
addOutput(createOutput<PJ301MPort>(Vec(x, y), module, i));
}
CCGridDisplay* const display = createWidget<CCGridDisplay>(Vec(startX_In - 3.0f, 70.0f));
display->box.size = Vec(box.size.x - startX_In * 2.0f + 6.0f, startY - 74.0f - 9.0f);
display->setModule(m);
addChild(display);
}
void draw(const DrawArgs& args) override
{
nvgBeginPath(args.vg);
nvgRect(args.vg, 0, 0, box.size.x, box.size.y);
nvgFillPaint(args.vg, nvgLinearGradient(args.vg, 0, 0, 0, box.size.y,
nvgRGB(0x18, 0x19, 0x19), nvgRGB(0x21, 0x22, 0x22)));
nvgFill(args.vg);
nvgBeginPath(args.vg);
nvgRoundedRect(args.vg, startX_Out - 2.5f, startY - 2.0f, padding * 3, padding * 6, 4);
nvgFillColor(args.vg, nvgRGB(0xd0, 0xd0, 0xd0));
nvgFill(args.vg);
ModuleWidget::draw(args);
}
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);
}
};
#else
struct HostMIDICCWidget : ModuleWidget {
HostMIDICCWidget(HostMIDICC* const module) {
setModule(module);
for (int i=0; i<18; ++i)
{
addInput(createInput<PJ301MPort>({}, module, i));
addOutput(createOutput<PJ301MPort>({}, module, i));
}
}
};
#endif
// --------------------------------------------------------------------------------------------------------------------
Model* modelHostMIDICC = createModel<HostMIDICC, HostMIDICCWidget>("HostMIDICC");
// --------------------------------------------------------------------------------------------------------------------