diff --git a/src/core/MidiCCToCV.cpp b/src/core/MidiCCToCV.cpp new file mode 100644 index 00000000..e2f8f918 --- /dev/null +++ b/src/core/MidiCCToCV.cpp @@ -0,0 +1,234 @@ +#include +#include +#include "rtmidi/RtMidi.h" +#include "core.hpp" +#include "MidiInterface.hpp" + +/* + * MIDIToCVInterface converts midi note on/off events, velocity , channel aftertouch, pitch wheel and mod weel to + * CV + */ +struct MIDICCToCVInterface : MidiIO, Module { + enum ParamIds { + NUM_PARAMS + }; + enum InputIds { + NUM_INPUTS + }; + enum OutputIds { + NUM_OUTPUTS = 16 + }; + + int cc[NUM_OUTPUTS]; + int ccNum[NUM_OUTPUTS]; + bool ccNumInited[NUM_OUTPUTS]; + + MIDICCToCVInterface() : MidiIO(), Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS) { + for (int i = 0; i < NUM_OUTPUTS; i++) { + cc[i] = 0; + ccNum[i] = i; + } + } + + ~MIDICCToCVInterface() { + setPortId(-1); + } + + void step(); + + void processMidi(std::vector msg); + + virtual void resetMidi(); + + virtual json_t *toJson() { + json_t *rootJ = json_object(); + addBaseJson(rootJ); + for (int i = 0; i < NUM_OUTPUTS; i++) { + json_object_set_new(rootJ, std::to_string(i).c_str(), json_integer(ccNum[i])); + } + return rootJ; + } + + virtual void fromJson(json_t *rootJ) { + baseFromJson(rootJ); + for (int i = 0; i < NUM_OUTPUTS; i++) { + json_t *ccNumJ = json_object_get(rootJ, std::to_string(i).c_str()); + if (ccNumJ) { + ccNum[i] = json_integer_value(ccNumJ); + ccNumInited[i] = true; + } + + } + } + + virtual void initialize() { + setPortId(-1); + } + +}; + + +void MIDICCToCVInterface::step() { + if (rtMidi->isPortOpen()) { + std::vector message; + + // midiIn->getMessage returns empty vector if there are no messages in the queue + + dynamic_cast(rtMidi)->getMessage(&message); + while (message.size() > 0) { + processMidi(message); + dynamic_cast(rtMidi)->getMessage(&message); + } + } + + for (int i = 0; i < NUM_OUTPUTS; i++) { + outputs[i].value = cc[i] / 127.0 * 10.0; + } +} + +void MIDICCToCVInterface::resetMidi() { + for (int i = 0; i < NUM_OUTPUTS; i++) { + cc[i] = 0; + } +}; + +void MIDICCToCVInterface::processMidi(std::vector msg) { + int channel = msg[0] & 0xf; + int status = (msg[0] >> 4) & 0xf; + int data1 = msg[1]; + int data2 = msg[2]; + + //fprintf(stderr, "channel %d status %d data1 %d data2 %d\n", channel, status, data1,data2); + + // Filter channels + if (this->channel >= 0 && this->channel != channel) + return; + + if (status == 0xb) { + for (int i = 0; i < NUM_OUTPUTS; i++) { + if (data1 == ccNum[i]) { + this->cc[i] = data2; + } + + } + } +} + +struct CCTextField : TextField { + void onTextChange(); + + void draw(NVGcontext *vg); + + int *ccNum; + bool *inited; +}; + +void CCTextField::draw(NVGcontext *vg) { + /* This is necessary, since the save + * file is loaded after constructing the widget*/ + if (*inited) { + *inited = false; + text = std::to_string(*ccNum); + } + + TextField::draw(vg); +} + +void CCTextField::onTextChange() { + if (text.size() > 0) { + try { + *ccNum = std::stoi(text); + // Only allow valid cc numbers + if (*ccNum < 0 || *ccNum > 127 || text.size() > 3) { + text = ""; + begin = end = 0; + *ccNum = -1; + } + } catch (...) { + text = ""; + begin = end = 0; + *ccNum = -1; + } + }; +} + +MIDICCToCVWidget::MIDICCToCVWidget() { + MIDICCToCVInterface *module = new MIDICCToCVInterface(); + setModule(module); + box.size = Vec(16 * 15, 380); + + { + Panel *panel = new LightPanel(); + panel->box.size = box.size; + addChild(panel); + } + + float margin = 5; + float labelHeight = 15; + float yPos = margin; + + addChild(createScrew(Vec(15, 0))); + addChild(createScrew(Vec(box.size.x - 30, 0))); + addChild(createScrew(Vec(15, 365))); + addChild(createScrew(Vec(box.size.x - 30, 365))); + { + Label *label = new Label(); + label->box.pos = Vec(box.size.x - margin - 11 * 15, margin); + label->text = "MIDI CC to CV"; + addChild(label); + yPos = labelHeight * 2; + } + + { + Label *label = new Label(); + label->box.pos = Vec(margin, yPos); + label->text = "MIDI Interface"; + addChild(label); + + MidiChoice *midiChoice = new MidiChoice(); + midiChoice->midiModule = dynamic_cast(module); + midiChoice->box.pos = Vec((box.size.x - 10) / 2 + margin, yPos); + midiChoice->box.size.x = (box.size.x / 2.0) - margin; + addChild(midiChoice); + yPos += midiChoice->box.size.y + margin; + } + + { + Label *label = new Label(); + label->box.pos = Vec(margin, yPos); + label->text = "Channel"; + addChild(label); + + ChannelChoice *channelChoice = new ChannelChoice(); + channelChoice->midiModule = dynamic_cast(module); + channelChoice->box.pos = Vec((box.size.x - 10) / 2 + margin, yPos); + channelChoice->box.size.x = (box.size.x / 2.0) - margin; + addChild(channelChoice); + yPos += channelChoice->box.size.y + margin * 3; + } + + for (int i = 0; i < MIDICCToCVInterface::NUM_OUTPUTS; i++) { + CCTextField *ccNumChoice = new CCTextField(); + ccNumChoice->ccNum = &module->ccNum[i]; + ccNumChoice->inited = &module->ccNumInited[i]; + ccNumChoice->text = std::to_string(module->ccNum[i]); + ccNumChoice->box.pos = Vec(11 + (i % 4) * (63), yPos); + ccNumChoice->box.size.x = 29; + + addChild(ccNumChoice); + + yPos += labelHeight + margin; + addOutput(createOutput(Vec((i % 4) * (63) + 10, yPos + 5), module, i)); + + if ((i + 1) % 4 == 0) { + yPos += 47 + margin; + } else { + yPos -= labelHeight + margin; + } + } +} + +void MIDICCToCVWidget::step() { + + ModuleWidget::step(); +} diff --git a/src/core/MidiClockToCV.cpp b/src/core/MidiClockToCV.cpp new file mode 100644 index 00000000..9f906823 --- /dev/null +++ b/src/core/MidiClockToCV.cpp @@ -0,0 +1,341 @@ +#include +#include +#include "rtmidi/RtMidi.h" +#include "core.hpp" +#include "MidiInterface.hpp" +#include "dsp/digital.hpp" + +using namespace rack; + +struct MIDIClockToCVInterface : MidiIO, Module { + enum ParamIds { + NUM_PARAMS + }; + enum InputIds { + NUM_INPUTS + }; + enum OutputIds { + CLOCK1_PULSE, + CLOCK2_PULSE, + CLOCK_START_PULSE, + CLOCK_STOP_PULSE, + NUM_OUTPUTS + }; + + int clock1ratio = 0; + int clock2ratio = 0; + + PulseGenerator clock1Pulse; + PulseGenerator clock2Pulse; + PulseGenerator clockStartPulse; + PulseGenerator clockStopPulse; + bool tick = false; + bool running = false; + bool start = false; + bool stop = false; + + MIDIClockToCVInterface() : MidiIO(), Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS) { + (dynamic_cast(rtMidi))->ignoreTypes(true, false); + } + + ~MIDIClockToCVInterface() { + setPortId(-1); + } + + void step(); + + void processMidi(std::vector msg); + + virtual void resetMidi(); + + virtual json_t *toJson() { + json_t *rootJ = json_object(); + addBaseJson(rootJ); + json_object_set_new(rootJ, "clock1ratio", json_integer(clock1ratio)); + json_object_set_new(rootJ, "clock2ratio", json_integer(clock2ratio)); + return rootJ; + } + + virtual void fromJson(json_t *rootJ) { + baseFromJson(rootJ); + json_t *c1rJ = json_object_get(rootJ, "clock1ratio"); + if (c1rJ) { + clock1ratio = json_integer_value(c1rJ); + } + + json_t *c2rJ = json_object_get(rootJ, "clock2ratio"); + if (c2rJ) { + clock2ratio = json_integer_value(c2rJ); + } + } + + virtual void initialize() { + setPortId(-1); + } + +}; + +void MIDIClockToCVInterface::step() { + static int c_bar = 0; + static float trigger_length = 0.05; + + /* Note this is in relation to the Midi clock's Tick (6x per 16th note). + * Therefore, e.g. the 2:3 is calculated: + * + * 24 (Ticks per quarter note) * 2 / 3 = 16 + * + * Implying that every 16 midi clock ticks we need to send a pulse + * */ + static int ratios[] = {6, 8, 12, 16, 24, 32, 48, 96, 192}; + + if (rtMidi->isPortOpen()) { + std::vector message; + + // midiIn->getMessage returns empty vector if there are no messages in the queue + + dynamic_cast(rtMidi)->getMessage(&message); + while (message.size() > 0) { + processMidi(message); + dynamic_cast(rtMidi)->getMessage(&message); + } + + } + + if (start) { + clockStartPulse.trigger(trigger_length); + start = false; + c_bar = 0; + } + + if (stop) { + clockStopPulse.trigger(trigger_length); + stop = false; + clock1Pulse.time = 0.0; + clock1Pulse.pulseTime = 0.0; + clock2Pulse.time = 0.0; + clock2Pulse.pulseTime = 0.0; + } + + if (tick) { + tick = false; + + /* Note: At least for my midi clock, the clock ticks are sent + * even if the midi clock is stopped. + * Therefore, we need to keep track of ticks even when the clock + * is stopped. Otherwise we can run into weird timing issues. + */ + if (running) { + if (c_bar % ratios[clock1ratio] == 0) { + clock1Pulse.trigger(trigger_length); + } + + if (c_bar % ratios[clock2ratio] == 0) { + clock2Pulse.trigger(trigger_length); + } + } + + c_bar++; + + // One "midi bar" = 4 whole notes = (6 ticks per 16th) 6 * 16 *4 = 384 + if (c_bar >= 384) { + c_bar = 0; + } + } + + + bool pulse = clock1Pulse.process(1.0 / gSampleRate); + outputs[CLOCK1_PULSE].value = pulse ? 10.0 : 0.0; + + pulse = clock2Pulse.process(1.0 / gSampleRate); + outputs[CLOCK2_PULSE].value = pulse ? 10.0 : 0.0; + + pulse = clockStartPulse.process(1.0 / gSampleRate); + outputs[CLOCK_START_PULSE].value = pulse ? 10.0 : 0.0; + + pulse = clockStopPulse.process(1.0 / gSampleRate); + outputs[CLOCK_STOP_PULSE].value = pulse ? 10.0 : 0.0; + +} + +void MIDIClockToCVInterface::resetMidi() { + outputs[CLOCK1_PULSE].value = 0.0; +} + +void MIDIClockToCVInterface::processMidi(std::vector msg) { + + switch (msg[0]) { + case 0xfa: + start = true; + running = true; + break; + case 0xfc: + stop = true; + running = false; + break; + case 0xf8: + tick = true; + break; + } + + +} + +struct ClockRatioItem : MenuItem { + int ratio; + int *clockRatio; + + void onAction() { + *clockRatio = ratio; + } +}; + +struct ClockRatioChoice : ChoiceButton { + int *clockRatio; + const std::vector ratioNames = {"Sixteenth note (1:4 ratio)", "Eighth note triplet (1:3 ratio)", + "Eighth note (1:2 ratio)", "Quarter note triplet (2:3 ratio)", + "Quarter note (tap speed)", "Half note triplet (4:3 ratio)", + "Half note (2:1 ratio)", "Whole note (4:1 ratio)", + "Two whole notes (8:1 ratio)"}; + + const std::vector ratioNames_short = {"1:4 ratio", "1:3 ratio", "1:2 ratio", "2:3 ratio", "1:1 ratio", + "4:3", "2:1 ratio", "4:1 ratio", "8:1 ratio"}; + + void onAction() { + Menu *menu = gScene->createMenu(); + menu->box.pos = getAbsolutePos().plus(Vec(0, box.size.y)); + menu->box.size.x = box.size.x; + + for (int ratio = 0; ratio < ratioNames.size(); ratio++) { + ClockRatioItem *clockRatioItem = new ClockRatioItem(); + clockRatioItem->ratio = ratio; + clockRatioItem->clockRatio = clockRatio; + clockRatioItem->text = ratioNames[ratio]; + menu->pushChild(clockRatioItem); + } + } + + void step() { + text = ratioNames_short[*clockRatio]; + } +}; + +MIDIClockToCVWidget::MIDIClockToCVWidget() { + MIDIClockToCVInterface *module = new MIDIClockToCVInterface(); + setModule(module); + box.size = Vec(15 * 9, 380); + + { + Panel *panel = new LightPanel(); + panel->box.size = box.size; + addChild(panel); + } + + float margin = 5; + float labelHeight = 15; + float yPos = margin; + + addChild(createScrew(Vec(15, 0))); + addChild(createScrew(Vec(box.size.x - 30, 0))); + addChild(createScrew(Vec(15, 365))); + addChild(createScrew(Vec(box.size.x - 30, 365))); + + { + Label *label = new Label(); + label->box.pos = Vec(box.size.x - margin - 7 * 15, margin); + label->text = "MIDI Clock to CV"; + addChild(label); + yPos = labelHeight * 2; + } + + { + Label *label = new Label(); + label->box.pos = Vec(margin, yPos); + label->text = "MIDI Interface"; + addChild(label); + yPos += labelHeight + margin; + + MidiChoice *midiChoice = new MidiChoice(); + midiChoice->midiModule = dynamic_cast(module); + midiChoice->box.pos = Vec(margin, yPos); + midiChoice->box.size.x = box.size.x - 10; + addChild(midiChoice); + yPos += midiChoice->box.size.y + margin * 6; + } + + + { + Label *label = new Label(); + label->box.pos = Vec(margin, yPos); + label->text = "Clock 1 Ratio"; + addChild(label); + yPos += labelHeight + margin; + + ClockRatioChoice *ratioChoice = new ClockRatioChoice(); + ratioChoice->clockRatio = &module->clock1ratio; + ratioChoice->box.pos = Vec(margin, yPos); + ratioChoice->box.size.x = box.size.x - 10; + addChild(ratioChoice); + yPos += ratioChoice->box.size.y + margin + 5; + + } + + { + Label *label = new Label(); + label->box.pos = Vec(margin, yPos); + label->text = "Clock 1 Pulse"; + addChild(label); + + addOutput(createOutput(Vec(15 * 6, yPos - 5), module, MIDIClockToCVInterface::CLOCK1_PULSE)); + yPos += labelHeight + margin * 4; + } + + + { + Label *label = new Label(); + label->box.pos = Vec(margin, yPos); + label->text = "Clock 2 Ratio"; + addChild(label); + yPos += labelHeight + margin; + + ClockRatioChoice *ratioChoice = new ClockRatioChoice(); + ratioChoice->clockRatio = &module->clock2ratio; + ratioChoice->box.pos = Vec(margin, yPos); + ratioChoice->box.size.x = box.size.x - 10; + addChild(ratioChoice); + yPos += ratioChoice->box.size.y + margin + 5; + + } + + { + Label *label = new Label(); + label->box.pos = Vec(margin, yPos); + label->text = "Clock 2 Pulse"; + addChild(label); + + addOutput(createOutput(Vec(15 * 6, yPos - 5), module, MIDIClockToCVInterface::CLOCK2_PULSE)); + yPos += labelHeight + margin * 7; + } + + { + Label *label = new Label(); + label->box.pos = Vec(margin, yPos); + label->text = "Clock Start"; + addChild(label); + addOutput(createOutput(Vec(15 * 6, yPos - 5), module, MIDIClockToCVInterface::CLOCK_START_PULSE)); + yPos += 40; + } + + { + Label *label = new Label(); + label->box.pos = Vec(margin, yPos); + label->text = "Clock Stop"; + addChild(label); + + addOutput(createOutput(Vec(15 * 6, yPos - 5), module, MIDIClockToCVInterface::CLOCK_STOP_PULSE)); + } +} + +void MIDIClockToCVWidget::step() { + + ModuleWidget::step(); +} \ No newline at end of file diff --git a/src/core/MidiInterface.cpp b/src/core/MidiInterface.cpp index 9b17ace2..6d9f0991 100644 --- a/src/core/MidiInterface.cpp +++ b/src/core/MidiInterface.cpp @@ -1,12 +1,8 @@ -#include #include #include #include "rtmidi/RtMidi.h" #include "core.hpp" -#include "gui.hpp" -#include "engine.hpp" -#include "dsp/digital.hpp" - +#include "MidiInterface.hpp" using namespace rack; @@ -16,73 +12,57 @@ using namespace rack; * + Interface Selection (including helper for storing json) * + rtMidi initialisation (input or output) */ -struct MidiIO { - int portId = -1; - RtMidi *rtMidi = NULL; - - /** Filter MIDI channel - -1 means all MIDI channels - */ - int channel = -1; +MidiIO::MidiIO(bool isOut) { + portId = -1; + rtMidi = NULL; + channel = -1; /* * If isOut is set to true, creates a RtMidiOut, RtMidiIn otherwise */ - MidiIO(bool isOut = false) { - try { - if (isOut) { - rtMidi = new RtMidiOut(RtMidi::UNSPECIFIED, "Rack"); - } else { - rtMidi = new RtMidiIn(RtMidi::UNSPECIFIED, "Rack"); - } - } - catch (RtMidiError &error) { - fprintf(stderr, "Failed to create RtMidiIn: %s\n", error.getMessage().c_str()); + try { + if (isOut) { + rtMidi = new RtMidiOut(RtMidi::UNSPECIFIED, "Rack"); + } else { + rtMidi = new RtMidiIn(RtMidi::UNSPECIFIED, "Rack"); } } - - ~MidiIO() {} - - int getPortCount(); - - std::string getPortName(int portId); - - // -1 will close the port - void setPortId(int portId); - - void setChannel(int channel) { - this->channel = channel; + catch (RtMidiError &error) { + fprintf(stderr, "Failed to create RtMidiIn: %s\n", error.getMessage().c_str()); } +}; - json_t *addBaseJson(json_t *rootJ) { - if (portId >= 0) { - std::string portName = getPortName(portId); - json_object_set_new(rootJ, "portName", json_string(portName.c_str())); - json_object_set_new(rootJ, "channel", json_integer(channel)); - } - return rootJ; +void MidiIO::setChannel(int channel) { + this->channel = channel; +} + +json_t *MidiIO::addBaseJson(json_t *rootJ) { + if (portId >= 0) { + std::string portName = getPortName(portId); + json_object_set_new(rootJ, "portName", json_string(portName.c_str())); + json_object_set_new(rootJ, "channel", json_integer(channel)); } + return rootJ; +} - void baseFromJson(json_t *rootJ) { - json_t *portNameJ = json_object_get(rootJ, "portName"); - if (portNameJ) { - std::string portName = json_string_value(portNameJ); - for (int i = 0; i < getPortCount(); i++) { - if (portName == getPortName(i)) { - setPortId(i); - break; - } +void MidiIO::baseFromJson(json_t *rootJ) { + json_t *portNameJ = json_object_get(rootJ, "portName"); + if (portNameJ) { + std::string portName = json_string_value(portNameJ); + for (int i = 0; i < getPortCount(); i++) { + if (portName == getPortName(i)) { + setPortId(i); + break; } } + } - json_t *channelJ = json_object_get(rootJ, "channel"); - if (channelJ) { - setChannel(json_integer_value(channelJ)); - } + json_t *channelJ = json_object_get(rootJ, "channel"); + if (channelJ) { + setChannel(json_integer_value(channelJ)); } +} - virtual void resetMidi()=0; // called when midi port is set -}; int MidiIO::getPortCount() { return rtMidi->getPortCount(); @@ -114,599 +94,69 @@ void MidiIO::setPortId(int portId) { this->portId = portId; } -struct MidiItem : MenuItem { - MidiIO *midiModule; - int portId; - - void onAction() { - midiModule->resetMidi(); // reset Midi values - midiModule->setPortId(portId); - } -}; - -struct MidiChoice : ChoiceButton { - MidiIO *midiModule; - - void onAction() { - Menu *menu = gScene->createMenu(); - menu->box.pos = getAbsolutePos().plus(Vec(0, box.size.y)); - menu->box.size.x = box.size.x; - - int portCount = midiModule->getPortCount(); - { - MidiItem *midiItem = new MidiItem(); - midiItem->midiModule = midiModule; - midiItem->portId = -1; - midiItem->text = "No device"; - menu->pushChild(midiItem); - } - for (int portId = 0; portId < portCount; portId++) { - MidiItem *midiItem = new MidiItem(); - midiItem->midiModule = midiModule; - midiItem->portId = portId; - midiItem->text = midiModule->getPortName(portId); - menu->pushChild(midiItem); - } - } - - void step() { - if (midiModule->portId < 0) { - text = "No Device"; - return; - } - std::string name = midiModule->getPortName(midiModule->portId); - text = ellipsize(name, 15); - } -}; - -struct ChannelItem : MenuItem { - MidiIO *midiModule; - int channel; - - void onAction() { - midiModule->resetMidi(); // reset Midi values - midiModule->setChannel(channel); - } -}; - -struct ChannelChoice : ChoiceButton { - MidiIO *midiModule; - - void onAction() { - Menu *menu = gScene->createMenu(); - menu->box.pos = getAbsolutePos().plus(Vec(0, box.size.y)); - menu->box.size.x = box.size.x; - - { - ChannelItem *channelItem = new ChannelItem(); - channelItem->midiModule = midiModule; - channelItem->channel = -1; - channelItem->text = "All"; - menu->pushChild(channelItem); - } - for (int channel = 0; channel < 16; channel++) { - ChannelItem *channelItem = new ChannelItem(); - channelItem->midiModule = midiModule; - channelItem->channel = channel; - channelItem->text = stringf("%d", channel + 1); - menu->pushChild(channelItem); - } - } - - void step() { - text = (midiModule->channel >= 0) ? stringf("%d", midiModule->channel + 1) : "All"; - } -}; - -/* - * MIDIToCVInterface converts midi note on/off events, velocity , channel aftertouch, pitch wheel and mod wheel to - * CV - */ -struct MIDIToCVInterface : MidiIO, Module { - enum ParamIds { - RESET_PARAM, - NUM_PARAMS - }; - enum InputIds { - NUM_INPUTS - }; - enum OutputIds { - PITCH_OUTPUT = 0, - GATE_OUTPUT, - VELOCITY_OUTPUT, - MOD_OUTPUT, - PITCHWHEEL_OUTPUT, - CHANNEL_AFTERTOUCH_OUTPUT, - NUM_OUTPUTS - }; - - std::list notes; - bool pedal = false; - int note = 60; // C4, most modules should use 261.626 Hz - int mod = 0; - int vel = 0; - int afterTouch = 0; - int pitchWheel = 64; - bool retrigger = false; - bool retriggered = false; - - SchmittTrigger resetTrigger; - float resetLight = 0.0; - - MIDIToCVInterface() : MidiIO(), Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS) { - - } - - ~MIDIToCVInterface() { - setPortId(-1); - }; - - void step(); - - void pressNote(int note); - - void releaseNote(int note); - - void processMidi(std::vector msg); - - virtual json_t *toJson() { - json_t *rootJ = json_object(); - addBaseJson(rootJ); - return rootJ; - } - - virtual void fromJson(json_t *rootJ) { - baseFromJson(rootJ); - } - - virtual void initialize() { - setPortId(-1); - } - - virtual void resetMidi(); - -}; - -void MIDIToCVInterface::resetMidi() { - mod = 0; - pitchWheel = 64; - afterTouch = 0; - vel = 0; - resetLight = 1.0; - outputs[GATE_OUTPUT].value = 0.0; - notes.clear(); -} - -void MIDIToCVInterface::step() { - if (rtMidi->isPortOpen()) { - std::vector message; - - // midiIn->getMessage returns empty vector if there are no messages in the queue - dynamic_cast(rtMidi)->getMessage(&message); - while (message.size() > 0) { - processMidi(message); - dynamic_cast(rtMidi)->getMessage(&message); - } - } - - outputs[PITCH_OUTPUT].value = ((note - 60)) / 12.0; - - bool gate = pedal || !notes.empty(); - if (retrigger && retriggered) { - gate = false; - retriggered = false; - } - if (resetTrigger.process(params[RESET_PARAM].value)) { - resetMidi(); - return; - } - - if (resetLight > 0) { - resetLight -= resetLight / 0.55 / gSampleRate; // fade out light - } - - - outputs[GATE_OUTPUT].value = gate ? 10.0 : 0.0; - outputs[MOD_OUTPUT].value = mod / 127.0 * 10.0; - outputs[PITCHWHEEL_OUTPUT].value = (pitchWheel - 64) / 64.0 * 10.0; - outputs[CHANNEL_AFTERTOUCH_OUTPUT].value = afterTouch / 127.0 * 10.0; - outputs[VELOCITY_OUTPUT].value = vel / 127.0 * 10.0; - -} - -void MIDIToCVInterface::pressNote(int note) { - // Remove existing similar note - auto it = std::find(notes.begin(), notes.end(), note); - if (it != notes.end()) - notes.erase(it); - // Push note - notes.push_back(note); - this->note = note; - retriggered = true; -} - -void MIDIToCVInterface::releaseNote(int note) { - // Remove the note - auto it = std::find(notes.begin(), notes.end(), note); - if (it != notes.end()) - notes.erase(it); - - if (pedal) { - // Don't release if pedal is held - } else if (!notes.empty()) { - // Play previous note - auto it2 = notes.end(); - it2--; - this->note = *it2; - retriggered = true; - } -} - -void MIDIToCVInterface::processMidi(std::vector msg) { - int channel = msg[0] & 0xf; - int status = (msg[0] >> 4) & 0xf; - int data1 = msg[1]; - int data2 = msg[2]; - - //fprintf(stderr, "channel %d status %d data1 %d data2 %d\n", channel, status, data1,data2); - - // Filter channels - if (this->channel >= 0 && this->channel != channel) - return; - - switch (status) { - // note off - case 0x8: { - releaseNote(data1); - } - break; - case 0x9: // note on - if (data2 > 0) { - pressNote(data1); - this->vel = data2; - } else { - // For some reason, some keyboards send a "note on" event with a velocity of 0 to signal that the key has been released. - releaseNote(data1); - } - break; - case 0xb: // cc - switch (data1) { - case 0x01: // mod - this->mod = data2; - break; - case 0x40: // sustain - pedal = (data2 >= 64); - releaseNote(-1); - break; - } - break; - case 0xe: // pitch wheel - this->pitchWheel = data2; - break; - case 0xd: // channel aftertouch - this->afterTouch = data1; - break; - } +void MidiItem::onAction() { + midiModule->resetMidi(); // reset Midi values + midiModule->setPortId(portId); } +void MidiChoice::onAction() { + Menu *menu = gScene->createMenu(); + menu->box.pos = getAbsolutePos().plus(Vec(0, box.size.y)); + menu->box.size.x = box.size.x; -MidiToCVWidget::MidiToCVWidget() { - MIDIToCVInterface *module = new MIDIToCVInterface(); - setModule(module); - box.size = Vec(15 * 9, 380); - - { - Panel *panel = new LightPanel(); - panel->box.size = box.size; - addChild(panel); - } - - float margin = 5; - float labelHeight = 15; - float yPos = margin; - float yGap = 35; - - addChild(createScrew(Vec(15, 0))); - addChild(createScrew(Vec(box.size.x - 30, 0))); - addChild(createScrew(Vec(15, 365))); - addChild(createScrew(Vec(box.size.x - 30, 365))); - - { - Label *label = new Label(); - label->box.pos = Vec(box.size.x - margin - 7 * 15, margin); - label->text = "MIDI to CV"; - addChild(label); - yPos = labelHeight * 2; - } - - addParam(createParam(Vec(7 * 15, labelHeight), module, MIDIToCVInterface::RESET_PARAM, 0.0, 1.0, 0.0)); - addChild(createValueLight>(Vec(7 * 15 + 5, labelHeight + 5), &module->resetLight)); - { - Label *label = new Label(); - label->box.pos = Vec(margin, yPos); - label->text = "MIDI Interface"; - addChild(label); - yPos += labelHeight + margin; - - MidiChoice *midiChoice = new MidiChoice(); - midiChoice->midiModule = dynamic_cast(module); - midiChoice->box.pos = Vec(margin, yPos); - midiChoice->box.size.x = box.size.x - 10; - addChild(midiChoice); - yPos += midiChoice->box.size.y + margin; - } - + int portCount = midiModule->getPortCount(); { - Label *label = new Label(); - label->box.pos = Vec(margin, yPos); - label->text = "Channel"; - addChild(label); - yPos += labelHeight + margin; - - ChannelChoice *channelChoice = new ChannelChoice(); - channelChoice->midiModule = dynamic_cast(module); - channelChoice->box.pos = Vec(margin, yPos); - channelChoice->box.size.x = box.size.x - 10; - addChild(channelChoice); - yPos += channelChoice->box.size.y + margin + 15; - } - - std::string labels[MIDIToCVInterface::NUM_OUTPUTS] = {"1V/oct", "Gate", "Velocity", "Mod Wheel", "Pitch Wheel", - "Aftertouch"}; - - for (int i = 0; i < MIDIToCVInterface::NUM_OUTPUTS; i++) { - Label *label = new Label(); - label->box.pos = Vec(margin, yPos); - label->text = labels[i]; - addChild(label); - - addOutput(createOutput(Vec(15 * 6, yPos - 5), module, i)); - - yPos += yGap + margin; - } -} - -void MidiToCVWidget::step() { - // Assume QWERTY -#define MIDI_KEY(key, midi) if (glfwGetKey(gWindow, key)) printf("%d\n", midi); - - // MIDI_KEY(GLFW_KEY_Z, 48); - - ModuleWidget::step(); -} - - -/* - * MIDIToCVInterface converts midi note on/off events, velocity , channel aftertouch, pitch wheel and mod weel to - * CV - */ -struct MIDICCToCVInterface : MidiIO, Module { - enum ParamIds { - NUM_PARAMS - }; - enum InputIds { - NUM_INPUTS - }; - enum OutputIds { - NUM_OUTPUTS = 16 - }; - - int cc[NUM_OUTPUTS]; - int ccNum[NUM_OUTPUTS]; - bool ccNumInited[NUM_OUTPUTS]; - - MIDICCToCVInterface() : MidiIO(), Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS) { - for (int i = 0; i < NUM_OUTPUTS; i++) { - cc[i] = 0; - ccNum[i] = i; - } - } - - ~MIDICCToCVInterface() { - setPortId(-1); - } - - void step(); - - void processMidi(std::vector msg); - - virtual void resetMidi(); - - virtual json_t *toJson() { - json_t *rootJ = json_object(); - addBaseJson(rootJ); - for (int i = 0; i < NUM_OUTPUTS; i++) { - json_object_set_new(rootJ, std::to_string(i).c_str(), json_integer(ccNum[i])); - } - return rootJ; - } - - virtual void fromJson(json_t *rootJ) { - baseFromJson(rootJ); - for (int i = 0; i < NUM_OUTPUTS; i++) { - json_t *ccNumJ = json_object_get(rootJ, std::to_string(i).c_str()); - if (ccNumJ) { - ccNum[i] = json_integer_value(ccNumJ); - ccNumInited[i] = true; - } - - } - } - - virtual void initialize() { - setPortId(-1); - } - -}; - - -void MIDICCToCVInterface::step() { - if (rtMidi->isPortOpen()) { - std::vector message; - - // midiIn->getMessage returns empty vector if there are no messages in the queue - - dynamic_cast(rtMidi)->getMessage(&message); - while (message.size() > 0) { - processMidi(message); - dynamic_cast(rtMidi)->getMessage(&message); - } + MidiItem *midiItem = new MidiItem(); + midiItem->midiModule = midiModule; + midiItem->portId = -1; + midiItem->text = "No device"; + menu->pushChild(midiItem); } - - for (int i = 0; i < NUM_OUTPUTS; i++) { - outputs[i].value = cc[i] / 127.0 * 10.0; + for (int portId = 0; portId < portCount; portId++) { + MidiItem *midiItem = new MidiItem(); + midiItem->midiModule = midiModule; + midiItem->portId = portId; + midiItem->text = midiModule->getPortName(portId); + menu->pushChild(midiItem); } } -void MIDICCToCVInterface::resetMidi() { - for (int i = 0; i < NUM_OUTPUTS; i++) { - cc[i] = 0; - } -}; - -void MIDICCToCVInterface::processMidi(std::vector msg) { - int channel = msg[0] & 0xf; - int status = (msg[0] >> 4) & 0xf; - int data1 = msg[1]; - int data2 = msg[2]; - - //fprintf(stderr, "channel %d status %d data1 %d data2 %d\n", channel, status, data1,data2); - - // Filter channels - if (this->channel >= 0 && this->channel != channel) +void MidiChoice::step() { + if (midiModule->portId < 0) { + text = "No Device"; return; - - if (status == 0xb) { - for (int i = 0; i < NUM_OUTPUTS; i++) { - if (data1 == ccNum[i]) { - this->cc[i] = data2; - } - - } } + std::string name = midiModule->getPortName(midiModule->portId); + text = ellipsize(name, 15); } - -struct CCTextField : TextField { - void onTextChange(); - - void draw(NVGcontext *vg); - - int *ccNum; - bool *inited; -}; - -void CCTextField::draw(NVGcontext *vg) { - /* This is necessary, since the save - * file is loaded after constructing the widget*/ - if (*inited) { - *inited = false; - text = std::to_string(*ccNum); - } - - TextField::draw(vg); -} - -void CCTextField::onTextChange() { - if (text.size() > 0) { - try { - *ccNum = std::stoi(text); - // Only allow valid cc numbers - if (*ccNum < 0 || *ccNum > 127 || text.size() > 3) { - text = ""; - begin = end = 0; - *ccNum = -1; - } - } catch (...) { - text = ""; - begin = end = 0; - *ccNum = -1; - } - }; -} - -MIDICCToCVWidget::MIDICCToCVWidget() { - MIDICCToCVInterface *module = new MIDICCToCVInterface(); - setModule(module); - box.size = Vec(16 * 15, 380); - - { - Panel *panel = new LightPanel(); - panel->box.size = box.size; - addChild(panel); +void ChannelItem::onAction() { + midiModule->resetMidi(); // reset Midi values + midiModule->setChannel(channel); } - float margin = 5; - float labelHeight = 15; - float yPos = margin; +void ChannelChoice::onAction() { + Menu *menu = gScene->createMenu(); + menu->box.pos = getAbsolutePos().plus(Vec(0, box.size.y)); + menu->box.size.x = box.size.x; - addChild(createScrew(Vec(15, 0))); - addChild(createScrew(Vec(box.size.x - 30, 0))); - addChild(createScrew(Vec(15, 365))); - addChild(createScrew(Vec(box.size.x - 30, 365))); { - Label *label = new Label(); - label->box.pos = Vec(box.size.x - margin - 11 * 15, margin); - label->text = "MIDI CC to CV"; - addChild(label); - yPos = labelHeight * 2; + ChannelItem *channelItem = new ChannelItem(); + channelItem->midiModule = midiModule; + channelItem->channel = -1; + channelItem->text = "All"; + menu->pushChild(channelItem); } - - { - Label *label = new Label(); - label->box.pos = Vec(margin, yPos); - label->text = "MIDI Interface"; - addChild(label); - - MidiChoice *midiChoice = new MidiChoice(); - midiChoice->midiModule = dynamic_cast(module); - midiChoice->box.pos = Vec((box.size.x - 10) / 2 + margin, yPos); - midiChoice->box.size.x = (box.size.x / 2.0) - margin; - addChild(midiChoice); - yPos += midiChoice->box.size.y + margin; - } - - { - Label *label = new Label(); - label->box.pos = Vec(margin, yPos); - label->text = "Channel"; - addChild(label); - - ChannelChoice *channelChoice = new ChannelChoice(); - channelChoice->midiModule = dynamic_cast(module); - channelChoice->box.pos = Vec((box.size.x - 10) / 2 + margin, yPos); - channelChoice->box.size.x = (box.size.x / 2.0) - margin; - addChild(channelChoice); - yPos += channelChoice->box.size.y + margin * 3; - } - - for (int i = 0; i < MIDICCToCVInterface::NUM_OUTPUTS; i++) { - CCTextField *ccNumChoice = new CCTextField(); - ccNumChoice->ccNum = &module->ccNum[i]; - ccNumChoice->inited = &module->ccNumInited[i]; - ccNumChoice->text = std::to_string(module->ccNum[i]); - ccNumChoice->box.pos = Vec(11 + (i % 4) * (63), yPos); - ccNumChoice->box.size.x = 29; - - addChild(ccNumChoice); - - yPos += labelHeight + margin; - addOutput(createOutput(Vec((i % 4) * (63) + 10, yPos + 5), module, i)); - - if ((i + 1) % 4 == 0) { - yPos += 47 + margin; - } else { - yPos -= labelHeight + margin; - } + for (int channel = 0; channel < 16; channel++) { + ChannelItem *channelItem = new ChannelItem(); + channelItem->midiModule = midiModule; + channelItem->channel = channel; + channelItem->text = stringf("%d", channel + 1); + menu->pushChild(channelItem); } } -void MIDICCToCVWidget::step() { - // Assume QWERTY -#define MIDI_KEY(key, midi) if (glfwGetKey(gWindow, key)) printf("%d\n", midi); - - // MIDI_KEY(GLFW_KEY_Z, 48); - - ModuleWidget::step(); -} +void ChannelChoice::step() { + text = (midiModule->channel >= 0) ? stringf("%d", midiModule->channel + 1) : "All"; +} \ No newline at end of file diff --git a/src/core/MidiInterface.hpp b/src/core/MidiInterface.hpp new file mode 100644 index 00000000..120b41d0 --- /dev/null +++ b/src/core/MidiInterface.hpp @@ -0,0 +1,83 @@ +#include "rack.hpp" +#include "rtmidi/RtMidi.h" + + +using namespace rack; + + +////////////////////// +// MIDI module widgets +////////////////////// + +struct MidiIO { + int portId; + int channel; + RtMidi *rtMidi; + + MidiIO(bool isOut=false); + + int getPortCount(); + + std::string getPortName(int portId); + + void setPortId(int portId); + + void setChannel(int channel); + + json_t *addBaseJson(json_t *rootJ); + + void baseFromJson(json_t *rootJ); + + /* called when midi port is set */ + virtual void resetMidi()=0; +}; + +struct MidiItem : MenuItem { + MidiIO *midiModule; + int portId; + + void onAction(); +}; + +struct MidiChoice : ChoiceButton { + MidiIO *midiModule; + + void onAction(); + + void step(); +}; + +struct ChannelItem : MenuItem { + MidiIO *midiModule; + int channel; + + void onAction(); +}; + +struct ChannelChoice : ChoiceButton { + MidiIO *midiModule; + + void onAction(); + + void step(); +}; + + +struct MidiToCVWidget : ModuleWidget{ + MidiToCVWidget(); + + void step(); +}; + +struct MIDICCToCVWidget : ModuleWidget{ + MIDICCToCVWidget(); + + void step(); +}; + +struct MIDIClockToCVWidget : ModuleWidget { + MIDIClockToCVWidget(); + + void step(); +}; + diff --git a/src/core/MidiToCV.cpp b/src/core/MidiToCV.cpp new file mode 100644 index 00000000..ee78b135 --- /dev/null +++ b/src/core/MidiToCV.cpp @@ -0,0 +1,280 @@ +#include +#include +#include "rtmidi/RtMidi.h" +#include "core.hpp" +#include "MidiInterface.hpp" +#include "dsp/digital.hpp" + +/* + * MIDIToCVInterface converts midi note on/off events, velocity , channel aftertouch, pitch wheel and mod wheel to + * CV + */ +struct MIDIToCVInterface : MidiIO, Module { + enum ParamIds { + RESET_PARAM, + NUM_PARAMS + }; + enum InputIds { + NUM_INPUTS + }; + enum OutputIds { + PITCH_OUTPUT = 0, + GATE_OUTPUT, + VELOCITY_OUTPUT, + MOD_OUTPUT, + PITCHWHEEL_OUTPUT, + CHANNEL_AFTERTOUCH_OUTPUT, + NUM_OUTPUTS + }; + + std::list notes; + bool pedal = false; + int note = 60; // C4, most modules should use 261.626 Hz + int mod = 0; + int vel = 0; + int afterTouch = 0; + int pitchWheel = 64; + bool retrigger = false; + bool retriggered = false; + + SchmittTrigger resetTrigger; + float resetLight = 0.0; + + MIDIToCVInterface() : MidiIO(), Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS) { + + } + + ~MIDIToCVInterface() { + setPortId(-1); + }; + + void step(); + + void pressNote(int note); + + void releaseNote(int note); + + void processMidi(std::vector msg); + + virtual json_t *toJson() { + json_t *rootJ = json_object(); + addBaseJson(rootJ); + return rootJ; + } + + virtual void fromJson(json_t *rootJ) { + baseFromJson(rootJ); + } + + virtual void initialize() { + setPortId(-1); + } + + virtual void resetMidi(); + +}; + +void MIDIToCVInterface::resetMidi() { + mod = 0; + pitchWheel = 64; + afterTouch = 0; + vel = 0; + resetLight = 1.0; + outputs[GATE_OUTPUT].value = 0.0; + notes.clear(); +} + +void MIDIToCVInterface::step() { + if (rtMidi->isPortOpen()) { + std::vector message; + + // midiIn->getMessage returns empty vector if there are no messages in the queue + + dynamic_cast(rtMidi)->getMessage(&message); + while (message.size() > 0) { + processMidi(message); + dynamic_cast(rtMidi)->getMessage(&message); + } + } + + outputs[PITCH_OUTPUT].value = ((note - 60)) / 12.0; + + bool gate = pedal || !notes.empty(); + if (retrigger && retriggered) { + gate = false; + retriggered = false; + } + if (resetTrigger.process(params[RESET_PARAM].value)) { + resetMidi(); + return; + } + + if (resetLight > 0) { + resetLight -= resetLight / 0.55 / gSampleRate; // fade out light + } + + + outputs[GATE_OUTPUT].value = gate ? 10.0 : 0.0; + outputs[MOD_OUTPUT].value = mod / 127.0 * 10.0; + outputs[PITCHWHEEL_OUTPUT].value = (pitchWheel - 64) / 64.0 * 10.0; + outputs[CHANNEL_AFTERTOUCH_OUTPUT].value = afterTouch / 127.0 * 10.0; + outputs[VELOCITY_OUTPUT].value = vel / 127.0 * 10.0; + +} + +void MIDIToCVInterface::pressNote(int note) { + // Remove existing similar note + auto it = std::find(notes.begin(), notes.end(), note); + if (it != notes.end()) + notes.erase(it); + // Push note + notes.push_back(note); + this->note = note; + retriggered = true; +} + +void MIDIToCVInterface::releaseNote(int note) { + // Remove the note + auto it = std::find(notes.begin(), notes.end(), note); + if (it != notes.end()) + notes.erase(it); + + if (pedal) { + // Don't release if pedal is held + } else if (!notes.empty()) { + // Play previous note + auto it2 = notes.end(); + it2--; + this->note = *it2; + retriggered = true; + } +} + +void MIDIToCVInterface::processMidi(std::vector msg) { + int channel = msg[0] & 0xf; + int status = (msg[0] >> 4) & 0xf; + int data1 = msg[1]; + int data2 = msg[2]; + + //fprintf(stderr, "channel %d status %d data1 %d data2 %d\n", channel, status, data1,data2); + + // Filter channels + if (this->channel >= 0 && this->channel != channel) + return; + + switch (status) { + // note off + case 0x8: { + releaseNote(data1); + } + break; + case 0x9: // note on + if (data2 > 0) { + pressNote(data1); + this->vel = data2; + } else { + // For some reason, some keyboards send a "note on" event with a velocity of 0 to signal that the key has been released. + releaseNote(data1); + } + break; + case 0xb: // cc + switch (data1) { + case 0x01: // mod + this->mod = data2; + break; + case 0x40: // sustain + pedal = (data2 >= 64); + releaseNote(-1); + break; + } + break; + case 0xe: // pitch wheel + this->pitchWheel = data2; + break; + case 0xd: // channel aftertouch + this->afterTouch = data1; + break; + } +} + + +MidiToCVWidget::MidiToCVWidget() { + MIDIToCVInterface *module = new MIDIToCVInterface(); + setModule(module); + box.size = Vec(15 * 9, 380); + + { + Panel *panel = new LightPanel(); + panel->box.size = box.size; + addChild(panel); + } + + float margin = 5; + float labelHeight = 15; + float yPos = margin; + float yGap = 35; + + addChild(createScrew(Vec(15, 0))); + addChild(createScrew(Vec(box.size.x - 30, 0))); + addChild(createScrew(Vec(15, 365))); + addChild(createScrew(Vec(box.size.x - 30, 365))); + + { + Label *label = new Label(); + label->box.pos = Vec(box.size.x - margin - 7 * 15, margin); + label->text = "MIDI to CV"; + addChild(label); + yPos = labelHeight * 2; + } + + addParam(createParam(Vec(7 * 15, labelHeight), module, MIDIToCVInterface::RESET_PARAM, 0.0, 1.0, 0.0)); + addChild(createValueLight>(Vec(7 * 15 + 5, labelHeight + 5), &module->resetLight)); + { + Label *label = new Label(); + label->box.pos = Vec(margin, yPos); + label->text = "MIDI Interface"; + addChild(label); + yPos += labelHeight + margin; + + MidiChoice *midiChoice = new MidiChoice(); + midiChoice->midiModule = dynamic_cast(module); + midiChoice->box.pos = Vec(margin, yPos); + midiChoice->box.size.x = box.size.x - 10; + addChild(midiChoice); + yPos += midiChoice->box.size.y + margin; + } + + { + Label *label = new Label(); + label->box.pos = Vec(margin, yPos); + label->text = "Channel"; + addChild(label); + yPos += labelHeight + margin; + + ChannelChoice *channelChoice = new ChannelChoice(); + channelChoice->midiModule = dynamic_cast(module); + channelChoice->box.pos = Vec(margin, yPos); + channelChoice->box.size.x = box.size.x - 10; + addChild(channelChoice); + yPos += channelChoice->box.size.y + margin + 15; + } + + std::string labels[MIDIToCVInterface::NUM_OUTPUTS] = {"1V/oct", "Gate", "Velocity", "Mod Wheel", "Pitch Wheel", + "Aftertouch"}; + + for (int i = 0; i < MIDIToCVInterface::NUM_OUTPUTS; i++) { + Label *label = new Label(); + label->box.pos = Vec(margin, yPos); + label->text = labels[i]; + addChild(label); + + addOutput(createOutput(Vec(15 * 6, yPos - 5), module, i)); + + yPos += yGap + margin; + } +} + +void MidiToCVWidget::step() { + + ModuleWidget::step(); +} diff --git a/src/core/core.cpp b/src/core/core.cpp index 3af312dd..8a35b4da 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -1,4 +1,5 @@ #include "core.hpp" +#include "MidiInterface.hpp" void init(rack::Plugin *plugin) { @@ -8,6 +9,7 @@ void init(rack::Plugin *plugin) { createModel(plugin, "AudioInterface", "Audio Interface"); createModel(plugin, "MIDIToCVInterface", "MIDI-to-CV Interface"); createModel(plugin, "MIDICCToCVInterface", "MIDI CC-to-CV Interface"); + createModel(plugin, "MIDIClockToCVInterface", "MIDI Clock-to-CV Interface"); // createModel(plugin, "Bridge", "Bridge"); createModel(plugin, "Blank", "Blank"); } diff --git a/src/core/core.hpp b/src/core/core.hpp index f16502c8..968576bb 100644 --- a/src/core/core.hpp +++ b/src/core/core.hpp @@ -12,16 +12,6 @@ struct AudioInterfaceWidget : ModuleWidget { AudioInterfaceWidget(); }; -struct MidiToCVWidget : ModuleWidget { - MidiToCVWidget(); - void step(); -}; - -struct MIDICCToCVWidget : ModuleWidget { - MIDICCToCVWidget(); - void step(); -}; - struct BridgeWidget : ModuleWidget { BridgeWidget(); };