From c0c3d167bda7926329ad739f876e25575e5ca12e Mon Sep 17 00:00:00 2001 From: Andrew Belt Date: Sun, 9 Jun 2024 20:32:39 -0400 Subject: [PATCH] Add dsp::MidiParser based on MIDI_CV module. --- include/dsp/midi.hpp | 485 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 485 insertions(+) diff --git a/include/dsp/midi.hpp b/include/dsp/midi.hpp index 3e2583a6..fd050b14 100644 --- a/include/dsp/midi.hpp +++ b/include/dsp/midi.hpp @@ -1,6 +1,9 @@ #pragma once #include +#include +#include #include +#include 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 +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 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 rack