Browse Source

Add dsp::MidiParser based on MIDI_CV module.

tags/v2.6.0
Andrew Belt 11 months ago
parent
commit
c0c3d167bd
1 changed files with 485 additions and 0 deletions
  1. +485
    -0
      include/dsp/midi.hpp

+ 485
- 0
include/dsp/midi.hpp View File

@@ -1,6 +1,9 @@
#pragma once #pragma once
#include <dsp/common.hpp> #include <dsp/common.hpp>
#include <dsp/filter.hpp>
#include <dsp/digital.hpp>
#include <midi.hpp> #include <midi.hpp>
#include <jansson.h>




namespace rack { namespace rack {
@@ -232,5 +235,487 @@ struct MidiGenerator {
}; };




/** Converts MIDI note and transport messages to gates, CV, and other states.
MAX_CHANNELS is the maximum number of polyphonic channels.
*/
template <uint8_t MAX_CHANNELS>
struct MidiParser {
// Settings

/** Number of semitones to bend up/down by pitch wheel */
float pwRange;

/** Enables pitch-wheel and mod-wheel exponential smoothing */
bool smooth;

/** Clock output pulses per quarter note */
uint32_t clockDivision;

/** Actual number of polyphonic channels */
uint8_t channels;

enum PolyMode {
ROTATE_MODE,
REUSE_MODE,
RESET_MODE,
MPE_MODE,
NUM_POLY_MODES
};
PolyMode polyMode;

// States

/** Clock index from song start */
int64_t clock;

/** Whether sustain pedal is held. */
bool pedal;

uint8_t notes[MAX_CHANNELS];
bool gates[MAX_CHANNELS];
uint8_t velocities[MAX_CHANNELS];
uint8_t aftertouches[MAX_CHANNELS];
std::vector<uint8_t> heldNotes;
int8_t rotateIndex;

/** Pitch wheel values, from -8192 to 8191.
When MPE is disabled, only the first channel is used.
*/
int16_t pws[MAX_CHANNELS];
/** Mod wheel values, from 0 to 127.
*/
uint8_t mods[MAX_CHANNELS];
/** Smoothing filters for wheel values */
dsp::ExponentialFilter pwFilters[MAX_CHANNELS];
dsp::ExponentialFilter modFilters[MAX_CHANNELS];

dsp::PulseGenerator clockPulse;
dsp::PulseGenerator clockDividerPulse;
dsp::PulseGenerator retriggerPulses[MAX_CHANNELS];
dsp::PulseGenerator startPulse;
dsp::PulseGenerator stopPulse;
dsp::PulseGenerator continuePulse;

MidiParser() {
heldNotes.reserve(128);
reset();
}

/** Resets settings and performance state */
void reset() {
clock = 0;
smooth = true;
channels = 1;
polyMode = ROTATE_MODE;
pwRange = 2.f;
clockDivision = 24;
setFilterLambda(30.f);
panic();
}

/** Resets performance state */
void panic() {
for (uint8_t c = 0; c < MAX_CHANNELS; c++) {
// Middle C
notes[c] = 60;
gates[c] = false;
velocities[c] = 0;
aftertouches[c] = 0;
pws[c] = 0;
mods[c] = 0;
pwFilters[c].reset();
modFilters[c].reset();
}
pedal = false;
rotateIndex = -1;
heldNotes.clear();
}

void processFilters(float deltaTime) {
uint8_t wheelChannels = getWheelChannels();
for (uint8_t c = 0; c < wheelChannels; c++) {
float pw = pws[c] / 8191.f;
pw = math::clamp(pw, -1.f, 1.f);
if (smooth)
pw = pwFilters[c].process(deltaTime, pw);
else
pwFilters[c].out = pw;

float mod = mods[c] / 127.f;
mod = math::clamp(mod, 0.f, 1.f);
if (smooth)
mod = modFilters[c].process(deltaTime, mod);
else
modFilters[c].out = mod;
}
}

void processPulses(float deltaTime) {
clockPulse.process(deltaTime);
clockDividerPulse.process(deltaTime);
startPulse.process(deltaTime);
stopPulse.process(deltaTime);
continuePulse.process(deltaTime);
for (uint8_t c = 0; c < channels; c++) {
retriggerPulses[c].process(deltaTime);
}
}

void processMessage(const midi::Message& msg) {
// DEBUG("MIDI: %ld %s", msg.getFrame(), msg.toString().c_str());

switch (msg.getStatus()) {
// note off
case 0x8: {
releaseNote(msg.getNote());
} break;
// note on
case 0x9: {
if (msg.getValue() > 0) {
uint8_t c = msg.getChannel();
c = pressNote(msg.getNote(), c);
velocities[c] = msg.getValue();
}
else {
// Note-on event with velocity 0 is an alternative for note-off event.
releaseNote(msg.getNote());
}
} break;
// key pressure
case 0xa: {
// Set the aftertouches with the same note
// TODO Should we handle the MPE case differently?
for (uint8_t c = 0; c < MAX_CHANNELS; c++) {
if (notes[c] == msg.getNote())
aftertouches[c] = msg.getValue();
}
} break;
// cc
case 0xb: {
processCC(msg);
} break;
// channel pressure
case 0xd: {
if (polyMode == MPE_MODE) {
// Set the channel aftertouch
aftertouches[msg.getChannel()] = msg.getNote();
}
else {
// Set all aftertouches
for (uint8_t c = 0; c < MAX_CHANNELS; c++) {
aftertouches[c] = msg.getNote();
}
}
} break;
// pitch wheel
case 0xe: {
uint8_t c = (polyMode == MPE_MODE) ? msg.getChannel() : 0;
int16_t pw = msg.getValue();
pw <<= 7;
pw |= msg.getNote();
pw -= 8192;
pws[c] = pw;
} break;
case 0xf: {
processSystem(msg);
} break;
default: break;
}
}

void processCC(const midi::Message& msg) {
switch (msg.getNote()) {
// mod
case 0x01: {
uint8_t c = (polyMode == MPE_MODE) ? msg.getChannel() : 0;
mods[c] = msg.getValue();
} break;
// sustain
case 0x40: {
if (msg.getValue() >= 64)
pressPedal();
else
releasePedal();
} break;
// all notes off (panic)
case 0x7b: {
if (msg.getValue() == 0) {
panic();
}
} break;
default: break;
}
}

void processSystem(const midi::Message& msg) {
switch (msg.getChannel()) {
// Song Position Pointer
case 0x2: {
int64_t pos = int64_t(msg.getNote()) | (int64_t(msg.getValue()) << 7);
clock = pos * 6;
} break;
// Timing
case 0x8: {
clockPulse.trigger(1e-3);
if (clock % clockDivision == 0) {
clockDividerPulse.trigger(1e-3);
}
clock++;
} break;
// Start
case 0xa: {
startPulse.trigger(1e-3);
clock = 0;
} break;
// Continue
case 0xb: {
continuePulse.trigger(1e-3);
} break;
// Stop
case 0xc: {
stopPulse.trigger(1e-3);
} break;
default: break;
}
}

uint8_t assignChannel(uint8_t note) {
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.
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;
}
}

/** Returns actual assigned channel */
uint8_t pressNote(uint8_t note, uint8_t channel) {
// 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);
// Determine actual channel
if (polyMode == MPE_MODE) {
// Channel is already decided for us
}
else {
channel = assignChannel(note);
}
// Set note
notes[channel] = note;
gates[channel] = true;
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);
// Hold note if pedal is pressed
if (pedal)
return;
// Turn off gate of all channels with note
for (uint8_t c = 0; c < channels; c++) {
if (notes[c] == note) {
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;
}
}
}

