| @@ -0,0 +1,269 @@ | |||||
| #include <list> | |||||
| #include <algorithm> | |||||
| #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<unsigned char> 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<unsigned char> 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<unsigned char> 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<ScrewSilver>(Vec(15, 0))); | |||||
| addChild(createScrew<ScrewSilver>(Vec(box.size.x - 30, 0))); | |||||
| addChild(createScrew<ScrewSilver>(Vec(15, 365))); | |||||
| addChild(createScrew<ScrewSilver>(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<MidiIO *>(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<MidiIO *>(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<PJ3410Port>(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(); | |||||
| } | |||||
| @@ -0,0 +1,336 @@ | |||||
| #include <list> | |||||
| #include <algorithm> | |||||
| #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<unsigned char> 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<unsigned char> 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<unsigned char> 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<std::string> 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<std::string> 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<ScrewSilver>(Vec(15, 0))); | |||||
| addChild(createScrew<ScrewSilver>(Vec(box.size.x - 30, 0))); | |||||
| addChild(createScrew<ScrewSilver>(Vec(15, 365))); | |||||
| addChild(createScrew<ScrewSilver>(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<MidiIO *>(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<PJ3410Port>(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<PJ3410Port>(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<PJ3410Port>(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<PJ3410Port>(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<PJ3410Port>(Vec(15 * 6, yPos - 5), module, MIDIClockToCVInterface::RESET_PULSE)); | |||||
| } | |||||
| } | |||||
| void MIDIClockToCVWidget::step() { | |||||
| ModuleWidget::step(); | |||||
| } | |||||
| @@ -0,0 +1,241 @@ | |||||
| #include <list> | |||||
| #include <algorithm> | |||||
| #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<std::string, MidiInWrapper *> 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<std::string> MidiIO::getDevices() { | |||||
| /* Note: we could also use an existing interface if one exists */ | |||||
| static RtMidiIn *m = new RtMidiIn(); | |||||
| std::vector<std::string> 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<unsigned char> *msg) { | |||||
| std::vector<unsigned char> 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<std::string> 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"; | |||||
| } | |||||
| @@ -0,0 +1,154 @@ | |||||
| #include <unordered_map> | |||||
| #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<int, std::list<std::vector<unsigned char>>> idMessagesMap; | |||||
| std::unordered_map<int, std::list<double>> idStampsMap; | |||||
| /* Stores Ignore settings for each instance in the following order: | |||||
| * {ignore_midiSysex, ignore_midiTime, ignore_midiSense} | |||||
| */ | |||||
| std::unordered_map<int, bool[3]> 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<std::string, MidiInWrapper *> 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<std::string> 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<unsigned char> *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(); | |||||
| }; | |||||
| @@ -1,712 +0,0 @@ | |||||
| #include <assert.h> | |||||
| #include <list> | |||||
| #include <algorithm> | |||||
| #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<int> 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<unsigned char> 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<unsigned char> message; | |||||
| // midiIn->getMessage returns empty vector if there are no messages in the queue | |||||
| dynamic_cast<RtMidiIn *>(rtMidi)->getMessage(&message); | |||||
| while (message.size() > 0) { | |||||
| processMidi(message); | |||||
| dynamic_cast<RtMidiIn *>(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<unsigned char> 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<ScrewSilver>(Vec(15, 0))); | |||||
| addChild(createScrew<ScrewSilver>(Vec(box.size.x - 30, 0))); | |||||
| addChild(createScrew<ScrewSilver>(Vec(15, 365))); | |||||
| addChild(createScrew<ScrewSilver>(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<LEDButton>(Vec(7 * 15, labelHeight), module, MIDIToCVInterface::RESET_PARAM, 0.0, 1.0, 0.0)); | |||||
| addChild(createValueLight<SmallLight<RedValueLight>>(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<MidiIO *>(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<MidiIO *>(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<PJ3410Port>(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<unsigned char> 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<unsigned char> message; | |||||
| // midiIn->getMessage returns empty vector if there are no messages in the queue | |||||
| dynamic_cast<RtMidiIn *>(rtMidi)->getMessage(&message); | |||||
| while (message.size() > 0) { | |||||
| processMidi(message); | |||||
| dynamic_cast<RtMidiIn *>(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<unsigned char> 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<ScrewSilver>(Vec(15, 0))); | |||||
| addChild(createScrew<ScrewSilver>(Vec(box.size.x - 30, 0))); | |||||
| addChild(createScrew<ScrewSilver>(Vec(15, 365))); | |||||
| addChild(createScrew<ScrewSilver>(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<MidiIO *>(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<MidiIO *>(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<PJ3410Port>(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(); | |||||
| } | |||||
| @@ -0,0 +1,280 @@ | |||||
| #include <list> | |||||
| #include <algorithm> | |||||
| #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<int> 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<unsigned char> 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<unsigned char> 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<unsigned char> 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<ScrewSilver>(Vec(15, 0))); | |||||
| addChild(createScrew<ScrewSilver>(Vec(box.size.x - 30, 0))); | |||||
| addChild(createScrew<ScrewSilver>(Vec(15, 365))); | |||||
| addChild(createScrew<ScrewSilver>(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<LEDButton>(Vec(7 * 15, labelHeight), module, MIDIToCVInterface::RESET_PARAM, 0.0, 1.0, 0.0)); | |||||
| addChild(createValueLight<SmallLight<RedValueLight>>(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<MidiIO *>(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<MidiIO *>(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<PJ3410Port>(Vec(15 * 6, yPos - 5), module, i)); | |||||
| yPos += yGap + margin; | |||||
| } | |||||
| } | |||||
| void MidiToCVWidget::step() { | |||||
| ModuleWidget::step(); | |||||
| } | |||||
| @@ -0,0 +1,285 @@ | |||||
| #include <list> | |||||
| #include <algorithm> | |||||
| #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<unsigned char> 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<unsigned char> 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<unsigned char> 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<ScrewSilver>(Vec(15, 0))); | |||||
| addChild(createScrew<ScrewSilver>(Vec(box.size.x - 30, 0))); | |||||
| addChild(createScrew<ScrewSilver>(Vec(15, 365))); | |||||
| addChild(createScrew<ScrewSilver>(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<MidiIO *>(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<MidiIO *>(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<PJ3410Port>(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(); | |||||
| } | |||||
| @@ -1,4 +1,5 @@ | |||||
| #include "core.hpp" | #include "core.hpp" | ||||
| #include "MidiIO.hpp" | |||||
| void init(rack::Plugin *plugin) { | void init(rack::Plugin *plugin) { | ||||
| @@ -8,6 +9,8 @@ void init(rack::Plugin *plugin) { | |||||
| createModel<AudioInterfaceWidget>(plugin, "AudioInterface", "Audio Interface"); | createModel<AudioInterfaceWidget>(plugin, "AudioInterface", "Audio Interface"); | ||||
| createModel<MidiToCVWidget>(plugin, "MIDIToCVInterface", "MIDI-to-CV Interface"); | createModel<MidiToCVWidget>(plugin, "MIDIToCVInterface", "MIDI-to-CV Interface"); | ||||
| createModel<MIDICCToCVWidget>(plugin, "MIDICCToCVInterface", "MIDI CC-to-CV Interface"); | createModel<MIDICCToCVWidget>(plugin, "MIDICCToCVInterface", "MIDI CC-to-CV Interface"); | ||||
| createModel<MIDIClockToCVWidget>(plugin, "MIDIClockToCVInterface", "MIDI Clock-to-CV Interface"); | |||||
| createModel<MIDITriggerToCVWidget>(plugin, "MIDITriggerToCVInterface", "MIDI Trigger-to-CV Interface"); | |||||
| // createModel<BridgeWidget>(plugin, "Bridge", "Bridge"); | // createModel<BridgeWidget>(plugin, "Bridge", "Bridge"); | ||||
| createModel<BlankWidget>(plugin, "Blank", "Blank"); | createModel<BlankWidget>(plugin, "Blank", "Blank"); | ||||
| } | } | ||||
| @@ -12,16 +12,6 @@ struct AudioInterfaceWidget : ModuleWidget { | |||||
| AudioInterfaceWidget(); | AudioInterfaceWidget(); | ||||
| }; | }; | ||||
| struct MidiToCVWidget : ModuleWidget { | |||||
| MidiToCVWidget(); | |||||
| void step() override; | |||||
| }; | |||||
| struct MIDICCToCVWidget : ModuleWidget { | |||||
| MIDICCToCVWidget(); | |||||
| void step() override; | |||||
| }; | |||||
| struct BridgeWidget : ModuleWidget { | struct BridgeWidget : ModuleWidget { | ||||
| BridgeWidget(); | BridgeWidget(); | ||||
| }; | }; | ||||