|
|
@@ -0,0 +1,686 @@ |
|
|
|
#include "plugin.hpp" |
|
|
|
|
|
|
|
|
|
|
|
/*! \brief Decode System Exclusive messages. |
|
|
|
SysEx messages are encoded to guarantee transmission of data bytes higher than |
|
|
|
127 without breaking the MIDI protocol. Use this static method to reassemble |
|
|
|
your received message. |
|
|
|
\param inSysEx The SysEx data received from MIDI in. |
|
|
|
\param outData The output buffer where to store the decrypted message. |
|
|
|
\param inLength The length of the input buffer. |
|
|
|
\param inFlipHeaderBits True for Korg and other who store MSB in reverse order |
|
|
|
\return The length of the output buffer. |
|
|
|
@see encodeSysEx @see getSysExArrayLength |
|
|
|
Code inspired from Ruin & Wesen's SysEx encoder/decoder - http://ruinwesen.com |
|
|
|
*/ |
|
|
|
unsigned decodeSysEx(const uint8_t* inSysEx, |
|
|
|
uint8_t* outData, |
|
|
|
unsigned inLength, |
|
|
|
bool inFlipHeaderBits) { |
|
|
|
unsigned count = 0; |
|
|
|
uint8_t msbStorage = 0; |
|
|
|
uint8_t byteIndex = 0; |
|
|
|
|
|
|
|
for (unsigned i = 0; i < inLength; ++i) { |
|
|
|
if ((i % 8) == 0) { |
|
|
|
msbStorage = inSysEx[i]; |
|
|
|
byteIndex = 6; |
|
|
|
} |
|
|
|
else { |
|
|
|
const uint8_t body = inSysEx[i]; |
|
|
|
const uint8_t shift = inFlipHeaderBits ? 6 - byteIndex : byteIndex; |
|
|
|
const uint8_t msb = uint8_t(((msbStorage >> shift) & 1) << 7); |
|
|
|
byteIndex--; |
|
|
|
outData[count++] = msb | body; |
|
|
|
} |
|
|
|
} |
|
|
|
return count; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
struct MidiThing : Module { |
|
|
|
enum ParamId { |
|
|
|
REFRESH_PARAM, |
|
|
|
PARAMS_LEN |
|
|
|
}; |
|
|
|
enum InputId { |
|
|
|
A1_INPUT, |
|
|
|
B1_INPUT, |
|
|
|
C1_INPUT, |
|
|
|
A2_INPUT, |
|
|
|
B2_INPUT, |
|
|
|
C2_INPUT, |
|
|
|
A3_INPUT, |
|
|
|
B3_INPUT, |
|
|
|
C3_INPUT, |
|
|
|
A4_INPUT, |
|
|
|
B4_INPUT, |
|
|
|
C4_INPUT, |
|
|
|
INPUTS_LEN |
|
|
|
}; |
|
|
|
enum OutputId { |
|
|
|
OUTPUTS_LEN |
|
|
|
}; |
|
|
|
enum LightId { |
|
|
|
LIGHTS_LEN |
|
|
|
}; |
|
|
|
/// Port mode |
|
|
|
enum PORTMODE_t : uint8_t { |
|
|
|
NOPORTMODE = 0, |
|
|
|
MODE10V, |
|
|
|
MODEPN5V, |
|
|
|
MODENEG10V, |
|
|
|
MODE8V, |
|
|
|
MODE5V, |
|
|
|
|
|
|
|
LASTPORTMODE |
|
|
|
}; |
|
|
|
|
|
|
|
const char* cfgPortModeNames[7] = { |
|
|
|
"No Mode", |
|
|
|
"0/10v", |
|
|
|
"-5/5v", |
|
|
|
"-10/0v", |
|
|
|
"0/8v", |
|
|
|
"0/5v", |
|
|
|
"" |
|
|
|
}; |
|
|
|
|
|
|
|
// use Pre-def 4 for bridge mode |
|
|
|
const static int VCV_BRIDGE_PREDEF = 4; |
|
|
|
|
|
|
|
midi::Output midiOut; |
|
|
|
|
|
|
|
MidiThing() { |
|
|
|
config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN); |
|
|
|
configButton(REFRESH_PARAM, ""); |
|
|
|
|
|
|
|
for (int i = 0; i < NUM_INPUTS; ++i) { |
|
|
|
portModes[i] = MODE10V; |
|
|
|
configInput(A1_INPUT + i, string::f("Port %d", i + 1)); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
void onReset() override { |
|
|
|
midiOut.reset(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
void refreshConfig() { |
|
|
|
for (int row = 0; row < 4; ++row) { |
|
|
|
for (int col = 0; col < 3; ++col) { |
|
|
|
requestParamOverSysex(row, col, 2); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// request that MidiThing loads a pre-defined template, 1-4 |
|
|
|
void setPredef(uint8_t predef) { |
|
|
|
predef = clamp(predef, 1, 4); |
|
|
|
midi::Message msg; |
|
|
|
msg.bytes.resize(8); |
|
|
|
// Midi spec is zeroo indexed |
|
|
|
uint8_t predefToSend = predef - 1; |
|
|
|
msg.bytes = {0xF0, 0x7D, 0x17, 0x00, 0x00, 0x02, 0x00, predefToSend, 0xF7}; |
|
|
|
midiOut.sendMessage(msg); |
|
|
|
DEBUG("Predef %d msg request sent", predef); |
|
|
|
} |
|
|
|
|
|
|
|
uint8_t port = 0; |
|
|
|
void setVoltageMode(uint8_t row, uint8_t col, uint8_t outputMode_) { |
|
|
|
port = 3 * row + col; |
|
|
|
// +1 because enum starts at 1 |
|
|
|
portModes[port] = (PORTMODE_t)(outputMode_ + 1); |
|
|
|
|
|
|
|
midi::Message msg; |
|
|
|
msg.bytes.resize(8); |
|
|
|
// F0 7D 17 2n 02 02 00 0m F7 |
|
|
|
// Where n = 0 based port number |
|
|
|
// and m is the volt output mode to select from: |
|
|
|
msg.bytes = {0xF0, 0x7D, 0x17, static_cast<unsigned char>(32 + port), 0x02, 0x02, 0x00, portModes[port], 0xF7}; |
|
|
|
midiOut.sendMessage(msg); |
|
|
|
DEBUG("Voltage mode msg sent: port %d (%d), mode %d", port, static_cast<unsigned char>(32 + port), portModes[port]); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
midi::InputQueue inputQueue; |
|
|
|
void requestParamOverSysex(uint8_t row, uint8_t col, uint8_t mode) { |
|
|
|
|
|
|
|
midi::Message msg; |
|
|
|
msg.bytes.resize(8); |
|
|
|
// F0 7D 17 00 01 03 00 nm pp F7 |
|
|
|
uint8_t port = 3 * row + col; |
|
|
|
//Where n is: |
|
|
|
// 0 = Full configuration request. The module will send only pre def, port functions and modified parameters |
|
|
|
// 2 = Send Port configuration |
|
|
|
// 4 = Send MIDI Channel configuration |
|
|
|
// 6 = Send Voice Configuration |
|
|
|
|
|
|
|
uint8_t n = mode * 16; |
|
|
|
uint8_t m = port; // element number: 0-11 port number, 1-16 channel or voice number |
|
|
|
uint8_t pp = 2; |
|
|
|
msg.bytes = {0xF0, 0x7D, 0x17, 0x00, 0x01, 0x03, 0x00, static_cast<uint8_t>(n + m), pp, 0xF7}; |
|
|
|
midiOut.sendMessage(msg); |
|
|
|
DEBUG("API request mode msg sent: port %d, pp %s", port, msg.toString().c_str()); |
|
|
|
} |
|
|
|
|
|
|
|
int getVoltageMode(uint8_t row, uint8_t col) { |
|
|
|
// -1 because menu is zero indexed but enum is not |
|
|
|
int channel = clamp(3 * row + col, 0, NUM_INPUTS - 1); |
|
|
|
return portModes[channel] - 1; |
|
|
|
} |
|
|
|
|
|
|
|
const static int NUM_INPUTS = 12; |
|
|
|
bool isClipping[NUM_INPUTS] = {}; |
|
|
|
|
|
|
|
bool checkIsVoltageWithinRange(uint8_t channel, float voltage) { |
|
|
|
const float tol = 0.001; |
|
|
|
switch (portModes[channel]) { |
|
|
|
case MODE10V: return 0 - tol < voltage && voltage < 10 + tol; |
|
|
|
case MODEPN5V: return -5 - tol < voltage && voltage < 5 + tol; |
|
|
|
case MODENEG10V: return -10 - tol < voltage && voltage < 0 + tol; |
|
|
|
case MODE8V: return 0 - tol < voltage && voltage < 8 + tol; |
|
|
|
case MODE5V: return 0 - tol < voltage && voltage < 5 + tol; |
|
|
|
default: return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
uint16_t rescaleVoltageForChannel(uint8_t channel, float voltage) { |
|
|
|
switch (portModes[channel]) { |
|
|
|
case MODE10V: return rescale(clamp(voltage, 0.f, 10.f), 0.f, +10.f, 0, 16383); |
|
|
|
case MODEPN5V: return rescale(clamp(voltage, -5.f, 5.f), -5.f, +5.f, 0, 16383); |
|
|
|
case MODENEG10V: return rescale(clamp(voltage, -10.f, 0.f), -10.f, +0.f, 0, 16383); |
|
|
|
case MODE8V: return rescale(clamp(voltage, 0.f, 8.f), 0.f, +8.f, 0, 16383); |
|
|
|
case MODE5V: return rescale(clamp(voltage, 0.f, 5.f), 0.f, +5.f, 0, 16383); |
|
|
|
default: return 0; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
dsp::BooleanTrigger buttonTrigger; |
|
|
|
dsp::Timer rateLimiterTimer; |
|
|
|
PORTMODE_t portModes[NUM_INPUTS] = {}; |
|
|
|
void process(const ProcessArgs& args) override { |
|
|
|
|
|
|
|
if (buttonTrigger.process(params[REFRESH_PARAM].getValue())) { |
|
|
|
DEBUG("Refreshing config over SysEx..."); |
|
|
|
refreshConfig(); |
|
|
|
} |
|
|
|
|
|
|
|
midi::Message msg; |
|
|
|
uint8_t outData[32] = {}; |
|
|
|
while (inputQueue.tryPop(&msg, args.frame)) { |
|
|
|
DEBUG("msg (size: %d): %s", msg.getSize(), msg.toString().c_str()); |
|
|
|
|
|
|
|
uint8_t outLen = decodeSysEx(&msg.bytes[0], outData, msg.bytes.size(), false); |
|
|
|
if (outLen > 3) { |
|
|
|
|
|
|
|
int channel = (outData[2] & 0x0f) >> 0; |
|
|
|
|
|
|
|
if (channel >= 0 && channel < NUM_INPUTS) { |
|
|
|
if (outData[outLen - 1] < LASTPORTMODE) { |
|
|
|
portModes[channel] = (PORTMODE_t) outData[outLen - 1]; |
|
|
|
DEBUG("Channel %d, %d: mode %d (%s)", outData[2], channel, portModes[channel], cfgPortModeNames[portModes[channel]]); |
|
|
|
} |
|
|
|
else { |
|
|
|
DEBUG("Error got %d, out of range", outData[outLen - 1]); |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 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. |
|
|
|
const float rateLimiterPeriod = 1 / 200.f; |
|
|
|
bool rateLimiterTriggered = (rateLimiterTimer.process(args.sampleTime) >= rateLimiterPeriod); |
|
|
|
if (rateLimiterTriggered) |
|
|
|
rateLimiterTimer.time -= rateLimiterPeriod; |
|
|
|
|
|
|
|
if (rateLimiterTriggered) { |
|
|
|
|
|
|
|
for (int c = 0; c < NUM_INPUTS; ++c) { |
|
|
|
|
|
|
|
if (!inputs[A1_INPUT + c].isConnected()) { |
|
|
|
continue; |
|
|
|
} |
|
|
|
const float channelVoltage = inputs[A1_INPUT + c].getVoltage(); |
|
|
|
uint16_t pw = rescaleVoltageForChannel(c, channelVoltage); |
|
|
|
isClipping[c] = !checkIsVoltageWithinRange(c, channelVoltage); |
|
|
|
midi::Message m; |
|
|
|
m.setStatus(0xe); |
|
|
|
m.setNote(pw & 0x7f); |
|
|
|
m.setValue((pw >> 7) & 0x7f); |
|
|
|
m.setFrame(args.frame); |
|
|
|
|
|
|
|
midiOut.setChannel(c); |
|
|
|
midiOut.sendMessage(m); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
json_t* dataToJson() override { |
|
|
|
json_t* rootJ = json_object(); |
|
|
|
json_object_set_new(rootJ, "midiOutput", midiOut.toJson()); |
|
|
|
json_object_set_new(rootJ, "inputQueue", inputQueue.toJson()); |
|
|
|
|
|
|
|
return rootJ; |
|
|
|
} |
|
|
|
|
|
|
|
void dataFromJson(json_t* rootJ) override { |
|
|
|
json_t* midiOutputJ = json_object_get(rootJ, "midiOutput"); |
|
|
|
if (midiOutputJ) { |
|
|
|
midiOut.fromJson(midiOutputJ); |
|
|
|
} |
|
|
|
|
|
|
|
json_t* midiInputQueueJ = json_object_get(rootJ, "inputQueue"); |
|
|
|
if (midiInputQueueJ) { |
|
|
|
inputQueue.fromJson(midiInputQueueJ); |
|
|
|
} |
|
|
|
|
|
|
|
refreshConfig(); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
struct MidiThingPort : PJ301MPort { |
|
|
|
int row = 0, col = 0; |
|
|
|
MidiThing* module; |
|
|
|
|
|
|
|
void appendContextMenu(Menu* menu) override { |
|
|
|
|
|
|
|
menu->addChild(new MenuSeparator()); |
|
|
|
std::string label = string::f("Voltage Mode Port %d", 3 * row + col + 1); |
|
|
|
|
|
|
|
menu->addChild(createIndexSubmenuItem(label, |
|
|
|
{"0 to 10v", "-5 to 5v", "-10 to 0v", "0 to 8v", "0 to 5v"}, |
|
|
|
[ = ]() { |
|
|
|
return module->getVoltageMode(row, col); |
|
|
|
}, |
|
|
|
[ = ](int mode) { |
|
|
|
module->setVoltageMode(row, col, mode); |
|
|
|
} |
|
|
|
)); |
|
|
|
|
|
|
|
menu->addChild(createIndexSubmenuItem("Get Port Info", |
|
|
|
{"Full", "Port", "MIDI", "Voice"}, |
|
|
|
[ = ]() { |
|
|
|
return -1; |
|
|
|
}, |
|
|
|
[ = ](int mode) { |
|
|
|
module->requestParamOverSysex(row, col, 2 * mode); |
|
|
|
} |
|
|
|
)); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
// dervied from https://github.com/countmodula/VCVRackPlugins/blob/v2.0.0/src/components/CountModulaLEDDisplay.hpp |
|
|
|
struct LEDDisplay : LightWidget { |
|
|
|
float fontSize = 9; |
|
|
|
Vec textPos = Vec(1, 13); |
|
|
|
int numChars = 7; |
|
|
|
int row = 0, col = 0; |
|
|
|
MidiThing* module; |
|
|
|
|
|
|
|
LEDDisplay() { |
|
|
|
box.size = mm2px(Vec(9.298, 5.116)); |
|
|
|
} |
|
|
|
|
|
|
|
void setCentredPos(Vec pos) { |
|
|
|
box.pos.x = pos.x - box.size.x / 2; |
|
|
|
box.pos.y = pos.y - box.size.y / 2; |
|
|
|
} |
|
|
|
|
|
|
|
void drawBackground(const DrawArgs& args) override { |
|
|
|
// Background |
|
|
|
NVGcolor backgroundColor = nvgRGB(0x20, 0x20, 0x20); |
|
|
|
NVGcolor borderColor = nvgRGB(0x10, 0x10, 0x10); |
|
|
|
nvgBeginPath(args.vg); |
|
|
|
nvgRoundedRect(args.vg, 0.0, 0.0, box.size.x, box.size.y, 2.0); |
|
|
|
nvgFillColor(args.vg, backgroundColor); |
|
|
|
nvgFill(args.vg); |
|
|
|
nvgStrokeWidth(args.vg, 1.0); |
|
|
|
nvgStrokeColor(args.vg, borderColor); |
|
|
|
nvgStroke(args.vg); |
|
|
|
} |
|
|
|
|
|
|
|
void drawLight(const DrawArgs& args) override { |
|
|
|
// Background |
|
|
|
NVGcolor backgroundColor = nvgRGB(0x20, 0x20, 0x20); |
|
|
|
NVGcolor borderColor = nvgRGB(0x10, 0x10, 0x10); |
|
|
|
NVGcolor textColor = nvgRGB(0xff, 0x10, 0x10); |
|
|
|
|
|
|
|
nvgBeginPath(args.vg); |
|
|
|
nvgRoundedRect(args.vg, 0.0, 0.0, box.size.x, box.size.y, 2.0); |
|
|
|
nvgFillColor(args.vg, backgroundColor); |
|
|
|
nvgFill(args.vg); |
|
|
|
nvgStrokeWidth(args.vg, 1.0); |
|
|
|
|
|
|
|
if (module) { |
|
|
|
const bool isClipping = module->isClipping[col + row * 3]; |
|
|
|
if (isClipping) { |
|
|
|
borderColor = nvgRGB(0xff, 0x20, 0x20); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
nvgStrokeColor(args.vg, borderColor); |
|
|
|
nvgStroke(args.vg); |
|
|
|
|
|
|
|
std::shared_ptr<Font> font = APP->window->loadFont(asset::plugin(pluginInstance, "res/fonts/miso.otf")); |
|
|
|
|
|
|
|
if (font && font->handle >= 0) { |
|
|
|
|
|
|
|
std::string text = "?-?v"; // fallback if module not yet defined |
|
|
|
if (module) { |
|
|
|
text = module->cfgPortModeNames[module->getVoltageMode(row, col) + 1]; |
|
|
|
} |
|
|
|
char buffer[numChars + 1]; |
|
|
|
int l = text.size(); |
|
|
|
if (l > numChars) |
|
|
|
l = numChars; |
|
|
|
|
|
|
|
nvgGlobalTint(args.vg, color::WHITE); |
|
|
|
|
|
|
|
text.copy(buffer, l); |
|
|
|
buffer[l] = '\0'; |
|
|
|
|
|
|
|
nvgFontSize(args.vg, fontSize); |
|
|
|
nvgFontFaceId(args.vg, font->handle); |
|
|
|
nvgFillColor(args.vg, textColor); |
|
|
|
nvgTextAlign(args.vg, NVG_ALIGN_CENTER | NVG_ALIGN_BOTTOM); |
|
|
|
NVGtextRow textRow; |
|
|
|
nvgTextBreakLines(args.vg, text.c_str(), NULL, box.size.x, &textRow, 1); |
|
|
|
nvgTextBox(args.vg, textPos.x, textPos.y, box.size.x, textRow.start, textRow.end); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
void onButton(const ButtonEvent& e) override { |
|
|
|
if (e.button == GLFW_MOUSE_BUTTON_RIGHT && e.action == GLFW_PRESS) { |
|
|
|
ui::Menu* menu = createMenu(); |
|
|
|
|
|
|
|
menu->addChild(createMenuLabel(string::f("Voltage mode port %d:", col + 3 * row + 1))); |
|
|
|
|
|
|
|
const std::string labels[5] = {"0 to 10v", "-5 to 5v", "-10 to 0v", "0 to 8v", "0 to 5v"}; |
|
|
|
|
|
|
|
for (int i = 0; i < 5; ++i) { |
|
|
|
menu->addChild(createCheckMenuItem(labels[i], "", |
|
|
|
[ = ]() { |
|
|
|
return module->getVoltageMode(row, col) == i; |
|
|
|
}, |
|
|
|
[ = ]() { |
|
|
|
module->setVoltageMode(row, col, i);; |
|
|
|
} |
|
|
|
)); |
|
|
|
} |
|
|
|
|
|
|
|
e.consume(this); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
LightWidget::onButton(e); |
|
|
|
} |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
struct MidiThingWidget : ModuleWidget { |
|
|
|
|
|
|
|
struct LedDisplayCenterChoiceEx : LedDisplayChoice { |
|
|
|
LedDisplayCenterChoiceEx() { |
|
|
|
box.size = mm2px(math::Vec(0, 8.0)); |
|
|
|
color = nvgRGB(0xf0, 0xf0, 0xf0); |
|
|
|
bgColor = nvgRGBAf(0, 0, 0, 0); |
|
|
|
textOffset = math::Vec(0, 16); |
|
|
|
} |
|
|
|
|
|
|
|
void drawLayer(const DrawArgs& args, int layer) override { |
|
|
|
nvgScissor(args.vg, RECT_ARGS(args.clipBox)); |
|
|
|
if (layer == 1) { |
|
|
|
if (bgColor.a > 0.0) { |
|
|
|
nvgBeginPath(args.vg); |
|
|
|
nvgRect(args.vg, 0, 0, box.size.x, box.size.y); |
|
|
|
nvgFillColor(args.vg, bgColor); |
|
|
|
nvgFill(args.vg); |
|
|
|
} |
|
|
|
|
|
|
|
std::shared_ptr<window::Font> font = APP->window->loadFont(asset::plugin(pluginInstance, "res/fonts/miso.otf")); |
|
|
|
|
|
|
|
if (font && font->handle >= 0 && !text.empty()) { |
|
|
|
nvgFillColor(args.vg, color); |
|
|
|
nvgFontFaceId(args.vg, font->handle); |
|
|
|
nvgTextLetterSpacing(args.vg, -0.6f); |
|
|
|
nvgFontSize(args.vg, 10); |
|
|
|
nvgTextAlign(args.vg, NVG_ALIGN_CENTER | NVG_ALIGN_BOTTOM); |
|
|
|
NVGtextRow textRow; |
|
|
|
nvgTextBreakLines(args.vg, text.c_str(), NULL, box.size.x, &textRow, 1); |
|
|
|
nvgTextBox(args.vg, textOffset.x, textOffset.y, box.size.x, textRow.start, textRow.end); |
|
|
|
} |
|
|
|
} |
|
|
|
nvgResetScissor(args.vg); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
struct MidiDriverItem : ui::MenuItem { |
|
|
|
midi::Port* port; |
|
|
|
int driverId; |
|
|
|
void onAction(const event::Action& e) override { |
|
|
|
port->setDriverId(driverId); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
struct MidiDriverChoice : LedDisplayCenterChoiceEx { |
|
|
|
midi::Port* port; |
|
|
|
void onAction(const event::Action& e) override { |
|
|
|
if (!port) |
|
|
|
return; |
|
|
|
createContextMenu(); |
|
|
|
} |
|
|
|
|
|
|
|
virtual ui::Menu* createContextMenu() { |
|
|
|
ui::Menu* menu = createMenu(); |
|
|
|
menu->addChild(createMenuLabel("MIDI driver")); |
|
|
|
for (int driverId : midi::getDriverIds()) { |
|
|
|
MidiDriverItem* item = new MidiDriverItem; |
|
|
|
item->port = port; |
|
|
|
item->driverId = driverId; |
|
|
|
item->text = midi::getDriver(driverId)->getName(); |
|
|
|
item->rightText = CHECKMARK(item->driverId == port->driverId); |
|
|
|
menu->addChild(item); |
|
|
|
} |
|
|
|
return menu; |
|
|
|
} |
|
|
|
|
|
|
|
void step() override { |
|
|
|
text = port ? port->getDriver()->getName() : ""; |
|
|
|
if (text.empty()) { |
|
|
|
text = "(No driver)"; |
|
|
|
color.a = 0.5f; |
|
|
|
} |
|
|
|
else { |
|
|
|
color.a = 1.f; |
|
|
|
} |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
struct MidiDeviceItem : ui::MenuItem { |
|
|
|
midi::Port* port; |
|
|
|
int deviceId; |
|
|
|
void onAction(const event::Action& e) override { |
|
|
|
port->setDeviceId(deviceId); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
struct MidiDeviceChoice : LedDisplayCenterChoiceEx { |
|
|
|
midi::Port* port; |
|
|
|
void onAction(const event::Action& e) override { |
|
|
|
if (!port) |
|
|
|
return; |
|
|
|
createContextMenu(); |
|
|
|
} |
|
|
|
|
|
|
|
virtual ui::Menu* createContextMenu() { |
|
|
|
ui::Menu* menu = createMenu(); |
|
|
|
menu->addChild(createMenuLabel("MIDI device")); |
|
|
|
{ |
|
|
|
MidiDeviceItem* item = new MidiDeviceItem; |
|
|
|
item->port = port; |
|
|
|
item->deviceId = -1; |
|
|
|
item->text = "(No device)"; |
|
|
|
item->rightText = CHECKMARK(item->deviceId == port->deviceId); |
|
|
|
menu->addChild(item); |
|
|
|
} |
|
|
|
for (int deviceId : port->getDeviceIds()) { |
|
|
|
MidiDeviceItem* item = new MidiDeviceItem; |
|
|
|
item->port = port; |
|
|
|
item->deviceId = deviceId; |
|
|
|
item->text = port->getDeviceName(deviceId); |
|
|
|
item->rightText = CHECKMARK(item->deviceId == port->deviceId); |
|
|
|
menu->addChild(item); |
|
|
|
} |
|
|
|
return menu; |
|
|
|
} |
|
|
|
|
|
|
|
void step() override { |
|
|
|
text = port ? port->getDeviceName(port->deviceId) : ""; |
|
|
|
if (text.empty()) { |
|
|
|
text = "(No device)"; |
|
|
|
color.a = 0.5f; |
|
|
|
} |
|
|
|
else { |
|
|
|
color.a = 1.f; |
|
|
|
} |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
struct MidiWidget : LedDisplay { |
|
|
|
MidiDriverChoice* driverChoice; |
|
|
|
LedDisplaySeparator* driverSeparator; |
|
|
|
MidiDeviceChoice* deviceChoice; |
|
|
|
LedDisplaySeparator* deviceSeparator; |
|
|
|
|
|
|
|
void setMidiPort(midi::Port* port) { |
|
|
|
|
|
|
|
DEBUG("Entering setMidiPort"); |
|
|
|
clearChildren(); |
|
|
|
math::Vec pos; |
|
|
|
|
|
|
|
MidiDriverChoice* driverChoice = createWidget<MidiDriverChoice>(pos); |
|
|
|
driverChoice->box.size = Vec(box.size.x, 20.f); |
|
|
|
//driverChoice->textOffset = Vec(6.f, 14.7f); |
|
|
|
driverChoice->color = nvgRGB(0xf0, 0xf0, 0xf0); |
|
|
|
driverChoice->port = port; |
|
|
|
addChild(driverChoice); |
|
|
|
pos = driverChoice->box.getBottomLeft(); |
|
|
|
this->driverChoice = driverChoice; |
|
|
|
|
|
|
|
this->driverSeparator = createWidget<LedDisplaySeparator>(pos); |
|
|
|
this->driverSeparator->box.size.x = box.size.x; |
|
|
|
addChild(this->driverSeparator); |
|
|
|
|
|
|
|
MidiDeviceChoice* deviceChoice = createWidget<MidiDeviceChoice>(pos); |
|
|
|
deviceChoice->box.size = Vec(box.size.x, 21.f); |
|
|
|
//deviceChoice->textOffset = Vec(6.f, 14.7f); |
|
|
|
deviceChoice->color = nvgRGB(0xf0, 0xf0, 0xf0); |
|
|
|
deviceChoice->port = port; |
|
|
|
addChild(deviceChoice); |
|
|
|
pos = deviceChoice->box.getBottomLeft(); |
|
|
|
this->deviceChoice = deviceChoice; |
|
|
|
|
|
|
|
DEBUG("Leaving setMidiPort"); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
MidiThingWidget(MidiThing* module) { |
|
|
|
setModule(module); |
|
|
|
setPanel(createPanel(asset::plugin(pluginInstance, "res/panels/MidiThing.svg"))); |
|
|
|
|
|
|
|
addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, 0))); |
|
|
|
addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); |
|
|
|
|
|
|
|
MidiWidget* midiInputWidget = createWidget<MidiWidget>(Vec(1.5f, 36.4f)); //mm2px(Vec(0.5f, 10.f))); |
|
|
|
midiInputWidget->box.size = mm2px(Vec(5.08 * 6 - 1, 13.5f)); |
|
|
|
midiInputWidget->setMidiPort(module ? &module->midiOut : NULL); |
|
|
|
addChild(midiInputWidget); |
|
|
|
|
|
|
|
addParam(createParamCentered<BefacoButton>(mm2px(Vec(21.12, 57.32)), module, MidiThing::REFRESH_PARAM)); |
|
|
|
|
|
|
|
const float xStartLed = 0.2 + 0.628; |
|
|
|
const float yStartLed = 28.019; |
|
|
|
|
|
|
|
for (int row = 0; row < 4; row++) { |
|
|
|
for (int col = 0; col < 3; col++) { |
|
|
|
|
|
|
|
LEDDisplay* display = createWidget<LEDDisplay>(mm2px(Vec(xStartLed + 9.751 * col, yStartLed + 5.796 * row))); |
|
|
|
display->module = module; |
|
|
|
display->row = row; |
|
|
|
display->col = col; |
|
|
|
addChild(display); |
|
|
|
|
|
|
|
auto input = createInputCentered<MidiThingPort>(mm2px(Vec(5.08 + 10 * col, 69.77 + 14.225 * row)), module, MidiThing::A1_INPUT + 3 * row + col); |
|
|
|
input->row = row; |
|
|
|
input->col = col; |
|
|
|
input->module = module; |
|
|
|
addInput(input); |
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
DEBUG("MidiThingWidget construction complete!"); |
|
|
|
} |
|
|
|
|
|
|
|
void appendContextMenu(Menu* menu) override { |
|
|
|
MidiThing* module = dynamic_cast<MidiThing*>(this->module); |
|
|
|
assert(module); |
|
|
|
|
|
|
|
menu->addChild(new MenuSeparator()); |
|
|
|
|
|
|
|
menu->addChild(createSubmenuItem("Select Device", "", |
|
|
|
[ = ](Menu * menu) { |
|
|
|
|
|
|
|
for (auto driverId : rack::midi::getDriverIds()) { |
|
|
|
midi::Driver* driver = midi::getDriver(driverId); |
|
|
|
const bool activeDriver = module->midiOut.getDriverId() == driverId; |
|
|
|
|
|
|
|
menu->addChild(createSubmenuItem(driver->getName(), CHECKMARK(activeDriver), |
|
|
|
[ = ](Menu * menu) { |
|
|
|
|
|
|
|
for (auto deviceId : driver->getOutputDeviceIds()) { |
|
|
|
const bool activeDevice = activeDriver && module->midiOut.getDeviceId() == deviceId; |
|
|
|
|
|
|
|
menu->addChild(createMenuItem(driver->getOutputDeviceName(deviceId), |
|
|
|
CHECKMARK(activeDevice), |
|
|
|
[ = ]() { |
|
|
|
module->midiOut.setDriverId(driverId); |
|
|
|
module->midiOut.setDeviceId(deviceId); |
|
|
|
|
|
|
|
module->inputQueue.setDriverId(driverId); |
|
|
|
module->inputQueue.setDeviceId(deviceId); |
|
|
|
module->inputQueue.setChannel(0); // TODO update |
|
|
|
|
|
|
|
module->refreshConfig(); |
|
|
|
DEBUG("Updating Output MIDI settings - driver: %s, device: %s", |
|
|
|
driver->getName().c_str(), driver->getOutputDeviceName(deviceId).c_str()); |
|
|
|
})); |
|
|
|
} |
|
|
|
})); |
|
|
|
} |
|
|
|
})); |
|
|
|
|
|
|
|
|
|
|
|
menu->addChild(createMenuItem("Request Bridge Mode", "", |
|
|
|
[ = ]() { |
|
|
|
module->setPredef(MidiThing::VCV_BRIDGE_PREDEF); |
|
|
|
module->refreshConfig(); |
|
|
|
} |
|
|
|
)); |
|
|
|
|
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
Model* modelMidiThing = createModel<MidiThing, MidiThingWidget>("MidiThingV2"); |