| @@ -1,5 +1,6 @@ | |||||
| #include "Core.hpp" | #include "Core.hpp" | ||||
| #include "midi.hpp" | #include "midi.hpp" | ||||
| #include "dsp/digital.hpp" | |||||
| #include <algorithm> | #include <algorithm> | ||||
| @@ -26,6 +27,9 @@ struct QuadMIDIToCVInterface : Module { | |||||
| enum PolyMode { | enum PolyMode { | ||||
| ROTATE_MODE, | ROTATE_MODE, | ||||
| // Added REUSE option that reuses a channel when receiving the same note. | |||||
| // Good when using sustain pedal so it doesn't "stack" unisons ... not sure this is the best name but it is descriptive... | |||||
| REUSE_MODE, | |||||
| RESET_MODE, | RESET_MODE, | ||||
| REASSIGN_MODE, | REASSIGN_MODE, | ||||
| UNISON_MODE, | UNISON_MODE, | ||||
| @@ -39,13 +43,17 @@ struct QuadMIDIToCVInterface : Module { | |||||
| }; | }; | ||||
| NoteData noteData[128]; | NoteData noteData[128]; | ||||
| std::vector<uint8_t> heldNotes; | |||||
| // cachedNotes : UNISON_MODE and REASSIGN_MODE cache all played notes. The other polyModes cache stolen notes (after the 4th one). | |||||
| std::vector<uint8_t> cachedNotes; | |||||
| uint8_t notes[4]; | uint8_t notes[4]; | ||||
| bool gates[4]; | bool gates[4]; | ||||
| // gates set to TRUE by pedal and current gate. FALSE by pedal. | |||||
| bool pedalgates[4]; | |||||
| bool pedal; | bool pedal; | ||||
| int rotateIndex; | int rotateIndex; | ||||
| int stealIndex; | |||||
| QuadMIDIToCVInterface() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS), heldNotes(128) { | |||||
| QuadMIDIToCVInterface() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS), cachedNotes(128) { | |||||
| onReset(); | onReset(); | ||||
| } | } | ||||
| @@ -70,71 +78,107 @@ struct QuadMIDIToCVInterface : Module { | |||||
| for (int i = 0; i < 4; i++) { | for (int i = 0; i < 4; i++) { | ||||
| notes[i] = 60; | notes[i] = 60; | ||||
| gates[i] = false; | gates[i] = false; | ||||
| pedalgates[i] = false; | |||||
| } | } | ||||
| pedal = false; | pedal = false; | ||||
| rotateIndex = 0; | |||||
| rotateIndex = -1; | |||||
| cachedNotes.clear(); | |||||
| } | } | ||||
| void pressNote(uint8_t note) { | |||||
| // Remove existing similar note | |||||
| auto it = std::find(heldNotes.begin(), heldNotes.end(), note); | |||||
| if (it != heldNotes.end()) | |||||
| heldNotes.erase(it); | |||||
| // Push note | |||||
| heldNotes.push_back(note); | |||||
| int getPolyIndex(int nowIndex) { | |||||
| for (int i = 0; i < 4; i++) { | |||||
| nowIndex++; | |||||
| if (nowIndex > 3) | |||||
| nowIndex = 0; | |||||
| if (!(gates[nowIndex] || pedalgates[nowIndex])) { | |||||
| stealIndex = nowIndex; | |||||
| return nowIndex; | |||||
| } | |||||
| } | |||||
| // All taken = steal (stealIndex always rotates) | |||||
| stealIndex++; | |||||
| if (stealIndex > 3) | |||||
| stealIndex = 0; | |||||
| if ((polyMode < REASSIGN_MODE) && (gates[stealIndex])) | |||||
| cachedNotes.push_back(notes[stealIndex]); | |||||
| return stealIndex; | |||||
| } | |||||
| void pressNote(uint8_t note) { | |||||
| // Set notes and gates | // Set notes and gates | ||||
| switch (polyMode) { | switch (polyMode) { | ||||
| case ROTATE_MODE: { | case ROTATE_MODE: { | ||||
| rotateIndex = getPolyIndex(rotateIndex); | |||||
| } break; | } break; | ||||
| case RESET_MODE: { | |||||
| case REUSE_MODE: { | |||||
| bool reuse = false; | |||||
| for (int i = 0; i < 4; i++) { | |||||
| if (notes[i] == note) { | |||||
| rotateIndex = i; | |||||
| reuse = true; | |||||
| break; | |||||
| } | |||||
| } | |||||
| if (!reuse) | |||||
| rotateIndex = getPolyIndex(rotateIndex); | |||||
| } break; | |||||
| case RESET_MODE: { | |||||
| rotateIndex = getPolyIndex(-1); | |||||
| } break; | } break; | ||||
| case REASSIGN_MODE: { | case REASSIGN_MODE: { | ||||
| cachedNotes.push_back(note); | |||||
| rotateIndex = getPolyIndex(-1); | |||||
| } break; | } break; | ||||
| case UNISON_MODE: { | case UNISON_MODE: { | ||||
| cachedNotes.push_back(note); | |||||
| for (int i = 0; i < 4; i++) { | for (int i = 0; i < 4; i++) { | ||||
| notes[i] = note; | notes[i] = note; | ||||
| gates[i] = true; | gates[i] = true; | ||||
| pedalgates[i] = pedal; | |||||
| // reTrigger[i].trigger(1e-3); | |||||
| } | } | ||||
| return; | |||||
| } break; | } break; | ||||
| default: break; | default: break; | ||||
| } | } | ||||
| // Set notes and gates | |||||
| // if (gates[rotateIndex] || pedalgates[rotateIndex]) | |||||
| // reTrigger[rotateIndex].trigger(1e-3); | |||||
| notes[rotateIndex] = note; | |||||
| gates[rotateIndex] = true; | |||||
| pedalgates[rotateIndex] = pedal; | |||||
| } | } | ||||
| void releaseNote(uint8_t note) { | void releaseNote(uint8_t note) { | ||||
| // Remove the note | // Remove the note | ||||
| auto it = std::find(heldNotes.begin(), heldNotes.end(), note); | |||||
| if (it != heldNotes.end()) | |||||
| heldNotes.erase(it); | |||||
| // Hold note if pedal is pressed | |||||
| if (pedal) | |||||
| return; | |||||
| // Set last note | |||||
| switch (polyMode) { | |||||
| case ROTATE_MODE: { | |||||
| } break; | |||||
| case RESET_MODE: { | |||||
| } break; | |||||
| auto it = std::find(cachedNotes.begin(), cachedNotes.end(), note); | |||||
| if (it != cachedNotes.end()) | |||||
| cachedNotes.erase(it); | |||||
| switch (polyMode) { | |||||
| case REASSIGN_MODE: { | case REASSIGN_MODE: { | ||||
| for (int i = 0; i < 4; i++) { | |||||
| if (i < (int) cachedNotes.size()) { | |||||
| if (!pedalgates[i]) | |||||
| notes[i] = cachedNotes[i]; | |||||
| pedalgates[i] = pedal; | |||||
| } | |||||
| else { | |||||
| gates[i] = false; | |||||
| } | |||||
| } | |||||
| } break; | } break; | ||||
| case UNISON_MODE: { | case UNISON_MODE: { | ||||
| if (!heldNotes.empty()) { | |||||
| auto it2 = heldNotes.end(); | |||||
| it2--; | |||||
| if (!cachedNotes.empty()) { | |||||
| uint8_t backnote = cachedNotes.back(); | |||||
| for (int i = 0; i < 4; i++) { | for (int i = 0; i < 4; i++) { | ||||
| notes[i] = *it2; | |||||
| notes[i] = backnote; | |||||
| gates[i] = true; | gates[i] = true; | ||||
| } | } | ||||
| } | } | ||||
| @@ -145,18 +189,57 @@ struct QuadMIDIToCVInterface : Module { | |||||
| } | } | ||||
| } break; | } break; | ||||
| default: break; | |||||
| // default ROTATE_MODE REUSE_MODE RESET_MODE | |||||
| default: { | |||||
| for (int i = 0; i < 4; i++) { | |||||
| if (notes[i] == note) { | |||||
| if (pedalgates[i]) { | |||||
| gates[i] = false; | |||||
| } | |||||
| else if (!cachedNotes.empty()) { | |||||
| notes[i] = cachedNotes.back(); | |||||
| cachedNotes.pop_back(); | |||||
| } | |||||
| else { | |||||
| gates[i] = false; | |||||
| } | |||||
| } | |||||
| } | |||||
| } break; | |||||
| } | } | ||||
| } | } | ||||
| void pressPedal() { | void pressPedal() { | ||||
| pedal = true; | pedal = true; | ||||
| for (int i = 0; i < 4; i++) { | |||||
| pedalgates[i] = gates[i]; | |||||
| } | |||||
| } | } | ||||
| void releasePedal() { | void releasePedal() { | ||||
| pedal = false; | pedal = false; | ||||
| releaseNote(255); | |||||
| // When pedal is off, recover notes for pressed keys (if any) after they were already being "cycled" out by pedal-sustained notes. | |||||
| for (int i = 0; i < 4; i++) { | |||||
| pedalgates[i] = false; | |||||
| if (!cachedNotes.empty()) { | |||||
| if (polyMode < REASSIGN_MODE) { | |||||
| notes[i] = cachedNotes.back(); | |||||
| cachedNotes.pop_back(); | |||||
| gates[i] = true; | |||||
| } | |||||
| } | |||||
| } | |||||
| if (polyMode == REASSIGN_MODE) { | |||||
| for (int i = 0; i < 4; i++) { | |||||
| if (i < (int) cachedNotes.size()) { | |||||
| notes[i] = cachedNotes[i]; | |||||
| gates[i] = true; | |||||
| } | |||||
| else { | |||||
| gates[i] = false; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | } | ||||
| void step() override { | void step() override { | ||||
| @@ -167,14 +250,19 @@ struct QuadMIDIToCVInterface : Module { | |||||
| for (int i = 0; i < 4; i++) { | for (int i = 0; i < 4; i++) { | ||||
| uint8_t lastNote = notes[i]; | uint8_t lastNote = notes[i]; | ||||
| uint8_t lastGate = (gates[i] || pedalgates[i]); | |||||
| outputs[CV_OUTPUT + i].value = (lastNote - 60) / 12.f; | outputs[CV_OUTPUT + i].value = (lastNote - 60) / 12.f; | ||||
| outputs[GATE_OUTPUT + i].value = gates[i] ? 10.f : 0.f; | |||||
| outputs[GATE_OUTPUT + i].value = lastGate ? 10.f : 0.f; | |||||
| outputs[VELOCITY_OUTPUT + i].value = rescale(noteData[lastNote].velocity, 0, 127, 0.f, 10.f); | outputs[VELOCITY_OUTPUT + i].value = rescale(noteData[lastNote].velocity, 0, 127, 0.f, 10.f); | ||||
| outputs[VELOCITY_OUTPUT + i].value = rescale(noteData[lastNote].aftertouch, 0, 127, 0.f, 10.f); | |||||
| outputs[AFTERTOUCH_OUTPUT + i].value = rescale(noteData[lastNote].aftertouch, 0, 127, 0.f, 10.f); | |||||
| } | } | ||||
| } | } | ||||
| void processMessage(MidiMessage msg) { | void processMessage(MidiMessage msg) { | ||||
| // filter MIDI channel | |||||
| if ((midiInput.channel > -1) && (midiInput.channel != msg.channel())) | |||||
| return; | |||||
| switch (msg.status()) { | switch (msg.status()) { | ||||
| // note off | // note off | ||||
| case 0x8: { | case 0x8: { | ||||
| @@ -262,7 +350,13 @@ struct QuadMIDIToCVInterfaceWidget : ModuleWidget { | |||||
| menu->addChild(MenuEntry::create()); | menu->addChild(MenuEntry::create()); | ||||
| menu->addChild(MenuLabel::create("Polyphony mode")); | menu->addChild(MenuLabel::create("Polyphony mode")); | ||||
| std::vector<std::string> polyModeNames = {"Rotate", "Reset", "Reassign", "Unison"}; | |||||
| std::vector<std::string> polyModeNames = { | |||||
| "Rotate", | |||||
| "Reuse", | |||||
| "Reset", | |||||
| "Reassign", | |||||
| "Unison" | |||||
| }; | |||||
| for (int i = 0; i < QuadMIDIToCVInterface::NUM_MODES; i++) { | for (int i = 0; i < QuadMIDIToCVInterface::NUM_MODES; i++) { | ||||
| PolyphonyItem *item = MenuItem::create<PolyphonyItem>(polyModeNames[i], CHECKMARK(module->polyMode == i)); | PolyphonyItem *item = MenuItem::create<PolyphonyItem>(polyModeNames[i], CHECKMARK(module->polyMode == i)); | ||||
| item->module = module; | item->module = module; | ||||