Add implementation for MIDI-CC
Signed-off-by: falkTX <falktx@falktx.com>
This commit is contained in:
parent
d68c303627
commit
b91af832e1
2 changed files with 360 additions and 17 deletions
|
@ -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<rack::engine::Output>& 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<CardinalPluginContext*>(APP))
|
||||
: 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 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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue