diff --git a/src/core/MidiCCToCV.cpp b/src/core/MidiCCToCV.cpp new file mode 100644 index 00000000..c5967965 --- /dev/null +++ b/src/core/MidiCCToCV.cpp @@ -0,0 +1,269 @@ +#include +#include +#include "rtmidi/RtMidi.h" +#include "core.hpp" +#include "MidiIO.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]; + bool onFocus[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; + onFocus[i] = false; + } + } + + ~MIDICCToCVInterface() { + + } + + void step(); + + void processMidi(std::vector msg); + + void resetMidi(); + + 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; + } + + 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; + } + + } + } + + void reset() { + resetMidi(); + } + +}; + + +void MIDICCToCVInterface::step() { + if (isPortOpen()) { + std::vector message; + + // midiIn->getMessage returns empty vector if there are no messages in the queue + getMessage(&message); + while (message.size() > 0) { + processMidi(message); + 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 (onFocus[i]) { + this->ccNum[i] = data1; + } + } + 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); + + void onMouseDownOpaque(int button); + + void onMouseUpOpaque(int button); + + void onMouseLeave(); + + int *ccNum; + bool *inited; + bool *onFocus; +}; + +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); + } + + if (*onFocus) { + text = std::to_string(*ccNum); + } + + TextField::draw(vg); +} + +void CCTextField::onMouseUpOpaque(int button) { + if (button == 1) { + *onFocus = false; + } + +} + +void CCTextField::onMouseDownOpaque(int button) { + if (button == 1) { + *onFocus = true; + } +} + +void CCTextField::onMouseLeave() { + *onFocus = false; +} + + +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->onFocus = &module->onFocus[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..cd94991f --- /dev/null +++ b/src/core/MidiClockToCV.cpp @@ -0,0 +1,336 @@ +#include +#include +#include "rtmidi/RtMidi.h" +#include "core.hpp" +#include "MidiIO.hpp" +#include "dsp/digital.hpp" + +using namespace rack; + +struct MIDIClockToCVInterface : MidiIO, Module { + enum ParamIds { + NUM_PARAMS + }; + enum InputIds { + CLOCK1_RATIO, + CLOCK2_RATIO, + NUM_INPUTS + }; + enum OutputIds { + CLOCK1_PULSE, + CLOCK2_PULSE, + RESET_PULSE, + NUM_OUTPUTS + }; + + int clock1ratio = 0; + int clock2ratio = 0; + + PulseGenerator clock1Pulse; + PulseGenerator clock2Pulse; + PulseGenerator resetPulse; + bool tick = false; + bool running = false; + bool reset = false; + + + MIDIClockToCVInterface() : MidiIO(), Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS) { + + } + + ~MIDIClockToCVInterface() { + } + + void step(); + + void processMidi(std::vector msg); + + void onDeviceChange(); + + void resetMidi(); + + 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; + } + + 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); + } + } +}; + +void MIDIClockToCVInterface::step() { + static int c_bar = 0; + static float trigger_length = 0.05; + static float sampleRate = engineGetSampleRate(); + + /* 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}; + static int numratios = sizeof(ratios) / sizeof(*ratios); + + if (isPortOpen()) { + std::vector message; + + // midiIn->getMessage returns empty vector if there are no messages in the queue + getMessage(&message); + while (message.size() > 0) { + processMidi(message); + getMessage(&message); + } + } + + if (inputs[CLOCK1_RATIO].active) { + clock1ratio = int(clampf(inputs[CLOCK1_RATIO].value, 0.0, 10.0) * (numratios - 1) / 10); + } + + if (inputs[CLOCK2_RATIO].active) { + clock2ratio = int(clampf(inputs[CLOCK2_RATIO].value, 0.0, 10.0) * (numratios - 1) / 10); + } + + if (reset) { + resetPulse.trigger(trigger_length); + reset = false; + c_bar = 0; + 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 / sampleRate); + outputs[CLOCK1_PULSE].value = pulse ? 10.0 : 0.0; + + pulse = clock2Pulse.process(1.0 / sampleRate); + outputs[CLOCK2_PULSE].value = pulse ? 10.0 : 0.0; + + pulse = resetPulse.process(1.0 / sampleRate); + outputs[RESET_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: + reset = true; + running = true; + break; + case 0xfc: + running = false; + break; + case 0xf8: + tick = true; + break; + } + + +} + +void MIDIClockToCVInterface::onDeviceChange() { + setIgnores(true, false); +} + +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 (unsigned long 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 = "C1 Ratio"; + addChild(label); + + addInput(createInput(Vec(15 * 6, yPos - 5), module, MIDIClockToCVInterface::CLOCK1_RATIO)); + + yPos += margin * 6; + + 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 * 2; + + } + + { + Label *label = new Label(); + label->box.pos = Vec(margin, yPos); + label->text = "C1 Pulse"; + addChild(label); + + addOutput(createOutput(Vec(15 * 6, yPos - 5), module, MIDIClockToCVInterface::CLOCK1_PULSE)); + yPos += margin * 10; + } + + + { + Label *label = new Label(); + label->box.pos = Vec(margin, yPos); + label->text = "C2 Ratio"; + addChild(label); + + addInput(createInput(Vec(15 * 6, yPos - 5), module, MIDIClockToCVInterface::CLOCK2_RATIO)); + + yPos += margin * 6; + + 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 * 2; + + } + + { + Label *label = new Label(); + label->box.pos = Vec(margin, yPos); + label->text = "C2 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 = "Reset"; + addChild(label); + addOutput(createOutput(Vec(15 * 6, yPos - 5), module, MIDIClockToCVInterface::RESET_PULSE)); + } +} + +void MIDIClockToCVWidget::step() { + + ModuleWidget::step(); +} diff --git a/src/core/MidiIO.cpp b/src/core/MidiIO.cpp new file mode 100644 index 00000000..3d7eb727 --- /dev/null +++ b/src/core/MidiIO.cpp @@ -0,0 +1,241 @@ +#include +#include +#include "rtmidi/RtMidi.h" +#include "core.hpp" +#include "MidiIO.hpp" + +using namespace rack; + + +/** + * MidiIO implements the shared functionality of all midi modules, namely: + * + Channel Selection (including helper for storing json) + * + Interface Selection (including helper for storing json) + * + rtMidi initialisation (input or output) + */ +MidiIO::MidiIO(bool isOut) { + channel = -1; + this->isOut = isOut; + + if (isOut) { + fprintf(stderr, "Midi Out is currently not supported (will be added soon)"); + } +}; + +void MidiIO::setChannel(int channel) { + this->channel = channel; +} + +std::unordered_map MidiIO::midiInMap = {}; + +json_t *MidiIO::addBaseJson(json_t *rootJ) { + if (deviceName != "") { + json_object_set_new(rootJ, "interfaceName", json_string(deviceName.c_str())); + json_object_set_new(rootJ, "channel", json_integer(channel)); + } + return rootJ; +} + +void MidiIO::baseFromJson(json_t *rootJ) { + json_t *portNameJ = json_object_get(rootJ, "interfaceName"); + if (portNameJ) { + openDevice(json_string_value(portNameJ)); + } + + json_t *channelJ = json_object_get(rootJ, "channel"); + if (channelJ) { + setChannel(json_integer_value(channelJ)); + } +} + +std::vector MidiIO::getDevices() { + /* Note: we could also use an existing interface if one exists */ + static RtMidiIn *m = new RtMidiIn(); + + std::vector names = {}; + + for (unsigned int i = 0; i < m->getPortCount(); i++) { + names.push_back(m->getPortName(i)); + } + + return names; +} + +void MidiIO::openDevice(std::string deviceName) { + + MidiInWrapper *mw = midiInMap[deviceName]; + + if (this->id > 0 || this->deviceName != "") { + close(); + } + + if (!mw) { + try { + mw = new MidiInWrapper(); + midiInMap[deviceName] = mw; + + + for (unsigned int i = 0; i < mw->getPortCount(); i++) { + if (deviceName == mw->getPortName(i)) { + mw->openPort(i); + break; + } + } + } + catch (RtMidiError &error) { + fprintf(stderr, "Failed to create RtMidiIn: %s\n", error.getMessage().c_str()); + this->deviceName = ""; + this->id = -1; + return; + } + } + + this->deviceName = deviceName; + + id = midiInMap[deviceName]->add(); + onDeviceChange(); +} + +void MidiIO::setIgnores(bool ignoreSysex, bool ignoreTime, bool ignoreSense) { + bool sy = true, ti = true, se = true; + + midiInMap[deviceName]->ignoresMap[id][0] = ignoreSysex; + midiInMap[deviceName]->ignoresMap[id][1] = ignoreTime; + midiInMap[deviceName]->ignoresMap[id][2] = ignoreSense; + + for (auto kv : midiInMap[deviceName]->ignoresMap) { + sy = sy && kv.second[0]; + ti = ti && kv.second[1]; + se = se && kv.second[2]; + } + + midiInMap[deviceName]->ignoreTypes(se,ti,se); + + +} + +std::string MidiIO::getDeviceName() { + return deviceName; +} + +double MidiIO::getMessage(std::vector *msg) { + std::vector next_msg; + + MidiInWrapper *mw = midiInMap[deviceName]; + + if (!mw) { + fprintf(stderr, "Device not opened!: %s\n", deviceName.c_str()); + return 0; + } + + double stamp = midiInMap[deviceName]->getMessage(&next_msg); + + if (next_msg.size() > 0) { + for (auto kv : mw->idMessagesMap) { + mw->idMessagesMap[kv.first].push_back(next_msg); + mw->idStampsMap[kv.first].push_back(stamp); + } + } + + if (mw->idMessagesMap[id].size() <= 0) { + *msg = next_msg; + return stamp; + } + + *msg = mw->idMessagesMap[id].front(); + stamp = mw->idStampsMap[id].front(); + mw->idMessagesMap[id].pop_front(); + return stamp; +} + +bool MidiIO::isPortOpen() { + return id > 0; +} + +void MidiIO::close() { + + MidiInWrapper *mw = midiInMap[deviceName]; + + if (!mw || id < 0) { + //fprintf(stderr, "Trying to close already closed device!\n"); + return; + } + + setIgnores(); // reset ignore types for this instance + + mw->erase(id); + + if (mw->subscribers == 0) { + mw->closePort(); + midiInMap.erase(deviceName); + } + + id = -1; + deviceName = ""; +} + + +void MidiItem::onAction() { + midiModule->resetMidi(); // reset Midi values + midiModule->openDevice(text); +} + +void MidiChoice::onAction() { + Menu *menu = gScene->createMenu(); + menu->box.pos = getAbsolutePos().plus(Vec(0, box.size.y)); + menu->box.size.x = box.size.x; + + { + MidiItem *midiItem = new MidiItem(); + midiItem->midiModule = midiModule; + midiItem->text = ""; + menu->pushChild(midiItem); + } + + std::vector deviceNames = midiModule->getDevices(); + for (unsigned int i = 0; i < deviceNames.size(); i++) { + MidiItem *midiItem = new MidiItem(); + midiItem->midiModule = midiModule; + midiItem->text = deviceNames[i]; + menu->pushChild(midiItem); + } +} + +void MidiChoice::step() { + if (midiModule->getDeviceName() == "") { + text = "No Device"; + return; + } + std::string name = midiModule->getDeviceName(); + text = ellipsize(name, 15); +} + +void ChannelItem::onAction() { + midiModule->resetMidi(); // reset Midi values + midiModule->setChannel(channel); +} + +void ChannelChoice::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 ChannelChoice::step() { + text = (midiModule->channel >= 0) ? stringf("%d", midiModule->channel + 1) : "All"; +} \ No newline at end of file diff --git a/src/core/MidiIO.hpp b/src/core/MidiIO.hpp new file mode 100644 index 00000000..c88287e4 --- /dev/null +++ b/src/core/MidiIO.hpp @@ -0,0 +1,154 @@ +#include +#include "rack.hpp" +#include "rtmidi/RtMidi.h" + + +using namespace rack; + + +/** + * This class allows to use one instance of rtMidiIn with + * multiple modules. A MidiIn port will be opened only once while multiple + * instances can use it simultaniously, each receiving all its incoming messages. + */ + +struct MidiInWrapper : RtMidiIn { + std::unordered_map>> idMessagesMap; + std::unordered_map> idStampsMap; + + /* Stores Ignore settings for each instance in the following order: + * {ignore_midiSysex, ignore_midiTime, ignore_midiSense} + */ + std::unordered_map ignoresMap; + + int uid_c = 0; + int subscribers = 0; + + MidiInWrapper() : RtMidiIn() { + idMessagesMap = {}; + idStampsMap = {}; + }; + + int add() { + int id = ++uid_c; + subscribers++; + idMessagesMap[id] = {}; + idStampsMap[id] = {}; + + ignoresMap[id][0] = true; + ignoresMap[id][1] = true; + ignoresMap[id][2] = true; + return id; + } + + void erase(int id) { + subscribers--; + idMessagesMap.erase(id); + idStampsMap.erase(id); + ignoresMap.erase(id); + } +}; + +struct MidiIO { +private: + static std::unordered_map midiInMap; + /* TODO: add for midi out*/ + int id = -1; + std::string deviceName = ""; + bool isOut = false; + +public: + int channel; + + + MidiIO(bool isOut = false); + + ~MidiIO() { + close(); + } + + std::vector getDevices(); + + void openDevice(std::string deviceName); + + void setIgnores(bool ignoreSysex = true, bool ignoreTime = true, bool ignoreSense = true); + + std::string getDeviceName(); + + void setChannel(int channel); + + double getMessage(std::vector *msg); + + json_t *addBaseJson(json_t *rootJ); + + void baseFromJson(json_t *rootJ); + + bool isPortOpen(); + + void close(); + + /* called when midi port is set */ + virtual void resetMidi()=0; + + /* called if a user switches or sets the deivce (and after this device is initialised)*/ + virtual void onDeviceChange(){}; +}; + +////////////////////// +// MIDI module widgets +////////////////////// + +struct MidiItem : MenuItem { + MidiIO *midiModule; + + 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(); +}; + +struct MIDITriggerToCVWidget : ModuleWidget { + MIDITriggerToCVWidget(); + + void step(); +}; + diff --git a/src/core/MidiInterface.cpp b/src/core/MidiInterface.cpp deleted file mode 100644 index a07b127a..00000000 --- a/src/core/MidiInterface.cpp +++ /dev/null @@ -1,712 +0,0 @@ -#include -#include -#include -#include "rtmidi/RtMidi.h" -#include "core.hpp" -#include "gui.hpp" -#include "engine.hpp" -#include "dsp/digital.hpp" - - -using namespace rack; - -/** - * MidiIO implements the shared functionality of all midi modules, namely: - * + Channel Selection (including helper for storing json) - * + 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; - - /* - * 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()); - } - } - - ~MidiIO() {} - - int getPortCount(); - - std::string getPortName(int portId); - - // -1 will close the port - void setPortId(int portId); - - void setChannel(int channel) { - this->channel = channel; - } - - 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 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)); - } - } - - virtual void resetMidi()=0; // called when midi port is set -}; - -int MidiIO::getPortCount() { - return rtMidi->getPortCount(); -} - -std::string MidiIO::getPortName(int portId) { - std::string portName; - try { - portName = rtMidi->getPortName(portId); - } - catch (RtMidiError &error) { - fprintf(stderr, "Failed to get Port Name: %d, %s\n", portId, error.getMessage().c_str()); - } - return portName; -} - -void MidiIO::setPortId(int portId) { - - // Close port if it was previously opened - if (rtMidi->isPortOpen()) { - rtMidi->closePort(); - } - this->portId = -1; - - // Open new port - if (portId >= 0) { - rtMidi->openPort(portId, "Midi Interface"); - } - 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 reset() { - 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 / engineGetSampleRate(); // 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() { - // 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 reset() { - 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() { - // Assume QWERTY -#define MIDI_KEY(key, midi) if (glfwGetKey(gWindow, key)) printf("%d\n", midi); - - // MIDI_KEY(GLFW_KEY_Z, 48); - - ModuleWidget::step(); -} diff --git a/src/core/MidiToCV.cpp b/src/core/MidiToCV.cpp new file mode 100644 index 00000000..4e12a25a --- /dev/null +++ b/src/core/MidiToCV.cpp @@ -0,0 +1,280 @@ +#include +#include +#include "rtmidi/RtMidi.h" +#include "core.hpp" +#include "MidiIO.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() { + }; + + void step(); + + void pressNote(int note); + + void releaseNote(int note); + + void processMidi(std::vector msg); + + json_t *toJson() { + json_t *rootJ = json_object(); + addBaseJson(rootJ); + return rootJ; + } + + void fromJson(json_t *rootJ) { + baseFromJson(rootJ); + } + + void reset() { + resetMidi(); + } + + 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() { + static float sampleRate = engineGetSampleRate(); + + if (isPortOpen()) { + std::vector message; + + // midiIn->getMessage returns empty vector if there are no messages in the queue + getMessage(&message); + while (message.size() > 0) { + processMidi(message); + 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 / sampleRate; // 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/MidiTriggerToCV.cpp b/src/core/MidiTriggerToCV.cpp new file mode 100644 index 00000000..ee507119 --- /dev/null +++ b/src/core/MidiTriggerToCV.cpp @@ -0,0 +1,285 @@ +#include +#include +#include "rtmidi/RtMidi.h" +#include "core.hpp" +#include "MidiIO.hpp" +#include "dsp/digital.hpp" + +using namespace rack; + +/* + * MIDIToCVInterface converts midi note on/off events, velocity , channel aftertouch, pitch wheel and mod weel to + * CV + */ +struct MIDITriggerToCVInterface : MidiIO, Module { + enum ParamIds { + NUM_PARAMS + }; + enum InputIds { + NUM_INPUTS + }; + enum OutputIds { + NUM_OUTPUTS = 16 + }; + + int trigger[NUM_OUTPUTS]; + int triggerNum[NUM_OUTPUTS]; + bool triggerNumInited[NUM_OUTPUTS]; + bool onFocus[NUM_OUTPUTS]; + + MIDITriggerToCVInterface() : MidiIO(), Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS) { + for (int i = 0; i < NUM_OUTPUTS; i++) { + trigger[i] = 0; + triggerNum[i] = i; + onFocus[i] = false; + } + } + + ~MIDITriggerToCVInterface() { + } + + void step(); + + void processMidi(std::vector msg); + + 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(triggerNum[i])); + } + return rootJ; + } + + 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) { + triggerNum[i] = json_integer_value(ccNumJ); + triggerNumInited[i] = true; + } + + } + } + + void reset() final { + resetMidi(); + } + +}; + + +void MIDITriggerToCVInterface::step() { + if (isPortOpen()) { + std::vector message; + + // midiIn->getMessage returns empty vector if there are no messages in the queue + getMessage(&message); + while (message.size() > 0) { + processMidi(message); + getMessage(&message); + } + } + + for (int i = 0; i < NUM_OUTPUTS; i++) { + // Note: Could have an option to select between gate and velocity + // but trigger seams more useful + // outputs[i].value = trigger[i] / 127.0 * 10; + outputs[i].value = trigger[i] > 0 ? 10.0 : 0.0; + } +} + +void MIDITriggerToCVInterface::resetMidi() { + for (int i = 0; i < NUM_OUTPUTS; i++) { + trigger[i] = 0; + } +}; + +void MIDITriggerToCVInterface::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 == 0x8) { // note off + for (int i = 0; i < NUM_OUTPUTS; i++) { + if (data1 == triggerNum[i]) { + trigger[i] = data2; + } + } + return; + } + + if (status == 0x9) { // note on + for (int i = 0; i < NUM_OUTPUTS; i++) { + if (onFocus[i]) { + this->triggerNum[i] = data1; + } + } + + for (int i = 0; i < NUM_OUTPUTS; i++) { + if (data1 == triggerNum[i]) { + trigger[i] = data2; + } + } + } + +} + +struct TriggerTextField : TextField { + void onTextChange(); + + void draw(NVGcontext *vg); + + void onMouseDownOpaque(int button); + + void onMouseUpOpaque(int button); + + void onMouseLeave(); + + + int *triggerNum; + bool *inited; + bool *onFocus; +}; + +void TriggerTextField::draw(NVGcontext *vg) { + /* This is necessary, since the save + * file is loaded after constructing the widget*/ + if (*inited) { + *inited = false; + text = std::to_string(*triggerNum); + } + + if (*onFocus) { + text = std::to_string(*triggerNum); + } + + TextField::draw(vg); +} + +void TriggerTextField::onTextChange() { + if (text.size() > 0) { + try { + *triggerNum = std::stoi(text); + // Only allow valid cc numbers + if (*triggerNum < 0 || *triggerNum > 127 || text.size() > 3) { + text = ""; + begin = end = 0; + *triggerNum = -1; + } + } catch (...) { + text = ""; + begin = end = 0; + *triggerNum = -1; + } + }; +} + +void TriggerTextField::onMouseUpOpaque(int button) { + if (button == 1) { + *onFocus = false; + } + +} + +void TriggerTextField::onMouseDownOpaque(int button) { + if (button == 1) { + *onFocus = true; + } +} + +void TriggerTextField::onMouseLeave() { + *onFocus = false; +} + +MIDITriggerToCVWidget::MIDITriggerToCVWidget() { + MIDITriggerToCVInterface *module = new MIDITriggerToCVInterface(); + 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 Trigger 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 < MIDITriggerToCVInterface::NUM_OUTPUTS; i++) { + TriggerTextField *triggerNumChoice = new TriggerTextField(); + triggerNumChoice->triggerNum = &module->triggerNum[i]; + triggerNumChoice->inited = &module->triggerNumInited[i]; + triggerNumChoice->onFocus = &module->onFocus[i]; + triggerNumChoice->text = std::to_string(module->triggerNum[i]); + triggerNumChoice->box.pos = Vec(11 + (i % 4) * (63), yPos); + triggerNumChoice->box.size.x = 29; + + addChild(triggerNumChoice); + + 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 MIDITriggerToCVWidget::step() { + + ModuleWidget::step(); +} diff --git a/src/core/core.cpp b/src/core/core.cpp index 3af312dd..a083c008 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -1,4 +1,5 @@ #include "core.hpp" +#include "MidiIO.hpp" void init(rack::Plugin *plugin) { @@ -8,6 +9,8 @@ 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, "MIDITriggerToCVInterface", "MIDI Trigger-to-CV Interface"); // createModel(plugin, "Bridge", "Bridge"); createModel(plugin, "Blank", "Blank"); } diff --git a/src/core/core.hpp b/src/core/core.hpp index 41cefe8f..661cd87b 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() override; -}; - -struct MIDICCToCVWidget : ModuleWidget { - MIDICCToCVWidget(); - void step() override; -}; - struct BridgeWidget : ModuleWidget { BridgeWidget(); };