Add code logic go MIDI-Gate
Signed-off-by: falkTX <falktx@falktx.com>
This commit is contained in:
parent
bcee774f7f
commit
d68c303627
2 changed files with 391 additions and 4 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/Gate_MIDI.cpp and core/MIDI_Gate.cpp
|
||||||
* Copyright (C) 2016-2021 VCV.
|
* Copyright (C) 2016-2021 VCV.
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or
|
* This program is free software: you can redistribute it and/or
|
||||||
|
@ -38,9 +38,11 @@ struct HostMIDIGate : Module {
|
||||||
NUM_PARAMS
|
NUM_PARAMS
|
||||||
};
|
};
|
||||||
enum InputIds {
|
enum InputIds {
|
||||||
|
ENUMS(GATE_INPUTS, 16),
|
||||||
NUM_INPUTS
|
NUM_INPUTS
|
||||||
};
|
};
|
||||||
enum OutputIds {
|
enum OutputIds {
|
||||||
|
ENUMS(GATE_OUTPUTS, 16),
|
||||||
NUM_OUTPUTS
|
NUM_OUTPUTS
|
||||||
};
|
};
|
||||||
enum LightIds {
|
enum LightIds {
|
||||||
|
@ -49,21 +51,309 @@ struct HostMIDIGate : Module {
|
||||||
|
|
||||||
CardinalPluginContext* const pcontext;
|
CardinalPluginContext* const pcontext;
|
||||||
|
|
||||||
|
struct MidiInput {
|
||||||
|
// Cardinal specific
|
||||||
|
CardinalPluginContext* const pcontext;
|
||||||
|
midi::Message converterMsg;
|
||||||
|
const MidiEvent* midiEvents;
|
||||||
|
uint32_t midiEventsLeft;
|
||||||
|
uint32_t midiEventFrame;
|
||||||
|
int64_t lastBlockFrame;
|
||||||
|
uint8_t channel;
|
||||||
|
|
||||||
|
// stuff from Rack
|
||||||
|
/** [cell][channel] */
|
||||||
|
bool gates[16][16];
|
||||||
|
/** [cell][channel] */
|
||||||
|
float gateTimes[16][16];
|
||||||
|
/** [cell][channel] */
|
||||||
|
uint8_t velocities[16][16];
|
||||||
|
/** Cell ID in learn mode, or -1 if none. */
|
||||||
|
int learningId;
|
||||||
|
|
||||||
|
bool mpeMode;
|
||||||
|
|
||||||
|
MidiInput(CardinalPluginContext* const pc)
|
||||||
|
: pcontext(pc)
|
||||||
|
{
|
||||||
|
converterMsg.bytes.resize(0xff);
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset()
|
||||||
|
{
|
||||||
|
midiEvents = nullptr;
|
||||||
|
midiEventsLeft = 0;
|
||||||
|
midiEventFrame = 0;
|
||||||
|
lastBlockFrame = -1;
|
||||||
|
channel = 0;
|
||||||
|
learningId = -1;
|
||||||
|
mpeMode = false;
|
||||||
|
panic();
|
||||||
|
}
|
||||||
|
|
||||||
|
void panic() {
|
||||||
|
for (int i = 0; i < 16; ++i)
|
||||||
|
{
|
||||||
|
for (int c = 0; c < 16; ++c)
|
||||||
|
{
|
||||||
|
gates[i][c] = false;
|
||||||
|
gateTimes[i][c] = 0.f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool process(const ProcessArgs& args, std::vector<rack::engine::Output>& outputs,
|
||||||
|
const bool velocityMode, uint8_t learnedNotes[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* data;
|
||||||
|
|
||||||
|
if (midiEvent.size > MidiEvent::kDataSize)
|
||||||
|
{
|
||||||
|
data = midiEvent.dataExt;
|
||||||
|
converterMsg.bytes.resize(midiEvent.size);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
data = midiEvent.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel != 0 && data[0] < 0xF0)
|
||||||
|
{
|
||||||
|
if ((data[0] & 0x0F) != (channel - 1))
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// adapted from Rack
|
||||||
|
switch (data[0] & 0xF0)
|
||||||
|
{
|
||||||
|
// note on
|
||||||
|
case 0x90:
|
||||||
|
if (data[2] > 0)
|
||||||
|
{
|
||||||
|
const int c = mpeMode ? (data[0] & 0x0F) : 0;
|
||||||
|
// Learn
|
||||||
|
if (learningId >= 0) {
|
||||||
|
learnedNotes[learningId] = data[1];
|
||||||
|
learningId = -1;
|
||||||
|
}
|
||||||
|
// Find id
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
if (learnedNotes[i] == data[1]) {
|
||||||
|
gates[i][c] = true;
|
||||||
|
gateTimes[i][c] = 1e-3f;
|
||||||
|
velocities[i][c] = data[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// fall-through
|
||||||
|
// note off
|
||||||
|
case 0x80:
|
||||||
|
const int c = mpeMode ? (data[0] & 0x0F) : 0;
|
||||||
|
// Find id
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
if (learnedNotes[i] == data[1]) {
|
||||||
|
gates[i][c] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
++midiEventFrame;
|
||||||
|
|
||||||
|
// Rack stuff
|
||||||
|
const int channels = mpeMode ? 16 : 1;
|
||||||
|
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
outputs[GATE_OUTPUTS + i].setChannels(channels);
|
||||||
|
for (int c = 0; c < channels; c++) {
|
||||||
|
// Make sure all pulses last longer than 1ms
|
||||||
|
if (gates[i][c] || gateTimes[i][c] > 0.f)
|
||||||
|
{
|
||||||
|
float velocity = velocityMode ? (velocities[i][c] / 127.f) : 1.f;
|
||||||
|
outputs[GATE_OUTPUTS + i].setVoltage(velocity * 10.f, c);
|
||||||
|
gateTimes[i][c] -= args.sampleTime;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
outputs[GATE_OUTPUTS + i].setVoltage(0.f, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return blockFrameChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
} midiInput;
|
||||||
|
|
||||||
|
struct MidiOutput {
|
||||||
|
// cardinal specific
|
||||||
|
CardinalPluginContext* const pcontext;
|
||||||
|
uint8_t channel = 0;
|
||||||
|
|
||||||
|
// base class vars
|
||||||
|
int vels[128];
|
||||||
|
bool lastGates[128];
|
||||||
|
int64_t frame = 0;
|
||||||
|
|
||||||
|
MidiOutput(CardinalPluginContext* const pc)
|
||||||
|
: pcontext(pc)
|
||||||
|
{
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset()
|
||||||
|
{
|
||||||
|
// base class vars
|
||||||
|
for (int note = 0; note < 128; ++note)
|
||||||
|
{
|
||||||
|
vels[note] = 100;
|
||||||
|
lastGates[note] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// cardinal specific
|
||||||
|
channel = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void panic()
|
||||||
|
{
|
||||||
|
// TODO send all notes off CC
|
||||||
|
|
||||||
|
// Send all note off commands
|
||||||
|
for (int note = 0; note < 128; note++)
|
||||||
|
{
|
||||||
|
// Note off
|
||||||
|
midi::Message m;
|
||||||
|
m.setStatus(0x8);
|
||||||
|
m.setNote(note);
|
||||||
|
m.setValue(0);
|
||||||
|
m.setFrame(frame);
|
||||||
|
sendMessage(m);
|
||||||
|
lastGates[note] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setVelocity(int vel, int note)
|
||||||
|
{
|
||||||
|
vels[note] = vel;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setGate(bool gate, int note)
|
||||||
|
{
|
||||||
|
if (gate && !lastGates[note])
|
||||||
|
{
|
||||||
|
// Note on
|
||||||
|
midi::Message m;
|
||||||
|
m.setStatus(0x9);
|
||||||
|
m.setNote(note);
|
||||||
|
m.setValue(vels[note]);
|
||||||
|
m.setFrame(frame);
|
||||||
|
sendMessage(m);
|
||||||
|
}
|
||||||
|
else if (!gate && lastGates[note])
|
||||||
|
{
|
||||||
|
// Note off
|
||||||
|
midi::Message m;
|
||||||
|
m.setStatus(0x8);
|
||||||
|
m.setNote(note);
|
||||||
|
m.setValue(vels[note]);
|
||||||
|
m.setFrame(frame);
|
||||||
|
sendMessage(m);
|
||||||
|
}
|
||||||
|
lastGates[note] = gate;
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendMessage(const midi::Message& message)
|
||||||
|
{
|
||||||
|
pcontext->writeMidiMessage(message, channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
} midiOutput;
|
||||||
|
|
||||||
|
bool velocityMode = false;
|
||||||
|
uint8_t learnedNotes[16] = {};
|
||||||
|
|
||||||
HostMIDIGate()
|
HostMIDIGate()
|
||||||
: pcontext(static_cast<CardinalPluginContext*>(APP))
|
: pcontext(static_cast<CardinalPluginContext*>(APP)),
|
||||||
|
midiInput(pcontext),
|
||||||
|
midiOutput(pcontext)
|
||||||
{
|
{
|
||||||
if (pcontext == nullptr)
|
if (pcontext == nullptr)
|
||||||
throw rack::Exception("Plugin context is null");
|
throw rack::Exception("Plugin context is null");
|
||||||
|
|
||||||
config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
|
config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
|
||||||
|
|
||||||
|
for (int i = 0; i < 16; i++)
|
||||||
|
configInput(GATE_INPUTS + i, string::f("Cell %d", i + 1));
|
||||||
|
|
||||||
|
for (int i = 0; i < 16; i++)
|
||||||
|
configOutput(GATE_OUTPUTS + i, string::f("Gate %d", i + 1));
|
||||||
|
|
||||||
|
onReset();
|
||||||
}
|
}
|
||||||
|
|
||||||
void onReset() override
|
void onReset() override
|
||||||
{
|
{
|
||||||
|
for (int y = 0; y < 4; ++y)
|
||||||
|
for (int x = 0; x < 4; ++x)
|
||||||
|
learnedNotes[4 * y + x] = 36 + 4 * (3 - y) + x;
|
||||||
|
|
||||||
|
velocityMode = false;
|
||||||
|
|
||||||
|
midiInput.reset();
|
||||||
|
midiOutput.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
void process(const ProcessArgs& args) override
|
void process(const ProcessArgs& args) override
|
||||||
{
|
{
|
||||||
|
if (midiInput.process(args, outputs, velocityMode, learnedNotes))
|
||||||
|
midiOutput.frame = 0;
|
||||||
|
else
|
||||||
|
++midiOutput.frame;
|
||||||
|
|
||||||
|
for (int i = 0; i < 16; i++)
|
||||||
|
{
|
||||||
|
const int note = learnedNotes[i];
|
||||||
|
|
||||||
|
if (velocityMode)
|
||||||
|
{
|
||||||
|
int vel = (int) std::round(inputs[GATE_INPUTS + i].getVoltage() / 10.f * 127);
|
||||||
|
vel = clamp(vel, 0, 127);
|
||||||
|
midiOutput.setVelocity(vel, note);
|
||||||
|
midiOutput.setGate(vel > 0, note);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const bool gate = inputs[GATE_INPUTS + i].getVoltage() >= 1.f;
|
||||||
|
midiOutput.setVelocity(100, note);
|
||||||
|
midiOutput.setGate(gate, note);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
json_t* dataToJson() override
|
json_t* dataToJson() override
|
||||||
|
@ -71,11 +361,52 @@ struct HostMIDIGate : Module {
|
||||||
json_t* const rootJ = json_object();
|
json_t* const rootJ = json_object();
|
||||||
DISTRHO_SAFE_ASSERT_RETURN(rootJ != nullptr, nullptr);
|
DISTRHO_SAFE_ASSERT_RETURN(rootJ != nullptr, nullptr);
|
||||||
|
|
||||||
|
// input and output
|
||||||
|
if (json_t* const notesJ = json_array())
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 16; i++)
|
||||||
|
json_array_append_new(notesJ, json_integer(learnedNotes[i]));
|
||||||
|
json_object_set_new(rootJ, "notes", notesJ);
|
||||||
|
}
|
||||||
|
json_object_set_new(rootJ, "velocity", json_boolean(velocityMode));
|
||||||
|
|
||||||
|
// input only
|
||||||
|
json_object_set_new(rootJ, "mpeMode", json_boolean(midiInput.mpeMode));
|
||||||
|
|
||||||
|
// separate
|
||||||
|
json_object_set_new(rootJ, "inputChannel", json_integer(midiInput.channel));
|
||||||
|
json_object_set_new(rootJ, "outputChannel", json_integer(midiOutput.channel));
|
||||||
|
|
||||||
return rootJ;
|
return rootJ;
|
||||||
}
|
}
|
||||||
|
|
||||||
void dataFromJson(json_t* rootJ) override
|
void dataFromJson(json_t* const rootJ) override
|
||||||
{
|
{
|
||||||
|
// input and output
|
||||||
|
if (json_t* const notesJ = json_object_get(rootJ, "notes"))
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 16; i++)
|
||||||
|
{
|
||||||
|
if (json_t* const noteJ = json_array_get(notesJ, i))
|
||||||
|
learnedNotes[i] = json_integer_value(noteJ);
|
||||||
|
else
|
||||||
|
learnedNotes[i] = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json_t* const velocityJ = json_object_get(rootJ, "velocity"))
|
||||||
|
velocityMode = json_boolean_value(velocityJ);
|
||||||
|
|
||||||
|
// input only
|
||||||
|
if (json_t* const mpeModeJ = json_object_get(rootJ, "mpeMode"))
|
||||||
|
midiInput.mpeMode = json_boolean_value(mpeModeJ);
|
||||||
|
|
||||||
|
// 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 +446,61 @@ struct HostMIDIGateWidget : ModuleWidget {
|
||||||
|
|
||||||
void appendContextMenu(Menu* const menu) override
|
void appendContextMenu(Menu* const menu) override
|
||||||
{
|
{
|
||||||
|
menu->addChild(new MenuSeparator);
|
||||||
|
menu->addChild(createMenuLabel("MIDI Input"));
|
||||||
|
|
||||||
|
menu->addChild(createBoolPtrMenuItem("MPE mode", "", &module->midiInput.mpeMode));
|
||||||
|
|
||||||
|
struct InputChannelItem : MenuItem {
|
||||||
|
HostMIDIGate* 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 {
|
||||||
|
HostMIDIGate* 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);
|
||||||
|
|
||||||
|
menu->addChild(new MenuSeparator);
|
||||||
|
menu->addChild(createMenuLabel("MIDI Input & Output"));
|
||||||
|
|
||||||
|
menu->addChild(createBoolPtrMenuItem("Velocity mode", "", &module->velocityMode));
|
||||||
|
|
||||||
|
menu->addChild(createMenuItem("Panic", "",
|
||||||
|
[=]() { module->midiInput.panic(); module->midiOutput.panic(); }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -274,7 +274,8 @@ struct HostMIDI : Module {
|
||||||
return blockFrameChanged;
|
return blockFrameChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
void processMessage(const midi::Message& msg) {
|
void processMessage(const midi::Message& msg)
|
||||||
|
{
|
||||||
// DEBUG("MIDI: %ld %s", msg.getFrame(), msg.toString().c_str());
|
// DEBUG("MIDI: %ld %s", msg.getFrame(), msg.toString().c_str());
|
||||||
|
|
||||||
switch (msg.getStatus()) {
|
switch (msg.getStatus()) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue