From e87ec7f4031c214b258edf18d0a9c2c76dbf219c Mon Sep 17 00:00:00 2001 From: Paul Dempsey Date: Sun, 17 Dec 2023 15:25:40 -0800 Subject: [PATCH] Begin PolyMidi --- Makefile | 4 + design/Comp.svg | 792 ----------- design/PolyMidi-design.svg | 2092 ++++++++++++++++++++++++++++ plugin.json | 14 +- res/PolyMidi.svg | 61 + src/Compress/Compress-ui.cpp | 11 +- src/Compress/Compress.cpp | 6 +- src/HC-1/HC-1-midi.cpp | 50 + src/HC-1/HC-1.cpp | 35 + src/HC-1/HC-1.hpp | 4 + src/HC-2/HC-2-ui.cpp | 9 +- src/HC-4/HC-4.cpp | 14 - src/HC-4/HC-4.hpp | 9 +- src/PolyMidi/PolyMidi-ui.cpp | 137 ++ src/PolyMidi/PolyMidi.cpp | 376 +++++ src/PolyMidi/PolyMidi.hpp | 144 ++ src/PolyMidi/bend_param.hpp | 59 + src/PolyMidi/mpe_burger.hpp | 69 + src/Round/Round-ui.cpp | 8 - src/common_layout.hpp | 4 +- src/em.hpp | 16 +- src/em_midi.hpp | 53 +- src/em_types/em_compressor.hpp | 2 +- src/em_types/em_mpe.hpp | 150 ++ src/em_types/em_polyphony.hpp | 28 + src/em_types/em_priority.cpp | 21 + src/em_types/em_priority.hpp | 44 + src/em_types/em_velocity_split.hpp | 35 + src/hc_events.hpp | 25 + src/misc.hpp | 12 + src/plugin.cpp | 3 +- src/plugin.hpp | 1 + src/widgets/enum_param.hpp | 72 + src/widgets/hamburger.hpp | 45 + 34 files changed, 3553 insertions(+), 852 deletions(-) delete mode 100644 design/Comp.svg create mode 100644 design/PolyMidi-design.svg create mode 100644 res/PolyMidi.svg create mode 100644 src/PolyMidi/PolyMidi-ui.cpp create mode 100644 src/PolyMidi/PolyMidi.cpp create mode 100644 src/PolyMidi/PolyMidi.hpp create mode 100644 src/PolyMidi/bend_param.hpp create mode 100644 src/PolyMidi/mpe_burger.hpp create mode 100644 src/em_types/em_mpe.hpp create mode 100644 src/em_types/em_polyphony.hpp create mode 100644 src/em_types/em_priority.cpp create mode 100644 src/em_types/em_priority.hpp create mode 100644 src/em_types/em_velocity_split.hpp create mode 100644 src/widgets/enum_param.hpp create mode 100644 src/widgets/hamburger.hpp diff --git a/Makefile b/Makefile index 169cc89..7df9197 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,7 @@ SOURCES += src/colors.cpp SOURCES += src/em_device.cpp SOURCES += src/em_midi.cpp SOURCES += src/em_types/em_pedal.cpp +SOURCES += src/em_types/em_priority.cpp SOURCES += src/em_types/em_rounding.cpp SOURCES += src/em_types/em_tuning.cpp SOURCES += src/he_group.cpp @@ -59,6 +60,9 @@ SOURCES += src/Round/Round-ui.cpp SOURCES += src/Compress/Compress.cpp SOURCES += src/Compress/Compress-ui.cpp +SOURCES += src/PolyMidi/PolyMidi.cpp +SOURCES += src/PolyMidi/PolyMidi-ui.cpp + DISTRIBUTABLES += res # DISTRIBUTABLES += presets # DISTRIBUTABLES += selections diff --git a/design/Comp.svg b/design/Comp.svg deleted file mode 100644 index 2671fea..0000000 --- a/design/Comp.svg +++ /dev/null @@ -1,792 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/design/PolyMidi-design.svg b/design/PolyMidi-design.svg new file mode 100644 index 0000000..9e58029 --- /dev/null +++ b/design/PolyMidi-design.svg @@ -0,0 +1,2092 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SurfaceMIDIDSPCVCMIDI inPolyMidiRouting + + + + + + + + + + + + + + + + + + + + + + + + +ExpandComputePolyphony4+^ + + + + + + + + + + + + + + + + + + + + + + + + +Priority + + + + + + + + + + + + + + + + + + + + + + + + +XMPEbend 96MPE+OldestVelocity + + + + + + + + + + + + + + + + + + + + + + + + +Ycc74 + + + + + + + + + + + + + + + + + + + + + + + + +127 + + + + + + + + + + + + + + + + + + + + + + + + +ZCh PressContinuum 700108 diff --git a/plugin.json b/plugin.json index 0610cca..b1c4e04 100644 --- a/plugin.json +++ b/plugin.json @@ -8,7 +8,7 @@ "authorEmail": "pcdempsey@live.com", "authorUrl": "", "pluginUrl": "", - "manualUrl": "https://github.com/Paul-Dempsey/pachde-hc-one/blob/main/doc/index.md", + "manualUrl": "https://github.com/Paul-Dempsey/pachde-hc-one/blob/main/doc/index.md#pachde-d-hc-one", "sourceUrl": "https://github.com/Paul-Dempsey/pachde-hc-one", "donateUrl": "https://venmo.com/u/pcdempsey", "changelogUrl": "", @@ -41,19 +41,25 @@ "slug": "pachde-hc-pedal-1", "name": "Pedal-1", "description": "Controller for EaganMatrix Pedal 1 (HC-1 companion)", - "tags": [ "Controller", "Expander" ] + "tags": [ "Controller", "MIDI", "Expander" ] }, { "slug": "pachde-hc-pedal-2", "name": "Pedal-2", "description": "Controller for EaganMatrix Pedal 2 (HC-1 companion)", - "tags": [ "Controller", "Expander" ] + "tags": [ "Controller", "MIDI", "Expander" ] }, { "slug": "pachde-hc-compressor", "name": "Compressor", "description": "Controller for EaganMatrix Compressor (HC-1 companion)", - "tags": [ "Controller", "Expander" ] + "tags": [ "Controller", "MIDI", "Expander" ] + }, + { + "slug": "pachde-hc-polymidi", + "name": "PolyMidi", + "description": "Controller for EaganMatrix Polyphony and MIDI (HC-1 companion)", + "tags": [ "Controller", "Expander", "MIDI" ] } ] } diff --git a/res/PolyMidi.svg b/res/PolyMidi.svg new file mode 100644 index 0000000..f4cac2e --- /dev/null +++ b/res/PolyMidi.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Compress/Compress-ui.cpp b/src/Compress/Compress-ui.cpp index 2bb7d5e..c63a47c 100644 --- a/src/Compress/Compress-ui.cpp +++ b/src/Compress/Compress-ui.cpp @@ -17,20 +17,11 @@ using CmpI = CompressModule::Inputs; using CmpO = CompressModule::Outputs; using CmpL = CompressModule::Lights; -inline uint8_t GetSmallParamValue(rack::app::ModuleWidget* w, int id, uint8_t default_value = 0) { - auto p = w->getParam(id); - if (!p) return default_value; - auto pq = p->getParamQuantity(); - if (!pq) return default_value; - return U8(pq->getValue()); -} - - void CompressModuleWidget::createCompressorUI() { addChild(createLightCentered>(Vec(CENTER, 40.f), my_module, CmpL::L_COMPRESSOR)); - float x_rel = CENTER - VK_REL_VOFFSET; + float x_rel = CENTER - VK_REL_OFFSET; float y = 88.f; // Threshold addChild(createModKnob( diff --git a/src/Compress/Compress.cpp b/src/Compress/Compress.cpp index 178dd46..b4ce9e3 100644 --- a/src/Compress/Compress.cpp +++ b/src/Compress/Compress.cpp @@ -15,9 +15,9 @@ CompressModule::CompressModule() config(Params::NUM_PARAMS, Inputs::NUM_INPUTS, Outputs::NUM_OUTPUTS, Lights::NUM_LIGHTS); configCCParam(EMCC_CompressorThreshold, false, this, P_COMP_THRESHOLD, IN_COMP_THRESHOLD, P_COMP_THRESHOLD_REL, L_COMP_THRESHOLD_REL, 0.f, 127.f, 127.f, "Threshold", "%", 0.f, 100.f/127.f)->snapEnabled = true; - configCCParam(EMCC_CompressorAttack, false, this, P_COMP_ATTACK, IN_COMP_ATTACK, P_COMP_ATTACK_REL, L_COMP_ATTACK_REL, 0.f, 127.f, 64.f, "Attack", "%", 0.f, 100.f/127.f)->snapEnabled = true; - configCCParam(EMCC_CompressorRatio, false, this, P_COMP_RATIO, IN_COMP_RATIO, P_COMP_RATIO_REL, L_COMP_RATIO_REL, 0.f, 127.f, 64.f, "Ratio", "%", 0.f, 100.f/127.f)->snapEnabled = true; - configCCParam(EMCC_CompressorMix, false, this, P_COMP_MIX, IN_COMP_MIX, P_COMP_MIX_REL, L_COMP_MIX_REL, 0.f, 127.f, 0.f, "Mix", "%", 0.f, 100.f/127.f)->snapEnabled = true; + configCCParam(EMCC_CompressorAttack, false, this, P_COMP_ATTACK, IN_COMP_ATTACK, P_COMP_ATTACK_REL, L_COMP_ATTACK_REL, 0.f, 127.f, 64.f, "Attack", "%", 0.f, 100.f/127.f)->snapEnabled = true; + configCCParam(EMCC_CompressorRatio, false, this, P_COMP_RATIO, IN_COMP_RATIO, P_COMP_RATIO_REL, L_COMP_RATIO_REL, 0.f, 127.f, 64.f, "Ratio", "%", 0.f, 100.f/127.f)->snapEnabled = true; + configCCParam(EMCC_CompressorMix, false, this, P_COMP_MIX, IN_COMP_MIX, P_COMP_MIX_REL, L_COMP_MIX_REL, 0.f, 127.f, 0.f, "Mix", "%", 0.f, 100.f/127.f)->snapEnabled = true; configInput(IN_COMP_THRESHOLD, "Compression threshold"); configInput(IN_COMP_ATTACK, "Compression attack"); diff --git a/src/HC-1/HC-1-midi.cpp b/src/HC-1/HC-1-midi.cpp index b98f1a8..7841afb 100644 --- a/src/HC-1/HC-1-midi.cpp +++ b/src/HC-1/HC-1-midi.cpp @@ -108,6 +108,11 @@ void Hc1Module::onChannel16CC(uint8_t cc, uint8_t value) } break; + case EMCC_Routing: + em.routing = value; + notifyRoutingChanged(); + break; + case EMCC_PedalType: { auto new_p1 = static_cast(value & 0x07); bool p1_change = new_p1 != em.pedal1.type; @@ -123,10 +128,55 @@ void Hc1Module::onChannel16CC(uint8_t cc, uint8_t value) } } break; + case EMCC_Polyphony: + //if (em.polyphony.to_raw() != value) { + em.polyphony.set_raw(value); + notifyPolyphonyChanged(); + //} + break; + + case EMCC_BendRange: + //if (em.mpe.get_bend() != value) { + // Mpe old = em.mpe; + em.mpe.set_bend_with_side_effects(value); + // if (old != em.mpe) { + notifyMpeChanged(); + // } + //} + break; + + case EMCC_YCC: + //if (U8(em.mpe.get_y()) != value) { + // Mpe old = em.mpe; + em.mpe.set_y_with_side_effects(static_cast(value)); + // if (old != em.mpe) { + notifyMpeChanged(); + // } + //} + break; + + case EMCC_ZCC: + //if (U8(em.mpe.get_z()) != value) { + // Mpe old = em.mpe; + em.mpe.set_z_with_side_effects(static_cast(value)); + // if (old != em.mpe) { + notifyMpeChanged(); + // } + //} + break; + case EMCC_MiddleC: em.middle_c = value; break; + case EMCC_Priority: + //if (em.priority.to_raw() != value) + //{ + em.priority.set_raw(value); + notifyNotePriorityChanged(); + //} + break; + case EMCC_TuningGrid: { auto new_value = static_cast(value); if (em.rounding.tuning != new_value) { diff --git a/src/HC-1/HC-1.cpp b/src/HC-1/HC-1.cpp index 5cc1bdd..72bebe6 100644 --- a/src/HC-1/HC-1.cpp +++ b/src/HC-1/HC-1.cpp @@ -174,6 +174,41 @@ void Hc1Module::notifyTiltEqChanged() } } +void Hc1Module::notifyRoutingChanged() +{ + if (hc_event_subscriptions.empty()) return; + auto event = IHandleHcEvents::RoutingChangedEvent{em.routing}; + for (auto client: hc_event_subscriptions) { + client->onRoutingChanged(event); + } +} + +void Hc1Module::notifyPolyphonyChanged() +{ + if (hc_event_subscriptions.empty()) return; + auto event = IHandleHcEvents::PolyphonyChangedEvent{em.polyphony}; + for (auto client: hc_event_subscriptions) { + client->onPolyphonyChanged(event); + } +} + +void Hc1Module::notifyNotePriorityChanged() +{ + if (hc_event_subscriptions.empty()) return; + auto event = IHandleHcEvents::NotePriorityChangedEvent{em.priority}; + for (auto client: hc_event_subscriptions) { + client->onNotePriorityChanged(event); + } +} +void Hc1Module::notifyMpeChanged() +{ + if (hc_event_subscriptions.empty()) return; + auto event = IHandleHcEvents::MpeChangedEvent{em.mpe}; + for (auto client: hc_event_subscriptions) { + client->onMpeChanged(event); + } +} + void Hc1Module::notifyDeviceChanged() { if (hc_event_subscriptions.empty()) return; diff --git a/src/HC-1/HC-1.hpp b/src/HC-1/HC-1.hpp index 603afe2..74f8d3f 100644 --- a/src/HC-1/HC-1.hpp +++ b/src/HC-1/HC-1.hpp @@ -297,6 +297,10 @@ struct Hc1Module : IPresetHolder, ISendMidi, IMidiDeviceHolder, IMidiDeviceChang void notifyRoundingChanged(); void notifyCompressorChanged(); void notifyTiltEqChanged(); + void notifyRoutingChanged(); + void notifyPolyphonyChanged(); + void notifyNotePriorityChanged(); + void notifyMpeChanged(); void notifyDeviceChanged(); void notifyDisconnect(); void notifyFavoritesFileChanged(); diff --git a/src/HC-2/HC-2-ui.cpp b/src/HC-2/HC-2-ui.cpp index 9c9926f..648fd55 100644 --- a/src/HC-2/HC-2-ui.cpp +++ b/src/HC-2/HC-2-ui.cpp @@ -19,14 +19,6 @@ using Hc2I = Hc2Module::Inputs; using Hc2O = Hc2Module::Outputs; using Hc2L = Hc2Module::Lights; -inline uint8_t GetSmallParamValue(rack::app::ModuleWidget* w, int id, uint8_t default_value = 0) { - auto p = w->getParam(id); - if (!p) return default_value; - auto pq = p->getParamQuantity(); - if (!pq) return default_value; - return U8(pq->getValue()); -} - void Hc2ModuleWidget::createTiltEqUI(float x, float y) { addChild(createHeaderWidget(x, y, TEQ_BOX_WIDTH, KNOB_BOX_HEIGHT)); @@ -68,6 +60,7 @@ Hc2ModuleWidget::Hc2ModuleWidget(Hc2Module * module) } setPanel(createPanel(asset::plugin(pluginInstance, "res/HC-2.svg"))); addChild(partner_picker = createPartnerPicker()); + partner_picker->setFormat(TextFormatLength::Short); createTiltEqUI(TEQ_BOX_LEFT, TEQ_BOX_TOP); diff --git a/src/HC-4/HC-4.cpp b/src/HC-4/HC-4.cpp index 5484d7b..54f246b 100644 --- a/src/HC-4/HC-4.cpp +++ b/src/HC-4/HC-4.cpp @@ -34,11 +34,6 @@ Hc1Module* Hc4Module::getPartner() return partner_binding.getPartner(); } -void Hc4Module::onPedalChanged(const PedalChangedEvent& e) -{ - -} - void Hc4Module::onDeviceChanged(const DeviceChangedEvent& e) { partner_binding.onDeviceChanged(e); @@ -55,20 +50,11 @@ void Hc4Module::onDisconnect(const DisconnectEvent& e) } } -// void Hc4Module::onFavoritesFileChanged(const FavoritesFileChangedEvent& e) -// { -// if (ui_event_sink) { -// ui_event_sink->onFavoritesFileChanged(e); -// } -// } - void Hc4Module::process(const ProcessArgs& args) { if (0 == ((args.frame + id) % CV_INTERVAL)) { auto partner = getPartner(); if (partner) { - // getOutput(Outputs::O_PEDAL1).setVoltage(10.f * partner->pedal1.value / 127); - // getOutput(Outputs::O_PEDAL2).setVoltage(10.f * partner->pedal2.value / 127); } } } diff --git a/src/HC-4/HC-4.hpp b/src/HC-4/HC-4.hpp index 0dac3cd..cf9b55f 100644 --- a/src/HC-4/HC-4.hpp +++ b/src/HC-4/HC-4.hpp @@ -42,12 +42,13 @@ struct Hc4Module : Module, IHandleHcEvents Hc1Module * getPartner(); // IHandleHcEvents -// void onPresetChanged(const PresetChangedEvent& e) override; -// void onRoundingChanged(const RoundingChangedEvent& e) override; - void onPedalChanged(const PedalChangedEvent& e) override; + // void onPresetChanged(const PresetChangedEvent& e) override; + // void onRoundingChanged(const RoundingChangedEvent& e) override; + // void onPedalChanged(const PedalChangedEvent& e) override; + // void onRoutingChanged(const RoutingChangedEvent& e) override; void onDeviceChanged(const DeviceChangedEvent& e) override; void onDisconnect(const DisconnectEvent& e) override; - //void onFavoritesFileChanged(const FavoritesFileChangedEvent& e) override; + // void onFavoritesFileChanged(const FavoritesFileChangedEvent& e) override; // Module json_t *dataToJson() override; diff --git a/src/PolyMidi/PolyMidi-ui.cpp b/src/PolyMidi/PolyMidi-ui.cpp new file mode 100644 index 0000000..e635495 --- /dev/null +++ b/src/PolyMidi/PolyMidi-ui.cpp @@ -0,0 +1,137 @@ +#include "PolyMidi.hpp" +#include "../widgets/port.hpp" +#include "mpe_burger.hpp" + +namespace pachde { + +using PMP = PolyMidiModule::Params; +using PMI = PolyMidiModule::Inputs; +using PML = PolyMidiModule::Lights; +constexpr const float CENTER = 52.5f; +constexpr const float R_LEFT = 36.f; +constexpr const float R_RIGHT = 70.f; +constexpr const float L_KNOB = 26.f; +constexpr const float R_KNOB = 78.f; + +std::string MakePolyphonyLabel(const Polyphony& polyphony, const NotePriority& priority) +{ + auto text = format_string("%d", polyphony.polyphony()); + if (polyphony.expanded_polyphony()) { + text.push_back('+'); + } + if (priority.increased_computation()) { + text.push_back('^'); + } + return text; +} + +PolyMidiModuleWidget::PolyMidiModuleWidget(PolyMidiModule * module) +: my_module(module) +{ + setModule(module); + if (module) { + my_module->ui_event_sink = this; + } + const NVGcolor gold = GetStockColor(StockColor::Gold); + + setPanel(createPanel(asset::plugin(pluginInstance, "res/PolyMidi.svg"))); + addChild(partner_picker = createPartnerPicker()); + partner_picker->setFormat(TextFormatLength::Short); + + addChild(createParamCentered(Vec(L_KNOB, 58.f), module, PMP::P_POLY)); + addChild(poly_text = createLazyDynamicTextLabel( + Vec(L_KNOB, 70.f), Vec(100.f, 12.f), + [&](){ return my_module ? MakePolyphonyLabel(my_module->polyphony, my_module->priority) : "4+"; }, + 9.f, false, TextAlignment::Center, gold, false)); + + addParam(createLightParamCentered>>( + Vec(CENTER, 52.f), module, PMP::P_EXPAND, PML::L_EXPAND)); + addParam(createLightParamCentered>>( + Vec(CENTER, 68.f), module, PMP::P_COMPUTE, PML::L_COMPUTE)); + + auto tw = createStaticTextLabel(Vec(L_KNOB, 130.f), 80.f, "MPE+", TextAlignment::Center, 9.f, false, gold); + midi_ui = createWidgetCentered(Vec(L_KNOB, 120)); + midi_ui->setLabel(tw); + if (module) midi_ui->setParam(module->getParamQuantity(PMP::P_MPE)); + addChild(midi_ui); + addChild(tw); + + addChild(createParamCentered(Vec(L_KNOB, 180.f), module, PMP::P_PRI)); + addChild(priority_text = createLazyDynamicTextLabel( + Vec(L_KNOB, 194.f), Vec(100.f, 12.f), + [&](){ return my_module ? NotePriorityName(my_module->priority.priority()) : NotePriorityName(NotePriorityType::LRU); }, + 9.f, false, TextAlignment::Center, gold, false)); + + addChild(createParamCentered(Vec(L_KNOB, 240.f), module, PMP::P_VELOCITY)); + + addChild(createParamCentered(Vec(R_KNOB, 120.f), module, PMP::P_X_BEND)); + addChild(createParamCentered(Vec(R_KNOB, 180.f), module, PMP::P_Y)); + addChild(createParamCentered(Vec(R_KNOB, 240.f), module, PMP::P_Z)); + + createRouting(); +} + +void PolyMidiModuleWidget::createRouting() +{ + float y = 308.25f; + addParam(createLightParamCentered>>( + Vec(R_LEFT, y), + module, PMP::P_ROUTE_SURFACE_MIDI, PML::L_ROUTE_SURFACE_MIDI)); + addParam(createLightParamCentered>>( + Vec(CENTER, y), + module, PMP::P_ROUTE_SURFACE_DSP, PML::L_ROUTE_SURFACE_DSP)); + addParam(createLightParamCentered>>( + Vec(R_RIGHT, y), + module, PMP::P_ROUTE_SURFACE_CVC, PML::L_ROUTE_SURFACE_CVC)); + + y = 342.f; + addParam(createLightParamCentered>>( + Vec(R_LEFT, y), + module, PMP::P_ROUTE_MIDI_MIDI, PML::L_ROUTE_MIDI_MIDI)); + addParam(createLightParamCentered>>( + Vec(CENTER, y), + module, PMP::P_ROUTE_MIDI_DSP, PML::L_ROUTE_MIDI_DSP)); + addParam(createLightParamCentered>>( + Vec(R_RIGHT, y), + module, PMP::P_ROUTE_MIDI_CVC, PML::L_ROUTE_MIDI_CVC)); +} + +Hc1Module* PolyMidiModuleWidget::getPartner() +{ + return module ? my_module->getPartner() : nullptr; +} + +void PolyMidiModuleWidget::onPolyphonyChanged(const PolyphonyChangedEvent &e) +{ + poly_text->modified(); +} +void PolyMidiModuleWidget::onNotePriorityChanged(const NotePriorityChangedEvent& e) +{ + poly_text->modified(); + priority_text->modified(); +} + +void PolyMidiModuleWidget::onMpeChanged(const MpeChangedEvent &e) +{ + midi_ui->setMode(e.mpe.mode()); + +} + +void PolyMidiModuleWidget::onDeviceChanged(const DeviceChangedEvent &e) +{ + partner_picker->onDeviceChanged(e); +} + +void PolyMidiModuleWidget::onDisconnect(const DisconnectEvent& e) +{ + partner_picker->onDisconnect(e); +} + +void PolyMidiModuleWidget::appendContextMenu(Menu *menu) +{ + if (!my_module) return; + menu->addChild(new MenuSeparator); + my_module->partner_binding.appendContextMenu(menu); +} + +} \ No newline at end of file diff --git a/src/PolyMidi/PolyMidi.cpp b/src/PolyMidi/PolyMidi.cpp new file mode 100644 index 0000000..026cc5a --- /dev/null +++ b/src/PolyMidi/PolyMidi.cpp @@ -0,0 +1,376 @@ +#include "PolyMidi.hpp" +#include "../widgets/enum_param.hpp" +namespace pachde { + + +PolyMidiModule::PolyMidiModule() +{ + config(Params::NUM_PARAMS, Inputs::NUM_INPUTS, Outputs::NUM_OUTPUTS, Lights::NUM_LIGHTS); + partner_binding.setClient(this); + std::vector connection = {"disconnected", "connected"}; + std::vector offon = {"off", "on"}; + + configParam(Params::P_POLY, 1.f, 16.f, 1.f, "Base Polyphony")->snapEnabled = true; + configSwitch(Params::P_EXPAND, 0.f, 1.f, 1.f, "Allow expanded polyphony", offon); + configSwitch(Params::P_COMPUTE, 0.f, 1.f, 0.f, "Allow increased computation rate", offon); + + configSwitch(Params::P_MPE, U8(MidiMode::Midi), U8(MidiMode::MpePlus), U8(MidiMode::MpePlus), "MIDI/MPE", {"MIDI", "MPE", "MPE+"}); + configBendParam(this, Params::P_X_BEND); + + //configSwitch(Params::P_Y, 0.f, 8.f, 7.f, "Y", { "Off", "cc1 Modulation", "cc2 Breath", "cc3", "cc4 Foot", "cc7 Volume", "cc11 Expression", "cc74 Brightness", "cc74 (no shelf)"}); + configEnumParam(Params::P_Y, "Y",this, EMY::Default_Y, + { EMY::None, EMY::CC_1_Modulation, EMY::CC_2_Breath, EMY::CC_3, EMY::CC_4_Foot, EMY::CC_7_Volume, EMY::CC_11_Expression, EMY::CC_74_Bright, EMY::CC_74_NoShelf }, + { "Off", "cc1 Modulation", "cc2 Breath", "cc3", "cc4 Foot", "cc7 Volume", "cc11 Expression", "cc74 Brightness", "cc74 (no shelf)" } + ); + + //configSwitch(Params::P_Z, 0.f, 7.f, 7.f, "Z", { "Off", "cc1 Modulation", "cc2 Breath", "cc3", "cc4 Foot", "cc7 Volume", "cc11 Expression", "Channel pressure"}); + configEnumParam(Params::P_Z, "Z",this, EMZ::Midi_ChannelPressure, + { EMZ::None, EMZ::CC_1_Modulation, EMZ::CC_2_Breath, EMZ::CC_3, EMZ::CC_4_Foot, EMZ::CC_7_Volume, EMZ::CC_11_Expression, EMZ::Midi_ChannelPressure, }, + { "Off", "cc1 Modulation", "cc2 Breath", "cc3", "cc4 Foot", "cc7 Volume", "cc11 Expression", "Channel pressure" }, + true + ); + + configSwitch(Params::P_PRI, 0.f, 6.f, 0.f, "Note priority", { "Oldest", "Same note", "Lowest", "Highest", "High 2", "High 3", "High 4"}); + configSwitch(Params::P_VELOCITY, 0.f, 5.f, 0.f, "Velocity", {"Static 127", "Dynamic", "Formula V", "No Notes", "Theremin", "Kyma"}); + + configSwitch(Params::P_ROUTE_SURFACE_MIDI,0.f, 1.f, 1.f, "Surface to MIDI", connection); + configSwitch(Params::P_ROUTE_SURFACE_DSP,0.f, 1.f, 1.f, "Surface to DSP", connection); + configSwitch(Params::P_ROUTE_SURFACE_CVC,0.f, 1.f, 1.f, "Surface to CVC", connection); + configSwitch(Params::P_ROUTE_MIDI_MIDI,0.f, 1.f, 1.f, "Midi to MIDI", connection); + configSwitch(Params::P_ROUTE_MIDI_DSP,0.f, 1.f, 1.f, "Midi to DSP", connection); + configSwitch(Params::P_ROUTE_MIDI_CVC,0.f, 1.f, 1.f, "Midi to CVC", connection); + setRoutingLights(); +} + +PolyMidiModule::~PolyMidiModule() +{ + partner_binding.unsubscribe(); +} + +json_t * PolyMidiModule::dataToJson() +{ + auto root = json_object(); + json_object_set_new(root, "device", json_string(partner_binding.claim.c_str())); + return root; +} + +void PolyMidiModule::dataFromJson(json_t *root) +{ + auto j = json_object_get(root, "device"); + if (j) { + partner_binding.setClaim(json_string_value(j)); + } + getPartner(); +} + +Hc1Module* PolyMidiModule::getPartner() +{ + return partner_binding.getPartner(); +} + +void PolyMidiModule::onPolyphonyChanged(const PolyphonyChangedEvent& e) +{ + if (polyphony.to_raw() == e.polyphony.to_raw()) return; + polyphony = e.polyphony; + getParamQuantity(Params::P_POLY)->setValue(polyphony.polyphony()); + getParamQuantity(Params::P_EXPAND)->setValue(polyphony.expanded_polyphony()); + + if (ui_event_sink) { + ui_event_sink->onPolyphonyChanged(e); + } +} + +void PolyMidiModule::onNotePriorityChanged(const NotePriorityChangedEvent &e) +{ + if (priority.to_raw() == e.piority.to_raw()) return; + priority = e.piority; + getParamQuantity(Params::P_PRI)->setValue(static_cast(U8(priority.priority()))); + getParamQuantity(Params::P_COMPUTE)->setValue(priority.increased_computation()); + + if (ui_event_sink) { + ui_event_sink->onNotePriorityChanged(e); + } +} + +void PolyMidiModule::onMpeChanged(const MpeChangedEvent& e) +{ + getParamQuantity(P_MPE)->setValue(U8(e.mpe.mode())); + bool mpe = e.mpe.is_any_mpe(); + auto pq = static_cast(getParamQuantity(Params::P_X_BEND)); + pq->is_mpe = mpe; + pq->setValue(e.mpe.get_bend()); + static_cast*>(getParamQuantity(Params::P_Y))->setEnumValue(e.mpe.get_y()); + + EMZ z_param = mpe ? EMZ::Midi_ChannelPressure : e.mpe.get_z(); + static_cast*>(getParamQuantity(Params::P_Z))->setEnumValue(z_param); + if (ui_event_sink) { + ui_event_sink->onMpeChanged(e); + } +} + +void PolyMidiModule::onRoutingChanged(const RoutingChangedEvent &e) +{ + if (routing == e.routing) return; + routing = e.routing; + getParamQuantity(Params::P_ROUTE_SURFACE_MIDI)->setValue(routing & EM_ROUTE_BITS::Surface_Midi); + getParamQuantity(Params::P_ROUTE_SURFACE_DSP)->setValue(routing & EM_ROUTE_BITS::Surface_Dsp); + getParamQuantity(Params::P_ROUTE_SURFACE_CVC)->setValue(routing & EM_ROUTE_BITS::Surface_Cvc); + getParamQuantity(Params::P_ROUTE_MIDI_MIDI)->setValue(routing & EM_ROUTE_BITS::Midi_Midi); + getParamQuantity(Params::P_ROUTE_MIDI_DSP)->setValue(routing & EM_ROUTE_BITS::Midi_Dsp); + getParamQuantity(Params::P_ROUTE_MIDI_CVC)->setValue(routing & EM_ROUTE_BITS::Midi_Cvc); + + setRoutingLights(); + // not needed, but will be if the routing graphic is enhanced beyound just lights + // if (ui_event_sink) { + // ui_event_sink->onRoutingChanged(e); + // } +} + +void PolyMidiModule::onVelocityChanged(const VelocityChangedEvent& e) +{ + if (velocity == e.velocity) return; + velocity = e.velocity; + getParamQuantity(Params::P_VELOCITY)->setValue(e.velocity.getVelocityByte()); + // if (ui_event_sink) { + // ui_event_sink->onVelocityChanged(e); + // } +} + +void PolyMidiModule::onDeviceChanged(const DeviceChangedEvent &e) +{ + partner_binding.onDeviceChanged(e); + if (ui_event_sink) { + ui_event_sink->onDeviceChanged(e); + } +} + +void PolyMidiModule::onDisconnect(const DisconnectEvent& e) +{ + partner_binding.onDisconnect(e); + if (ui_event_sink) { + ui_event_sink->onDisconnect(e); + } +} + +uint8_t PolyMidiModule::getKnobRouting() +{ + uint8_t result = 0; + if (getParamQuantity(Params::P_ROUTE_SURFACE_MIDI)->getValue() > 0.5f) { result |= EM_ROUTE_BITS::Surface_Midi; } + if (getParamQuantity(Params::P_ROUTE_SURFACE_DSP)->getValue() > 0.5f) { result |= EM_ROUTE_BITS::Surface_Dsp; } + if (getParamQuantity(Params::P_ROUTE_SURFACE_CVC)->getValue() > 0.5f) { result |= EM_ROUTE_BITS::Surface_Cvc; } + if (getParamQuantity(Params::P_ROUTE_MIDI_MIDI)->getValue() > 0.5f) { result |= EM_ROUTE_BITS::Midi_Midi; } + if (getParamQuantity(Params::P_ROUTE_MIDI_DSP)->getValue() > 0.5f) { result |= EM_ROUTE_BITS::Midi_Dsp; } + if (getParamQuantity(Params::P_ROUTE_MIDI_CVC)->getValue() > 0.5f) { result |= EM_ROUTE_BITS::Midi_Cvc; } + return result; +} + +void PolyMidiModule::setRoutingLights() +{ + getLight(Lights::L_ROUTE_SURFACE_MIDI).setBrightness(routing & EM_ROUTE_BITS::Surface_Midi); + getLight(Lights::L_ROUTE_SURFACE_DSP).setBrightness(routing & EM_ROUTE_BITS::Surface_Dsp); + getLight(Lights::L_ROUTE_SURFACE_CVC).setBrightness(routing & EM_ROUTE_BITS::Surface_Cvc); + getLight(Lights::L_ROUTE_MIDI_MIDI).setBrightness(routing & EM_ROUTE_BITS::Midi_Midi); + getLight(Lights::L_ROUTE_MIDI_DSP).setBrightness(routing & EM_ROUTE_BITS::Midi_Dsp); + getLight(Lights::L_ROUTE_MIDI_CVC).setBrightness(routing & EM_ROUTE_BITS::Midi_Cvc); +} + +Hc1Module* PolyMidiModule::processPolyphony(Hc1Module* partner) +{ + bool poly_changed = false; + bool ui_expanded = getParamQuantity(Params::P_EXPAND)->getValue() > 0.5f; + if (ui_expanded != polyphony.expanded_polyphony()) { + poly_changed = true; + polyphony.set_expanded_polyphony(ui_expanded); + } + uint8_t ui_poly = std::round(getParamQuantity(Params::P_POLY)->getValue()); + if (ui_poly != polyphony.polyphony()) { + poly_changed = true; + polyphony.set_polyphony(ui_poly); + } + if (poly_changed) { + if (ui_event_sink) { + ui_event_sink->onPolyphonyChanged(PolyphonyChangedEvent{polyphony}); + } + auto partner = getPartner(); + if (partner) { + partner->em.polyphony.raw = polyphony.raw; + if (partner->readyToSend()) { + partner->sendControlChange(EM_SettingsChannel, EMCC_Polyphony, polyphony.raw); + } + } + } + getLight(Lights::L_EXPAND).setBrightness(polyphony.expanded_polyphony()); + return partner; +} + +Hc1Module* PolyMidiModule::processPriority(Hc1Module* partner) +{ + bool pri_changed = false; + NotePriorityType pri = static_cast(U8(std::round(getParamQuantity(Params::P_PRI)->getValue()))); + if (pri != priority.priority()) { + pri_changed = true; + priority.set_priority(pri); + } + bool compute = getParamQuantity(Params::P_COMPUTE)->getValue() > 0.5f; + if (compute != priority.increased_computation()) { + pri_changed = true; + priority.set_increased_computation(compute); + } + if (pri_changed) { + if (ui_event_sink) { + ui_event_sink->onNotePriorityChanged(NotePriorityChangedEvent{priority}); + } + if (!partner) partner = getPartner(); + if (partner) { + partner->em.priority.raw = priority.raw; + if (partner->readyToSend()) { + partner->sendControlChange(EM_SettingsChannel, EMCC_Priority, priority.to_raw()); + } + } + } + getLight(Lights::L_COMPUTE).setBrightness(priority.increased_computation()); + return partner; +} + +Hc1Module* PolyMidiModule::processRouting(Hc1Module *partner) +{ + auto knob_route = getKnobRouting(); + if (routing != knob_route) { + routing = knob_route; + if (!partner) partner = getPartner(); + if (partner) { + partner->em.routing = routing; + if (partner->readyToSend()) { + partner->sendControlChange(EM_SettingsChannel, EMCC_Routing, routing); + } + } + } + setRoutingLights(); + return partner; +} + +Hc1Module* PolyMidiModule::processMpe(Hc1Module* partner) +{ + auto mode = static_cast(GetByteParamValue(getParamQuantity(P_MPE))); + if (mode != mpe.mode()) { + auto old = mpe; + mpe.set_mode(mode); + + auto bpq = static_cast(getParamQuantity(P_X_BEND)); + bpq->is_mpe = mpe.is_any_mpe(); + bpq->setValue(mpe.get_bend()); + + static_cast*>(getParamQuantity(P_Y))->setEnumValue(mpe.get_y()); + static_cast*>(getParamQuantity(P_Z))->setEnumValue(mpe.get_z()); + + if (!partner) partner = getPartner(); + if (partner) { + partner->em.mpe = mpe; + if (partner->readyToSend()) { + if (old.get_bend() != mpe.get_bend()) { + partner->sendControlChange(EM_SettingsChannel, EMCC_BendRange, mpe.get_bend()); + } + if (old.get_y() != mpe.get_y()) { + partner->sendControlChange(EM_SettingsChannel, EMCC_YCC, U8(mpe.get_y())); + } + if (old.get_z() != mpe.get_z()) { + partner->sendControlChange(EM_SettingsChannel, EMCC_ZCC, U8(mpe.get_z())); + } + } + } + } + return partner; +} + +Hc1Module* PolyMidiModule::processXBend(Hc1Module* partner) +{ + uint8_t knob_bend = static_cast(getParamQuantity(P_X_BEND))->getBendValue(); + if (knob_bend != mpe.get_bend()) { + mpe.set_bend_checked(knob_bend); + if (!partner) partner = getPartner(); + if (partner) { + partner->em.mpe = mpe; + if (partner->readyToSend()) { + partner->sendControlChange(EM_SettingsChannel, EMCC_BendRange, mpe.get_bend()); + } + } + } + return partner; +} + +Hc1Module* PolyMidiModule::processY(Hc1Module* partner) +{ + auto knob_y = static_cast*>(getParamQuantity(P_Y))->getItemValue(); + if (knob_y != mpe.get_y()) { + mpe.set_y_checked(knob_y); + if (!partner) partner = getPartner(); + if (partner) { + partner->em.mpe = mpe; + if (partner->readyToSend()) { + partner->sendControlChange(EM_SettingsChannel, EMCC_YCC, U8(mpe.get_y())); + } + } + } + return partner; +} + +Hc1Module* PolyMidiModule::processZ(Hc1Module* partner) +{ + auto knob_z = static_cast*>(getParamQuantity(P_Z))->getItemValue(); + if (knob_z == EMZ::Midi_ChannelPressure) { + // knob clips to MidiChannelPressure, so we must + // translate to the unclipped value per em mode + switch (mpe.mode()) { + case MidiMode::Midi: break; + case MidiMode::Mpe: knob_z = EMZ::Mpe_ChannelPressure; break; + case MidiMode::MpePlus: knob_z = EMZ::MpePlus_ChannelPressure; break; + } + } + if (knob_z != mpe.get_z()) { + mpe.set_z_checked(knob_z); + assert(mpe.get_z() == knob_z); // may need to handle side effects + if (!partner) partner = getPartner(); + if (partner) { + partner->em.mpe = mpe; + if (partner->readyToSend()) { + partner->sendControlChange(EM_SettingsChannel, EMCC_ZCC, U8(mpe.get_z())); + } + } + } + return partner; +} + +Hc1Module* PolyMidiModule::processVelocity(Hc1Module* partner) +{ + auto knob_velocity = static_cast(GetByteParamValue(getParamQuantity(P_VELOCITY))); + if (knob_velocity != velocity.getVelocity()) { + velocity.setVelocity(knob_velocity); + if (!partner) partner = getPartner(); + if (partner) { + partner->em.velocity = velocity; + if (partner->readyToSend()) { + partner->sendControlChange(EM_SettingsChannel, EMCC_VelSplit, velocity.vs); + } + } + } + return partner; +} + +void PolyMidiModule::process(const ProcessArgs& args) +{ + Hc1Module* partner = nullptr; + if (0 == ((args.frame + id) % CV_INTERVAL)) { + partner = processPolyphony(partner); + partner = processPriority(partner); + partner = processMpe(partner); + partner = processXBend(partner); + partner = processY(partner); + partner = processZ(partner); + partner = processVelocity(partner); + partner = processRouting(partner); + } +} + +} + +Model *modelPolyMidi = createModel("pachde-hc-polymidi"); \ No newline at end of file diff --git a/src/PolyMidi/PolyMidi.hpp b/src/PolyMidi/PolyMidi.hpp new file mode 100644 index 0000000..7d53eec --- /dev/null +++ b/src/PolyMidi/PolyMidi.hpp @@ -0,0 +1,144 @@ +#pragma once +#ifndef POLYMIDI_HPP_INCLUDED +#define POLYMIDI_HPP_INCLUDED +#include "bend_param.hpp" +#include "../hc_events.hpp" +#include "../module_broker.hpp" +#include "../plugin.hpp" +#include "../widgets/label_widget.hpp" +#include "../widgets/partner_picker.hpp" +#include "../widgets/symbol_widget.hpp" +#include "mpe_burger.hpp" +namespace pachde { + +using Symbol = SymbolWidget::Symbol; + + +struct PolyMidiModule : Module, IHandleHcEvents +{ + enum Params + { + P_POLY, + P_EXPAND, + P_COMPUTE, + P_MPE, + P_X_BEND, + P_Y, + P_Z, + P_PRI, + P_VELOCITY, + P_ROUTE_SURFACE_MIDI, + P_ROUTE_SURFACE_DSP, + P_ROUTE_SURFACE_CVC, + P_ROUTE_MIDI_MIDI, + P_ROUTE_MIDI_DSP, + P_ROUTE_MIDI_CVC, + NUM_PARAMS, + }; + enum Inputs + { + NUM_INPUTS + }; + enum Outputs + { + NUM_OUTPUTS + }; + enum Lights + { + L_EXPAND, + L_COMPUTE, + L_ROUTE_SURFACE_MIDI, + L_ROUTE_SURFACE_DSP, + L_ROUTE_SURFACE_CVC, + L_ROUTE_MIDI_MIDI, + L_ROUTE_MIDI_DSP, + L_ROUTE_MIDI_CVC, + NUM_LIGHTS + }; + + PartnerBinding partner_binding; + + IHandleHcEvents * ui_event_sink = nullptr; + const int CV_INTERVAL = 128; + + PolyMidiModule(); + virtual ~PolyMidiModule(); + Hc1Module * getPartner(); + + uint8_t routing; + Mpe mpe; + Polyphony polyphony; + NotePriority priority; + VelocitySplit velocity; + + uint8_t getKnobRouting(); + void setRoutingLights(); + + // IHandleHcEvents + // void onPresetChanged(const PresetChangedEvent& e) override; + // void onRoundingChanged(const RoundingChangedEvent& e) override; + // void onPedalChanged(const PedalChangedEvent& e) override; + void onRoutingChanged(const RoutingChangedEvent& e) override; + void onPolyphonyChanged(const PolyphonyChangedEvent& e) override; + void onNotePriorityChanged(const NotePriorityChangedEvent& e) override; + void onMpeChanged(const MpeChangedEvent& e) override; + void onVelocityChanged(const VelocityChangedEvent& e) override; + void onDeviceChanged(const DeviceChangedEvent& e) override; + void onDisconnect(const DisconnectEvent& e) override; + // void onFavoritesFileChanged(const FavoritesFileChangedEvent& e) override; + + Hc1Module * processPolyphony(Hc1Module *partner); + Hc1Module * processPriority(Hc1Module *partner); + Hc1Module * processRouting(Hc1Module *partner); + Hc1Module * processMpe(Hc1Module *partner); + Hc1Module * processXBend(Hc1Module *partner); + Hc1Module * processY(Hc1Module *partner); + Hc1Module * processZ(Hc1Module *partner); + Hc1Module * processVelocity(Hc1Module *partner); + + // Module + json_t *dataToJson() override; + void dataFromJson(json_t *root) override; + void process(const ProcessArgs& args) override; +}; + +struct PolyMidiModuleWidget : ModuleWidget, IHandleHcEvents +{ + PolyMidiModule * my_module; + PartnerPicker* partner_picker = nullptr; + DynamicTextLabel* poly_text = nullptr; + DynamicTextLabel* priority_text = nullptr; + MpeBurger* midi_ui = nullptr; + DynamicTextLabel* midi_text = nullptr; + + explicit PolyMidiModuleWidget(PolyMidiModule * module); + + virtual ~PolyMidiModuleWidget() { + if (my_module) { + my_module->ui_event_sink = nullptr; + } + } + + Hc1Module * getPartner(); + + void createRouting(); + + // IHandleHcEvents + // void onPresetChanged(const PresetChangedEvent& e) override; + // void onRoundingChanged(const RoundingChangedEvent& e) override; + // void onFavoritesFileChanged(const FavoritesFileChangedEvent& e) override; + // void onRoutingChanged(const RoutingChangedEvent& e) override; + void onPolyphonyChanged(const PolyphonyChangedEvent& e) override; + void onNotePriorityChanged(const NotePriorityChangedEvent& e) override; + void onMpeChanged(const MpeChangedEvent& e) override; + //void onVelocityChanged(const VelocityChangedEvent& e) override; + void onDeviceChanged(const DeviceChangedEvent& e) override; + void onDisconnect(const DisconnectEvent& e) override; + + void appendContextMenu(Menu *menu) override; +}; + + + +} +#endif \ No newline at end of file diff --git a/src/PolyMidi/bend_param.hpp b/src/PolyMidi/bend_param.hpp new file mode 100644 index 0000000..867570a --- /dev/null +++ b/src/PolyMidi/bend_param.hpp @@ -0,0 +1,59 @@ +#pragma once +#ifndef BEND_PARAM_HPP_INCLUDED +#define BEND_PARAM_HPP_INCLUDED +#include +#include "../em.hpp" +using namespace ::rack; +namespace pachde { + +struct BendParamQuantity : engine::ParamQuantity +{ + bool is_mpe; + + uint8_t getBendValue() { + return U8(std::round(getValue())); + } + + std::string getDisplayValueString() override { + auto value = getBendValue(); + if (value <= 96) { + return format_string("%d", value); + } else { + int ch1Bend = std::max(1, value - 96); + return format_string("96:%d", ch1Bend); + } + } + + void setValue(float v) override { + if (is_mpe && v < 12.f) { v = 12.f; } + ParamQuantity::setValue(v); + } + +}; + +template +TPQ * configBendParam(Module * module, int paramId) +{ + assert(paramId >= 0 && static_cast(paramId) < module->params.size() && static_cast(paramId) < module->paramQuantities.size()); + if (module->paramQuantities[paramId]) { + delete module->paramQuantities[paramId]; + } + TPQ* q = new TPQ; + q->module = module; + q->paramId = paramId; + q->minValue = 1.f; + q->maxValue = 120.f; + q->name = "Pitch Bend Range"; + q->is_mpe = true; + q->snapEnabled = true; + + module->paramQuantities[paramId] = q; + + Param* p = &module->params[paramId]; + p->value = q->getDefaultValue(); + + return q; +} + +} +#endif \ No newline at end of file diff --git a/src/PolyMidi/mpe_burger.hpp b/src/PolyMidi/mpe_burger.hpp new file mode 100644 index 0000000..135b119 --- /dev/null +++ b/src/PolyMidi/mpe_burger.hpp @@ -0,0 +1,69 @@ +#pragma once +#ifndef MPE_BURGERM_HPP_INCLUDED +#define MPE_BURGER_HPP_INCLUDED +#include +#include "../em_types/em_mpe.hpp" +#include "../text.hpp" +#include "../widgets/hamburger.hpp" +#include "../widgets/label_widget.hpp" +using namespace ::rack; +using namespace ::eagan_matrix; + +namespace pachde { + +struct MpeBurger : Hamburger +{ + StaticTextLabel* tw; + MidiMode mode; + ParamQuantity* pq; + + MpeBurger() : tw(nullptr), pq(nullptr) + { + setMode(MidiMode::MpePlus); + } + + void setLabel(StaticTextLabel* label) { + tw = label; + } + void setParam(ParamQuantity* param) { + pq = param; + } + + void setMode(MidiMode new_mode) { + mode = new_mode; + switch (mode) { + default: + case MidiMode::Midi: + describe("Midi"); + if (tw) tw->text("Midi"); + if (pq) pq->setValue(U8(MidiMode::Midi)); + break; + case MidiMode::Mpe: + describe("MPE"); + if (tw) tw->text("MPE"); + if (pq) pq->setValue(U8(MidiMode::Mpe)); + break; + case MidiMode::MpePlus: + describe("MPE+"); + if (tw) tw->text("MPE+"); + if (pq) pq->setValue(U8(MidiMode::MpePlus)); + break; + } + } + + void step() override { + if (!pq) return; + mode = static_cast(GetByteParamValue(pq)); + } + + void appendContextMenu(Menu * menu) override + { + menu->addChild(new MenuSeparator()); + menu->addChild(createCheckMenuItem("MIDI", "", [=](){ return MidiMode::Midi == mode; }, [=](){ setMode(MidiMode::Midi); })); + menu->addChild(createCheckMenuItem("MPE", "", [=](){ return MidiMode::Mpe == mode; }, [=](){ setMode(MidiMode::Mpe); })); + menu->addChild(createCheckMenuItem("MPE+", "", [=](){ return MidiMode::MpePlus == mode; }, [=](){ setMode(MidiMode::MpePlus); })); + } +}; + +} +#endif \ No newline at end of file diff --git a/src/Round/Round-ui.cpp b/src/Round/Round-ui.cpp index 9720826..8aa788f 100644 --- a/src/Round/Round-ui.cpp +++ b/src/Round/Round-ui.cpp @@ -17,14 +17,6 @@ constexpr const float KNOB_SPREAD = 80.f; constexpr const float LIGHT_SPREAD = 4.f; constexpr const float CENTER = 22.5f; -inline uint8_t GetSmallParamValue(rack::app::ModuleWidget* w, int id, uint8_t default_value = 0) { - auto p = w->getParam(id); - if (!p) return default_value; - auto pq = p->getParamQuantity(); - if (!pq) return default_value; - return U8(pq->getValue()); -} - RoundModuleWidget::RoundModuleWidget(RoundModule * module) : my_module(module) { diff --git a/src/common_layout.hpp b/src/common_layout.hpp index 9aa996c..52728e8 100644 --- a/src/common_layout.hpp +++ b/src/common_layout.hpp @@ -13,8 +13,8 @@ constexpr const float PARTNER_TOP = 14.f; constexpr const float PARTNER_WIDTH = 180.f; // Vertical knob+CV layout -constexpr const float VK_REL_OFFSET = 14.f; -constexpr const float VK_REL_VOFFSET = 15.f; +constexpr const float VK_REL_OFFSET = 13.f; +constexpr const float VK_REL_VOFFSET = 16.25f; constexpr const float VK_CV_VOFFSET = 23.f; } #endif \ No newline at end of file diff --git a/src/em.hpp b/src/em.hpp index 78fd326..30e6a83 100644 --- a/src/em.hpp +++ b/src/em.hpp @@ -3,11 +3,15 @@ #define EM_HPP_INCLUDED #include "em_midi.hpp" #include "em_types/em_compressor.hpp" +#include "em_types/em_mpe.hpp" #include "em_types/em_pedal.hpp" +#include "em_types/em_polyphony.hpp" +#include "em_types/em_priority.hpp" #include "em_types/em_recirculator.hpp" #include "em_types/em_rounding.hpp" #include "em_types/em_tilteq.hpp" #include "em_types/em_tuning.hpp" +#include "em_types/em_velocity_split.hpp" namespace eagan_matrix { @@ -25,11 +29,15 @@ struct EaganMatrix PedalInfo pedal2; bool reverse_surface; uint8_t global_ActionAesMenuRecirc; + uint8_t routing; + Mpe mpe; + Polyphony polyphony; + NotePriority priority; + VelocitySplit velocity; // levels (PreLevel/PostLevel/AudioIn/LineOut/HeadphoneOut) // convolution // preserve - // routing EaganMatrix() : firmware_version(0), @@ -38,7 +46,8 @@ struct EaganMatrix middle_c(60), pedal1(0), pedal2(1), - reverse_surface(false) + reverse_surface(false), + routing(static_cast(EM_ROUTE_BITS::DefaultRoute)) { } @@ -52,6 +61,9 @@ struct EaganMatrix pedal2 = PedalInfo(1); recirculator.clear(); reverse_surface = false; + routing = static_cast(EM_ROUTE_BITS::DefaultRoute); + polyphony = Polyphony(1); + mpe.clear(); } }; diff --git a/src/em_midi.hpp b/src/em_midi.hpp index 8aa230c..c81516d 100644 --- a/src/em_midi.hpp +++ b/src/em_midi.hpp @@ -136,18 +136,65 @@ constexpr const uint8_t EMCC_Category = 32; constexpr const uint8_t EMCC_ActionAesMenuRecirc = 33; constexpr const uint8_t EMCC_Routing = 36; +enum EM_ROUTE_BITS : uint8_t { + Surface_Midi = 0x01, // route playing surface to Midi Out + Surface_Dsp = 0x02, // route playing surface to internal sounds + Surface_Cvc = 0x04, // route playing surface to CVC + Midi_Midi = 0x08, // route Midi In to Midi Out + Midi_Dsp = 0x10, // route Midi In to internal sounds + Midi_Cvc = 0x20, // route Midi In to CVC + DefaultRoute = 63, // default is to set all routing bits +}; + constexpr const uint8_t EMCC_PedalType = 37; constexpr const uint8_t EMCC_Polyphony = 39; constexpr const uint8_t EMCC_BendRange = 40; //MPE_MIN=12, default|max=96 -constexpr const uint8_t EMCC_YCC = 41; //0=none, 127 = no_shelf -constexpr const uint8_t EMCC_ZCC = 42; //0=none 11=default(Expression) 69=channel pressure, 70=MPE+, 127=MPE + +constexpr const uint8_t EMCC_YCC = 41; +enum class EMY : uint8_t { + None = 0, + CC_1_Modulation = 1, + CC_2_Breath = 2, + CC_3 = 3, + CC_4_Foot = 4, + CC_7_Volume = 7, + CC_11_Expression = 11, + CC_74_Bright = 74, + Default_Y = CC_74_Bright, + CC_74_NoShelf = 127 +}; + +constexpr const uint8_t EMCC_ZCC = 42; +enum class EMZ : uint8_t { + None = 0, + CC_1_Modulation = 1, + CC_2_Breath = 2, + CC_3 = 3, + CC_4_Foot = 4, + CC_7_Volume = 7, + CC_11_Expression = 11, + Default_Zcc = CC_11_Expression, + Midi_ChannelPressure = 69, + MpePlus_ChannelPressure = 70, + Mpe_ChannelPressure = 127 +}; +constexpr const uint8_t EMCC_VelSplit = 43; // includes split mode, but that is being removed +enum class EM_Velocity : uint8_t { + Static127 = 0, + Dynamic = 1, + FormulaV = 2, + NoNotes = 3, + Theremin = 4, + Kyma = 5, + MASK = 0x07 +}; constexpr const uint8_t EMCC_MiddleC = 44; constexpr const uint8_t EMCC_SplitPoint = 45; constexpr const uint8_t EMCC_MonoFunction = 46; //ctlReciCol constexpr const uint8_t EMCC_MonoInterval = 48; -constexpr const uint8_t EMCC_Priority = 49; +constexpr const uint8_t EMCC_Priority = 49; // includes NotePriority comp rate, immediate/toggle oct shift constexpr const uint8_t EMCC_TuningGrid = 51; constexpr const uint8_t EMCC_Pedal1CC = 52; constexpr const uint8_t EMCC_Pedal2CC = 53; diff --git a/src/em_types/em_compressor.hpp b/src/em_types/em_compressor.hpp index dc8adeb..c2cdff5 100644 --- a/src/em_types/em_compressor.hpp +++ b/src/em_types/em_compressor.hpp @@ -10,7 +10,7 @@ namespace eagan_matrix { struct Compressor { - uint8_t threshold; // EMCC_CompressorThreshold = 90; + uint8_t threshold; // EMCC_CompressorThreshold = 90; uint8_t attack; // EMCC_CompressorAttack = 91; uint8_t ratio; // EMCC_CompressorRatio = 92; uint8_t mix; // EMCC_CompressorMix = 93; diff --git a/src/em_types/em_mpe.hpp b/src/em_types/em_mpe.hpp new file mode 100644 index 0000000..56f2f76 --- /dev/null +++ b/src/em_types/em_mpe.hpp @@ -0,0 +1,150 @@ +#pragma once +#ifndef EM_MPE_HPP_INCLUDED +#define EM_MPE_HPP_INCLUDED +#include +#include "../misc.hpp" +#include "../em_midi.hpp" +using namespace ::rack; +using namespace ::em_midi; +namespace eagan_matrix { + +enum class MidiMode : uint8_t { + Midi, + Mpe, + MpePlus +}; + +inline bool is_z_mpe(EMZ the_z) { return the_z > EMZ::Midi_ChannelPressure; } + +struct Mpe +{ + Mpe() + : x_bend(96), + y(EMY::Default_Y), + z(EMZ::MpePlus_ChannelPressure) + { + } + + void clear() + { + x_bend = 96; + y = EMY::Default_Y; + z = EMZ::MpePlus_ChannelPressure; + } + + bool operator ==(const Mpe& rhs) const { + return x_bend == rhs.x_bend + && y == rhs.y + && z == rhs.z; + } + bool operator !=(const Mpe& rhs) const { + return x_bend != rhs.x_bend + || y != rhs.y + || z != rhs.z; + } + + MidiMode mode() const { + switch (z) { + case EMZ::Mpe_ChannelPressure: return MidiMode::Mpe; + case EMZ::MpePlus_ChannelPressure: return MidiMode::MpePlus; + default: return MidiMode::Midi; + } + } + bool is_any_mpe() const { return is_z_mpe(z); } + bool is_mpe() const { return z == EMZ::Mpe_ChannelPressure; } + bool is_mpe_plus() const { return z == EMZ::MpePlus_ChannelPressure; } + bool is_channel_pressure() const { return z >= EMZ::Midi_ChannelPressure; } + uint8_t get_bend() const { return x_bend; } + EMY get_y() const { return y; } + EMZ get_z() const { return z; } + + std::string bend_display() + { + if (x_bend <= 96) { + return pachde::format_string("%d", x_bend); + } else { + int ch1Bend = std::max(1, x_bend - 96); + return pachde::format_string("96:%d", ch1Bend); + } + } + + void set_bend_checked(uint8_t bend) + { + x_bend = is_any_mpe() ? std::max(U8(12), bend) : bend; + } + + void set_bend_with_side_effects(uint8_t bend) + { + if (bend < 12 && is_any_mpe()) { + z = EMZ::Midi_ChannelPressure; + } + x_bend = bend; + } + + void set_y_checked(EMY new_y) + { + y = is_any_mpe() ? EMY::Default_Y : new_y; + } + + void set_y_with_side_effects(EMY new_y) + { + if (new_y != EMY::Default_Y && is_any_mpe()) { + z = EMZ::Midi_ChannelPressure; + } + y = new_y; + } + + void set_z_checked(EMZ new_z) + { + if ((y == EMY::Default_Y) && (x_bend >= 12) && is_z_mpe(new_z)) { + z = new_z; + } else { + z = new_z; + } + } + + void set_z_with_side_effects(EMZ new_z) + { + switch (new_z) { + case EMZ::Midi_ChannelPressure: set_mode(MidiMode::Midi); break; + case EMZ::MpePlus_ChannelPressure: set_mode(MidiMode::MpePlus); break; + case EMZ::Mpe_ChannelPressure: set_mode(MidiMode::Mpe); break; + default: + z = new_z; + break; + } + } + + void set_mode(MidiMode new_mode) + { + if (new_mode == mode()) return; + switch (new_mode) + { + case MidiMode::Midi: + if (is_any_mpe()) { + z = EMZ::Midi_ChannelPressure; + } + break; + + case MidiMode::Mpe: + x_bend = std::max(U8(12), x_bend); + y = EMY::Default_Y; + z = EMZ::Mpe_ChannelPressure; + break; + + case MidiMode::MpePlus: + x_bend = std::max(U8(12), x_bend); + y = EMY::Default_Y; + z = EMZ::MpePlus_ChannelPressure; + break; + } + } + +private: + uint8_t x_bend; + EMY y; + EMZ z; +}; + +} +#endif \ No newline at end of file diff --git a/src/em_types/em_polyphony.hpp b/src/em_types/em_polyphony.hpp new file mode 100644 index 0000000..fded903 --- /dev/null +++ b/src/em_types/em_polyphony.hpp @@ -0,0 +1,28 @@ +#pragma once +#ifndef EM_POLYPHONY_HPP_INCLUDED +#define EM_POLYPHONY_HPP_INCLUDED +#include +#include "../misc.hpp" +using namespace ::rack; +namespace eagan_matrix { + +struct Polyphony +{ + uint8_t raw; + + Polyphony() : raw(1) { } + explicit Polyphony(uint8_t b) : raw(b) { } + + static Polyphony from_raw(uint8_t b) { return Polyphony(b); } + uint8_t to_raw() const { return raw; } + void set_raw(uint8_t b) { raw = b; } + + uint8_t polyphony() const { return raw & 0x1f; } + void set_polyphony(uint8_t voices) { raw = voices | (raw & 0x40); } + + bool expanded_polyphony() const { return raw & 0x40; } + void set_expanded_polyphony(bool expanded) { raw = (raw & ~0x40) | (expanded * 0x40); } +}; + +} +#endif \ No newline at end of file diff --git a/src/em_types/em_priority.cpp b/src/em_types/em_priority.cpp new file mode 100644 index 0000000..587d4ea --- /dev/null +++ b/src/em_types/em_priority.cpp @@ -0,0 +1,21 @@ +#include "em_priority.hpp" + +namespace eagan_matrix { + +const char * NotePriorityName(NotePriorityType pri) +{ + switch (pri) + { + case NotePriorityType::LRU: return "Oldest"; + case NotePriorityType::LRR: return "Same note"; + case NotePriorityType::LCN: return "Lowest"; + case NotePriorityType::HI1: return "Highest"; + case NotePriorityType::HI2: return "Highest 2"; + case NotePriorityType::HI3: return "Highest 3"; + case NotePriorityType::HI4: return "Highest 4"; + default: return "(unknown)"; + } +} + + +} \ No newline at end of file diff --git a/src/em_types/em_priority.hpp b/src/em_types/em_priority.hpp new file mode 100644 index 0000000..2aab486 --- /dev/null +++ b/src/em_types/em_priority.hpp @@ -0,0 +1,44 @@ +#pragma once +#ifndef EM_PRIORITY_HPP_INCLUDED +#define EM_PRIORITY_HPP_INCLUDED +#include +#include "../misc.hpp" +using namespace ::rack; +namespace eagan_matrix { + +enum class NotePriorityType: uint8_t { + LRU, + LRR, + LCN, + HI1, + HI2, + HI3, + HI4 +}; + +const char * NotePriorityName(NotePriorityType pri); + +struct NotePriority +{ + uint8_t raw; + + NotePriority() : raw(0) { } + explicit NotePriority(uint8_t b) : raw(b) { } + + static NotePriority from_raw(uint8_t b) { return NotePriority(b); } + uint8_t to_raw() const { return raw; } + void set_raw(uint8_t b) { raw = b; } + + NotePriorityType priority() const { return static_cast((raw & 0x1Cu) >> 2u); } + void set_priority(NotePriorityType pri) { raw = (raw & ~0x1Cu) | ((U8(pri) << 2u) & 0x1Cu); } + + bool increased_computation() const { return raw & 1; } + void set_increased_computation(bool comp) { raw = (raw & ~1) | comp; } + + bool octave_toggle() const { return raw & 0x60; } + void set_octave_toggle(bool toggle) { raw = (raw & ~0x60u) | (toggle * 0x60); } + +}; + +} +#endif \ No newline at end of file diff --git a/src/em_types/em_velocity_split.hpp b/src/em_types/em_velocity_split.hpp new file mode 100644 index 0000000..5eeb78f --- /dev/null +++ b/src/em_types/em_velocity_split.hpp @@ -0,0 +1,35 @@ +// +// EMCC_VelSplit velocity (note processing) and split modes +// +#pragma once +#ifndef EM_VELOCITY_SPLIT_HPP_INCLUDED +#define EM_VELOCITY_SPLIT_HPP_INCLUDED +#include +#include "../misc.hpp" +#include "../em_midi.hpp" +using namespace ::rack; +using namespace ::em_midi; +namespace eagan_matrix { + +namespace VelocitySplit_private { +constexpr const uint8_t MASK = U8(EM_Velocity::MASK); +}; + +struct VelocitySplit +{ + uint8_t vs; + + bool operator == (const VelocitySplit& rhs) const { return vs == rhs.vs; } + bool operator != (const VelocitySplit& rhs) const { return vs != rhs.vs; } + + void setVelocity(EM_Velocity vel) { + vs = (vs & ~VelocitySplit_private::MASK) | U8(vel); + } + uint8_t getVelocityByte() const { return vs & VelocitySplit_private::MASK; } + EM_Velocity getVelocity() const { + return static_cast(getVelocityByte()); + } + // Split not implemented: it's being removed from the next Haken firmware release +}; +} +#endif \ No newline at end of file diff --git a/src/hc_events.hpp b/src/hc_events.hpp index 10b3a29..5c24aeb 100644 --- a/src/hc_events.hpp +++ b/src/hc_events.hpp @@ -45,6 +45,31 @@ struct IHandleHcEvents }; virtual void onDeviceChanged(const DeviceChangedEvent& e) {} + struct RoutingChangedEvent { + const uint8_t routing; + }; + virtual void onRoutingChanged(const RoutingChangedEvent& e) {} + + struct PolyphonyChangedEvent { + const eagan_matrix::Polyphony polyphony; + }; + virtual void onPolyphonyChanged(const PolyphonyChangedEvent& e) {} + + struct NotePriorityChangedEvent { + const eagan_matrix::NotePriority piority; + }; + virtual void onNotePriorityChanged(const NotePriorityChangedEvent& e) {} + + struct MpeChangedEvent { + const eagan_matrix::Mpe& mpe; + }; + virtual void onMpeChanged(const MpeChangedEvent& e) {} + + struct VelocityChangedEvent { + const eagan_matrix::VelocitySplit & velocity; + }; + virtual void onVelocityChanged(const VelocityChangedEvent& e) {} + struct DisconnectEvent { }; virtual void onDisconnect(const DisconnectEvent& e) {} diff --git a/src/misc.hpp b/src/misc.hpp index 6f101b8..9e5727a 100644 --- a/src/misc.hpp +++ b/src/misc.hpp @@ -38,6 +38,18 @@ inline bool in_range_limit(T value, T minimum, T limit) { return minimum <= valu bool GetBool(const json_t* root, const char* key, bool default_value); float GetFloat(const json_t* root, const char* key, float default_value); +inline uint8_t GetByteParamValue(rack::ParamQuantity* pq, uint8_t default_value = 0) { + if (!pq) return default_value; + return U8(std::round(pq->getValue())); +} + +inline uint8_t GetSmallParamValue(rack::app::ModuleWidget* w, int id, uint8_t default_value = 0) { + auto p = w->getParam(id); + if (!p) return default_value; + return GetByteParamValue(p->getParamQuantity(), default_value); +} + + enum class InitState : uint8_t { Uninitialized, Pending, diff --git a/src/plugin.cpp b/src/plugin.cpp index 75e8145..c7e1650 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -13,7 +13,8 @@ void init(Plugin *p) p->addModel(modelPedal2); p->addModel(modelRound); p->addModel(modelCompress); - + p->addModel(modelPolyMidi); + // Any other plugin initialization may go here. // As an alternative, consider lazy-loading assets and lookup tables when your module is created to reduce startup times of Rack. } diff --git a/src/plugin.hpp b/src/plugin.hpp index d8b64de..65542a1 100644 --- a/src/plugin.hpp +++ b/src/plugin.hpp @@ -11,4 +11,5 @@ extern Model* modelPedal1; extern Model* modelPedal2; extern Model* modelRound; extern Model* modelCompress; +extern Model* modelPolyMidi; diff --git a/src/widgets/enum_param.hpp b/src/widgets/enum_param.hpp new file mode 100644 index 0000000..b091813 --- /dev/null +++ b/src/widgets/enum_param.hpp @@ -0,0 +1,72 @@ +#pragma once +#ifndef INDEX_PARAM_HPP_INCLUDED +#define INDEX_PARAM_HPP_INCLUDED +#include +#include "../misc.hpp" +using namespace ::rack; +namespace pachde { + +template +struct EnumQuantity : SwitchQuantity +{ + std::vector map; + bool clip_max; // clip to max value (else default value) + + EnumQuantity( + std::vector items, + std::vector labels = {}) + : map(items), clip_max(false) + { + SwitchQuantity::labels = labels; + } + + TEnum getItemValue() { + return getItemValue(getValue()); + } + TEnum getItemValue(float index_value) { + size_t index = static_cast(std::round(clamp(index_value, minValue, maxValue))); + return map[index]; + } + void setEnumValue(TEnum evalue) { + auto it = std::find(map.cbegin(), map.cend(), evalue); + if (it == map.cend()) { + setValue(clipToMax ? maxValue : defaultValue); + } else { + setValue(static_cast(std::distance(map.cbegin(), it))); + } + } +}; + +template +EnumQuantity* configEnumParam( + int paramId, + std::string name, + Module* module, + TEnum defaultValue, + std::vector items, + std::vector labels, + bool clipToMax = false /* otherwise out-of range values are set to default */ + ) +{ + assert(paramId >= 0 && static_cast(paramId) < module->params.size() && static_cast(paramId) < module->paramQuantities.size()); + if (module->paramQuantities[paramId]) { + delete module->paramQuantities[paramId]; + } + auto q = new EnumQuantity(items, labels); + q->clip_max = clipToMax; + q->module = module; + q->paramId = paramId; + q->name = name; + q->minValue = 0.f; + q->maxValue = items.size() - 1; + q->setEnumValue(defaultValue); + q->defaultValue = q->getValue(); + q->snapEnabled = true; + q->smoothEnabled = false; + + module->paramQuantities[paramId] = q; + return q; +} + +} +#endif \ No newline at end of file diff --git a/src/widgets/hamburger.hpp b/src/widgets/hamburger.hpp new file mode 100644 index 0000000..3d7535a --- /dev/null +++ b/src/widgets/hamburger.hpp @@ -0,0 +1,45 @@ +#pragma once +#ifndef HAMBURGER_HPP_INCLUDED +#define HAMBURGER_HPP_INCLUDED +#include +#include "../colors.hpp" +#include "tip_widget.hpp" +using namespace ::rack; +namespace pachde { + +struct Hamburger : TipWidget +{ + uint8_t patties; + + Hamburger() : patties(3) + { + box.size.x = 10.f; + box.size.y = 10.f; + } + + void onButton(const ButtonEvent& e) override + { + TipWidget::onButton(e); + if (e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_LEFT && (e.mods & RACK_MOD_MASK) == 0) { + createContextMenu(); + e.consume(this); + } + } + + void draw(const DrawArgs& args) override + { + TipWidget::draw(args); + + auto vg = args.vg; + float y = 1.5f; + const float step = 2.5f; + const float line_width = 1.5f; + auto color = RampGray(G_90); + for (auto n = 0; n < patties; ++n) { + Line(vg, 1.5f, y, box.size.x - 1.5f, y, color, line_width); y += step; + } + } +}; + +} +#endif