| @@ -1,6 +1,9 @@ | |||
| #pragma once | |||
| #include <dsp/common.hpp> | |||
| #include <dsp/filter.hpp> | |||
| #include <dsp/digital.hpp> | |||
| #include <midi.hpp> | |||
| #include <jansson.h> | |||
| 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 rack | |||