| @@ -1,5 +1,6 @@ | |||
| #include "Core.hpp" | |||
| #include "midi.hpp" | |||
| #include "dsp/digital.hpp" | |||
| #include <algorithm> | |||
| @@ -26,6 +27,9 @@ struct QuadMIDIToCVInterface : Module { | |||
| enum PolyMode { | |||
| 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, | |||
| REASSIGN_MODE, | |||
| UNISON_MODE, | |||
| @@ -39,13 +43,17 @@ struct QuadMIDIToCVInterface : Module { | |||
| }; | |||
| 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]; | |||
| bool gates[4]; | |||
| // gates set to TRUE by pedal and current gate. FALSE by pedal. | |||
| bool pedalgates[4]; | |||
| bool pedal; | |||
| 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(); | |||
| } | |||
| @@ -70,71 +78,107 @@ struct QuadMIDIToCVInterface : Module { | |||
| for (int i = 0; i < 4; i++) { | |||
| notes[i] = 60; | |||
| gates[i] = false; | |||
| pedalgates[i] = 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 | |||
| switch (polyMode) { | |||
| case ROTATE_MODE: { | |||
| rotateIndex = getPolyIndex(rotateIndex); | |||
| } 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; | |||
| case REASSIGN_MODE: { | |||
| cachedNotes.push_back(note); | |||
| rotateIndex = getPolyIndex(-1); | |||
| } break; | |||
| case UNISON_MODE: { | |||
| cachedNotes.push_back(note); | |||
| for (int i = 0; i < 4; i++) { | |||
| notes[i] = note; | |||
| gates[i] = true; | |||
| pedalgates[i] = pedal; | |||
| // reTrigger[i].trigger(1e-3); | |||
| } | |||
| return; | |||
| } 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) { | |||
| // 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: { | |||
| 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; | |||
| 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++) { | |||
| notes[i] = *it2; | |||
| notes[i] = backnote; | |||
| gates[i] = true; | |||
| } | |||
| } | |||
| @@ -145,18 +189,57 @@ struct QuadMIDIToCVInterface : Module { | |||
| } | |||
| } 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() { | |||
| pedal = true; | |||
| for (int i = 0; i < 4; i++) { | |||
| pedalgates[i] = gates[i]; | |||
| } | |||
| } | |||
| void releasePedal() { | |||
| 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 { | |||
| @@ -167,14 +250,19 @@ struct QuadMIDIToCVInterface : Module { | |||
| for (int i = 0; i < 4; i++) { | |||
| uint8_t lastNote = notes[i]; | |||
| uint8_t lastGate = (gates[i] || pedalgates[i]); | |||
| 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].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) { | |||
| // filter MIDI channel | |||
| if ((midiInput.channel > -1) && (midiInput.channel != msg.channel())) | |||
| return; | |||
| switch (msg.status()) { | |||
| // note off | |||
| case 0x8: { | |||
| @@ -262,7 +350,13 @@ struct QuadMIDIToCVInterfaceWidget : ModuleWidget { | |||
| menu->addChild(MenuEntry::create()); | |||
| 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++) { | |||
| PolyphonyItem *item = MenuItem::create<PolyphonyItem>(polyModeNames[i], CHECKMARK(module->polyMode == i)); | |||
| item->module = module; | |||