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 @@
+
+
+
+
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