diff --git a/src-ui/CMakeLists.txt b/src-ui/CMakeLists.txt index 588fa809..9b5cff77 100644 --- a/src-ui/CMakeLists.txt +++ b/src-ui/CMakeLists.txt @@ -30,6 +30,7 @@ add_library(${PROJECT_NAME} STATIC components/multi/PartGroupSidebar.cpp components/multi/ProcessorPane.cpp components/multi/ProcessorPaneEQsFilters.cpp + components/multi/SingleMacroEditor.cpp connectors/SCXTResources.cpp connectors/JSONLayoutConsumer.cpp diff --git a/src-ui/components/SCXTEditor.h b/src-ui/components/SCXTEditor.h index 93817cdc..febef934 100644 --- a/src-ui/components/SCXTEditor.h +++ b/src-ui/components/SCXTEditor.h @@ -213,6 +213,10 @@ struct SCXTEditor : sst::jucegui::components::WindowPanel, juce::DragAndDropCont allProcessors = v; } + std::array, scxt::numParts> macroCache; + void onMacroFullState(const scxt::messaging::client::macroFullState_t &); + void onMacroValue(const scxt::messaging::client::macroValue_t &); + // Originate client to serialization messages void doSelectionAction(const selection::SelectionManager::ZoneAddress &, bool selecting, bool distinct, bool asLead); diff --git a/src-ui/components/SCXTEditorResponseHandlers.cpp b/src-ui/components/SCXTEditorResponseHandlers.cpp index c96567b2..09b13d93 100644 --- a/src-ui/components/SCXTEditorResponseHandlers.cpp +++ b/src-ui/components/SCXTEditorResponseHandlers.cpp @@ -285,6 +285,8 @@ void SCXTEditor::onSelectedPart(const int16_t p) selectedPart = p; // I presume I will shortly get structure messages so don't do anything else if (multiScreen && multiScreen->parts) multiScreen->parts->selectedPartChanged(); + if (multiScreen && multiScreen->sample) + multiScreen->sample->selectedPartChanged(); repaint(); } @@ -330,4 +332,16 @@ void SCXTEditor::onDebugInfoGenerated(const scxt::messaging::client::debugRespon SCLOG(k << " " << s); } } + +void SCXTEditor::onMacroFullState(const scxt::messaging::client::macroFullState_t &s) +{ + const auto &[part, index, macro] = s; + macroCache[part][index] = macro; + multiScreen->sample->macroDataChanged(part, index); +} + +void SCXTEditor::onMacroValue(const scxt::messaging::client::macroValue_t &) +{ + SCLOG_WFUNC("Implement"); +} } // namespace scxt::ui \ No newline at end of file diff --git a/src-ui/components/multi/MappingPane.cpp b/src-ui/components/multi/MappingPane.cpp index f7450c2b..85d8fa74 100644 --- a/src-ui/components/multi/MappingPane.cpp +++ b/src-ui/components/multi/MappingPane.cpp @@ -27,6 +27,7 @@ #include "MappingPane.h" #include "components/SCXTEditor.h" +#include "components/multi/SingleMacroEditor.h" #include "datamodel/metadata.h" #include "selection/selection_manager.h" #include "sst/jucegui/components/DraggableTextEditableValue.h" @@ -38,6 +39,7 @@ #include "sst/jucegui/components/GlyphButton.h" #include "sst/jucegui/components/TabbedComponent.h" #include "sst/jucegui/components/Viewport.h" +#include "sst/jucegui/components/Knob.h" #include "connectors/PayloadDataAttachment.h" #include "messaging/client/client_serial.h" #include "messaging/client/client_messages.h" @@ -2966,12 +2968,45 @@ void SampleWaveform::updateSamplePlaybackPosition(int64_t samplePos) struct MacroDisplay : HasEditor, juce::Component { - MacroDisplay(SCXTEditor *e) : HasEditor(e) {} - void paint(juce::Graphics &g) + std::array, scxt::macrosPerPart> macros; + MacroDisplay(SCXTEditor *e) : HasEditor(e) { - g.setColour(editor->themeColor(theme::ColorMap::warning_1a)); - g.setFont(editor->themeApplier.interMediumFor(25)); - g.drawText("Macro Region Coming Soon", getLocalBounds(), juce::Justification::centred); + for (int i = 0; i < scxt::macrosPerPart; ++i) + { + macros[i] = std::make_unique(editor, editor->selectedPart, i); + addAndMakeVisible(*macros[i]); + // grab whatever data we have + macroDataChanged(editor->selectedPart, i); + } + } + void resized() override + { + auto b = getLocalBounds(); + auto dx = b.getWidth() / scxt::macrosPerPart * 2; + auto dy = b.getHeight() / 2; + auto kr = b.withWidth(dx).withHeight(dy).reduced(3); + for (int i = 0; i < scxt::macrosPerPart; ++i) + { + macros[i]->setBounds(kr); + kr = kr.translated(dx, 0); + if (i == scxt::macrosPerPart / 2 - 1) + kr = kr.withX(3).translated(0, dy); + } + } + + void selectedPartChanged() + { + for (auto &m : macros) + { + m->changePart(editor->selectedPart); + } + repaint(); + } + + void macroDataChanged(int part, int index) + { + assert(part == editor->selectedPart); + macros[index]->updateFromEditorData(); } }; @@ -3059,4 +3094,10 @@ void MappingPane::updateSamplePlaybackPosition(size_t sampleIndex, int64_t sampl .waveform->updateSamplePlaybackPosition(samplePos); } +void MappingPane::selectedPartChanged() { macroDisplay->selectedPartChanged(); } +void MappingPane::macroDataChanged(int part, int index) +{ + assert(part == editor->selectedPart); + macroDisplay->macroDataChanged(part, index); +} } // namespace scxt::ui::multi diff --git a/src-ui/components/multi/MappingPane.h b/src-ui/components/multi/MappingPane.h index 70cba28a..557b5096 100644 --- a/src-ui/components/multi/MappingPane.h +++ b/src-ui/components/multi/MappingPane.h @@ -51,6 +51,8 @@ struct MappingPane : sst::jucegui::components::NamedPanel, HasEditor void setMappingData(const engine::Zone::ZoneMappingData &); void setSampleData(const engine::Zone::AssociatedSampleSet &); void setGroupZoneMappingSummary(const engine::Part::zoneMappingSummary_t &); + void selectedPartChanged(); + void macroDataChanged(int part, int index); void editorSelectionChanged(); void setActive(bool b); diff --git a/src-ui/components/multi/ModPane.cpp b/src-ui/components/multi/ModPane.cpp index b7436d75..d70fe749 100644 --- a/src-ui/components/multi/ModPane.cpp +++ b/src-ui/components/multi/ModPane.cpp @@ -251,19 +251,32 @@ template struct ModRow : juce::Component, HasEditor target->setLabel("Target"); curve->setLabel("-"); + auto makeSourceName = [](auto &si, auto &sn) { + // This is the second location where we are assuming default macro name + // as mentioned in part.h + auto nm = sn.second; + + if (si.gid == 'zmac' || si.gid == 'gmac') + { + auto defname = "Macro " + std::to_string(si.index + 1); + if (nm != defname) + { + nm = "M" + std::to_string(si.index + 1) + ": " + nm; + } + } + + return nm; + }; + for (const auto &[si, sn] : srcs) { if (si == row.source) { - auto nm = /* sn.first + (sn.first.empty() ? "" : " - ") + */ sn.second; - - source->setLabel(nm); + source->setLabel(makeSourceName(si, sn)); } if (si == row.sourceVia) { - auto nm = /* sn.first + (sn.first.empty() ? "" : " - ") + */ sn.second; - - sourceVia->setLabel(nm); + sourceVia->setLabel(makeSourceName(si, sn)); } } @@ -441,6 +454,16 @@ template struct ModRow : juce::Component, HasEditor subTicked = true; auto nm = sn.second; + if (si.gid == 'gmac' || si.gid == 'zmac') + { + // This is where we are assuming default macro name + // from part.h + auto defName = "Macro " + std::to_string(si.index + 1); + if (nm != defName) + { + nm = defName + " (" + nm + ")"; + } + } sub.addItem(nm, true, selected, mkCallback(si)); } diff --git a/src-ui/components/multi/SingleMacroEditor.cpp b/src-ui/components/multi/SingleMacroEditor.cpp new file mode 100644 index 00000000..873233ba --- /dev/null +++ b/src-ui/components/multi/SingleMacroEditor.cpp @@ -0,0 +1,211 @@ +/* + * Shortcircuit XT - a Surge Synth Team product + * + * A fully featured creative sampler, available as a standalone + * and plugin for multiple platforms. + * + * Copyright 2019 - 2024, Various authors, as described in the github + * transaction log. + * + * ShortcircuitXT is released under the Gnu General Public Licence + * V3 or later (GPL-3.0-or-later). The license is found in the file + * "LICENSE" in the root of this repository or at + * https://www.gnu.org/licenses/gpl-3.0.en.html + * + * Individual sections of code which comprises ShortcircuitXT in this + * repository may also be used under an MIT license. Please see the + * section "Licensing" in "README.md" for details. + * + * ShortcircuitXT is inspired by, and shares code with, the + * commercial product Shortcircuit 1 and 2, released by VemberTech + * in the mid 2000s. The code for Shortcircuit 2 was opensourced in + * 2020 at the outset of this project. + * + * All source for ShortcircuitXT is available at + * https://github.com/surge-synthesizer/shortcircuit-xt + */ + +#include "SingleMacroEditor.h" +#include "components/SCXTEditor.h" +#include "connectors/PayloadDataAttachment.h" +#include "sst/jucegui/components/GlyphPainter.h" +#include "sst/jucegui/components/NamedPanel.h" +#include "messaging/client/client_messages.h" + +namespace scxt::ui::multi +{ +struct MacroValueAttachment : HasEditor, sst::jucegui::data::Continuous +{ + int part{-1}, index{-1}; + MacroValueAttachment(SCXTEditor *e, int p, int i) : HasEditor(e), part(p), index(i) {} + + const engine::Macro ¯o() const + { + assert(part >= 0 && part < scxt::numParts); + assert(index >= 0 && index < scxt::macrosPerPart); + return editor->macroCache[part][index]; + } + + engine::Macro ¯o() + { + assert(part >= 0 && part < scxt::numParts); + assert(index >= 0 && index < scxt::macrosPerPart); + return editor->macroCache[part][index]; + } + + std::string getLabel() const override { return macro().name; } + float getValue() const override { return macro().normalizedValue; } + void setValueFromGUI(const float &f) override + { + macro().normalizedValue = f; + sendToSerialization(scxt::messaging::client::SetMacroValue({part, index, f})); + editor->setTooltipContents(getLabel(), getValueAsString()); + } + void setValueFromModel(const float &) override {} + float getDefaultValue() const override { return 0; } +}; + +struct NarrowVerticalMenu : HasEditor, juce::Component +{ + bool isHovered{false}; + NarrowVerticalMenu(SCXTEditor *e) : HasEditor(e) {} + + void mouseEnter(const juce::MouseEvent &event) override + { + isHovered = true; + repaint(); + } + void mouseExit(const juce::MouseEvent &event) override + { + isHovered = false; + repaint(); + } + void mouseDown(const juce::MouseEvent &event) override + { + if (cb) + cb(); + } + void paint(juce::Graphics &g) override + { + auto b = getLocalBounds(); + b.expand((getHeight() - getWidth()) / 2, 0); + auto col = isHovered ? theme::ColorMap::generic_content_highest + : theme::ColorMap::generic_content_high; + sst::jucegui::components::GlyphPainter::paintGlyph( + g, b, sst::jucegui::components::GlyphPainter::ELLIPSIS_V, editor->themeColor(col)); + } + std::function cb; +}; +SingleMacroEditor::SingleMacroEditor(SCXTEditor *e, int p, int i) + : HasEditor(e), + sst::jucegui::style::StyleConsumer(sst::jucegui::components::NamedPanel::Styles::styleClass), + part(p), index(i) +{ + auto mb = std::make_unique(e); + addAndMakeVisible(*mb); + mb->cb = [w = juce::Component::SafePointer(this)]() { + if (w) + w->showMenu(); + }; + menuButton = std::move(mb); + knob = connectors::makeConnectedToDummy('mcro'); + addAndMakeVisible(*knob); + + knob->onBeginEdit = [w = juce::Component::SafePointer(this)]() { + if (!w) + return; + w->editor->showTooltip(*(w->knob)); + w->editor->setTooltipContents(w->valueAttachment->getLabel(), + w->valueAttachment->getValueAsString()); + }; + knob->onEndEdit = [w = juce::Component::SafePointer(this)]() { + if (!w) + return; + w->editor->hideTooltip(); + }; + knob->onIdleHover = knob->onBeginEdit; + knob->onIdleHoverEnd = knob->onEndEdit; + + valueAttachment = std::make_unique(editor, part, index); + knob->setSource(valueAttachment.get()); + + macroNameEditor = std::make_unique("Name"); + macroNameEditor->setText("Macro"); + macroNameEditor->setJustification(juce::Justification::centred); + macroNameEditor->addListener(this); + addAndMakeVisible(*macroNameEditor); +} + +SingleMacroEditor::~SingleMacroEditor() {} + +void SingleMacroEditor::changePart(int p) +{ + valueAttachment->part = p; + repaint(); +} + +void SingleMacroEditor::resized() +{ + auto b = getLocalBounds(); + knob->setBounds(b.withHeight(b.getWidth()).reduced(15)); + menuButton->setBounds(b.withWidth(12).withHeight(24).translated(0, 10)); + macroNameEditor->setBounds( + b.withTrimmedTop(b.getWidth() - 12 + 3).withHeight(18).reduced(3, 0)); +} + +void SingleMacroEditor::paint(juce::Graphics &g) +{ +#if 0 + g.setFont(12); + g.setColour(juce::Colours::red); + auto txt = std::to_string(part) + "/" + std::to_string(index); + g.drawText(txt, getLocalBounds(), juce::Justification::topRight); +#endif +} + +void SingleMacroEditor::showMenu() { SCLOG("Show Menu"); } +void SingleMacroEditor::onStyleChanged() +{ + // Update the text editor + macroNameEditor->setFont(editor->themeApplier.interMediumFor(12)); + macroNameEditor->setColour(juce::TextEditor::ColourIds::textColourId, + editor->themeColor(theme::ColorMap::generic_content_high)); + macroNameEditor->setColour(juce::TextEditor::ColourIds::backgroundColourId, + editor->themeColor(theme::ColorMap::bg_1)); + macroNameEditor->setColour(juce::TextEditor::ColourIds::outlineColourId, + juce::Colours::black.withAlpha(0.f)); + macroNameEditor->setColour(juce::TextEditor::ColourIds::focusedOutlineColourId, + juce::Colours::black.withAlpha(0.f)); + macroNameEditor->applyColourToAllText(editor->themeColor(theme::ColorMap::generic_content_high), + true); + macroNameEditor->applyFontToAllText(editor->themeApplier.interMediumFor(13), true); +} + +void SingleMacroEditor::updateFromEditorData() +{ + assert(part >= 0 && part < scxt::numParts); + assert(index >= 0 && index < scxt::macrosPerPart); + const auto ¯o = editor->macroCache[part][index]; + macroNameEditor->setText(macro.name, juce::NotificationType::dontSendNotification); +} +void SingleMacroEditor::textEditorReturnKeyPressed(juce::TextEditor &e) +{ + auto ¯o = editor->macroCache[part][index]; + macro.name = e.getText().toStdString(); + sendToSerialization(scxt::messaging::client::SetMacroFullState({part, index, macro})); +} + +void SingleMacroEditor::textEditorEscapeKeyPressed(juce::TextEditor &e) +{ + const auto ¯o = editor->macroCache[part][index]; + macroNameEditor->setText(macro.name, juce::NotificationType::dontSendNotification); +} + +void SingleMacroEditor::textEditorFocusLost(juce::TextEditor &e) +{ + auto ¯o = editor->macroCache[part][index]; + macro.name = e.getText().toStdString(); + sendToSerialization(scxt::messaging::client::SetMacroFullState({part, index, macro})); +} + +} // namespace scxt::ui::multi \ No newline at end of file diff --git a/src-ui/components/multi/SingleMacroEditor.h b/src-ui/components/multi/SingleMacroEditor.h new file mode 100644 index 00000000..159438c1 --- /dev/null +++ b/src-ui/components/multi/SingleMacroEditor.h @@ -0,0 +1,66 @@ +/* + * Shortcircuit XT - a Surge Synth Team product + * + * A fully featured creative sampler, available as a standalone + * and plugin for multiple platforms. + * + * Copyright 2019 - 2024, Various authors, as described in the github + * transaction log. + * + * ShortcircuitXT is released under the Gnu General Public Licence + * V3 or later (GPL-3.0-or-later). The license is found in the file + * "LICENSE" in the root of this repository or at + * https://www.gnu.org/licenses/gpl-3.0.en.html + * + * Individual sections of code which comprises ShortcircuitXT in this + * repository may also be used under an MIT license. Please see the + * section "Licensing" in "README.md" for details. + * + * ShortcircuitXT is inspired by, and shares code with, the + * commercial product Shortcircuit 1 and 2, released by VemberTech + * in the mid 2000s. The code for Shortcircuit 2 was opensourced in + * 2020 at the outset of this project. + * + * All source for ShortcircuitXT is available at + * https://github.com/surge-synthesizer/shortcircuit-xt + */ + +#ifndef SCXT_SRC_UI_COMPONENTS_MULTI_SINGLEMACROEDITOR_H +#define SCXT_SRC_UI_COMPONENTS_MULTI_SINGLEMACROEDITOR_H + +#include +#include "sst/jucegui/components/Knob.h" +#include "sst/jucegui/style/StyleAndSettingsConsumer.h" +#include "components/HasEditor.h" + +namespace scxt::ui::multi +{ +struct MacroValueAttachment; +struct SingleMacroEditor : HasEditor, + juce::Component, + juce::TextEditor::Listener, + sst::jucegui::style::StyleConsumer +{ + std::unique_ptr valueAttachment; + std::unique_ptr knob; + std::unique_ptr menuButton; + std::unique_ptr macroNameEditor; + SingleMacroEditor(SCXTEditor *e, int part, int index); + ~SingleMacroEditor(); + void resized() override; + void paint(juce::Graphics &g) override; + void showMenu(); + void updateFromEditorData(); + void changePart(int p); + + void textEditorReturnKeyPressed(juce::TextEditor &editor) override; + void textEditorEscapeKeyPressed(juce::TextEditor &editor) override; + void textEditorFocusLost(juce::TextEditor &editor) override; + + void onStyleChanged() override; + + private: + int16_t part{-1}, index{-1}; +}; +} // namespace scxt::ui::multi +#endif // SHORTCIRCUITXT_SINGLEMACROEDITOR_H diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index 02d823c2..158a4a54 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -962,6 +962,7 @@ void Engine::sendFullRefreshToClient() const sac.distinct = false; getSelectionManager()->selectAction(sac); } + getSelectionManager()->sendSelectedPartMacrosToClient(); } void Engine::clearAll() diff --git a/src/engine/group.cpp b/src/engine/group.cpp index 8e7d4598..4cffa134 100644 --- a/src/engine/group.cpp +++ b/src/engine/group.cpp @@ -290,6 +290,13 @@ engine::Engine *Group::getEngine() return nullptr; } +const engine::Engine *Group::getEngine() const +{ + if (parentPart && parentPart->parentPatch) + return parentPart->parentPatch->parentEngine; + return nullptr; +} + void Group::setupOnUnstream(const engine::Engine &e) { rePrepareAndBindGroupMatrix(); diff --git a/src/engine/group.h b/src/engine/group.h index 12d2790d..a37cbf36 100644 --- a/src/engine/group.h +++ b/src/engine/group.h @@ -87,6 +87,7 @@ struct Group : MoveableOnly, } outputInfo; Engine *getEngine(); + const Engine *getEngine() const; float output alignas(16)[2][blockSize << 1]; void attack(); diff --git a/src/engine/macros.cpp b/src/engine/macros.cpp index 5afe1858..ff1f7826 100644 --- a/src/engine/macros.cpp +++ b/src/engine/macros.cpp @@ -30,4 +30,9 @@ namespace scxt::engine { sst::basic_blocks::params::ParamMetaData Macro::getMetadata() const { return {}; } +void Macro::setNormalizedValue(float f) +{ + normalizedValue = f; + modulationValue = f; +} } // namespace scxt::engine \ No newline at end of file diff --git a/src/engine/macros.h b/src/engine/macros.h index 99ad86e2..ac4fea5c 100644 --- a/src/engine/macros.h +++ b/src/engine/macros.h @@ -38,6 +38,7 @@ struct Macro float modulationValue{0.f}; // Calcluated from normalized value bool isBipolar{false}; + std::string name{}; void setNormalizedValue(float f); diff --git a/src/engine/part.h b/src/engine/part.h index 3664c0dd..6b626d7b 100644 --- a/src/engine/part.h +++ b/src/engine/part.h @@ -50,6 +50,15 @@ struct Part : MoveableOnly, SampleRateSupport Part(int16_t c) : id(PartID::next()), partNumber(c), channel(c) { pitchBendSmoother.setTarget(0); + int idx{1}; + for (auto &m : macros) + { + // If you change this format you also need to change + // the two comparisons with default in src-ui/.../ModPane.cpp + // at the comments "assuming default macro name" + m.name = "Macro " + std::to_string(idx); + idx++; + } } virtual ~Part() = default; diff --git a/src/engine/zone.cpp b/src/engine/zone.cpp index f74350cf..3418eeab 100644 --- a/src/engine/zone.cpp +++ b/src/engine/zone.cpp @@ -161,6 +161,13 @@ engine::Engine *Zone::getEngine() return nullptr; } +const engine::Engine *Zone::getEngine() const +{ + if (parentGroup && parentGroup->parentPart && parentGroup->parentPart->parentPatch) + return parentGroup->parentPart->parentPatch->parentEngine; + return nullptr; +} + void Zone::initialize() { for (auto &v : voiceWeakPointers) diff --git a/src/engine/zone.h b/src/engine/zone.h index 361d9dd1..557a23bb 100644 --- a/src/engine/zone.h +++ b/src/engine/zone.h @@ -234,6 +234,7 @@ struct Zone : MoveableOnly, HasGroupZoneProcessors, SampleRateSuppor void setupOnUnstream(const engine::Engine &e); engine::Engine *getEngine(); + const engine::Engine *getEngine() const; sst::basic_blocks::dsp::UIComponentLagHandler mUILag; void onSampleRateChanged() override; diff --git a/src/json/engine_traits.h b/src/json/engine_traits.h index 5d271362..fef023f6 100644 --- a/src/json/engine_traits.h +++ b/src/json/engine_traits.h @@ -40,6 +40,7 @@ #include "engine/group.h" #include "engine/zone.h" #include "engine/keyboard.h" +#include "engine/macros.h" #include "engine/engine.h" #include "scxt_traits.h" @@ -121,22 +122,39 @@ SC_STREAMDEF(scxt::engine::Patch, SC_FROM({ findIf(v, "busses", patch.busses); })) -SC_STREAMDEF(scxt::engine::Part, SC_FROM({ - // TODO: Do a non-empty part stream with the If variant - v = {{"channel", from.channel}, {"groups", from.getGroups()}}; +SC_STREAMDEF(scxt::engine::Macro, SC_FROM({ + v = { + {"nv", t.normalizedValue}, + {"bp", t.isBipolar}, + {"nm", t.name}, + }; }), SC_TO({ - auto &part = to; - part.clearGroups(); + float normVal; + findIf(v, "nv", normVal); + findIf(v, "bp", result.isBipolar); + findIf(v, "nm", result.name); + result.setNormalizedValue(normVal); + })); - findIf(v, "channel", part.channel); - auto vzones = v.at("groups").get_array(); - for (const auto vz : vzones) - { - auto idx = part.addGroup() - 1; - vz.to(*(part.getGroup(idx))); - } - })) +SC_STREAMDEF( + scxt::engine::Part, SC_FROM({ + // TODO: Do a non-empty part stream with the If variant + v = {{"channel", from.channel}, {"groups", from.getGroups()}, {"macros", from.macros}}; + }), + SC_TO({ + auto &part = to; + part.clearGroups(); + + findIf(v, "channel", part.channel); + findIf(v, "macros", part.macros); + auto vzones = v.at("groups").get_array(); + for (const auto vz : vzones) + { + auto idx = part.addGroup() - 1; + vz.to(*(part.getGroup(idx))); + } + })) SC_STREAMDEF(scxt::engine::Group::GroupOutputInfo, SC_FROM({ v = {{"amplitude", t.amplitude}, {"pan", t.pan}, @@ -552,5 +570,6 @@ template <> struct scxt_traits r.reconfigureOutputBusses(); } }; + } // namespace scxt::json #endif // SHORTCIRCUIT_ENGINE_TRAITS_H diff --git a/src/messaging/client/client_messages.h b/src/messaging/client/client_messages.h index 8eb50fd5..a6ccb252 100644 --- a/src/messaging/client/client_messages.h +++ b/src/messaging/client/client_messages.h @@ -42,5 +42,6 @@ #include "browser_messages.h" #include "debug_messages.h" #include "patch_io_messages.h" +#include "macro_messages.h" #endif // SHORTCIRCUIT_CLIENT_MESSAGE_IMPLS_H diff --git a/src/messaging/client/client_serial.h b/src/messaging/client/client_serial.h index f7b8aedc..b9d59447 100644 --- a/src/messaging/client/client_serial.h +++ b/src/messaging/client/client_serial.h @@ -117,6 +117,9 @@ enum ClientToSerializationMessagesIds c2s_load_multi, c2s_load_part, + c2s_set_macro_full_state, + c2s_set_macro_value, + num_clientToSerializationMessages }; @@ -151,6 +154,9 @@ enum SerializationToClientMessageIds s2c_refresh_browser, + s2c_update_macro_full_state, + s2c_update_macro_value, + num_serializationToClientMessages }; diff --git a/src/messaging/client/macro_messages.h b/src/messaging/client/macro_messages.h new file mode 100644 index 00000000..4ca2a494 --- /dev/null +++ b/src/messaging/client/macro_messages.h @@ -0,0 +1,95 @@ +/* + * Shortcircuit XT - a Surge Synth Team product + * + * A fully featured creative sampler, available as a standalone + * and plugin for multiple platforms. + * + * Copyright 2019 - 2024, Various authors, as described in the github + * transaction log. + * + * ShortcircuitXT is released under the Gnu General Public Licence + * V3 or later (GPL-3.0-or-later). The license is found in the file + * "LICENSE" in the root of this repository or at + * https://www.gnu.org/licenses/gpl-3.0.en.html + * + * Individual sections of code which comprises ShortcircuitXT in this + * repository may also be used under an MIT license. Please see the + * section "Licensing" in "README.md" for details. + * + * ShortcircuitXT is inspired by, and shares code with, the + * commercial product Shortcircuit 1 and 2, released by VemberTech + * in the mid 2000s. The code for Shortcircuit 2 was opensourced in + * 2020 at the outset of this project. + * + * All source for ShortcircuitXT is available at + * https://github.com/surge-synthesizer/shortcircuit-xt + */ + +#ifndef SCXT_SRC_MESSAGING_CLIENT_MACRO_MESSAGES_H +#define SCXT_SRC_MESSAGING_CLIENT_MACRO_MESSAGES_H + +#include "client_macros.h" +#include "engine/group.h" +#include "selection/selection_manager.h" +#include "messaging/client/detail/message_helpers.h" + +namespace scxt::messaging::client +{ +using macroFullState_t = std::tuple; +SERIAL_TO_CLIENT(UpdateMacroFullState, s2c_update_macro_full_state, macroFullState_t, + onMacroFullState); + +using macroValue_t = std::tuple; +SERIAL_TO_CLIENT(UpdateMacroValue, s2c_update_macro_value, macroValue_t, onMacroValue); + +inline void updateMacroFullState(const macroFullState_t &t, const engine::Engine &engine, + MessageController &cont) +{ + const auto &[p, i, m] = t; + SCLOG("ToDo: Rebuild mod matrix when this is called"); + cont.scheduleAudioThreadCallback( + [part = p, index = i, macro = m](auto &e) { + // Set everything except the value + auto v = e.getPatch()->getPart(part)->macros[index].normalizedValue; + engine::Macro macroCopy = macro; + macroCopy.setNormalizedValue(v); + e.getPatch()->getPart(part)->macros[index] = macroCopy; + }, + [](const auto &e) { + const auto &lz = e.getSelectionManager()->currentLeadZone(e); + if (lz.has_value()) + { + auto &zone = + e.getPatch()->getPart(lz->part)->getGroup(lz->group)->getZone(lz->zone); + serializationSendToClient(messaging::client::s2c_update_zone_matrix_metadata, + voice::modulation::getVoiceMatrixMetadata(*zone), + *(e.getMessageController())); + } + + const auto &lg = e.getSelectionManager()->currentLeadGroup(e); + if (lg.has_value()) + { + auto &group = e.getPatch()->getPart(lg->part)->getGroup(lg->group); + serializationSendToClient(messaging::client::s2c_update_group_matrix_metadata, + modulation::getGroupMatrixMetadata(*group), + *(e.getMessageController())); + } + }); +} +CLIENT_TO_SERIAL(SetMacroFullState, c2s_set_macro_full_state, macroFullState_t, + updateMacroFullState(payload, engine, cont)); + +inline void updateMacroValue(const macroValue_t &t, const engine::Engine &engine, + MessageController &cont) +{ + const auto &[p, i, f] = t; + cont.scheduleAudioThreadCallback([part = p, index = i, value = f](auto &e) { + // Set everything except the value + e.getPatch()->getPart(part)->macros[index].setNormalizedValue(value); + }); +} +CLIENT_TO_SERIAL(SetMacroValue, c2s_set_macro_value, macroValue_t, + updateMacroValue(payload, engine, cont)); + +} // namespace scxt::messaging::client +#endif // SHORTCIRCUITXT_MACRO_MESSAGES_H diff --git a/src/messaging/client/structure_messages.h b/src/messaging/client/structure_messages.h index 4b21da6c..578229e8 100644 --- a/src/messaging/client/structure_messages.h +++ b/src/messaging/client/structure_messages.h @@ -70,6 +70,7 @@ inline void onRegister(engine::Engine &engine, MessageController &cont) sac.distinct = false; engine.getSelectionManager()->selectAction(sac); } + engine.getSelectionManager()->sendSelectedPartMacrosToClient(); } CLIENT_TO_SERIAL(OnRegister, c2s_on_register, bool, onRegister(engine, cont)); diff --git a/src/modulation/group_matrix.cpp b/src/modulation/group_matrix.cpp index 15f2a391..9cadb81a 100644 --- a/src/modulation/group_matrix.cpp +++ b/src/modulation/group_matrix.cpp @@ -156,6 +156,12 @@ void GroupMatrixEndpoints::Sources::bind(scxt::modulation::GroupMatrix &m, engin assert(g.getEngine()); m.bindSourceValue(transportSources.phasors[i], g.getEngine()->transportPhasors[i]); } + + auto *part = g.parentPart; + for (int i = 0; i < macrosPerPart; ++i) + { + m.bindSourceValue(macroSources.macros[i], part->macros[i].modulationValue); + } } void GroupMatrixEndpoints::registerGroupModTarget( @@ -184,7 +190,7 @@ void GroupMatrixEndpoints::registerGroupModSource( e->registerGroupModSource(t, pathFn, nameFn); } -groupMatrixMetadata_t getGroupMatrixMetadata(engine::Group &g) +groupMatrixMetadata_t getGroupMatrixMetadata(const engine::Group &g) { // somewhat dissatisfied with amount of copying here also auto e = g.getEngine(); @@ -194,8 +200,14 @@ groupMatrixMetadata_t getGroupMatrixMetadata(engine::Group &g) namedCurveVector_t cr; auto identCmp = [](const auto &a, const auto &b) { + const auto &srca = a.first; + const auto &srcb = b.first; const auto &ida = a.second; const auto &idb = b.second; + if (srca.gid == 'gmac' && srcb.gid == 'gmac') + { + return srca.index < srcb.index; + } if (ida.first == idb.first) { if (ida.second == idb.second) @@ -242,4 +254,14 @@ groupMatrixMetadata_t getGroupMatrixMetadata(engine::Group &g) return groupMatrixMetadata_t{true, sr, tg, cr}; } +GroupMatrixEndpoints::Sources::MacroSources::MacroSources(engine::Engine *e) +{ + for (auto i = 0U; i < macrosPerPart; ++i) + { + macros[i] = SR{'gmac', 'mcro', i}; + registerGroupModSource( + e, macros[i], [](auto &a, auto &b) { return "Macro"; }, + [i](auto &grp, auto &s) { return grp.parentPart->macros[i].name; }); + } +} } // namespace scxt::modulation diff --git a/src/modulation/group_matrix.h b/src/modulation/group_matrix.h index 2a584440..25950d8b 100644 --- a/src/modulation/group_matrix.h +++ b/src/modulation/group_matrix.h @@ -224,7 +224,8 @@ struct GroupMatrixEndpoints struct Sources { Sources(engine::Engine *e) - : lfoSources(e), egSource{{'greg', 'eg1 ', 0}, {'greg', 'eg2 ', 0}}, transportSources(e) + : lfoSources(e), egSource{{'greg', 'eg1 ', 0}, {'greg', 'eg2 ', 0}}, + transportSources(e), macroSources(e) { registerGroupModSource(e, egSource[0], "", "EG1"); registerGroupModSource(e, egSource[1], "", "EG2"); @@ -264,6 +265,13 @@ struct GroupMatrixEndpoints SR phasors[scxt::numTransportPhasors]; } transportSources; + struct MacroSources + { + MacroSources(engine::Engine *e); + + SR macros[macrosPerPart]; + } macroSources; + float zeroSource{0.f}; void bind(GroupMatrix &matrix, engine::Group &group); @@ -282,6 +290,6 @@ typedef std::vector namedCurveVector_t; typedef std::tuple groupMatrixMetadata_t; -groupMatrixMetadata_t getGroupMatrixMetadata(engine::Group &z); +groupMatrixMetadata_t getGroupMatrixMetadata(const engine::Group &z); } // namespace scxt::modulation #endif // SHORTCIRCUITXT_GROUP_MATRIX_H diff --git a/src/modulation/voice_matrix.cpp b/src/modulation/voice_matrix.cpp index 2991ba93..c8653e92 100644 --- a/src/modulation/voice_matrix.cpp +++ b/src/modulation/voice_matrix.cpp @@ -162,6 +162,12 @@ void MatrixEndpoints::Sources::bind(scxt::voice::modulation::Matrix &m, engine:: int dist = (i < 4) ? 0 : 1; m.bindSourceConstantValue(rngSources.randoms[i], randomRoll(bip, dist)); } + + auto *part = z.parentGroup->parentPart; + for (int i = 0; i < macrosPerPart; ++i) + { + m.bindSourceValue(macroSources.macros[i], part->macros[i].modulationValue); + } } void MatrixEndpoints::registerVoiceModTarget( @@ -186,7 +192,7 @@ void MatrixEndpoints::registerVoiceModSource( e->registerVoiceModSource(t, pathFn, nameFn); } -voiceMatrixMetadata_t getVoiceMatrixMetadata(engine::Zone &z) +voiceMatrixMetadata_t getVoiceMatrixMetadata(const engine::Zone &z) { auto e = z.getEngine(); @@ -195,8 +201,15 @@ voiceMatrixMetadata_t getVoiceMatrixMetadata(engine::Zone &z) namedCurveVector_t cr; auto identCmp = [](const auto &a, const auto &b) { + const auto &srca = a.first; + const auto &srcb = b.first; const auto &ida = a.second; const auto &idb = b.second; + if (srca.gid == 'zmac' && srcb.gid == 'zmac') + { + return srca.index < srcb.index; + } + if (ida.first == idb.first) { if (ida.second == idb.second) @@ -345,4 +358,14 @@ MatrixEndpoints::LFOTarget::LFOTarget(engine::Engine *e, uint32_t p) } } +MatrixEndpoints::Sources::MacroSources::MacroSources(engine::Engine *e) +{ + for (auto i = 0U; i < macrosPerPart; ++i) + { + macros[i] = SR{'zmac', 'mcro', i}; + registerVoiceModSource( + e, macros[i], [](auto &a, auto &b) { return "Macro"; }, + [i](auto &zone, auto &s) { return zone.parentGroup->parentPart->macros[i].name; }); + } +} } // namespace scxt::voice::modulation diff --git a/src/modulation/voice_matrix.h b/src/modulation/voice_matrix.h index 729661a2..574e47a3 100644 --- a/src/modulation/voice_matrix.h +++ b/src/modulation/voice_matrix.h @@ -216,7 +216,7 @@ struct MatrixEndpoints { Sources(engine::Engine *e) : lfoSources(e), midiSources(e), aegSource{'zneg', 'aeg ', 0}, - eg2Source{'zneg', 'eg2 ', 0}, transportSources(e), rngSources(e) + eg2Source{'zneg', 'eg2 ', 0}, transportSources(e), rngSources(e), macroSources(e) { registerVoiceModSource(e, aegSource, "", "AEG"); registerVoiceModSource(e, eg2Source, "", "EG2"); @@ -287,6 +287,13 @@ struct MatrixEndpoints SR randoms[8]; } rngSources; + struct MacroSources + { + MacroSources(engine::Engine *e); + + SR macros[macrosPerPart]; + } macroSources; + SR aegSource, eg2Source; void bind(Matrix &m, engine::Zone &z, voice::Voice &v); @@ -343,7 +350,7 @@ typedef std::vector namedCurveVector_t; typedef std::tuple voiceMatrixMetadata_t; -voiceMatrixMetadata_t getVoiceMatrixMetadata(engine::Zone &z); +voiceMatrixMetadata_t getVoiceMatrixMetadata(const engine::Zone &z); } // namespace scxt::voice::modulation #endif // __SCXT_VOICE_MATRIX_H diff --git a/src/selection/selection_manager.cpp b/src/selection/selection_manager.cpp index 91da09bb..169d7f0e 100644 --- a/src/selection/selection_manager.cpp +++ b/src/selection/selection_manager.cpp @@ -202,6 +202,7 @@ void SelectionManager::selectPart(int16_t part) guaranteeSelectedLead(); sendClientDataForLeadSelectionState(); sendSelectedZonesToClient(); + sendSelectedPartMacrosToClient(); debugDumpSelectionState(); } @@ -737,4 +738,13 @@ void SelectionManager::configureAndSendZoneModMatrixMetadata(int p, int g, int z r.extraPayload = std::nullopt; } } + +void SelectionManager::sendSelectedPartMacrosToClient() +{ + const auto &part = engine.getPatch()->getPart(selectedPart); + for (int i = 0; i < macrosPerPart; ++i) + serializationSendToClient(cms::s2c_update_macro_full_state, + cms::macroFullState_t{selectedPart, i, part->macros[i]}, + *(engine.getMessageController())); +} } // namespace scxt::selection \ No newline at end of file diff --git a/src/selection/selection_manager.h b/src/selection/selection_manager.h index 01344a81..20404589 100644 --- a/src/selection/selection_manager.h +++ b/src/selection/selection_manager.h @@ -186,6 +186,7 @@ struct SelectionManager void sendDisplayDataForNoZoneSelected(); void sendDisplayDataForSingleGroup(int part, int group); void sendDisplayDataForNoGroupSelected(); + void sendSelectedPartMacrosToClient(); void configureAndSendZoneModMatrixMetadata(int p, int g, int z); // To ponder. Does this belong on this object or the engine?