void pressPedal() {
if (pedal)
return;
pedal = true;
}

void releasePedal() {
if (!pedal)
return;
pedal = false;
// Set last note if monophonic
if (channels == 1) {
if (!heldNotes.empty()) {
// Replace note with last held note
uint8_t lastNote = heldNotes.back();
notes[0] = lastNote;
}
else {
// Disable gate
gates[0] = false;
}
}
// Clear notes that are not held if polyphonic
else {
for (uint8_t c = 0; c < channels; c++) {
if (!gates[c])
continue;
// Disable all gates
gates[c] = false;
// Re-enable gate if channel's note is still held
for (uint8_t note : heldNotes) {
if (notes[c] == note) {
gates[c] = true;
break;
}
}
}
}
}

uint8_t getChannels() {
return channels;
}

void setChannels(uint8_t channels) {
if (channels == this->channels)
return;
this->channels = channels;
panic();
}

void setPolyMode(PolyMode polyMode) {
if (polyMode == this->polyMode)
return;
this->polyMode = polyMode;
panic();
}

float getPitchVoltage(uint8_t channel) {
uint8_t wheelChannel = (polyMode == MPE_MODE) ? channel : 0;
return (notes[channel] - 60.f + pwFilters[wheelChannel].out * pwRange) / 12.f;
}

