|
|
@@ -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. |
|
|
|
* |
|
|
|
* This program is free software: you can redistribute it and/or |
|
|
@@ -38,9 +38,11 @@ struct HostMIDIGate : Module { |
|
|
|
NUM_PARAMS |
|
|
|
}; |
|
|
|
enum InputIds { |
|
|
|
ENUMS(GATE_INPUTS, 16), |
|
|
|
NUM_INPUTS |
|
|
|
}; |
|
|
|
enum OutputIds { |
|
|
|
ENUMS(GATE_OUTPUTS, 16), |
|
|
|
NUM_OUTPUTS |
|
|
|
}; |
|
|
|
enum LightIds { |
|
|
@@ -49,21 +51,309 @@ struct HostMIDIGate : Module { |
|
|
|
|
|
|
|
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() |
|
|
|
: 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(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 |
|
|
|
{ |
|
|
|
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 |
|
|
|
{ |
|
|
|
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 |
|
|
@@ -71,11 +361,52 @@ struct HostMIDIGate : Module { |
|
|
|
json_t* const rootJ = json_object(); |
|
|
|
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; |
|
|
|
} |
|
|
|
|
|
|
|
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 |
|
|
|
{ |
|
|
|
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(); } |
|
|
|
)); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|