diff --git a/include/engine/Input.hpp b/include/engine/Input.hpp
index 30923b15..9d8f4845 100644
--- a/include/engine/Input.hpp
+++ b/include/engine/Input.hpp
@@ -8,8 +8,8 @@ namespace rack {
struct Input : Port {
/** Returns the value if a cable is plugged in, otherwise returns the given default value */
- float normalize(float normalVoltage, int index = 0) {
- return active ? getVoltage(index) : normalVoltage;
+ float normalize(float normalVoltage, int channel = 0) {
+ return active ? getVoltage(channel) : normalVoltage;
}
};
diff --git a/include/engine/Port.hpp b/include/engine/Port.hpp
index d1fc3d2c..bd8fe168 100644
--- a/include/engine/Port.hpp
+++ b/include/engine/Port.hpp
@@ -18,17 +18,20 @@ struct Port {
float value;
float values[PORT_MAX_CHANNELS] = {};
};
- /** Number of polyphonic channels */
- int numChannels = 1;
+ /** Number of polyphonic channels
+ May be 0 to PORT_MAX_CHANNELS.
+ */
+ int channels = 1;
/** Whether a cable is plugged in */
bool active = false;
Light plugLights[2];
- float getVoltage(int index = 0) {
- return values[index];
+ float getVoltage(int channel = 0) {
+ return values[channel];
}
- void setVoltage(float voltage, int index = 0) {
- values[index] = voltage;
+
+ void setVoltage(float voltage, int channel = 0) {
+ values[channel] = voltage;
}
};
diff --git a/include/rack.hpp b/include/rack.hpp
index 86e8b570..4876c96c 100644
--- a/include/rack.hpp
+++ b/include/rack.hpp
@@ -9,6 +9,7 @@
#include "asset.hpp"
#include "window.hpp"
#include "app.hpp"
+#include "midi.hpp"
#include "helpers.hpp"
#include "widgets/Widget.hpp"
diff --git a/res/Core/CV-MIDI.svg b/res/Core/CV-MIDI.svg
new file mode 100644
index 00000000..918d9bef
--- /dev/null
+++ b/res/Core/CV-MIDI.svg
@@ -0,0 +1,471 @@
+
+
+
+
diff --git a/src/Core/CV_MIDI.cpp b/src/Core/CV_MIDI.cpp
new file mode 100644
index 00000000..5ea1e6e7
--- /dev/null
+++ b/src/Core/CV_MIDI.cpp
@@ -0,0 +1,319 @@
+#include "Core.hpp"
+
+
+template
+struct PolyphonicMidiOutput : midi::Output {
+ int vels[C];
+ int lastNotes[C];
+ int notes[C];
+ bool lastGates[C];
+ bool gates[C];
+ int lastAfts[C];
+ int lastPw;
+ int lastMw;
+ bool lastClk;
+ int lastVol;
+ int lastPan;
+ bool lastStart;
+ bool lastStop;
+ bool lastCont;
+
+ PolyphonicMidiOutput() {
+ reset();
+ }
+
+ void reset() {
+ for (int c = 0; c < C; c++) {
+ vels[c] = 100;
+ lastNotes[c] = notes[c] = 60;
+ lastGates[c] = gates[c] = false;
+ lastAfts[c] = -1;
+ }
+ lastPw = 0x2000;
+ lastMw = 0;
+ lastClk = false;
+ lastVol = 127;
+ lastPan = 64;
+ lastStart = false;
+ lastStop = false;
+ lastCont = false;
+ }
+
+ void panic() {
+ reset();
+ // Send all note off commands
+ for (int note = 0; note <= 127; note++) {
+ // Note off
+ midi::Message m;
+ m.cmd = 0x8;
+ m.data1 = note;
+ m.data2 = 0;
+ sendMessage(m);
+ }
+ }
+
+ void setVel(int vel, int c) {
+ vels[c] = vel;
+ }
+
+ void setNote(int note, int c) {
+ notes[c] = note;
+ }
+
+ void setGate(bool gate, int c) {
+ gates[c] = gate;
+ }
+
+ void stepChannel(int c) {
+ bool changedNote = gates[c] && lastGates[c] && notes[c] != lastNotes[c];
+ bool enabledGate = gates[c] && !lastGates[c];
+ bool disabledGate = !gates[c] && lastGates[c];
+ if (changedNote || enabledGate) {
+ // Note on
+ midi::Message m;
+ m.cmd = 0x9;
+ m.data1 = notes[c];
+ m.data2 = vels[c];
+ sendMessage(m);
+ }
+ if (changedNote || disabledGate) {
+ // Note off
+ midi::Message m;
+ m.cmd = 0x8;
+ m.data1 = lastNotes[c];
+ m.data2 = vels[c];
+ sendMessage(m);
+ }
+ lastNotes[c] = notes[c];
+ lastGates[c] = gates[c];
+ }
+
+ void setAftertouch(int aft, int c) {
+ if (lastAfts[c] == aft)
+ return;
+ lastAfts[c] = aft;
+ // Polyphonic key pressure
+ midi::Message m;
+ m.cmd = 0xa;
+ m.data1 = notes[c];
+ m.data2 = aft;
+ sendMessage(m);
+ }
+
+ void setPitchWheel(int pw) {
+ if (lastPw == pw)
+ return;
+ lastPw = pw;
+ // Pitch wheel
+ midi::Message m;
+ m.cmd = 0xe;
+ m.data1 = pw & 0x7f;
+ m.data2 = (pw >> 7) & 0x7f;
+ sendMessage(m);
+ }
+
+ void setModWheel(int mw) {
+ if (lastMw == mw)
+ return;
+ lastMw = mw;
+ // CC Mod wheel
+ midi::Message m;
+ m.cmd = 0xb;
+ m.data1 = 0x01;
+ m.data2 = mw;
+ sendMessage(m);
+ }
+
+ void setClock(bool clk) {
+ if (lastClk == clk)
+ return;
+ lastClk = clk;
+ if (clk) {
+ // Timing clock
+ midi::Message m;
+ m.cmd = 0xf;
+ m.data1 = 0x8;
+ sendMessage(m);
+ }
+ }
+
+ void setVolume(int vol) {
+ if (lastVol == vol)
+ return;
+ lastVol = vol;
+ // CC Volume
+ midi::Message m;
+ m.cmd = 0xb;
+ m.data1 = 0x07;
+ m.data2 = vol;
+ sendMessage(m);
+ }
+
+ void setPan(int pan) {
+ if (lastPan == pan)
+ return;
+ lastPan = pan;
+ // CC Pan
+ midi::Message m;
+ m.cmd = 0xb;
+ m.data1 = 0x0a;
+ m.data2 = pan;
+ sendMessage(m);
+ }
+
+ void setStart(bool start) {
+ if (lastStart == start)
+ return;
+ lastStart = start;
+ if (start) {
+ // Start
+ midi::Message m;
+ m.cmd = 0xf;
+ m.data1 = 0xa;
+ sendMessage(m);
+ }
+ }
+
+ void setStop(bool stop) {
+ if (lastStop == stop)
+ return;
+ lastStop = stop;
+ if (stop) {
+ // Stop
+ midi::Message m;
+ m.cmd = 0xf;
+ m.data1 = 0xb;
+ sendMessage(m);
+ }
+ }
+
+ void setContinue(bool cont) {
+ if (lastCont == cont)
+ return;
+ lastCont = cont;
+ if (cont) {
+ // Continue
+ midi::Message m;
+ m.cmd = 0xf;
+ m.data1 = 0xc;
+ sendMessage(m);
+ }
+ }
+};
+
+
+struct CV_MIDI : Module {
+ enum ParamIds {
+ NUM_PARAMS
+ };
+ enum InputIds {
+ PITCH_INPUT,
+ GATE_INPUT,
+ VEL_INPUT,
+ AFT_INPUT,
+ PW_INPUT,
+ MW_INPUT,
+ CLK_INPUT,
+ VOL_INPUT,
+ PAN_INPUT,
+ START_INPUT,
+ STOP_INPUT,
+ CONTINUE_INPUT,
+ NUM_INPUTS
+ };
+ enum OutputIds {
+ NUM_OUTPUTS
+ };
+ enum LightIds {
+ NUM_LIGHTS
+ };
+
+ PolyphonicMidiOutput midiOutput;
+
+ CV_MIDI() {
+ config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
+ }
+
+ void step() override {
+ for (int c = 0; c < inputs[PITCH_INPUT].channels; c++) {
+ int vel = (int) std::round(inputs[VEL_INPUT].normalize(10.f * 100 / 127, c) / 10.f * 127);
+ vel = clamp(vel, 0, 127);
+ midiOutput.setVel(vel, c);
+
+ int note = (int) std::round(inputs[PITCH_INPUT].getVoltage(c) * 12.f + 60.f);
+ note = clamp(note, 0, 127);
+ midiOutput.setNote(note, c);
+
+ bool gate = inputs[GATE_INPUT].getVoltage(c) >= 1.f;
+ midiOutput.setGate(gate, c);
+
+ midiOutput.stepChannel(c);
+
+ int aft = (int) std::round(inputs[AFT_INPUT].getVoltage(c) / 10.f * 127);
+ aft = clamp(aft, 0, 127);
+ midiOutput.setAftertouch(aft, c);
+ }
+
+ int pw = (int) std::round((inputs[PW_INPUT].getVoltage() + 5.f) / 10.f * 0x4000);
+ pw = clamp(pw, 0, 0x3fff);
+ midiOutput.setPitchWheel(pw);
+
+ int mw = (int) std::round(inputs[MW_INPUT].getVoltage() / 10.f * 127);
+ mw = clamp(mw, 0, 127);
+ midiOutput.setModWheel(mw);
+
+ bool clk = inputs[CLK_INPUT].value >= 1.f;
+ midiOutput.setClock(clk);
+
+ int vol = (int) std::round(inputs[VOL_INPUT].normalize(10.f) / 10.f * 127);
+ vol = clamp(vol, 0, 127);
+ midiOutput.setVolume(vol);
+
+ int pan = (int) std::round((inputs[PAN_INPUT].getVoltage() + 5.f) / 10.f * 127);
+ pan = clamp(pan, 0, 127);
+ midiOutput.setPan(pan);
+
+ bool start = inputs[START_INPUT].value >= 1.f;
+ midiOutput.setStart(start);
+
+ bool stop = inputs[STOP_INPUT].value >= 1.f;
+ midiOutput.setStop(stop);
+
+ bool cont = inputs[CONTINUE_INPUT].value >= 1.f;
+ midiOutput.setContinue(cont);
+ }
+};
+
+
+struct CV_MIDIWidget : ModuleWidget {
+ CV_MIDIWidget(CV_MIDI *module) : ModuleWidget(module) {
+ setPanel(SVG::load(asset::system("res/Core/CV-MIDI.svg")));
+
+ addChild(createWidget(Vec(RACK_GRID_WIDTH, 0)));
+ addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0)));
+ addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
+ addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
+
+ addInput(createInputCentered(mm2px(Vec(9, 64)), module, CV_MIDI::PITCH_INPUT));
+ addInput(createInputCentered(mm2px(Vec(20, 64)), module, CV_MIDI::GATE_INPUT));
+ addInput(createInputCentered(mm2px(Vec(32, 64)), module, CV_MIDI::VEL_INPUT));
+ addInput(createInputCentered(mm2px(Vec(9, 80)), module, CV_MIDI::AFT_INPUT));
+ addInput(createInputCentered(mm2px(Vec(20, 80)), module, CV_MIDI::PW_INPUT));
+ addInput(createInputCentered(mm2px(Vec(32, 80)), module, CV_MIDI::MW_INPUT));
+ addInput(createInputCentered(mm2px(Vec(9, 96)), module, CV_MIDI::CLK_INPUT));
+ addInput(createInputCentered(mm2px(Vec(20, 96)), module, CV_MIDI::VOL_INPUT));
+ addInput(createInputCentered(mm2px(Vec(32, 96)), module, CV_MIDI::PAN_INPUT));
+ addInput(createInputCentered(mm2px(Vec(9, 112)), module, CV_MIDI::START_INPUT));
+ addInput(createInputCentered(mm2px(Vec(20, 112)), module, CV_MIDI::STOP_INPUT));
+ addInput(createInputCentered(mm2px(Vec(32, 112)), module, CV_MIDI::CONTINUE_INPUT));
+
+ MidiWidget *midiWidget = createWidget(mm2px(Vec(3.41891, 14.8373)));
+ midiWidget->box.size = mm2px(Vec(33.840, 28));
+ if (module)
+ midiWidget->midiIO = &module->midiOutput;
+ addChild(midiWidget);
+ }
+};
+
+
+Model *modelCV_MIDI = createModel("CV-MIDI");
diff --git a/src/Core/Core.cpp b/src/Core/Core.cpp
index ba7d790b..740bc894 100644
--- a/src/Core/Core.cpp
+++ b/src/Core/Core.cpp
@@ -38,6 +38,11 @@ void init(rack::Plugin *p) {
modelMIDITriggerToCVInterface->tags = {"External", "MIDI"};
p->addModel(modelMIDITriggerToCVInterface);
+ modelCV_MIDI->name = "CV-MIDI";
+ modelCV_MIDI->description = "";
+ modelCV_MIDI->tags = {"External", "MIDI"};
+ p->addModel(modelCV_MIDI);
+
modelBlank->name = "Blank";
modelBlank->description = "";
modelBlank->tags = {"Blank"};
diff --git a/src/Core/Core.hpp b/src/Core/Core.hpp
index 5dd364b9..c72e1f05 100644
--- a/src/Core/Core.hpp
+++ b/src/Core/Core.hpp
@@ -10,6 +10,7 @@ extern Model *modelMIDIToCVInterface;
extern Model *modelQuadMIDIToCVInterface;
extern Model *modelMIDICCToCVInterface;
extern Model *modelMIDITriggerToCVInterface;
+extern Model *modelCV_MIDI;
extern Model *modelBlank;
extern Model *modelNotes;
diff --git a/src/app/CableWidget.cpp b/src/app/CableWidget.cpp
index c627c230..bf0b77ad 100644
--- a/src/app/CableWidget.cpp
+++ b/src/app/CableWidget.cpp
@@ -182,7 +182,7 @@ void CableWidget::draw(NVGcontext *vg) {
float thickness = 5;
if (cable && cable->outputModule) {
Output *output = &cable->outputModule->outputs[cable->outputId];
- if (output->numChannels != 1) {
+ if (output->channels != 1) {
thickness = 7;
}
}
diff --git a/src/app/RackWidget.cpp b/src/app/RackWidget.cpp
index 9b9cd3a9..64deab0e 100644
--- a/src/app/RackWidget.cpp
+++ b/src/app/RackWidget.cpp
@@ -589,6 +589,29 @@ void RackWidget::draw(NVGcontext *vg) {
}
void RackWidget::onHover(const event::Hover &e) {
+ // Scroll with arrow keys
+ float arrowSpeed = 30.0;
+ if (app()->window->isShiftPressed() && app()->window->isModPressed())
+ arrowSpeed /= 16.0;
+ else if (app()->window->isShiftPressed())
+ arrowSpeed *= 4.0;
+ else if (app()->window->isModPressed())
+ arrowSpeed /= 4.0;
+
+ ScrollWidget *scrollWidget = app()->scene->scrollWidget;
+ if (glfwGetKey(app()->window->win, GLFW_KEY_LEFT) == GLFW_PRESS) {
+ scrollWidget->offset.x -= arrowSpeed;
+ }
+ if (glfwGetKey(app()->window->win, GLFW_KEY_RIGHT) == GLFW_PRESS) {
+ scrollWidget->offset.x += arrowSpeed;
+ }
+ if (glfwGetKey(app()->window->win, GLFW_KEY_UP) == GLFW_PRESS) {
+ scrollWidget->offset.y -= arrowSpeed;
+ }
+ if (glfwGetKey(app()->window->win, GLFW_KEY_DOWN) == GLFW_PRESS) {
+ scrollWidget->offset.y += arrowSpeed;
+ }
+
OpaqueWidget::onHover(e);
lastMousePos = e.pos;
}
diff --git a/src/engine/Cable.cpp b/src/engine/Cable.cpp
index 20de979b..9660526e 100644
--- a/src/engine/Cable.cpp
+++ b/src/engine/Cable.cpp
@@ -8,9 +8,9 @@ void Cable::step() {
Output *output = &outputModule->outputs[outputId];
Input *input = &inputModule->inputs[inputId];
// Match number of polyphonic channels to output port
- input->numChannels = output->numChannels;
+ input->channels = output->channels;
// Copy values from output to input
- for (int i = 0; i < output->numChannels; i++) {
+ for (int i = 0; i < output->channels; i++) {
input->values[i] = output->values[i];
}
}
diff --git a/src/engine/Engine.cpp b/src/engine/Engine.cpp
index 9ca53c53..6d048212 100644
--- a/src/engine/Engine.cpp
+++ b/src/engine/Engine.cpp
@@ -131,7 +131,7 @@ static void Engine_step(Engine *engine) {
if (module->bypass) {
// Bypass module
for (Output &output : module->outputs) {
- output.numChannels = 1;
+ output.channels = 1;
output.setVoltage(0.f);
}
module->cpuTime = 0.f;
diff --git a/src/midi.cpp b/src/midi.cpp
index 18abdf41..051a62f0 100644
--- a/src/midi.cpp
+++ b/src/midi.cpp
@@ -228,6 +228,7 @@ void Output::setDeviceId(int deviceId) {
}
void Output::sendMessage(Message message) {
+ // DEBUG("sendMessage %02x %02x %02x", message.cmd, message.data1, message.data2);
if (outputDevice) {
outputDevice->sendMessage(message);
}
diff --git a/src/ui/ScrollWidget.cpp b/src/ui/ScrollWidget.cpp
index 803ba159..8f181fdd 100644
--- a/src/ui/ScrollWidget.cpp
+++ b/src/ui/ScrollWidget.cpp
@@ -69,30 +69,6 @@ void ScrollWidget::step() {
}
void ScrollWidget::onHover(const event::Hover &e) {
- // Scroll with arrow keys
- if (!app()->event->selectedWidget) {
- float arrowSpeed = 30.0;
- if (app()->window->isShiftPressed() && app()->window->isModPressed())
- arrowSpeed /= 16.0;
- else if (app()->window->isShiftPressed())
- arrowSpeed *= 4.0;
- else if (app()->window->isModPressed())
- arrowSpeed /= 4.0;
-
- if (glfwGetKey(app()->window->win, GLFW_KEY_LEFT) == GLFW_PRESS) {
- offset.x -= arrowSpeed;
- }
- if (glfwGetKey(app()->window->win, GLFW_KEY_RIGHT) == GLFW_PRESS) {
- offset.x += arrowSpeed;
- }
- if (glfwGetKey(app()->window->win, GLFW_KEY_UP) == GLFW_PRESS) {
- offset.y -= arrowSpeed;
- }
- if (glfwGetKey(app()->window->win, GLFW_KEY_DOWN) == GLFW_PRESS) {
- offset.y += arrowSpeed;
- }
- }
-
OpaqueWidget::onHover(e);
}