diff --git a/CHANGELOG.md b/CHANGELOG.md
index cfaf1f7..1639fe2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,10 @@
## v2.5.0
* Burst
* Initial release
+ * Midi Thing 2
+ * Initial release
+ * Voltio
+ * Initial release
* PonyVCO
* Now polyphonic
* Misc
diff --git a/docs/MIDIThingV2.md b/docs/MIDIThingV2.md
new file mode 100644
index 0000000..13a028f
--- /dev/null
+++ b/docs/MIDIThingV2.md
@@ -0,0 +1,19 @@
+# MIDI Thing v2
+
+The original MIDI Thing v2 hardware unit is described as follows:
+
+> Midi Thing v2 is a flexible MIDI to CV converter. Allowing polyphonic notes handling, envelope and LFO generation as well as all available MIDI messages to be converted into CV. This is a huge upgrade from our previous beloved MIDI Thing, which adds a screen for easy configuration,12 assignable ports, TRS, USB Host and Device, MIDI merge OUT, a web configuration tool, and a VCV rack Bridge counterpart.
+
+The VCV counterpart is designed to allow users to quickly get up and running with their hardware, i.e. sending CV from VCV to the hardware unit.
+
+## Setup
+
+To use, first ensure the MIDI Thing v2 is plugged into your computer, and visible as a MIDI device. Then select it, either from the top of the module, or the right click context menu. Then select "Request Bridge Mode" from the context menu - this puts the MIDI Thing into a preset designed to work with VCV Rack. See below for details.
+
+![MIDI Thing Config](img/MidiThingV2.png "MIDI Thing v2 Setup")
+
+## Usage
+
+To use, simply wire CV which you wish to send to the hardware to the matching input on the VCV module. Note that you will need to select the range, which can be done by right-clicking on the matching box (see below). Options are 0/10v, -5/5v, -10/0v, 0/8v, 0/5v. Note that the module is **not** designed to work with audio rate signals, just CV.
+
+![MIDI Thing Voltage Range](img/VoltageRange.png "MIDI Thing v2 Voltage Range")
diff --git a/docs/img/MidiThingV2.png b/docs/img/MidiThingV2.png
new file mode 100644
index 0000000..b99c5f9
Binary files /dev/null and b/docs/img/MidiThingV2.png differ
diff --git a/docs/img/VoltageRange.png b/docs/img/VoltageRange.png
new file mode 100644
index 0000000..2da5ec4
Binary files /dev/null and b/docs/img/VoltageRange.png differ
diff --git a/plugin.json b/plugin.json
index 2f82f67..5138f5c 100644
--- a/plugin.json
+++ b/plugin.json
@@ -307,6 +307,30 @@
"Clock modulator",
"Hardware clone"
]
+ },
+ {
+ "slug": "MidiThingV2",
+ "name": "MIDI Thing V2",
+ "description": "Hardware MIDI Thing v2 is a flexible MIDI to CV converter, this module acts as a bridge from VCV",
+ "manualUrl": "https://github.com/VCVRack/Befaco/blob/v2/docs/MIDIThingV2.md",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-midi-thing-v2",
+ "tags": [
+ "External",
+ "MIDI",
+ "Hardware clone"
+ ]
+ },
+ {
+ "slug": "Voltio",
+ "name": "Voltio",
+ "description": "An accurate voltage source and precision adder.",
+ "manualUrl": "https://www.befaco.org/voltio/",
+ "modularGridUrl": "https://www.modulargrid.net/e/befaco-voltio",
+ "tags": [
+ "Hardware clone",
+ "Polyphonic",
+ "Utility"
+ ]
}
]
}
\ No newline at end of file
diff --git a/res/fonts/miso.otf b/res/fonts/miso.otf
new file mode 100644
index 0000000..2b0c62e
Binary files /dev/null and b/res/fonts/miso.otf differ
diff --git a/res/panels/MidiThing.svg b/res/panels/MidiThing.svg
new file mode 100644
index 0000000..c6d658d
--- /dev/null
+++ b/res/panels/MidiThing.svg
@@ -0,0 +1,6828 @@
+
+
diff --git a/res/panels/Voltio.svg b/res/panels/Voltio.svg
new file mode 100644
index 0000000..8a1c0d5
--- /dev/null
+++ b/res/panels/Voltio.svg
@@ -0,0 +1,1343 @@
+
+
diff --git a/src/MidiThing.cpp b/src/MidiThing.cpp
new file mode 100644
index 0000000..78bb3e0
--- /dev/null
+++ b/src/MidiThing.cpp
@@ -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(32 + port), 0x02, 0x02, 0x00, portModes[port], 0xF7};
+ midiOut.sendMessage(msg);
+ DEBUG("Voltage mode msg sent: port %d (%d), mode %d", port, static_cast(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(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 = 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 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(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(pos);
+ this->driverSeparator->box.size.x = box.size.x;
+ addChild(this->driverSeparator);
+
+ MidiDeviceChoice* deviceChoice = createWidget(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(Vec(RACK_GRID_WIDTH, 0)));
+ addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
+
+ MidiWidget* midiInputWidget = createWidget(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(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(mm2px(Vec(xStartLed + 9.751 * col, yStartLed + 5.796 * row)));
+ display->module = module;
+ display->row = row;
+ display->col = col;
+ addChild(display);
+
+ auto input = createInputCentered(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(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("MidiThingV2");
\ No newline at end of file
diff --git a/src/Voltio.cpp b/src/Voltio.cpp
new file mode 100644
index 0000000..160b672
--- /dev/null
+++ b/src/Voltio.cpp
@@ -0,0 +1,91 @@
+#include "plugin.hpp"
+
+using simd::float_4;
+
+struct Davies1900hLargeLightGreyKnobCustom : Davies1900hLargeLightGreyKnob {
+ widget::SvgWidget* bg;
+
+ Davies1900hLargeLightGreyKnobCustom() {
+ minAngle = -0.83 * M_PI;
+ maxAngle = M_PI;
+
+ bg = new widget::SvgWidget;
+ fb->addChildBelow(bg, tw);
+ }
+};
+
+struct Voltio : Module {
+ enum ParamId {
+ OCT_PARAM,
+ RANGE_PARAM,
+ SEMITONES_PARAM,
+ PARAMS_LEN
+ };
+ enum InputId {
+ SUM_INPUT,
+ INPUTS_LEN
+ };
+ enum OutputId {
+ OUT_OUTPUT,
+ OUTPUTS_LEN
+ };
+ enum LightId {
+ PLUSMINUS5_LIGHT,
+ ZEROTOTEN_LIGHT,
+ LIGHTS_LEN
+ };
+
+ Voltio() {
+ config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN);
+ auto octParam = configParam(OCT_PARAM, 0.f, 10.f, 0.f, "Octave");
+ octParam->snapEnabled = true;
+
+ configSwitch(RANGE_PARAM, 0.f, 1.f, 0.f, "Range", {"-5 to +5", "0 to 10"});
+ auto semitonesParam = configParam(SEMITONES_PARAM, 0.f, 11.f, 0.f, "Semitones");
+ semitonesParam->snapEnabled = true;
+
+ configInput(SUM_INPUT, "Sum");
+ configOutput(OUT_OUTPUT, "");
+ }
+
+ void process(const ProcessArgs& args) override {
+ const int channels = std::max(1, inputs[SUM_INPUT].getChannels());
+
+ for (int c = 0; c < channels; c += 4) {
+ float_4 in = inputs[SUM_INPUT].getPolyVoltageSimd(c);
+
+ float offset = params[RANGE_PARAM].getValue() ? -5.f : 0.f;
+ in += params[SEMITONES_PARAM].getValue() / 12.f + params[OCT_PARAM].getValue() + offset;
+
+ outputs[OUT_OUTPUT].setVoltageSimd(in, c);
+ }
+
+ outputs[OUT_OUTPUT].setChannels(channels);
+ }
+
+};
+
+
+struct VoltioWidget : ModuleWidget {
+ VoltioWidget(Voltio* module) {
+ setModule(module);
+ setPanel(createPanel(asset::plugin(pluginInstance, "res/panels/Voltio.svg")));
+
+ addChild(createWidget(Vec(RACK_GRID_WIDTH, 0)));
+ addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
+
+ addParam(createParamCentered(mm2px(Vec(15.0, 20.828)), module, Voltio::OCT_PARAM));
+ addParam(createParamCentered(mm2px(Vec(22.083, 44.061)), module, Voltio::RANGE_PARAM));
+ addParam(createParamCentered(mm2px(Vec(15.0, 67.275)), module, Voltio::SEMITONES_PARAM));
+
+ addInput(createInputCentered(mm2px(Vec(7.117, 111.003)), module, Voltio::SUM_INPUT));
+
+ addOutput(createOutputCentered(mm2px(Vec(22.661, 111.003)), module, Voltio::OUT_OUTPUT));
+
+ addChild(createLightCentered>(mm2px(Vec(5.695, 41.541)), module, Voltio::PLUSMINUS5_LIGHT));
+ addChild(createLightCentered>(mm2px(Vec(5.695, 46.633)), module, Voltio::ZEROTOTEN_LIGHT));
+ }
+};
+
+
+Model* modelVoltio = createModel("Voltio");
\ No newline at end of file
diff --git a/src/plugin.cpp b/src/plugin.cpp
index 82ba131..378cbf8 100644
--- a/src/plugin.cpp
+++ b/src/plugin.cpp
@@ -29,4 +29,6 @@ void init(rack::Plugin *p) {
p->addModel(modelPonyVCO);
p->addModel(modelMotionMTR);
p->addModel(modelBurst);
+ p->addModel(modelMidiThing);
+ p->addModel(modelVoltio);
}
diff --git a/src/plugin.hpp b/src/plugin.hpp
index 450957b..8e71739 100644
--- a/src/plugin.hpp
+++ b/src/plugin.hpp
@@ -30,6 +30,8 @@ extern Model* modelChannelStrip;
extern Model* modelPonyVCO;
extern Model* modelMotionMTR;
extern Model* modelBurst;
+extern Model* modelMidiThing;
+extern Model* modelVoltio;
struct Knurlie : SvgScrew {
Knurlie() {
@@ -223,6 +225,14 @@ struct BefacoSlidePotSmall : app::SvgSlider {
}
};
+struct BefacoButton : app::SvgSwitch {
+ BefacoButton() {
+ momentary = true;
+ addFrame(APP->window->loadSvg(asset::plugin(pluginInstance, "res/components/BefacoButton_0.svg")));
+ addFrame(APP->window->loadSvg(asset::plugin(pluginInstance, "res/components/BefacoButton_1.svg")));
+ }
+};
+
struct Davies1900hWhiteKnobEndless : Davies1900hKnob {
Davies1900hWhiteKnobEndless() {
setSvg(Svg::load(asset::plugin(pluginInstance, "res/components/Davies1900hWhiteEndless.svg")));