| @@ -1 +1 @@ | |||||
| Subproject commit 4dd22f56d6b733c8de13d6e8b12f13390aa5782e | |||||
| Subproject commit 015d020615e8169d2f227ad385c5f2aa1e091fd1 | |||||
| @@ -4,10 +4,22 @@ | |||||
| #include "core.hpp" | #include "core.hpp" | ||||
| #include "MidiIO.hpp" | #include "MidiIO.hpp" | ||||
| /* | |||||
| * MIDIToCVInterface converts midi note on/off events, velocity , channel aftertouch, pitch wheel and mod weel to | |||||
| * CV | |||||
| */ | |||||
| struct CCValue { | |||||
| int val = 0; // Controller value | |||||
| TransitionSmoother tSmooth; | |||||
| int num = 0; // Controller number | |||||
| bool numInited = false; // Num inited by config file | |||||
| bool numSelected = false; // Text field selected for midi learn | |||||
| bool changed = false; // Value has been changed by midi message (only if it is in sync!) | |||||
| int sync = 0; // Output value sync (implies diff) | |||||
| bool syncFirst = true; // First value after sync was reset | |||||
| void resetSync() { | |||||
| sync = 0; | |||||
| syncFirst = true; | |||||
| } | |||||
| }; | |||||
| struct MIDICCToCVInterface : MidiIO, Module { | struct MIDICCToCVInterface : MidiIO, Module { | ||||
| enum ParamIds { | enum ParamIds { | ||||
| NUM_PARAMS | NUM_PARAMS | ||||
| @@ -22,27 +34,15 @@ struct MIDICCToCVInterface : MidiIO, Module { | |||||
| NUM_LIGHTS = 16 | NUM_LIGHTS = 16 | ||||
| }; | }; | ||||
| int cc[NUM_OUTPUTS]; | |||||
| int ccNum[NUM_OUTPUTS]; | |||||
| int ccSync[NUM_OUTPUTS]; | |||||
| bool ccSyncFirst[NUM_OUTPUTS]; | |||||
| bool ccNumInited[NUM_OUTPUTS]; | |||||
| bool onFocus[NUM_OUTPUTS]; | |||||
| CCValue cc[NUM_OUTPUTS]; | |||||
| MIDICCToCVInterface() : MidiIO(), Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) { | MIDICCToCVInterface() : MidiIO(), Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) { | ||||
| for (int i = 0; i < NUM_OUTPUTS; i++) { | for (int i = 0; i < NUM_OUTPUTS; i++) { | ||||
| cc[i] = 0; | |||||
| ccNum[i] = i; | |||||
| ccSync[i] = 0; | |||||
| ccSyncFirst[i] = true; | |||||
| onFocus[i] = false; | |||||
| cc[i].num = i; | |||||
| } | } | ||||
| } | } | ||||
| ~MIDICCToCVInterface() { | |||||
| } | |||||
| ~MIDICCToCVInterface() {} | |||||
| void step() override; | void step() override; | ||||
| @@ -54,9 +54,9 @@ struct MIDICCToCVInterface : MidiIO, Module { | |||||
| json_t *rootJ = json_object(); | json_t *rootJ = json_object(); | ||||
| addBaseJson(rootJ); | addBaseJson(rootJ); | ||||
| for (int i = 0; i < NUM_OUTPUTS; i++) { | for (int i = 0; i < NUM_OUTPUTS; i++) { | ||||
| json_object_set_new(rootJ, ("ccNum" + std::to_string(i)).c_str(), json_integer(ccNum[i])); | |||||
| json_object_set_new(rootJ, ("ccNum" + std::to_string(i)).c_str(), json_integer(cc[i].num)); | |||||
| if (outputs[i].active) { | if (outputs[i].active) { | ||||
| json_object_set_new(rootJ, ("ccVal" + std::to_string(i)).c_str(), json_integer(cc[i])); | |||||
| json_object_set_new(rootJ, ("ccVal" + std::to_string(i)).c_str(), json_integer(cc[i].val)); | |||||
| } | } | ||||
| } | } | ||||
| return rootJ; | return rootJ; | ||||
| @@ -67,13 +67,15 @@ struct MIDICCToCVInterface : MidiIO, Module { | |||||
| for (int i = 0; i < NUM_OUTPUTS; i++) { | for (int i = 0; i < NUM_OUTPUTS; i++) { | ||||
| json_t *ccNumJ = json_object_get(rootJ, ("ccNum" + std::to_string(i)).c_str()); | json_t *ccNumJ = json_object_get(rootJ, ("ccNum" + std::to_string(i)).c_str()); | ||||
| if (ccNumJ) { | if (ccNumJ) { | ||||
| ccNum[i] = json_integer_value(ccNumJ); | |||||
| ccNumInited[i] = true; | |||||
| cc[i].num = json_integer_value(ccNumJ); | |||||
| cc[i].numInited = true; | |||||
| } | } | ||||
| json_t *ccValJ = json_object_get(rootJ, ("ccVal" + std::to_string(i)).c_str()); | json_t *ccValJ = json_object_get(rootJ, ("ccVal" + std::to_string(i)).c_str()); | ||||
| if (ccValJ) { | if (ccValJ) { | ||||
| cc[i] = json_integer_value(ccValJ); | |||||
| cc[i].val = json_integer_value(ccValJ); | |||||
| cc[i].tSmooth.set((cc[i].val / 127.0 * 10.0), (cc[i].val / 127.0 * 10.0)); | |||||
| cc[i].resetSync(); | |||||
| } | } | ||||
| } | } | ||||
| @@ -97,19 +99,25 @@ void MIDICCToCVInterface::step() { | |||||
| } | } | ||||
| } | } | ||||
| for (int i = 0; i < NUM_OUTPUTS; i++) { | for (int i = 0; i < NUM_OUTPUTS; i++) { | ||||
| lights[i].setBrightness(ccSync[i] / 127.0); | |||||
| lights[i].setBrightness(cc[i].sync / 127.0); | |||||
| if (cc[i].changed) { | |||||
| cc[i].tSmooth.set(outputs[i].value, (cc[i].val / 127.0 * 10.0), int(engineGetSampleRate() / 32)); | |||||
| cc[i].changed = false; | |||||
| } | |||||
| outputs[i].value = cc[i] / 127.0 * 10.0; | |||||
| outputs[i].value = cc[i].tSmooth.next(); | |||||
| } | } | ||||
| } | } | ||||
| void MIDICCToCVInterface::resetMidi() { | void MIDICCToCVInterface::resetMidi() { | ||||
| for (int i = 0; i < NUM_OUTPUTS; i++) { | for (int i = 0; i < NUM_OUTPUTS; i++) { | ||||
| cc[i] = 0; | |||||
| ccSync[i] = 0; | |||||
| ccSyncFirst[i] = true; | |||||
| cc[i].val = 0; | |||||
| cc[i].resetSync(); | |||||
| cc[i].tSmooth.set(0,0); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -127,27 +135,29 @@ void MIDICCToCVInterface::processMidi(std::vector<unsigned char> msg) { | |||||
| if (status == 0xb) { | if (status == 0xb) { | ||||
| for (int i = 0; i < NUM_OUTPUTS; i++) { | for (int i = 0; i < NUM_OUTPUTS; i++) { | ||||
| if (onFocus[i]) { | |||||
| ccSync[i] = true; | |||||
| ccSyncFirst[i] = true; | |||||
| ccNum[i] = data1; | |||||
| if (cc[i].numSelected) { | |||||
| cc[i].resetSync(); | |||||
| cc[i].num = data1; | |||||
| } | } | ||||
| if (data1 == ccNum[i]) { | |||||
| if (ccSyncFirst[i]) { | |||||
| ccSyncFirst[i] = false; | |||||
| if (data2 < cc[i] + 2 && data2 > cc[i] - 2) { | |||||
| ccSync[i] = 0; | |||||
| } else { | |||||
| ccSync[i] = absi(data2 - cc[i]); | |||||
| if (data1 == cc[i].num) { | |||||
| /* If the first value we received after sync was reset is +/- 1 of | |||||
| * the output value the values are in sync*/ | |||||
| if (cc[i].syncFirst) { | |||||
| cc[i].syncFirst = false; | |||||
| if (data2 < cc[i].val + 2 && data2 > cc[i].val - 2) { | |||||
| cc[i].sync = 0; | |||||
| }else { | |||||
| cc[i].sync = absi(data2 - cc[i].val); | |||||
| } | } | ||||
| return; | |||||
| } | } | ||||
| if (ccSync[i] == 0) { | |||||
| cc[i] = data2; | |||||
| if (cc[i].sync == 0) { | |||||
| cc[i].val = data2; | |||||
| cc[i].changed = true; | |||||
| } else { | } else { | ||||
| ccSync[i] = absi(data2 - cc[i]); | |||||
| cc[i].sync = absi(data2 - cc[i].val); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -160,12 +170,10 @@ struct CCTextField : TextField { | |||||
| void draw(NVGcontext *vg) override; | void draw(NVGcontext *vg) override; | ||||
| void onMouseDown(EventMouseDown &e) override; | void onMouseDown(EventMouseDown &e) override; | ||||
| void onMouseUp(EventMouseUp &e) override; | void onMouseUp(EventMouseUp &e) override; | ||||
| void onMouseLeave(EventMouseLeave &e) override; | void onMouseLeave(EventMouseLeave &e) override; | ||||
| int num; | |||||
| int outNum; | |||||
| MIDICCToCVInterface *module; | MIDICCToCVInterface *module; | ||||
| }; | }; | ||||
| @@ -173,59 +181,59 @@ struct CCTextField : TextField { | |||||
| void CCTextField::draw(NVGcontext *vg) { | void CCTextField::draw(NVGcontext *vg) { | ||||
| /* This is necessary, since the save | /* This is necessary, since the save | ||||
| * file is loaded after constructing the widget*/ | * file is loaded after constructing the widget*/ | ||||
| if (module->ccNumInited[num]) { | |||||
| module->ccNumInited[num] = false; | |||||
| text = std::to_string(module->ccNum[num]); | |||||
| if (module->cc[outNum].numInited) { | |||||
| module->cc[outNum].numInited = false; | |||||
| text = std::to_string(module->cc[outNum].num); | |||||
| } | } | ||||
| if (module->onFocus[num]) { | |||||
| text = std::to_string(module->ccNum[num]); | |||||
| /* If number is selected for midi learn*/ | |||||
| if (module->cc[outNum].numSelected) { | |||||
| text = std::to_string(module->cc[outNum].num); | |||||
| } | } | ||||
| TextField::draw(vg); | TextField::draw(vg); | ||||
| } | } | ||||
| void CCTextField::onMouseDown(EventMouseDown &e) { | |||||
| void CCTextField::onMouseUp(EventMouseUp &e) { | |||||
| if (e.button == 1) { | if (e.button == 1) { | ||||
| module->onFocus[num] = true; | |||||
| module->cc[outNum].numSelected = false; | |||||
| e.consumed = true; | |||||
| } | } | ||||
| e.consumed = true; | |||||
| TextField::onMouseUp(e); | |||||
| } | } | ||||
| void CCTextField::onMouseUp(EventMouseUp &e) { | |||||
| void CCTextField::onMouseDown(EventMouseDown &e) { | |||||
| if (e.button == 1) { | if (e.button == 1) { | ||||
| module->onFocus[num] = false; | |||||
| module->cc[outNum].numSelected = true; | |||||
| e.consumed = true; | |||||
| } | } | ||||
| e.consumed = true; | |||||
| TextField::onMouseDown(e); | |||||
| } | } | ||||
| void CCTextField::onMouseLeave(EventMouseLeave &e) { | void CCTextField::onMouseLeave(EventMouseLeave &e) { | ||||
| module->onFocus[num] = false; | |||||
| module->cc[outNum].numSelected = false; | |||||
| e.consumed = true; | |||||
| } | } | ||||
| void CCTextField::onTextChange() { | void CCTextField::onTextChange() { | ||||
| int *ccNum = &module->ccNum[num]; | |||||
| if (text.size() > 0) { | if (text.size() > 0) { | ||||
| try { | try { | ||||
| *ccNum = std::stoi(text); | |||||
| int num = std::stoi(text); | |||||
| // Only allow valid cc numbers | // Only allow valid cc numbers | ||||
| if (*ccNum < 0 || *ccNum > 127 || text.size() > 3) { | |||||
| if (num < 0 || num > 127 || text.size() > 3) { | |||||
| text = ""; | text = ""; | ||||
| begin = end = 0; | begin = end = 0; | ||||
| *ccNum = -1; | |||||
| return; | |||||
| } | |||||
| if (!module->ccNumInited[num] && *ccNum != std::stoi(text)) { | |||||
| module->ccSync[num] = 0; | |||||
| module->ccSyncFirst[num] = true; | |||||
| module->cc[outNum].num = -1; | |||||
| } else { | |||||
| module->cc[outNum].num = num; | |||||
| module->cc[outNum].resetSync(); | |||||
| } | } | ||||
| } catch (...) { | } catch (...) { | ||||
| text = ""; | text = ""; | ||||
| begin = end = 0; | begin = end = 0; | ||||
| *ccNum = -1; | |||||
| module->cc[outNum].num = -1; | |||||
| } | } | ||||
| }; | }; | ||||
| } | } | ||||
| @@ -288,8 +296,8 @@ MIDICCToCVWidget::MIDICCToCVWidget() { | |||||
| for (int i = 0; i < MIDICCToCVInterface::NUM_OUTPUTS; i++) { | for (int i = 0; i < MIDICCToCVInterface::NUM_OUTPUTS; i++) { | ||||
| CCTextField *ccNumChoice = new CCTextField(); | CCTextField *ccNumChoice = new CCTextField(); | ||||
| ccNumChoice->module = module; | ccNumChoice->module = module; | ||||
| ccNumChoice->num = i; | |||||
| ccNumChoice->text = std::to_string(module->ccNum[i]); | |||||
| ccNumChoice->outNum = i; | |||||
| ccNumChoice->text = std::to_string(module->cc[i].num); | |||||
| ccNumChoice->box.pos = Vec(11 + (i % 4) * (63), yPos); | ccNumChoice->box.pos = Vec(11 + (i % 4) * (63), yPos); | ||||
| ccNumChoice->box.size.x = 29; | ccNumChoice->box.size.x = 29; | ||||
| @@ -19,7 +19,9 @@ struct MIDIClockToCVInterface : MidiIO, Module { | |||||
| enum OutputIds { | enum OutputIds { | ||||
| CLOCK1_PULSE, | CLOCK1_PULSE, | ||||
| CLOCK2_PULSE, | CLOCK2_PULSE, | ||||
| RESET_PULSE, | |||||
| CONTINUE_PULSE, | |||||
| START_PULSE, | |||||
| STOP_PULSE, | |||||
| NUM_OUTPUTS | NUM_OUTPUTS | ||||
| }; | }; | ||||
| @@ -28,10 +30,30 @@ struct MIDIClockToCVInterface : MidiIO, Module { | |||||
| PulseGenerator clock1Pulse; | PulseGenerator clock1Pulse; | ||||
| PulseGenerator clock2Pulse; | PulseGenerator clock2Pulse; | ||||
| PulseGenerator resetPulse; | |||||
| PulseGenerator continuePulse; | |||||
| PulseGenerator startPulse; | |||||
| PulseGenerator stopPulse; | |||||
| bool tick = false; | bool tick = false; | ||||
| bool running = false; | bool running = false; | ||||
| bool reset = 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() : MidiIO(), Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS) { | ||||
| @@ -72,19 +94,7 @@ struct MIDIClockToCVInterface : MidiIO, Module { | |||||
| }; | }; | ||||
| void MIDIClockToCVInterface::step() { | 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); | |||||
| float sampleRate = engineGetSampleRate(); | |||||
| if (isPortOpen()) { | if (isPortOpen()) { | ||||
| std::vector<unsigned char> message; | std::vector<unsigned char> message; | ||||
| @@ -105,14 +115,23 @@ void MIDIClockToCVInterface::step() { | |||||
| clock2ratio = int(clampf(inputs[CLOCK2_RATIO].value, 0.0, 10.0) * (numratios - 1) / 10); | clock2ratio = int(clampf(inputs[CLOCK2_RATIO].value, 0.0, 10.0) * (numratios - 1) / 10); | ||||
| } | } | ||||
| if (reset) { | |||||
| resetPulse.trigger(trigger_length); | |||||
| reset = false; | |||||
| if (start) { | |||||
| start = false; | |||||
| running = true; | |||||
| startPulse.trigger(pulseTime); | |||||
| c_bar = 0; | c_bar = 0; | ||||
| clock1Pulse.time = 0.0; | |||||
| clock1Pulse.pulseTime = 0.0; | |||||
| clock2Pulse.time = 0.0; | |||||
| clock2Pulse.pulseTime = 0.0; | |||||
| } | |||||
| if (stop) { | |||||
| stop = false; | |||||
| running = false; | |||||
| stopPulse.trigger(pulseTime); | |||||
| } | |||||
| if (cont) { | |||||
| cont = false; | |||||
| running = true; | |||||
| continuePulse.trigger(pulseTime); | |||||
| } | } | ||||
| if (tick) { | if (tick) { | ||||
| @@ -125,11 +144,11 @@ void MIDIClockToCVInterface::step() { | |||||
| */ | */ | ||||
| if (running) { | if (running) { | ||||
| if (c_bar % ratios[clock1ratio] == 0) { | if (c_bar % ratios[clock1ratio] == 0) { | ||||
| clock1Pulse.trigger(trigger_length); | |||||
| clock1Pulse.trigger(pulseTime); | |||||
| } | } | ||||
| if (c_bar % ratios[clock2ratio] == 0) { | if (c_bar % ratios[clock2ratio] == 0) { | ||||
| clock2Pulse.trigger(trigger_length); | |||||
| clock2Pulse.trigger(pulseTime); | |||||
| } | } | ||||
| } | } | ||||
| @@ -141,31 +160,39 @@ void MIDIClockToCVInterface::step() { | |||||
| } | } | ||||
| } | } | ||||
| bool pulse = clock1Pulse.process(1.0 / sampleRate); | bool pulse = clock1Pulse.process(1.0 / sampleRate); | ||||
| outputs[CLOCK1_PULSE].value = pulse ? 10.0 : 0.0; | outputs[CLOCK1_PULSE].value = pulse ? 10.0 : 0.0; | ||||
| pulse = clock2Pulse.process(1.0 / sampleRate); | pulse = clock2Pulse.process(1.0 / sampleRate); | ||||
| outputs[CLOCK2_PULSE].value = pulse ? 10.0 : 0.0; | outputs[CLOCK2_PULSE].value = pulse ? 10.0 : 0.0; | ||||
| pulse = resetPulse.process(1.0 / sampleRate); | |||||
| outputs[RESET_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() { | void MIDIClockToCVInterface::resetMidi() { | ||||
| outputs[CLOCK1_PULSE].value = 0.0; | outputs[CLOCK1_PULSE].value = 0.0; | ||||
| outputs[CLOCK2_PULSE].value = 0.0; | |||||
| } | } | ||||
| void MIDIClockToCVInterface::processMidi(std::vector<unsigned char> msg) { | void MIDIClockToCVInterface::processMidi(std::vector<unsigned char> msg) { | ||||
| switch (msg[0]) { | switch (msg[0]) { | ||||
| case 0xfa: | case 0xfa: | ||||
| reset = true; | |||||
| running = true; | |||||
| start = true; | |||||
| break; | |||||
| case 0xfb: | |||||
| cont = true; | |||||
| break; | break; | ||||
| case 0xfc: | case 0xfc: | ||||
| running = false; | |||||
| stop = true; | |||||
| break; | break; | ||||
| case 0xf8: | case 0xf8: | ||||
| tick = true; | tick = true; | ||||
| @@ -243,7 +270,7 @@ MIDIClockToCVWidget::MIDIClockToCVWidget() { | |||||
| label->box.pos = Vec(box.size.x - margin - 7 * 15, margin); | label->box.pos = Vec(box.size.x - margin - 7 * 15, margin); | ||||
| label->text = "MIDI Clk-CV"; | label->text = "MIDI Clk-CV"; | ||||
| addChild(label); | addChild(label); | ||||
| yPos = labelHeight * 2; | |||||
| yPos = labelHeight*2; | |||||
| } | } | ||||
| { | { | ||||
| @@ -258,26 +285,46 @@ MIDIClockToCVWidget::MIDIClockToCVWidget() { | |||||
| midiChoice->box.pos = Vec(margin, yPos); | midiChoice->box.pos = Vec(margin, yPos); | ||||
| midiChoice->box.size.x = box.size.x - 10; | midiChoice->box.size.x = box.size.x - 10; | ||||
| addChild(midiChoice); | addChild(midiChoice); | ||||
| yPos += midiChoice->box.size.y + margin * 6; | |||||
| yPos += midiChoice->box.size.y + margin * 4; | |||||
| } | } | ||||
| { | |||||
| Label *label = new Label(); | |||||
| label->box.pos = Vec(margin, yPos); | |||||
| label->text = "Start"; | |||||
| addChild(label); | |||||
| addOutput(createOutput<PJ3410Port>(Vec(15 * 6, yPos - 5), module, MIDIClockToCVInterface::START_PULSE)); | |||||
| yPos += labelHeight + margin * 4; | |||||
| } | |||||
| { | { | ||||
| Label *label = new Label(); | Label *label = new Label(); | ||||
| label->box.pos = Vec(margin, yPos); | label->box.pos = Vec(margin, yPos); | ||||
| label->text = "C1 Ratio"; | |||||
| label->text = "Stop"; | |||||
| addChild(label); | addChild(label); | ||||
| addOutput(createOutput<PJ3410Port>(Vec(15 * 6, yPos - 5), module, MIDIClockToCVInterface::STOP_PULSE)); | |||||
| yPos += labelHeight + margin * 4; | |||||
| } | |||||
| addInput(createInput<PJ3410Port>(Vec(15 * 6, yPos - 5), module, MIDIClockToCVInterface::CLOCK1_RATIO)); | |||||
| { | |||||
| Label *label = new Label(); | |||||
| label->box.pos = Vec(margin, yPos); | |||||
| label->text = "Continue"; | |||||
| addChild(label); | |||||
| addOutput(createOutput<PJ3410Port>(Vec(15 * 6, yPos - 5), module, MIDIClockToCVInterface::CONTINUE_PULSE)); | |||||
| yPos += labelHeight + margin * 6; | |||||
| } | |||||
| yPos += margin * 6; | |||||
| { | |||||
| addInput(createInput<PJ3410Port>(Vec(margin, yPos - 5), module, MIDIClockToCVInterface::CLOCK1_RATIO)); | |||||
| ClockRatioChoice *ratioChoice = new ClockRatioChoice(); | ClockRatioChoice *ratioChoice = new ClockRatioChoice(); | ||||
| ratioChoice->clockRatio = &module->clock1ratio; | ratioChoice->clockRatio = &module->clock1ratio; | ||||
| ratioChoice->box.pos = Vec(margin, yPos); | |||||
| ratioChoice->box.size.x = box.size.x - 10; | |||||
| ratioChoice->box.pos = Vec(int(box.size.x/3), yPos); | |||||
| ratioChoice->box.size.x = int(box.size.x/1.5 - margin); | |||||
| addChild(ratioChoice); | addChild(ratioChoice); | ||||
| yPos += ratioChoice->box.size.y + margin * 2; | |||||
| yPos += ratioChoice->box.size.y + margin * 3; | |||||
| } | } | ||||
| @@ -293,22 +340,15 @@ MIDIClockToCVWidget::MIDIClockToCVWidget() { | |||||
| { | { | ||||
| 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; | |||||
| addInput(createInput<PJ3410Port>(Vec(margin, yPos - 5), module, MIDIClockToCVInterface::CLOCK2_RATIO)); | |||||
| ClockRatioChoice *ratioChoice = new ClockRatioChoice(); | ClockRatioChoice *ratioChoice = new ClockRatioChoice(); | ||||
| ratioChoice->clockRatio = &module->clock2ratio; | 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; | |||||
| 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; | |||||
| } | } | ||||
| { | { | ||||
| @@ -318,16 +358,10 @@ MIDIClockToCVWidget::MIDIClockToCVWidget() { | |||||
| addChild(label); | addChild(label); | ||||
| addOutput(createOutput<PJ3410Port>(Vec(15 * 6, yPos - 5), module, MIDIClockToCVInterface::CLOCK2_PULSE)); | addOutput(createOutput<PJ3410Port>(Vec(15 * 6, yPos - 5), module, MIDIClockToCVInterface::CLOCK2_PULSE)); | ||||
| yPos += labelHeight + margin * 7; | |||||
| yPos += labelHeight + margin * 3; | |||||
| } | } | ||||
| { | |||||
| 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() { | void MIDIClockToCVWidget::step() { | ||||
| @@ -49,15 +49,28 @@ void MidiIO::baseFromJson(json_t *rootJ) { | |||||
| } | } | ||||
| std::vector<std::string> MidiIO::getDevices() { | 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 = {}; | std::vector<std::string> names = {}; | ||||
| if (isOut) { | |||||
| // TODO | |||||
| return names; | |||||
| } | |||||
| RtMidiIn *m; | |||||
| try { | |||||
| m = new RtMidiIn(); | |||||
| } catch (RtMidiError &error) { | |||||
| fprintf(stderr, "Failed to create RtMidiIn: %s\n", error.getMessage().c_str()); | |||||
| return names; | |||||
| } | |||||
| for (unsigned int i = 0; i < m->getPortCount(); i++) { | for (unsigned int i = 0; i < m->getPortCount(); i++) { | ||||
| names.push_back(m->getPortName(i)); | names.push_back(m->getPortName(i)); | ||||
| } | } | ||||
| if (!isPortOpen()) | |||||
| delete (m); | |||||
| return names; | return names; | ||||
| } | } | ||||
| @@ -81,6 +94,13 @@ void MidiIO::openDevice(std::string deviceName) { | |||||
| break; | break; | ||||
| } | } | ||||
| } | } | ||||
| if (!mw->isPortOpen()) { | |||||
| fprintf(stderr, "Failed to create RtMidiIn: No such device %s\n", deviceName.c_str()); | |||||
| this->deviceName = ""; | |||||
| this->id = -1; | |||||
| return; | |||||
| } | |||||
| } | } | ||||
| catch (RtMidiError &error) { | catch (RtMidiError &error) { | ||||
| fprintf(stderr, "Failed to create RtMidiIn: %s\n", error.getMessage().c_str()); | fprintf(stderr, "Failed to create RtMidiIn: %s\n", error.getMessage().c_str()); | ||||
| @@ -99,17 +119,17 @@ void MidiIO::openDevice(std::string deviceName) { | |||||
| void MidiIO::setIgnores(bool ignoreSysex, bool ignoreTime, bool ignoreSense) { | void MidiIO::setIgnores(bool ignoreSysex, bool ignoreTime, bool ignoreSense) { | ||||
| bool sy = true, ti = true, se = true; | 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; | |||||
| midiInMap[deviceName]->ignoresMap[id].midiSysex = ignoreSysex; | |||||
| midiInMap[deviceName]->ignoresMap[id].midiTime = ignoreTime; | |||||
| midiInMap[deviceName]->ignoresMap[id].midiSense = ignoreSense; | |||||
| for (auto kv : midiInMap[deviceName]->ignoresMap) { | for (auto kv : midiInMap[deviceName]->ignoresMap) { | ||||
| sy = sy && kv.second[0]; | |||||
| ti = ti && kv.second[1]; | |||||
| se = se && kv.second[2]; | |||||
| sy = sy && kv.second.midiSysex; | |||||
| ti = ti && kv.second.midiTime; | |||||
| se = se && kv.second.midiSense; | |||||
| } | } | ||||
| midiInMap[deviceName]->ignoreTypes(se,ti,se); | |||||
| midiInMap[deviceName]->ignoreTypes(se, ti, se); | |||||
| } | } | ||||
| @@ -119,7 +139,7 @@ std::string MidiIO::getDeviceName() { | |||||
| } | } | ||||
| double MidiIO::getMessage(std::vector<unsigned char> *msg) { | double MidiIO::getMessage(std::vector<unsigned char> *msg) { | ||||
| std::vector<unsigned char> next_msg; | |||||
| MidiMessage next_msg = MidiMessage(); | |||||
| MidiInWrapper *mw = midiInMap[deviceName]; | MidiInWrapper *mw = midiInMap[deviceName]; | ||||
| @@ -128,24 +148,23 @@ double MidiIO::getMessage(std::vector<unsigned char> *msg) { | |||||
| return 0; | return 0; | ||||
| } | } | ||||
| double stamp = midiInMap[deviceName]->getMessage(&next_msg); | |||||
| next_msg.timeStamp = mw->getMessage(&next_msg.bytes); | |||||
| if (next_msg.bytes.size() > 0) { | |||||
| for (auto &kv : mw->idMessagesMap) { | |||||
| 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); | |||||
| kv.second.push_back(next_msg); | |||||
| } | } | ||||
| } | } | ||||
| if (mw->idMessagesMap[id].size() <= 0) { | |||||
| *msg = next_msg; | |||||
| return stamp; | |||||
| if (mw->idMessagesMap[id].size() > 0) { | |||||
| next_msg = mw->idMessagesMap[id].front(); | |||||
| mw->idMessagesMap[id].pop_front(); | |||||
| } | } | ||||
| *msg = mw->idMessagesMap[id].front(); | |||||
| stamp = mw->idStampsMap[id].front(); | |||||
| mw->idMessagesMap[id].pop_front(); | |||||
| return stamp; | |||||
| *msg = next_msg.bytes; | |||||
| return next_msg.timeStamp; | |||||
| } | } | ||||
| bool MidiIO::isPortOpen() { | bool MidiIO::isPortOpen() { | ||||
| @@ -165,9 +184,10 @@ void MidiIO::close() { | |||||
| mw->erase(id); | mw->erase(id); | ||||
| if (mw->subscribers == 0) { | |||||
| if (mw->idMessagesMap.size() == 0) { | |||||
| mw->closePort(); | mw->closePort(); | ||||
| midiInMap.erase(deviceName); | midiInMap.erase(deviceName); | ||||
| delete (mw); | |||||
| } | } | ||||
| id = -1; | id = -1; | ||||
| @@ -238,4 +258,4 @@ void ChannelChoice::onAction(EventAction &e) { | |||||
| void ChannelChoice::step() { | void ChannelChoice::step() { | ||||
| text = (midiModule->channel >= 0) ? stringf("%d", midiModule->channel + 1) : "All"; | text = (midiModule->channel >= 0) ? stringf("%d", midiModule->channel + 1) : "All"; | ||||
| } | |||||
| } | |||||
| @@ -5,50 +5,53 @@ | |||||
| using namespace rack; | using namespace rack; | ||||
| struct IgnoreFlags { | |||||
| bool midiSysex = true; | |||||
| bool midiTime = true; | |||||
| bool midiSense = true; | |||||
| }; | |||||
| struct MidiMessage { | |||||
| std::vector<unsigned char> bytes; | |||||
| double timeStamp; | |||||
| MidiMessage() : bytes(0), timeStamp(0.0) {}; | |||||
| }; | |||||
| /** | /** | ||||
| * This class allows to use one instance of rtMidiIn with | * This class allows to use one instance of rtMidiIn with | ||||
| * multiple modules. A MidiIn port will be opened only once while multiple | * multiple modules. A MidiIn port will be opened only once while multiple | ||||
| * instances can use it simultaniously, each receiving all its incoming messages. | * instances can use it simultaniously, each receiving all its incoming messages. | ||||
| */ | */ | ||||
| struct MidiInWrapper : RtMidiIn { | 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; | |||||
| std::unordered_map<int, std::list<MidiMessage>> idMessagesMap; | |||||
| std::unordered_map<int, IgnoreFlags> ignoresMap; | |||||
| int uid_c = 0; | int uid_c = 0; | ||||
| int subscribers = 0; | |||||
| MidiInWrapper() : RtMidiIn() { | MidiInWrapper() : RtMidiIn() { | ||||
| idMessagesMap = {}; | idMessagesMap = {}; | ||||
| idStampsMap = {}; | |||||
| }; | }; | ||||
| int add() { | int add() { | ||||
| int id = ++uid_c; | int id = ++uid_c; | ||||
| subscribers++; | |||||
| idMessagesMap[id] = {}; | idMessagesMap[id] = {}; | ||||
| idStampsMap[id] = {}; | |||||
| ignoresMap[id][0] = true; | |||||
| ignoresMap[id][1] = true; | |||||
| ignoresMap[id][2] = true; | |||||
| ignoresMap[id] = IgnoreFlags(); | |||||
| return id; | return id; | ||||
| } | } | ||||
| void erase(int id) { | void erase(int id) { | ||||
| subscribers--; | |||||
| idMessagesMap.erase(id); | idMessagesMap.erase(id); | ||||
| idStampsMap.erase(id); | |||||
| ignoresMap.erase(id); | ignoresMap.erase(id); | ||||
| } | } | ||||
| }; | }; | ||||
| /** | |||||
| * Note: MidiIO is not thread safe which might become | |||||
| * important in the future | |||||
| */ | |||||
| struct MidiIO { | struct MidiIO { | ||||
| private: | private: | ||||
| static std::unordered_map<std::string, MidiInWrapper *> midiInMap; | static std::unordered_map<std::string, MidiInWrapper *> midiInMap; | ||||
| @@ -90,10 +93,80 @@ public: | |||||
| /* called when midi port is set */ | /* called when midi port is set */ | ||||
| virtual void resetMidi() {} | virtual void resetMidi() {} | ||||
| /* called if a user switches or sets the deivce (and after this device is initialised)*/ | |||||
| /* called if a user switches or sets the device (and after this device is initialised)*/ | |||||
| virtual void onDeviceChange() {} | virtual void onDeviceChange() {} | ||||
| }; | }; | ||||
| struct TransitionSmoother { | |||||
| enum TransitionFunction { | |||||
| SMOOTHSTEP, | |||||
| EXP, | |||||
| LIN, | |||||
| }; | |||||
| enum TransitionMode { | |||||
| DELTA, | |||||
| CONST, | |||||
| }; | |||||
| float start; | |||||
| float end; | |||||
| float x; | |||||
| float delta; | |||||
| float step; | |||||
| TransitionFunction t; | |||||
| void set(float start, float end, int l = 1500, TransitionFunction t = LIN, TransitionMode m = DELTA, bool reset = true) { | |||||
| this->start = start; | |||||
| this->end = end; | |||||
| this->delta = end - start; | |||||
| this->t = t; | |||||
| if (reset || x >= 1) { | |||||
| this->x = 0; | |||||
| } | |||||
| switch (m) { | |||||
| case DELTA: | |||||
| /* If the change is smaller, the transition phase is longer */ | |||||
| this->step = delta > 0 ? delta/l : -delta/l; | |||||
| break; | |||||
| case CONST: | |||||
| this->step = 1.0/l; | |||||
| break; | |||||
| } | |||||
| } | |||||
| float next() { | |||||
| float next = start; | |||||
| x += step; | |||||
| if (x >= 1) | |||||
| return end; | |||||
| switch (t) { | |||||
| case SMOOTHSTEP: | |||||
| next += delta*x*x*(3-2*x); | |||||
| break; | |||||
| case EXP: | |||||
| next += delta*x*x; | |||||
| break; | |||||
| case LIN: | |||||
| next += delta*x; | |||||
| break; | |||||
| } | |||||
| if ((delta > 0 && next > end) || (delta <= 0 && next < end)) | |||||
| return end; | |||||
| return next;; | |||||
| } | |||||
| }; | |||||
| ////////////////////// | ////////////////////// | ||||
| // MIDI module widgets | // MIDI module widgets | ||||
| ////////////////////// | ////////////////////// | ||||
| @@ -9,6 +9,12 @@ | |||||
| * MIDIToCVInterface converts midi note on/off events, velocity , channel aftertouch, pitch wheel and mod wheel to | * MIDIToCVInterface converts midi note on/off events, velocity , channel aftertouch, pitch wheel and mod wheel to | ||||
| * CV | * CV | ||||
| */ | */ | ||||
| struct MidiValue { | |||||
| int val = 0; // Controller value | |||||
| TransitionSmoother tSmooth; | |||||
| bool changed = false; // Value has been changed by midi message (only if it is in sync!) | |||||
| }; | |||||
| struct MIDIToCVInterface : MidiIO, Module { | struct MIDIToCVInterface : MidiIO, Module { | ||||
| enum ParamIds { | enum ParamIds { | ||||
| RESET_PARAM, | RESET_PARAM, | ||||
| @@ -34,17 +40,17 @@ struct MIDIToCVInterface : MidiIO, Module { | |||||
| std::list<int> notes; | std::list<int> notes; | ||||
| bool pedal = false; | bool pedal = false; | ||||
| int note = 60; // C4, most modules should use 261.626 Hz | int note = 60; // C4, most modules should use 261.626 Hz | ||||
| int mod = 0; | |||||
| int vel = 0; | int vel = 0; | ||||
| int afterTouch = 0; | |||||
| int pitchWheel = 64; | |||||
| bool retrigger = false; | |||||
| bool retriggered = false; | |||||
| MidiValue mod; | |||||
| MidiValue afterTouch; | |||||
| MidiValue pitchWheel; | |||||
| bool gate = false; | |||||
| SchmittTrigger resetTrigger; | SchmittTrigger resetTrigger; | ||||
| MIDIToCVInterface() : MidiIO(), Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) { | MIDIToCVInterface() : MidiIO(), Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) { | ||||
| pitchWheel.val = 64; | |||||
| pitchWheel.tSmooth.set(0, 0); | |||||
| } | } | ||||
| ~MIDIToCVInterface() { | ~MIDIToCVInterface() { | ||||
| @@ -77,11 +83,14 @@ struct MIDIToCVInterface : MidiIO, Module { | |||||
| }; | }; | ||||
| void MIDIToCVInterface::resetMidi() { | void MIDIToCVInterface::resetMidi() { | ||||
| mod = 0; | |||||
| pitchWheel = 64; | |||||
| afterTouch = 0; | |||||
| mod.val = 0; | |||||
| mod.tSmooth.set(0, 0); | |||||
| pitchWheel.val = 64; | |||||
| pitchWheel.tSmooth.set(0, 0); | |||||
| afterTouch.val = 0; | |||||
| afterTouch.tSmooth.set(0, 0); | |||||
| vel = 0; | vel = 0; | ||||
| outputs[GATE_OUTPUT].value = 0.0; | |||||
| gate = false; | |||||
| notes.clear(); | notes.clear(); | ||||
| } | } | ||||
| @@ -99,11 +108,6 @@ void MIDIToCVInterface::step() { | |||||
| outputs[PITCH_OUTPUT].value = ((note - 60)) / 12.0; | 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)) { | if (resetTrigger.process(params[RESET_PARAM].value)) { | ||||
| resetMidi(); | resetMidi(); | ||||
| return; | return; | ||||
| @@ -112,11 +116,27 @@ void MIDIToCVInterface::step() { | |||||
| lights[RESET_LIGHT].value -= lights[RESET_LIGHT].value / 0.55 / engineGetSampleRate(); // fade out light | lights[RESET_LIGHT].value -= lights[RESET_LIGHT].value / 0.55 / engineGetSampleRate(); // fade out light | ||||
| outputs[GATE_OUTPUT].value = gate ? 10.0 : 0.0; | 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; | outputs[VELOCITY_OUTPUT].value = vel / 127.0 * 10.0; | ||||
| int steps = int(engineGetSampleRate() / 32); | |||||
| if (mod.changed) { | |||||
| mod.tSmooth.set(outputs[MOD_OUTPUT].value, (mod.val / 127.0 * 10.0), steps); | |||||
| mod.changed = false; | |||||
| } | |||||
| outputs[MOD_OUTPUT].value = mod.tSmooth.next(); | |||||
| if (pitchWheel.changed) { | |||||
| pitchWheel.tSmooth.set(outputs[PITCHWHEEL_OUTPUT].value, (pitchWheel.val - 64) / 64.0 * 10.0, steps); | |||||
| pitchWheel.changed = false; | |||||
| } | |||||
| outputs[PITCHWHEEL_OUTPUT].value = pitchWheel.tSmooth.next(); | |||||
| /* NOTE: I'll leave out value smoothing for after touch for now. I currently don't | |||||
| * have an after touch capable device around and I assume it would require different | |||||
| * smoothing*/ | |||||
| outputs[CHANNEL_AFTERTOUCH_OUTPUT].value = afterTouch.val / 127.0 * 10.0; | |||||
| } | } | ||||
| void MIDIToCVInterface::pressNote(int note) { | void MIDIToCVInterface::pressNote(int note) { | ||||
| @@ -127,7 +147,7 @@ void MIDIToCVInterface::pressNote(int note) { | |||||
| // Push note | // Push note | ||||
| notes.push_back(note); | notes.push_back(note); | ||||
| this->note = note; | this->note = note; | ||||
| retriggered = true; | |||||
| gate = true; | |||||
| } | } | ||||
| void MIDIToCVInterface::releaseNote(int note) { | void MIDIToCVInterface::releaseNote(int note) { | ||||
| @@ -138,12 +158,15 @@ void MIDIToCVInterface::releaseNote(int note) { | |||||
| if (pedal) { | if (pedal) { | ||||
| // Don't release if pedal is held | // Don't release if pedal is held | ||||
| gate = true; | |||||
| } else if (!notes.empty()) { | } else if (!notes.empty()) { | ||||
| // Play previous note | // Play previous note | ||||
| auto it2 = notes.end(); | auto it2 = notes.end(); | ||||
| it2--; | it2--; | ||||
| this->note = *it2; | this->note = *it2; | ||||
| retriggered = true; | |||||
| gate = true; | |||||
| } else { | |||||
| gate = false; | |||||
| } | } | ||||
| } | } | ||||
| @@ -153,7 +176,7 @@ void MIDIToCVInterface::processMidi(std::vector<unsigned char> msg) { | |||||
| int data1 = msg[1]; | int data1 = msg[1]; | ||||
| int data2 = msg[2]; | int data2 = msg[2]; | ||||
| //fprintf(stderr, "channel %d status %d data1 %d data2 %d\n", channel, status, data1,data2); | |||||
| // fprintf(stderr, "channel %d status %d data1 %d data2 %d\n", channel, status, data1, data2); | |||||
| // Filter channels | // Filter channels | ||||
| if (this->channel >= 0 && this->channel != channel) | if (this->channel >= 0 && this->channel != channel) | ||||
| @@ -177,19 +200,24 @@ void MIDIToCVInterface::processMidi(std::vector<unsigned char> msg) { | |||||
| case 0xb: // cc | case 0xb: // cc | ||||
| switch (data1) { | switch (data1) { | ||||
| case 0x01: // mod | case 0x01: // mod | ||||
| this->mod = data2; | |||||
| mod.val = data2; | |||||
| mod.changed = true; | |||||
| break; | break; | ||||
| case 0x40: // sustain | case 0x40: // sustain | ||||
| pedal = (data2 >= 64); | pedal = (data2 >= 64); | ||||
| releaseNote(-1); | |||||
| if (!pedal) { | |||||
| releaseNote(-1); | |||||
| } | |||||
| break; | break; | ||||
| } | } | ||||
| break; | break; | ||||
| case 0xe: // pitch wheel | case 0xe: // pitch wheel | ||||
| this->pitchWheel = data2; | |||||
| pitchWheel.val = data2; | |||||
| pitchWheel.changed = true; | |||||
| break; | break; | ||||
| case 0xd: // channel aftertouch | case 0xd: // channel aftertouch | ||||
| this->afterTouch = data1; | |||||
| afterTouch.val = data1; | |||||
| afterTouch.changed = true; | |||||
| break; | break; | ||||
| } | } | ||||
| } | } | ||||
| @@ -225,7 +253,8 @@ MidiToCVWidget::MidiToCVWidget() { | |||||
| } | } | ||||
| addParam(createParam<LEDButton>(Vec(7 * 15, labelHeight), module, MIDIToCVInterface::RESET_PARAM, 0.0, 1.0, 0.0)); | addParam(createParam<LEDButton>(Vec(7 * 15, labelHeight), module, MIDIToCVInterface::RESET_PARAM, 0.0, 1.0, 0.0)); | ||||
| addChild(createLight<SmallLight<RedLight>>(Vec(7 * 15 + 5, labelHeight + 5), module, MIDIToCVInterface::RESET_LIGHT)); | |||||
| addChild(createLight<SmallLight<RedLight>>(Vec(7 * 15 + 5, labelHeight + 5), module, | |||||
| MIDIToCVInterface::RESET_LIGHT)); | |||||
| { | { | ||||
| Label *label = new Label(); | Label *label = new Label(); | ||||
| label->box.pos = Vec(margin, yPos); | label->box.pos = Vec(margin, yPos); | ||||
| @@ -7,10 +7,13 @@ | |||||
| using namespace rack; | using namespace rack; | ||||
| /* | |||||
| * MIDIToCVInterface converts midi note on/off events, velocity , channel aftertouch, pitch wheel and mod weel to | |||||
| * CV | |||||
| */ | |||||
| struct TriggerValue { | |||||
| int val = 0; | |||||
| int num; | |||||
| bool numInited = false; | |||||
| bool onFocus = false; | |||||
| }; | |||||
| struct MIDITriggerToCVInterface : MidiIO, Module { | struct MIDITriggerToCVInterface : MidiIO, Module { | ||||
| enum ParamIds { | enum ParamIds { | ||||
| NUM_PARAMS | NUM_PARAMS | ||||
| @@ -22,16 +25,11 @@ struct MIDITriggerToCVInterface : MidiIO, Module { | |||||
| NUM_OUTPUTS = 16 | NUM_OUTPUTS = 16 | ||||
| }; | }; | ||||
| int trigger[NUM_OUTPUTS]; | |||||
| int triggerNum[NUM_OUTPUTS]; | |||||
| bool triggerNumInited[NUM_OUTPUTS]; | |||||
| bool onFocus[NUM_OUTPUTS]; | |||||
| TriggerValue trigger[NUM_OUTPUTS]; | |||||
| MIDITriggerToCVInterface() : MidiIO(), Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS) { | MIDITriggerToCVInterface() : MidiIO(), Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS) { | ||||
| for (int i = 0; i < NUM_OUTPUTS; i++) { | for (int i = 0; i < NUM_OUTPUTS; i++) { | ||||
| trigger[i] = 0; | |||||
| triggerNum[i] = i; | |||||
| onFocus[i] = false; | |||||
| trigger[i].num = i; | |||||
| } | } | ||||
| } | } | ||||
| @@ -48,7 +46,7 @@ struct MIDITriggerToCVInterface : MidiIO, Module { | |||||
| json_t *rootJ = json_object(); | json_t *rootJ = json_object(); | ||||
| addBaseJson(rootJ); | addBaseJson(rootJ); | ||||
| for (int i = 0; i < NUM_OUTPUTS; i++) { | for (int i = 0; i < NUM_OUTPUTS; i++) { | ||||
| json_object_set_new(rootJ, std::to_string(i).c_str(), json_integer(triggerNum[i])); | |||||
| json_object_set_new(rootJ, std::to_string(i).c_str(), json_integer(trigger[i].num)); | |||||
| } | } | ||||
| return rootJ; | return rootJ; | ||||
| } | } | ||||
| @@ -58,8 +56,8 @@ struct MIDITriggerToCVInterface : MidiIO, Module { | |||||
| for (int i = 0; i < NUM_OUTPUTS; i++) { | for (int i = 0; i < NUM_OUTPUTS; i++) { | ||||
| json_t *ccNumJ = json_object_get(rootJ, std::to_string(i).c_str()); | json_t *ccNumJ = json_object_get(rootJ, std::to_string(i).c_str()); | ||||
| if (ccNumJ) { | if (ccNumJ) { | ||||
| triggerNum[i] = json_integer_value(ccNumJ); | |||||
| triggerNumInited[i] = true; | |||||
| trigger[i].num = json_integer_value(ccNumJ); | |||||
| trigger[i].numInited = true; | |||||
| } | } | ||||
| } | } | ||||
| @@ -68,7 +66,6 @@ struct MIDITriggerToCVInterface : MidiIO, Module { | |||||
| void reset() override { | void reset() override { | ||||
| resetMidi(); | resetMidi(); | ||||
| } | } | ||||
| }; | }; | ||||
| @@ -88,13 +85,13 @@ void MIDITriggerToCVInterface::step() { | |||||
| // Note: Could have an option to select between gate and velocity | // Note: Could have an option to select between gate and velocity | ||||
| // but trigger seams more useful | // but trigger seams more useful | ||||
| // outputs[i].value = trigger[i] / 127.0 * 10; | // outputs[i].value = trigger[i] / 127.0 * 10; | ||||
| outputs[i].value = trigger[i] > 0 ? 10.0 : 0.0; | |||||
| outputs[i].value = trigger[i].val > 0 ? 10.0 : 0.0; | |||||
| } | } | ||||
| } | } | ||||
| void MIDITriggerToCVInterface::resetMidi() { | void MIDITriggerToCVInterface::resetMidi() { | ||||
| for (int i = 0; i < NUM_OUTPUTS; i++) { | for (int i = 0; i < NUM_OUTPUTS; i++) { | ||||
| trigger[i] = 0; | |||||
| trigger[i].val = 0; | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -112,8 +109,8 @@ void MIDITriggerToCVInterface::processMidi(std::vector<unsigned char> msg) { | |||||
| if (status == 0x8) { // note off | if (status == 0x8) { // note off | ||||
| for (int i = 0; i < NUM_OUTPUTS; i++) { | for (int i = 0; i < NUM_OUTPUTS; i++) { | ||||
| if (data1 == triggerNum[i]) { | |||||
| trigger[i] = data2; | |||||
| if (data1 == trigger[i].num) { | |||||
| trigger[i].val = data2; | |||||
| } | } | ||||
| } | } | ||||
| return; | return; | ||||
| @@ -121,14 +118,12 @@ void MIDITriggerToCVInterface::processMidi(std::vector<unsigned char> msg) { | |||||
| if (status == 0x9) { // note on | if (status == 0x9) { // note on | ||||
| for (int i = 0; i < NUM_OUTPUTS; i++) { | for (int i = 0; i < NUM_OUTPUTS; i++) { | ||||
| if (onFocus[i]) { | |||||
| this->triggerNum[i] = data1; | |||||
| if (trigger[i].onFocus && data2 > 0) { | |||||
| trigger[i].num = data1; | |||||
| } | } | ||||
| } | |||||
| for (int i = 0; i < NUM_OUTPUTS; i++) { | |||||
| if (data1 == triggerNum[i]) { | |||||
| trigger[i] = data2; | |||||
| if (data1 == trigger[i].num) { | |||||
| trigger[i].val = data2; | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -146,22 +141,20 @@ struct TriggerTextField : TextField { | |||||
| void onMouseLeave(EventMouseLeave &e) override; | void onMouseLeave(EventMouseLeave &e) override; | ||||
| int *triggerNum; | |||||
| bool *inited; | |||||
| bool *onFocus; | |||||
| int outNum; | |||||
| MIDITriggerToCVInterface *module; | |||||
| }; | }; | ||||
| void TriggerTextField::draw(NVGcontext *vg) { | void TriggerTextField::draw(NVGcontext *vg) { | ||||
| /* This is necessary, since the save | /* This is necessary, since the save | ||||
| * file is loaded after constructing the widget*/ | * file is loaded after constructing the widget*/ | ||||
| if (*inited) { | |||||
| *inited = false; | |||||
| text = std::to_string(*triggerNum); | |||||
| if (module->trigger[outNum].numInited) { | |||||
| module->trigger[outNum].numInited = false; | |||||
| text = std::to_string(module->trigger[outNum].num); | |||||
| } | } | ||||
| if (*onFocus) { | |||||
| text = std::to_string(*triggerNum); | |||||
| if (module->trigger[outNum].onFocus) { | |||||
| text = std::to_string(module->trigger[outNum].num); | |||||
| } | } | ||||
| TextField::draw(vg); | TextField::draw(vg); | ||||
| @@ -170,37 +163,42 @@ void TriggerTextField::draw(NVGcontext *vg) { | |||||
| void TriggerTextField::onTextChange() { | void TriggerTextField::onTextChange() { | ||||
| if (text.size() > 0) { | if (text.size() > 0) { | ||||
| try { | try { | ||||
| *triggerNum = std::stoi(text); | |||||
| int num = std::stoi(text); | |||||
| // Only allow valid cc numbers | // Only allow valid cc numbers | ||||
| if (*triggerNum < 0 || *triggerNum > 127 || text.size() > 3) { | |||||
| if (num < 0 || num > 127 || text.size() > 3) { | |||||
| text = ""; | text = ""; | ||||
| begin = end = 0; | begin = end = 0; | ||||
| *triggerNum = -1; | |||||
| module->trigger[outNum].num = -1; | |||||
| }else { | |||||
| module->trigger[outNum].num = num; | |||||
| } | } | ||||
| } catch (...) { | } catch (...) { | ||||
| text = ""; | text = ""; | ||||
| begin = end = 0; | begin = end = 0; | ||||
| *triggerNum = -1; | |||||
| module->trigger[outNum].num = -1; | |||||
| } | } | ||||
| }; | }; | ||||
| } | } | ||||
| void TriggerTextField::onMouseDown(EventMouseDown &e) { | |||||
| void TriggerTextField::onMouseUp(EventMouseUp &e) { | |||||
| if (e.button == 1) { | if (e.button == 1) { | ||||
| *onFocus = true; | |||||
| module->trigger[outNum].onFocus = false; | |||||
| e.consumed = true; | |||||
| } | } | ||||
| e.consumed = true; | |||||
| TextField::onMouseUp(e); | |||||
| } | } | ||||
| void TriggerTextField::onMouseUp(EventMouseUp &e) { | |||||
| void TriggerTextField::onMouseDown(EventMouseDown &e) { | |||||
| if (e.button == 1) { | if (e.button == 1) { | ||||
| *onFocus = false; | |||||
| module->trigger[outNum].onFocus = true; | |||||
| e.consumed = true; | |||||
| } | } | ||||
| e.consumed = true; | |||||
| TextField::onMouseDown(e); | |||||
| } | } | ||||
| void TriggerTextField::onMouseLeave(EventMouseLeave &e) { | void TriggerTextField::onMouseLeave(EventMouseLeave &e) { | ||||
| *onFocus = false; | |||||
| module->trigger[outNum].onFocus = false; | |||||
| e.consumed = true; | |||||
| } | } | ||||
| MIDITriggerToCVWidget::MIDITriggerToCVWidget() { | MIDITriggerToCVWidget::MIDITriggerToCVWidget() { | ||||
| @@ -260,10 +258,9 @@ MIDITriggerToCVWidget::MIDITriggerToCVWidget() { | |||||
| for (int i = 0; i < MIDITriggerToCVInterface::NUM_OUTPUTS; i++) { | for (int i = 0; i < MIDITriggerToCVInterface::NUM_OUTPUTS; i++) { | ||||
| TriggerTextField *triggerNumChoice = new TriggerTextField(); | 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->module = module; | |||||
| triggerNumChoice->outNum = i; | |||||
| triggerNumChoice->text = std::to_string(module->trigger[i].num); | |||||
| triggerNumChoice->box.pos = Vec(11 + (i % 4) * (63), yPos); | triggerNumChoice->box.pos = Vec(11 + (i % 4) * (63), yPos); | ||||
| triggerNumChoice->box.size.x = 29; | triggerNumChoice->box.size.x = 29; | ||||
| @@ -9,7 +9,6 @@ struct MidiKey { | |||||
| int pitch = 60; | int pitch = 60; | ||||
| int at = 0; // aftertouch | int at = 0; // aftertouch | ||||
| int vel = 0; // velocity | int vel = 0; // velocity | ||||
| int retriggerC = 0; | |||||
| bool gate = false; | bool gate = false; | ||||
| }; | }; | ||||
| @@ -97,10 +96,9 @@ void QuadMIDIToCVInterface::resetMidi() { | |||||
| } | } | ||||
| void QuadMIDIToCVInterface::step() { | void QuadMIDIToCVInterface::step() { | ||||
| static int msgsProcessed = 0; | |||||
| if (isPortOpen()) { | if (isPortOpen()) { | ||||
| std::vector<unsigned char> message; | std::vector<unsigned char> message; | ||||
| int msgsProcessed = 0; | |||||
| // midiIn->getMessage returns empty vector if there are no messages in the queue | // midiIn->getMessage returns empty vector if there are no messages in the queue | ||||
| // NOTE: For the quadmidi we will process max 4 midi messages per step to avoid | // NOTE: For the quadmidi we will process max 4 midi messages per step to avoid | ||||
| @@ -111,7 +109,6 @@ void QuadMIDIToCVInterface::step() { | |||||
| getMessage(&message); | getMessage(&message); | ||||
| msgsProcessed++; | msgsProcessed++; | ||||
| } | } | ||||
| msgsProcessed = 0; | |||||
| } | } | ||||
| @@ -136,8 +133,7 @@ void QuadMIDIToCVInterface::processMidi(std::vector<unsigned char> msg) { | |||||
| int status = (msg[0] >> 4) & 0xf; | int status = (msg[0] >> 4) & 0xf; | ||||
| int data1 = msg[1]; | int data1 = msg[1]; | ||||
| int data2 = msg[2]; | int data2 = msg[2]; | ||||
| static int gate; | |||||
| bool gate; | |||||
| // Filter channels | // Filter channels | ||||
| if (this->channel >= 0 && this->channel != channel) | if (this->channel >= 0 && this->channel != channel) | ||||
| @@ -168,6 +164,7 @@ void QuadMIDIToCVInterface::processMidi(std::vector<unsigned char> msg) { | |||||
| if (data1 == 0x40) { // pedal | if (data1 == 0x40) { // pedal | ||||
| pedal = (data2 >= 64); | pedal = (data2 >= 64); | ||||
| if (!pedal) { | if (!pedal) { | ||||
| open.clear(); | |||||
| for (int i = 0; i < 4; i++) { | for (int i = 0; i < 4; i++) { | ||||
| activeKeys[i].gate = false; | activeKeys[i].gate = false; | ||||
| open.push_back(i); | open.push_back(i); | ||||
| @@ -179,7 +176,11 @@ void QuadMIDIToCVInterface::processMidi(std::vector<unsigned char> msg) { | |||||
| return; | return; | ||||
| } | } | ||||
| if (!pedal && !gate) { | |||||
| if (pedal && !gate) { | |||||
| return; | |||||
| } | |||||
| if (!gate) { | |||||
| for (int i = 0; i < 4; i++) { | for (int i = 0; i < 4; i++) { | ||||
| if (activeKeys[i].pitch == data1) { | if (activeKeys[i].pitch == data1) { | ||||
| activeKeys[i].gate = false; | activeKeys[i].gate = false; | ||||
| @@ -194,21 +195,20 @@ void QuadMIDIToCVInterface::processMidi(std::vector<unsigned char> msg) { | |||||
| } | } | ||||
| if (open.empty()) { | if (open.empty()) { | ||||
| open.clear(); | |||||
| for (int i = 0; i < 4; i++) { | for (int i = 0; i < 4; i++) { | ||||
| open.push_back(i); | open.push_back(i); | ||||
| } | } | ||||
| } | } | ||||
| if (!activeKeys[0].gate && !activeKeys[1].gate && | if (!activeKeys[0].gate && !activeKeys[1].gate && | ||||
| !activeKeys[2].gate && !activeKeys[3].gate) { | !activeKeys[2].gate && !activeKeys[3].gate) { | ||||
| open.sort(); | open.sort(); | ||||
| } | } | ||||
| switch (mode) { | switch (mode) { | ||||
| case RESET: | case RESET: | ||||
| if (open.size() == 4 ) { | |||||
| if (open.size() >= 4) { | |||||
| for (int i = 0; i < 4; i++) { | for (int i = 0; i < 4; i++) { | ||||
| activeKeys[i].gate = false; | activeKeys[i].gate = false; | ||||
| open.push_back(i); | open.push_back(i); | ||||
| @@ -220,17 +220,25 @@ void QuadMIDIToCVInterface::processMidi(std::vector<unsigned char> msg) { | |||||
| break; | break; | ||||
| case ROTATE: | case ROTATE: | ||||
| break; | break; | ||||
| default: | |||||
| fprintf(stderr, "No mode selected?!\n"); | |||||
| } | } | ||||
| activeKeys[open.front()].gate = true; | |||||
| activeKeys[open.front()].pitch = data1; | |||||
| activeKeys[open.front()].vel = data2; | |||||
| int next = open.front(); | |||||
| open.pop_front(); | open.pop_front(); | ||||
| return; | |||||
| for (int i = 0; i < 4; i++) { | |||||
| if (activeKeys[i].pitch == data1 && activeKeys[i].gate) { | |||||
| activeKeys[i].vel = data2; | |||||
| if (std::find(open.begin(), open.end(), i) != open.end()) | |||||
| open.remove(i); | |||||
| open.push_front(i); | |||||
| activeKeys[i].gate = false; | |||||
| } | |||||
| } | |||||
| activeKeys[next].gate = true; | |||||
| activeKeys[next].pitch = data1; | |||||
| activeKeys[next].vel = data2; | |||||
| } | } | ||||
| int QuadMIDIToCVInterface::getMode() const { | int QuadMIDIToCVInterface::getMode() const { | ||||