From 976b1116734d7a9f3c0eefc7bb24c5ab5abd7b6c Mon Sep 17 00:00:00 2001 From: Andrew Belt Date: Sat, 23 Aug 2025 03:28:19 -0400 Subject: [PATCH] MIDI to CV: Add monophonic modes: Last, First, Lowest, Highest. Add "Release retrigger" mode. --- include/dsp/midi.hpp | 174 ++++++++++++++++++++++++++++--------------- src/core/MIDI_CV.cpp | 61 ++++++++++----- 2 files changed, 154 insertions(+), 81 deletions(-) diff --git a/include/dsp/midi.hpp b/include/dsp/midi.hpp index 5400cca8..9005a970 100644 --- a/include/dsp/midi.hpp +++ b/include/dsp/midi.hpp @@ -1,4 +1,5 @@ #pragma once +#include #include #include #include @@ -254,6 +255,17 @@ struct MidiParser { /** Actual number of polyphonic channels */ uint8_t channels; + enum MonoMode { + LAST_PRIORITY_MODE, + FIRST_PRIORITY_MODE, + LOWEST_PRIORITY_MODE, + HIGHEST_PRIORITY_MODE, + NUM_MONO_MODES + }; + MonoMode monoMode; + + bool retriggerOnResume; + /** Method for assigning notes to polyphony channels */ enum PolyMode { ROTATE_MODE, @@ -308,6 +320,8 @@ struct MidiParser { smooth = true; channels = 1; polyMode = ROTATE_MODE; + monoMode = LAST_PRIORITY_MODE; + retriggerOnResume = false; pwRange = 2.f; clockDivision = 24; setFilterLambda(30.f); @@ -372,10 +386,9 @@ struct MidiParser { } break; // note on case 0x9: { - if (msg.getValue() > 0) { - uint8_t c = msg.getChannel(); - c = pressNote(msg.getNote(), c); - velocities[c] = msg.getValue(); + uint8_t velocity = msg.getValue(); + if (velocity > 0) { + pressNote(msg.getNote(), msg.getChannel(), velocity); } else { // Note-on event with velocity 0 is an alternative for note-off event. @@ -481,78 +494,87 @@ struct MidiParser { } uint8_t assignChannel(uint8_t note) { - if (channels == 1) + if (channels <= 1) return 0; - switch (polyMode) { - case REUSE_MODE: { - // Find channel with the same note - for (uint8_t c = 0; c < channels; c++) { - if (notes[c] == note) - return c; - } - } // fallthrough - - case ROTATE_MODE: { - // Find next available channel - for (uint8_t i = 0; i < channels; i++) { - rotateIndex++; - if (rotateIndex >= channels) - rotateIndex = 0; - if (!gates[rotateIndex]) - return rotateIndex; - } - // No notes are available. Advance rotateIndex once more. + if (polyMode == REUSE_MODE) { + // Try to find channel with the same note + for (uint8_t c = 0; c < channels; c++) { + if (notes[c] == note) + return c; + } + } + if (polyMode == REUSE_MODE || polyMode == ROTATE_MODE) { + // Find next available channel + for (uint8_t i = 0; i < channels; i++) { rotateIndex++; if (rotateIndex >= channels) rotateIndex = 0; - return rotateIndex; - } break; - - case RESET_MODE: { - for (uint8_t c = 0; c < channels; c++) { - if (!gates[c]) - return c; - } - return channels - 1; - } break; - - case MPE_MODE: { - // This case is handled by querying the MIDI message channel. - return 0; - } break; - - default: return 0; + if (!gates[rotateIndex]) + return rotateIndex; + } + // No notes are available. Advance rotateIndex once more. + rotateIndex++; + if (rotateIndex >= channels) + rotateIndex = 0; + return rotateIndex; + } + if (polyMode == RESET_MODE) { + for (uint8_t c = 0; c < channels; c++) { + if (!gates[c]) + return c; + } + return channels - 1; + } + if (polyMode == MPE_MODE) { + // This case is handled by querying the MIDI message channel. + return 0; } + return 0; } - /** Returns actual assigned channel */ - uint8_t pressNote(uint8_t note, uint8_t channel) { + void pressNote(uint8_t note, uint8_t channel, uint8_t velocity) { // Remove existing similar note - auto it = std::find(heldNotes.begin(), heldNotes.end(), note); - if (it != heldNotes.end()) - heldNotes.erase(it); - // Push note + heldNotes.erase(std::remove(heldNotes.begin(), heldNotes.end(), note), heldNotes.end()); + // Push note to end heldNotes.push_back(note); - // Determine actual channel - if (polyMode == MPE_MODE) { - // Channel is already decided for us + // Handle polyphony modes + if (channels > 1) { + if (polyMode == MPE_MODE) { + // Output channel equals MIDI channel + } + else { + channel = assignChannel(note); + } } + // Handle monophonic modes else { - channel = assignChannel(note); + channel = 0; + if (monoMode == FIRST_PRIORITY_MODE) { + if (heldNotes.size() > 1) + return; + } + if (monoMode == LOWEST_PRIORITY_MODE) { + uint8_t minNote = *std::min_element(heldNotes.begin(), heldNotes.end()); + if (note != minNote) + return; + } + if (monoMode == HIGHEST_PRIORITY_MODE) { + uint8_t maxNote = *std::max_element(heldNotes.begin(), heldNotes.end()); + if (note != maxNote) + return; + } } // Set note notes[channel] = note; gates[channel] = true; + velocities[channel] = velocity; retriggerPulses[channel].trigger(1e-3); - return channel; } void releaseNote(uint8_t note) { // Remove the note - auto it = std::find(heldNotes.begin(), heldNotes.end(), note); - if (it != heldNotes.end()) - heldNotes.erase(it); + heldNotes.erase(std::remove(heldNotes.begin(), heldNotes.end(), note), heldNotes.end()); // Hold note if pedal is pressed if (pedal) return; @@ -562,13 +584,24 @@ struct MidiParser { gates[c] = false; } } - // Set last note if monophonic - if (channels == 1) { - if (note == notes[0] && !heldNotes.empty()) { - uint8_t lastNote = heldNotes.back(); - notes[0] = lastNote; - gates[0] = true; - return; + // In all monophonic modes, set a previous note if the released note was the active note + if (channels == 1 && note == notes[0] && !heldNotes.empty()) { + if (monoMode == LAST_PRIORITY_MODE) { + notes[0] = heldNotes.back(); + } + if (monoMode == FIRST_PRIORITY_MODE) { + notes[0] = heldNotes.front(); + } + if (monoMode == LOWEST_PRIORITY_MODE) { + notes[0] = *std::min_element(heldNotes.begin(), heldNotes.end()); + } + if (monoMode == HIGHEST_PRIORITY_MODE) { + notes[0] = *std::max_element(heldNotes.begin(), heldNotes.end()); + } + gates[0] = true; + // TODO Set velocity + if (retriggerOnResume) { + retriggerPulses[0].trigger(1e-3); } } } @@ -624,6 +657,13 @@ struct MidiParser { panic(); } + void setMonoMode(MonoMode monoMode) { + if (monoMode == this->monoMode) + return; + this->monoMode = monoMode; + panic(); + } + void setPolyMode(PolyMode polyMode) { if (polyMode == this->polyMode) return; @@ -664,6 +704,8 @@ struct MidiParser { json_object_set_new(rootJ, "pwRange", json_real(pwRange)); json_object_set_new(rootJ, "smooth", json_boolean(smooth)); json_object_set_new(rootJ, "channels", json_integer(channels)); + json_object_set_new(rootJ, "monoMode", json_integer(monoMode)); + json_object_set_new(rootJ, "retriggerOnResume", json_boolean(retriggerOnResume)); json_object_set_new(rootJ, "polyMode", json_integer(polyMode)); json_object_set_new(rootJ, "clockDivision", json_integer(clockDivision)); // Saving/restoring pitch and mod doesn't make much sense for MPE. @@ -689,6 +731,14 @@ struct MidiParser { if (channelsJ) setChannels(json_integer_value(channelsJ)); + json_t* monoModeJ = json_object_get(rootJ, "monoMode"); + if (monoModeJ) + monoMode = (MonoMode) json_integer_value(monoModeJ); + + json_t* retriggerOnResumeJ = json_object_get(rootJ, "retriggerOnResume"); + if (retriggerOnResumeJ) + retriggerOnResume = json_boolean_value(retriggerOnResumeJ); + json_t* polyModeJ = json_object_get(rootJ, "polyMode"); if (polyModeJ) polyMode = (PolyMode) json_integer_value(polyModeJ); diff --git a/src/core/MIDI_CV.cpp b/src/core/MIDI_CV.cpp index f4def99b..5f955942 100644 --- a/src/core/MIDI_CV.cpp +++ b/src/core/MIDI_CV.cpp @@ -156,14 +156,52 @@ struct MIDI_CVWidget : ModuleWidget { menu->addChild(new MenuSeparator); + menu->addChild(createSubmenuItem("Polyphony channels", string::f("%d", module->midiParser.channels), [=](Menu* menu) { + for (int c = 1; c <= 16; c++) { + std::string channelsLabel = (c == 1) ? "Monophonic" : string::f("%d", c); + menu->addChild(createCheckMenuItem(channelsLabel, "", + [=]() {return module->midiParser.channels == c;}, + [=]() {module->midiParser.setChannels(c);} + )); + } + })); + + // TODO Panic when set + menu->addChild(createIndexSubmenuItem("Monophonic priority", { + "Last", + "First", + "Lowest", + "Highest", + }, [=]() { + return module->midiParser.monoMode; + }, [=](size_t monoMode) { + module->midiParser.setMonoMode((dsp::MidiParser<16>::MonoMode) monoMode); + })); + + menu->addChild(createBoolPtrMenuItem("Release retrigger", "", &module->midiParser.retriggerOnResume)); + + // TODO Panic when set + menu->addChild(createIndexSubmenuItem("Polyphony mode", { + "Rotate", + "Reuse", + "Reset", + "MPE", + }, [=]() { + return module->midiParser.polyMode; + }, [=](size_t polyMode) { + module->midiParser.setPolyMode((dsp::MidiParser<16>::PolyMode) polyMode); + })); + + menu->addChild(new MenuSeparator); + static const std::vector pwRanges = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 24, 36, 48}; auto getPwRangeLabel = [](float pwRange) -> std::string { if (pwRange == 0) return "Off"; - else if (std::abs(pwRange) < 12) - return string::f("%g semitone", pwRange) + (pwRange == 1 ? "" : "s"); + else if (std::fmod(pwRange, 12) == 0.f) + return string::f("%g octave%s", pwRange / 12, pwRange / 12 == 1 ? "" : "s"); else - return string::f("%g octave", pwRange / 12) + (pwRange / 12 == 1 ? "" : "s"); + return string::f("%g semitone%s", pwRange, pwRange == 1 ? "" : "s"); }; menu->addChild(createSubmenuItem("Pitch bend range", getPwRangeLabel(module->midiParser.pwRange), [=](Menu* menu) { for (size_t i = 0; i < pwRanges.size(); i++) { @@ -189,22 +227,7 @@ struct MIDI_CVWidget : ModuleWidget { } })); - menu->addChild(createSubmenuItem("Polyphony channels", string::f("%d", module->midiParser.channels), [=](Menu* menu) { - for (int c = 1; c <= 16; c++) { - std::string channelsLabel = (c == 1) ? "Monophonic" : string::f("%d", c); - menu->addChild(createCheckMenuItem(channelsLabel, "", - [=]() {return module->midiParser.channels == c;}, - [=]() {module->midiParser.setChannels(c);} - )); - } - })); - - menu->addChild(createIndexPtrSubmenuItem("Polyphony mode", { - "Rotate", - "Reuse", - "Reset", - "MPE", - }, &module->midiParser.polyMode)); + menu->addChild(new MenuSeparator); menu->addChild(createMenuItem("Panic", "", [=]() {module->midiParser.panic();}