/** Sets exponential smoothing filter lambda speed. */
void setFilterLambda(float lambda) {
for (uint8_t c = 0; c < MAX_CHANNELS; c++) {
pwFilters[c].setLambda(lambda);
modFilters[c].setLambda(lambda);
}
}

/** Returns pitch wheel value, from -1 to 1. */
float getPw(uint8_t channel) {
return pwFilters[channel].out;
}

/** Returns mod wheel value, from 0 to 1. */
float getMod(uint8_t channel) {
return modFilters[channel].out;
}

/** Returns number of polyphonic channels for pitch and mod wheels. */
uint8_t getWheelChannels() {
return (polyMode == MPE_MODE) ? MAX_CHANNELS : 1;
}

json_t* toJson() {
json_t* rootJ = json_object();
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, "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.
if (polyMode != MPE_MODE) {
json_object_set_new(rootJ, "lastPw", json_integer(pws[0]));
json_object_set_new(rootJ, "lastMod", json_integer(mods[0]));
}
// Assume all filter lambdas are the same
json_object_set_new(rootJ, "filterLambda", json_real(pwFilters[0].lambda));
return rootJ;
}

void fromJson(json_t* rootJ) {
json_t* pwRangeJ = json_object_get(rootJ, "pwRange");
if (pwRangeJ)
pwRange = json_number_value(pwRangeJ);

json_t* smoothJ = json_object_get(rootJ, "smooth");
if (smoothJ)
smooth = json_boolean_value(smoothJ);

json_t* channelsJ = json_object_get(rootJ, "channels");
if (channelsJ)
setChannels(json_integer_value(channelsJ));

json_t* polyModeJ = json_object_get(rootJ, "polyMode");
if (polyModeJ)
polyMode = (PolyMode) json_integer_value(polyModeJ);

json_t* clockDivisionJ = json_object_get(rootJ, "clockDivision");
if (clockDivisionJ)
clockDivision = json_integer_value(clockDivisionJ);

json_t* lastPwJ = json_object_get(rootJ, "lastPw");
if (lastPwJ)
pws[0] = json_integer_value(lastPwJ);

// In Rack <2.5.3, `lastPitch` was used from 0 to 16383.
json_t* lastPitchJ = json_object_get(rootJ, "lastPitch");
if (lastPitchJ)
pws[0] = json_integer_value(lastPitchJ) - 8192;

json_t* lastModJ = json_object_get(rootJ, "lastMod");
if (lastModJ)
mods[0] = json_integer_value(lastModJ);

// Added in Rack 2.5.3
json_t* filterLambdaJ = json_object_get(rootJ, "filterLambda");
if (filterLambdaJ)
setFilterLambda(json_number_value(filterLambdaJ));
}
};


} // namespace dsp } // namespace dsp
} // namespace rack } // namespace rack

Loading…
Cancel
Save