#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, CONTINUE_PULSE, START_PULSE, STOP_PULSE, NUM_OUTPUTS }; int clock1ratio = 0; int clock2ratio = 0; PulseGenerator clock1Pulse; PulseGenerator clock2Pulse; PulseGenerator continuePulse; PulseGenerator startPulse; PulseGenerator stopPulse; bool tick = false; bool running = false; bool start = false; bool stop = false; bool cont = false; int c_bar = 0; /* 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 * */ const int ratios[9] = {6, 8, 12, 16, 24, 32, 48, 96, 192}; const int numratios = 9; /* * Length of clock pulse */ const float pulseTime = 0.005; MIDIClockToCVInterface() : MidiIO(), Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS) { } ~MIDIClockToCVInterface() { } void step() override; void processMidi(std::vector msg); void onDeviceChange() override; void resetMidi() override; json_t *toJson() override{ 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) override{ 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() { 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); } } 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 (start) { start = false; running = true; startPulse.trigger(pulseTime); c_bar = 0; } if (stop) { stop = false; running = false; stopPulse.trigger(pulseTime); } if (cont) { cont = false; running = true; continuePulse.trigger(pulseTime); } 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(pulseTime); } if (c_bar % ratios[clock2ratio] == 0) { clock2Pulse.trigger(pulseTime); } } 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 = continuePulse.process(1.0 / sampleRate); outputs[CONTINUE_PULSE].value = pulse ? 10.0 : 0.0; pulse = startPulse.process(1.0 / sampleRate); outputs[START_PULSE].value = pulse ? 10.0 : 0.0; pulse = stopPulse.process(1.0 / sampleRate); outputs[STOP_PULSE].value = pulse ? 10.0 : 0.0; } void MIDIClockToCVInterface::resetMidi() { outputs[CLOCK1_PULSE].value = 0.0; outputs[CLOCK2_PULSE].value = 0.0; } void MIDIClockToCVInterface::processMidi(std::vector msg) { switch (msg[0]) { case 0xfa: start = true; break; case 0xfb: cont = true; break; case 0xfc: stop = true; break; case 0xf8: tick = true; break; } } void MIDIClockToCVInterface::onDeviceChange() { setIgnores(true, false); } struct ClockRatioItem : MenuItem { int ratio; int *clockRatio; void onAction(EventAction &e) override { *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(EventAction &e) override { Menu *menu = gScene->createMenu(); menu->box.pos = getAbsoluteOffset(Vec(0, box.size.y)).round(); 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() override { 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 Clk-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 * 4; } { Label *label = new Label(); label->box.pos = Vec(margin, yPos); label->text = "Start"; addChild(label); addOutput(createOutput(Vec(15 * 6, yPos - 5), module, MIDIClockToCVInterface::START_PULSE)); yPos += labelHeight + margin * 4; } { Label *label = new Label(); label->box.pos = Vec(margin, yPos); label->text = "Stop"; addChild(label); addOutput(createOutput(Vec(15 * 6, yPos - 5), module, MIDIClockToCVInterface::STOP_PULSE)); yPos += labelHeight + margin * 4; } { Label *label = new Label(); label->box.pos = Vec(margin, yPos); label->text = "Continue"; addChild(label); addOutput(createOutput(Vec(15 * 6, yPos - 5), module, MIDIClockToCVInterface::CONTINUE_PULSE)); yPos += labelHeight + margin * 6; } { addInput(createInput(Vec(margin, yPos - 5), module, MIDIClockToCVInterface::CLOCK1_RATIO)); ClockRatioChoice *ratioChoice = new ClockRatioChoice(); ratioChoice->clockRatio = &module->clock1ratio; ratioChoice->box.pos = Vec(int(box.size.x/3), yPos); ratioChoice->box.size.x = int(box.size.x/1.5 - margin); addChild(ratioChoice); yPos += ratioChoice->box.size.y + margin * 3; } { 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; } { addInput(createInput(Vec(margin, yPos - 5), module, MIDIClockToCVInterface::CLOCK2_RATIO)); ClockRatioChoice *ratioChoice = new ClockRatioChoice(); ratioChoice->clockRatio = &module->clock2ratio; ratioChoice->box.pos = Vec(int(box.size.x/3), yPos); ratioChoice->box.size.x = int(box.size.x/1.5 - margin); addChild(ratioChoice); yPos += ratioChoice->box.size.y + margin * 3; } { 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 * 3; } } void MIDIClockToCVWidget::step() { ModuleWidget::step(); }