Browse Source

MIDI to CV: Add monophonic modes: Last, First, Lowest, Highest. Add "Release retrigger" mode.

tags/v2.6.5
Andrew Belt 5 months ago
parent
commit
976b111673
2 changed files with 154 additions and 81 deletions
  1. +112
    -62
      include/dsp/midi.hpp
  2. +42
    -19
      src/core/MIDI_CV.cpp

+ 112
- 62
include/dsp/midi.hpp View File

@@ -1,4 +1,5 @@
#pragma once
#include <algorithm>
#include <dsp/common.hpp>
#include <dsp/filter.hpp>
#include <dsp/digital.hpp>
@@ -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);


+ 42
- 19
src/core/MIDI_CV.cpp View File

@@ -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<float> 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();}


Loading…
Cancel
Save