#include "PluginEditor.h" namespace serum { namespace { juce::StringArray wavetableItems() { juce::StringArray a; for (auto n : kWavetableNames) a.add (n); return a; } juce::StringArray modSourceItems() { juce::StringArray a; for (int i = 0; i < kNumModSources; ++i) a.add (modSourceName ((ModSource) i)); return a; } juce::StringArray modTargetItems (std::vector& enums) { juce::StringArray a; for (int i = 0; i < kNumModTargets; ++i) { const auto t = (ModTarget) i; if (t == ModTarget::None) continue; a.add (modTargetName (t)); enums.push_back (t); } return a; } juce::String fmtUnison (float v) { return juce::String (1 + (int) std::llround (v * 15.0f)); } juce::String fmtSlope (float v) { const int s = (int) std::llround (v * 2.0f); return juce::String (s == 0 ? 6 : (s == 1 ? 12 : 24)) + " dB"; } juce::String fmtDepth (float v) { return juce::String (v, 2); } void placeKnobs (std::vector& knobs, int x, int y, int w, int h, int gap = 6) { for (int i = 0; i < (int) knobs.size(); ++i) knobs[(size_t) i]->setBounds (x + i * (w + gap), y, w, h); } const char* kFxType[8] = { ids::fx1Type, ids::fx2Type, ids::fx3Type, ids::fx4Type, ids::fx5Type, ids::fx6Type, ids::fx7Type, ids::fx8Type }; const char* kFxMix[8] = { ids::fx1Mix, ids::fx2Mix, ids::fx3Mix, ids::fx4Mix, ids::fx5Mix, ids::fx6Mix, ids::fx7Mix, ids::fx8Mix }; const char* kFxP1[8] = { ids::fx1P1, ids::fx2P1, ids::fx3P1, ids::fx4P1, ids::fx5P1, ids::fx6P1, ids::fx7P1, ids::fx8P1 }; const char* kFxP2[8] = { ids::fx1P2, ids::fx2P2, ids::fx3P2, ids::fx4P2, ids::fx5P2, ids::fx6P2, ids::fx7P2, ids::fx8P2 }; const char* kFxP3[8] = { ids::fx1P3, ids::fx2P3, ids::fx3P3, ids::fx4P3, ids::fx5P3, ids::fx6P3, ids::fx7P3, ids::fx8P3 }; const char* kFxP4[8] = { ids::fx1P4, ids::fx2P4, ids::fx3P4, ids::fx4P4, ids::fx5P4, ids::fx6P4, ids::fx7P4, ids::fx8P4 }; } // --------------------------------------------------------------------------- PluginEditor::PluginEditor (SerumAltAudioProcessor& p) : AudioProcessorEditor (p), processor (p), masterKnob ("Master", formatPercent) { root.setBounds (0, 0, kBaseW, kBaseH); addAndMakeVisible (root); logo = createLogoDrawable(); buildTopBar(); buildOscTab(); buildFilterTab(); buildModTab(); buildFxTab(); buildMacroTab(); setTab (0); applyUiScale(); startTimerHz (30); } PluginEditor::~PluginEditor() { stopTimer(); } // --------------------------------------------------------------------------- void PluginEditor::paint (juce::Graphics& g) { g.fillAll (theme::bg); g.setGradientFill (juce::ColourGradient (theme::panel, 0.0f, 0.0f, theme::bg, 0.0f, (float) getHeight(), false)); g.fillAll(); if (logo != nullptr) logo->drawWithin (g, juce::Rectangle (10.0f, 8.0f, 180.0f, 44.0f), juce::RectanglePlacement::centred, 1.0f); } void PluginEditor::resized() { root.setBounds (0, 0, kBaseW, kBaseH); } // --------------------------------------------------------------------------- Knob* PluginEditor::makeKnob (juce::Component* parent, const juce::String& name, const juce::String& paramId, std::function fmt) { auto* k = new Knob (name, std::move (fmt)); parent->addAndMakeVisible (k); if (paramId.isNotEmpty()) sliderAttachments.push_back (std::make_unique ( processor.parameters, paramId, *k)); return k; } juce::ComboBox* PluginEditor::makeCombo (juce::Component* parent, const juce::String& paramId, const juce::StringArray& items) { auto* c = new juce::ComboBox(); c->addItemList (items, 1); c->setSelectedItemIndex (0, juce::dontSendNotification); parent->addAndMakeVisible (c); if (paramId.isNotEmpty()) comboAttachments.push_back (std::make_unique ( processor.parameters, paramId, *c)); return c; } ToggleButton* PluginEditor::makeToggle (juce::Component* parent, const juce::String& label, const juce::String& paramId) { auto* t = new ToggleButton (label); parent->addAndMakeVisible (t); if (paramId.isNotEmpty()) t->attach (processor.parameters, paramId); return t; } // --------------------------------------------------------------------------- void PluginEditor::buildTopBar() { // Preset selector. juce::StringArray presetNames; for (int i = 0; i < processor.getNumPrograms(); ++i) presetNames.add (processor.getProgramName (i)); presetCombo.addItemList (presetNames, 1); presetCombo.setSelectedItemIndex (processor.getCurrentProgram(), juce::dontSendNotification); presetCombo.onChange = [this] { processor.setCurrentProgram (presetCombo.getSelectedItemIndex()); }; root.addAndMakeVisible (presetCombo); presetCombo.setBounds (200, 16, 220, 24); // UI scale. scaleCombo.addItemList ({ "75%", "100%", "125%", "150%", "200%" }, 1); scaleCombo.setSelectedItemIndex (processor.getUiScaleIndex(), juce::dontSendNotification); scaleCombo.onChange = [this] { processor.setUiScaleIndex (scaleCombo.getSelectedItemIndex()); }; root.addAndMakeVisible (scaleCombo); scaleCombo.setBounds (980, 16, 70, 24); // RAVE. raveButton.setLabel ("RAVE"); raveButton.setOnColour (theme::raveGlow); raveButton.setBounds (1062, 2, 50, 56); root.addAndMakeVisible (raveButton); // RAVE uses a callback rather than a direct parameter attachment. raveButton.setOnClick ([this] (bool on) { processor.setRaveEnabled (on); }); // Tab bar. juce::TextButton* tabs[5] = { &tabOsc, &tabFilter, &tabMod, &tabFx, &tabMacro }; const char* tabNames[5] = { "OSC", "FILTER", "MOD", "FX", "MACRO" }; for (int i = 0; i < 5; ++i) { tabs[i]->setButtonText (tabNames[i]); tabs[i]->setClickingTogglesState (true); tabs[i]->setRadioGroupId (1001); tabs[i]->setBounds (10 + i * 92, 36, 84, 22); tabs[i]->onClick = [this, i] { setTab (i); }; root.addAndMakeVisible (tabs[i]); } // Master. masterKnob.setBounds (906, 0, 64, 60); root.addAndMakeVisible (masterKnob); sliderAttachments.push_back (std::make_unique ( processor.parameters, ids::master, masterKnob)); masterDisplay.setTitle ("MASTER"); root.addAndMakeVisible (masterDisplay); masterDisplay.setBounds (846, 8, 56, 44); } void PluginEditor::buildOscTab() { oscView.setBounds (0, 60, kBaseW, kBaseH - 60); root.addChildComponent (oscView); oscAPanel.setBounds (10, 8, 545, 300); oscBPanel.setBounds (565, 8, 545, 300); subPanel.setBounds (10, 318, 360, 150); noisePanel.setBounds (380, 318, 360, 150); for (auto* panel : { &oscAPanel, &oscBPanel, &subPanel, &noisePanel }) oscView.addAndMakeVisible (panel); // --- Oscillator A --- makeToggle (&oscAPanel, "On", ids::oscAOn)->setBounds (10, 8, 40, 28); makeCombo (&oscAPanel, ids::oscAWave, wavetableItems())->setBounds (56, 10, 200, 24); makeCombo (&oscAPanel, ids::oscAWarp, { "None", "Bend+", "Bend-", "Sync", "PWM", "Asym", "Mirror", "Fold" })->setBounds (262, 10, 130, 24); oscAWave.setWavetables (&processor.engine.getWavetables()); oscAPanel.addAndMakeVisible (oscAWave); oscAWave.setBounds (10, 44, 525, 66); std::vector rowA1 = { makeKnob (&oscAPanel, "WT Pos", ids::oscAWtPos, formatPercent), makeKnob (&oscAPanel, "Warp Amt", ids::oscAWarpAmt, formatPercent), makeKnob (&oscAPanel, "Coarse", ids::oscACoarse, formatSemis), makeKnob (&oscAPanel, "Fine", ids::oscAFine, formatCents), makeKnob (&oscAPanel, "Level", ids::oscALevel, formatPercent), makeKnob (&oscAPanel, "Pan", ids::oscAPan, formatPan) }; placeKnobs (rowA1, 10, 118, 76, 76, 6); std::vector rowA2 = { makeKnob (&oscAPanel, "Unison", ids::oscAUnison, fmtUnison), makeKnob (&oscAPanel, "Detune", ids::oscADetune, formatPercent), makeKnob (&oscAPanel, "Spread", ids::oscASpread, formatPercent), makeKnob (&oscAPanel, "Phase", ids::oscAPhase, formatPercent), makeKnob (&oscAPanel, "Rand Ph", ids::oscARandPh, formatPercent) }; placeKnobs (rowA2, 10, 198, 76, 76, 6); // --- Oscillator B --- makeToggle (&oscBPanel, "On", ids::oscBOn)->setBounds (10, 8, 40, 28); makeCombo (&oscBPanel, ids::oscBWave, wavetableItems())->setBounds (56, 10, 200, 24); makeCombo (&oscBPanel, ids::oscBWarp, { "None", "Bend+", "Bend-", "Sync", "PWM", "Asym", "Mirror", "Fold" })->setBounds (262, 10, 130, 24); oscBWave.setWavetables (&processor.engine.getWavetables()); oscBPanel.addAndMakeVisible (oscBWave); oscBWave.setBounds (10, 44, 525, 66); std::vector rowB1 = { makeKnob (&oscBPanel, "WT Pos", ids::oscBWtPos, formatPercent), makeKnob (&oscBPanel, "Warp Amt", ids::oscBWarpAmt, formatPercent), makeKnob (&oscBPanel, "Coarse", ids::oscBCoarse, formatSemis), makeKnob (&oscBPanel, "Fine", ids::oscBFine, formatCents), makeKnob (&oscBPanel, "Level", ids::oscBLevel, formatPercent), makeKnob (&oscBPanel, "Pan", ids::oscBPan, formatPan) }; placeKnobs (rowB1, 10, 118, 76, 76, 6); std::vector rowB2 = { makeKnob (&oscBPanel, "Unison", ids::oscBUnison, fmtUnison), makeKnob (&oscBPanel, "Detune", ids::oscBDetune, formatPercent), makeKnob (&oscBPanel, "Spread", ids::oscBSpread, formatPercent), makeKnob (&oscBPanel, "Phase", ids::oscBPhase, formatPercent), makeKnob (&oscBPanel, "Rand Ph", ids::oscBRandPh, formatPercent) }; placeKnobs (rowB2, 10, 198, 76, 76, 6); // --- Sub --- makeToggle (&subPanel, "On", ids::subOn)->setBounds (10, 10, 40, 28); makeCombo (&subPanel, ids::subShape, { "Sine", "Triangle" })->setBounds (56, 12, 130, 24); makeCombo (&subPanel, ids::subOct, { "-2 oct", "-1 oct", "0 oct" })->setBounds (192, 12, 90, 24); makeKnob (&subPanel, "Level", ids::subLevel, formatPercent)->setBounds (10, 46, 76, 76); // --- Noise --- makeToggle (&noisePanel, "On", ids::noiseOn)->setBounds (10, 10, 40, 28); makeCombo (&noisePanel, ids::noiseType, { "White", "Pink" })->setBounds (56, 12, 130, 24); makeKnob (&noisePanel, "Level", ids::noiseLevel, formatPercent)->setBounds (10, 46, 76, 76); } void PluginEditor::buildFilterTab() { filterView.setBounds (0, 60, kBaseW, kBaseH - 60); root.addChildComponent (filterView); filter1Panel.setBounds (10, 8, 360, 300); filter2Panel.setBounds (380, 8, 360, 300); routingPanel.setBounds (750, 8, 360, 300); for (auto* panel : { &filter1Panel, &filter2Panel, &routingPanel }) filterView.addAndMakeVisible (panel); const juce::StringArray filterTypes = { "Ladder LP", "Ladder HP", "Ladder BP", "Diode", "Comb", "Formant", "Screamer" }; const juce::StringArray slopes = { "6 dB", "12 dB", "24 dB" }; // Filter 1 makeToggle (&filter1Panel, "On", ids::f1On)->setBounds (10, 8, 40, 28); makeCombo (&filter1Panel, ids::f1Type, filterTypes)->setBounds (56, 10, 180, 24); makeCombo (&filter1Panel, ids::f1Slope, slopes)->setBounds (242, 10, 100, 24); filter1Panel.addAndMakeVisible (filter1Display); filter1Display.setBounds (10, 44, 340, 70); std::vector f1 = { makeKnob (&filter1Panel, "Cutoff", ids::f1Cutoff, formatHz), makeKnob (&filter1Panel, "Res", ids::f1Res, formatPercent), makeKnob (&filter1Panel, "Drive", ids::f1Drive, formatPercent), makeKnob (&filter1Panel, "Keytrack", ids::f1Key, formatPercent) }; placeKnobs (f1, 10, 124, 76, 76, 6); // Filter 2 makeToggle (&filter2Panel, "On", ids::f2On)->setBounds (10, 8, 40, 28); makeCombo (&filter2Panel, ids::f2Type, filterTypes)->setBounds (56, 10, 180, 24); makeCombo (&filter2Panel, ids::f2Slope, slopes)->setBounds (242, 10, 100, 24); filter2Panel.addAndMakeVisible (filter2Display); filter2Display.setBounds (10, 44, 340, 70); std::vector f2 = { makeKnob (&filter2Panel, "Cutoff", ids::f2Cutoff, formatHz), makeKnob (&filter2Panel, "Res", ids::f2Res, formatPercent), makeKnob (&filter2Panel, "Drive", ids::f2Drive, formatPercent), makeKnob (&filter2Panel, "Keytrack", ids::f2Key, formatPercent) }; placeKnobs (f2, 10, 124, 76, 76, 6); // Routing makeCombo (&routingPanel, ids::fRoute, { "Serial", "Parallel", "Split" })->setBounds (10, 10, 150, 24); makeKnob (&routingPanel, "Mix", ids::fMix, formatPercent)->setBounds (10, 50, 76, 76); makeKnob (&routingPanel, "Output", ids::fOut, formatPercent)->setBounds (100, 50, 76, 76); } void PluginEditor::buildModTab() { modView.setBounds (0, 60, kBaseW, kBaseH - 60); root.addChildComponent (modView); const juce::StringArray lfoShapes = { "Sine", "Triangle", "Saw", "Square", "S&H", "Step", "Freehand" }; // Envelopes (4 across). for (int i = 0; i < kNumEnvelopes; ++i) { const int x = 10 + i * 275; envPanels[(size_t) i].setBounds (x, 8, 265, 150); modView.addAndMakeVisible (envPanels[(size_t) i]); envPanels[(size_t) i].addAndMakeVisible (envDisplays[(size_t) i]); envDisplays[(size_t) i].setBounds (8, 8, 249, 56); const char* idsA[4] = { ids::env1A, ids::env2A, ids::env3A, ids::env4A }; const char* idsD[4] = { ids::env1D, ids::env2D, ids::env3D, ids::env4D }; const char* idsS[4] = { ids::env1S, ids::env2S, ids::env3S, ids::env4S }; const char* idsR[4] = { ids::env1R, ids::env2R, ids::env3R, ids::env4R }; const char* idsC[4] = { ids::env1Curve, ids::env2Curve, ids::env3Curve, ids::env4Curve }; std::vector knobs = { makeKnob (&envPanels[(size_t) i], "A", idsA[i], formatSeconds), makeKnob (&envPanels[(size_t) i], "D", idsD[i], formatSeconds), makeKnob (&envPanels[(size_t) i], "S", idsS[i], formatPercent), makeKnob (&envPanels[(size_t) i], "R", idsR[i], formatSeconds), makeKnob (&envPanels[(size_t) i], "Curve", idsC[i], formatPercent) }; placeKnobs (knobs, 8, 72, 44, 60, 3); } // LFOs (4 across). for (int i = 0; i < kNumLfos; ++i) { const int x = 10 + i * 275; lfoPanels[(size_t) i].setBounds (x, 168, 265, 180); modView.addAndMakeVisible (lfoPanels[(size_t) i]); const char* rate[4] = { ids::lfo1Rate, ids::lfo2Rate, ids::lfo3Rate, ids::lfo4Rate }; const char* sync[4] = { ids::lfo1Sync, ids::lfo2Sync, ids::lfo3Sync, ids::lfo4Sync }; const char* beat[4] = { ids::lfo1Beat, ids::lfo2Beat, ids::lfo3Beat, ids::lfo4Beat }; const char* shape[4] = { ids::lfo1Shape, ids::lfo2Shape, ids::lfo3Shape, ids::lfo4Shape }; const char* phase[4] = { ids::lfo1Phase, ids::lfo2Phase, ids::lfo3Phase, ids::lfo4Phase }; const char* fade[4] = { ids::lfo1Fade, ids::lfo2Fade, ids::lfo3Fade, ids::lfo4Fade }; const char* delay[4] = { ids::lfo1Delay, ids::lfo2Delay, ids::lfo3Delay, ids::lfo4Delay }; makeCombo (&lfoPanels[(size_t) i], shape[i], lfoShapes)->setBounds (8, 8, 110, 22); makeToggle (&lfoPanels[(size_t) i], "Sync", sync[i])->setBounds (126, 4, 40, 28); lfoPanels[(size_t) i].addAndMakeVisible (lfoDisplays[(size_t) i]); lfoDisplays[(size_t) i].setBounds (8, 38, 249, 64); std::vector knobs = { makeKnob (&lfoPanels[(size_t) i], "Rate", rate[i], formatPercent), makeKnob (&lfoPanels[(size_t) i], "Beat", beat[i], formatPercent), makeKnob (&lfoPanels[(size_t) i], "Phase", phase[i], formatPercent), makeKnob (&lfoPanels[(size_t) i], "Fade", fade[i], formatPercent), makeKnob (&lfoPanels[(size_t) i], "Delay", delay[i], formatPercent) }; placeKnobs (knobs, 8, 106, 44, 60, 3); lfoDisplays[(size_t) i].setOnShapeEdited ([this, i] (const std::vector& data, int steps) { processor.engine.setLfoShapeData (i, data, steps); }); } // Modulation matrix. matrixPanel.setBounds (10, 356, 1100, 268); modView.addAndMakeVisible (matrixPanel); std::vector targetEnums; const juce::StringArray targetItems = modTargetItems (targetEnums); modSourceCombo.addItemList (modSourceItems(), 1); modSourceCombo.setSelectedItemIndex (0, juce::dontSendNotification); modSourceCombo.setBounds (10, 22, 170, 24); matrixPanel.addAndMakeVisible (modSourceCombo); modTargetCombo.addItemList (targetItems, 1); modTargetCombo.setSelectedItemIndex (0, juce::dontSendNotification); modTargetCombo.setBounds (190, 22, 200, 24); matrixPanel.addAndMakeVisible (modTargetCombo); modDepthKnob.setRange (-1.0, 1.0); modDepthKnob.setValue (0.5, juce::dontSendNotification); modDepthKnob.setFormatter (fmtDepth); modDepthKnob.setBounds (400, 4, 76, 60); matrixPanel.addAndMakeVisible (modDepthKnob); modBipolarToggle.setBounds (486, 10, 56, 48); matrixPanel.addAndMakeVisible (modBipolarToggle); modBipolarToggle.setToggleState (true); modAddButton.setButtonText ("Add"); modAddButton.setBounds (550, 22, 70, 26); modAddButton.onClick = [this, targetEnums] { const ModSource src = (ModSource) modSourceCombo.getSelectedItemIndex(); const int tid = modTargetCombo.getSelectedItemIndex(); if (tid >= 0 && tid < (int) targetEnums.size()) { processor.engine.getMatrix().addConnection (src, targetEnums[(size_t) tid], (float) modDepthKnob.getValue(), modBipolarToggle.getToggleState()); updateModList(); } }; matrixPanel.addAndMakeVisible (modAddButton); modRemoveButton.setButtonText ("Remove Last"); modRemoveButton.setBounds (626, 22, 110, 26); modRemoveButton.onClick = [this] { processor.engine.getMatrix().removeConnection (processor.engine.getMatrix().size() - 1); updateModList(); }; matrixPanel.addAndMakeVisible (modRemoveButton); modClearButton.setButtonText ("Clear"); modClearButton.setBounds (742, 22, 70, 26); modClearButton.onClick = [this] { processor.engine.getMatrix().clear(); updateModList(); }; matrixPanel.addAndMakeVisible (modClearButton); modList.setBounds (10, 56, 1080, 200); modList.setReadOnly (true); modList.setMultiLine (true, false); modList.setColour (juce::TextEditor::backgroundColourId, juce::Colours::transparentBlack); modList.setColour (juce::TextEditor::textColourId, theme::text); modList.setColour (juce::TextEditor::outlineColourId, juce::Colours::transparentBlack); matrixPanel.addAndMakeVisible (modList); } void PluginEditor::buildFxTab() { fxView.setBounds (0, 60, kBaseW, kBaseH - 60); root.addChildComponent (fxView); const juce::StringArray fxTypes = { "Off", "Hyper", "Chorus", "Flanger", "Phaser", "Distortion", "EQ", "Compressor", "Delay", "Reverb" }; for (int i = 0; i < kNumFxSlots; ++i) { const int col = i % 2; const int row = i / 2; const int x = 10 + col * 555; const int y = 8 + row * 156; fxPanels[(size_t) i].setBounds (x, y, 545, 148); fxView.addAndMakeVisible (fxPanels[(size_t) i]); fxTypeCombos[(size_t) i] = makeCombo (&fxPanels[(size_t) i], kFxType[i], fxTypes); fxTypeCombos[(size_t) i]->setBounds (8, 8, 160, 24); fxUp[(size_t) i] = new juce::TextButton ("\xe2\x86\x91"); fxUp[(size_t) i]->setBounds (176, 8, 26, 24); fxPanels[(size_t) i].addAndMakeVisible (fxUp[(size_t) i]); fxDown[(size_t) i] = new juce::TextButton ("\xe2\x86\x93"); fxDown[(size_t) i]->setBounds (204, 8, 26, 24); fxPanels[(size_t) i].addAndMakeVisible (fxDown[(size_t) i]); fxUp[(size_t) i]->onClick = [this, i] { if (i > 0) swapFxSlots (i, i - 1); }; fxDown[(size_t) i]->onClick = [this, i] { if (i < kNumFxSlots - 1) swapFxSlots (i, i + 1); }; std::vector knobs = { makeKnob (&fxPanels[(size_t) i], "Mix", kFxMix[i], formatPercent), makeKnob (&fxPanels[(size_t) i], "P1", kFxP1[i], formatPercent), makeKnob (&fxPanels[(size_t) i], "P2", kFxP2[i], formatPercent), makeKnob (&fxPanels[(size_t) i], "P3", kFxP3[i], formatPercent), makeKnob (&fxPanels[(size_t) i], "P4", kFxP4[i], formatPercent) }; placeKnobs (knobs, 8, 40, 76, 76, 6); fxKnobs[(size_t) i] = knobs; } } void PluginEditor::buildMacroTab() { macroView.setBounds (0, 60, kBaseW, kBaseH - 60); root.addChildComponent (macroView); for (int i = 0; i < kNumMacros; ++i) { const int x = 10 + i * 275; macroPanels[(size_t) i].setBounds (x, 8, 265, 240); macroView.addAndMakeVisible (macroPanels[(size_t) i]); const char* ids[4] = { ids::macro1, ids::macro2, ids::macro3, ids::macro4 }; macroKnobs[(size_t) i] = makeKnob (¯oPanels[(size_t) i], MacroControls::macroName (i), ids[i], formatPercent); macroKnobs[(size_t) i]->setBounds (70, 20, 120, 120); } // Macro assignment editor. macroAssignPanel.setBounds (10, 256, 1100, 380); macroView.addAndMakeVisible (macroAssignPanel); std::vector targetEnums; const juce::StringArray targetItems = modTargetItems (targetEnums); macroAssignIndex.addItemList ({ "Macro 1", "Macro 2", "Macro 3", "Macro 4" }, 1); macroAssignIndex.setSelectedItemIndex (0, juce::dontSendNotification); macroAssignIndex.setBounds (10, 24, 140, 24); macroAssignPanel.addAndMakeVisible (macroAssignIndex); macroAssignTarget.addItemList (targetItems, 1); macroAssignTarget.setSelectedItemIndex (0, juce::dontSendNotification); macroAssignTarget.setBounds (160, 24, 220, 24); macroAssignPanel.addAndMakeVisible (macroAssignTarget); macroDepthKnob.setFormatter (fmtDepth); macroDepthKnob.setBounds (390, 6, 76, 60); macroAssignPanel.addAndMakeVisible (macroDepthKnob); macroAssignButton.setButtonText ("Assign"); macroAssignButton.setBounds (476, 24, 80, 26); macroAssignButton.onClick = [this, targetEnums] { const int macro = macroAssignIndex.getSelectedItemIndex(); const int tid = macroAssignTarget.getSelectedItemIndex(); if (tid >= 0 && tid < (int) targetEnums.size()) { processor.engine.getMacros().addAssignment (macro, targetEnums[(size_t) tid], (float) macroDepthKnob.getValue()); updateMacroList(); } }; macroAssignPanel.addAndMakeVisible (macroAssignButton); macroClearButton.setButtonText ("Clear All"); macroClearButton.setBounds (562, 24, 80, 26); macroClearButton.onClick = [this] { processor.engine.getMacros().clear(); updateMacroList(); }; macroAssignPanel.addAndMakeVisible (macroClearButton); macroList.setBounds (10, 58, 1080, 310); macroList.setReadOnly (true); macroList.setMultiLine (true, false); macroList.setColour (juce::TextEditor::backgroundColourId, juce::Colours::transparentBlack); macroList.setColour (juce::TextEditor::textColourId, theme::text); macroList.setColour (juce::TextEditor::outlineColourId, juce::Colours::transparentBlack); macroAssignPanel.addAndMakeVisible (macroList); } void PluginEditor::swapFxSlots (int a, int b) { auto swapParam = [this] (const char* pa, const char* pb) { auto* p1 = processor.parameters.getParameter (pa); auto* p2 = processor.parameters.getParameter (pb); const float v1 = p1->getValue(); const float v2 = p2->getValue(); p1->setValueNotifyingHost (v2); p2->setValueNotifyingHost (v1); }; swapParam (kFxType[a], kFxType[b]); swapParam (kFxMix[a], kFxMix[b]); swapParam (kFxP1[a], kFxP1[b]); swapParam (kFxP2[a], kFxP2[b]); swapParam (kFxP3[a], kFxP3[b]); swapParam (kFxP4[a], kFxP4[b]); } // --------------------------------------------------------------------------- void PluginEditor::setTab (int index) { currentTab = juce::jlimit (0, 4, index); oscView.setVisible (currentTab == 0); filterView.setVisible(currentTab == 1); modView.setVisible (currentTab == 2); fxView.setVisible (currentTab == 3); macroView.setVisible (currentTab == 4); tabOsc.setToggleState (currentTab == 0, juce::dontSendNotification); tabFilter.setToggleState(currentTab == 1, juce::dontSendNotification); tabMod.setToggleState (currentTab == 2, juce::dontSendNotification); tabFx.setToggleState (currentTab == 3, juce::dontSendNotification); tabMacro.setToggleState (currentTab == 4, juce::dontSendNotification); } void PluginEditor::applyUiScale() { const float scale = processor.getUiScale(); currentScaleIndex = processor.getUiScaleIndex(); root.setTransform (juce::AffineTransform::scale (scale)); setSize ((int) (kBaseW * scale), (int) (kBaseH * scale)); } // --------------------------------------------------------------------------- void PluginEditor::updateVisuals() { const auto& apvts = processor.parameters; auto gv = [&] (const char* id) { return apvts.getRawParameterValue (id)->load(); }; masterDisplay.setValue (formatPercent (gv (ids::master))); // Waveforms. const int waveA = juce::jlimit (0, kNumWavetables - 1, (int) std::llround (gv (ids::oscAWave) * (kNumWavetables - 1))); const int waveB = juce::jlimit (0, kNumWavetables - 1, (int) std::llround (gv (ids::oscBWave) * (kNumWavetables - 1))); oscAWave.setWaveIndex (waveA); oscAWave.setFramePosition (gv (ids::oscAWtPos)); oscAWave.setEnabled (gv (ids::oscAOn) > 0.5f); oscBWave.setWaveIndex (waveB); oscBWave.setFramePosition (gv (ids::oscBWtPos)); oscBWave.setEnabled (gv (ids::oscBOn) > 0.5f); oscAWave.repaint(); oscBWave.repaint(); // Filters. filter1Display.setParams ((int) std::llround (gv (ids::f1Type) * 6.0f), gv (ids::f1Cutoff), gv (ids::f1Res), gv (ids::f1Drive), (int) std::llround (gv (ids::f1Slope) * 2.0f)); filter1Display.setEnabled (gv (ids::f1On) > 0.5f); filter2Display.setParams ((int) std::llround (gv (ids::f2Type) * 6.0f), gv (ids::f2Cutoff), gv (ids::f2Res), gv (ids::f2Drive), (int) std::llround (gv (ids::f2Slope) * 2.0f)); filter2Display.setEnabled (gv (ids::f2On) > 0.5f); filter1Display.repaint(); filter2Display.repaint(); // Envelopes. const char* a[4] = { ids::env1A, ids::env2A, ids::env3A, ids::env4A }; const char* d[4] = { ids::env1D, ids::env2D, ids::env3D, ids::env4D }; const char* s[4] = { ids::env1S, ids::env2S, ids::env3S, ids::env4S }; const char* r[4] = { ids::env1R, ids::env2R, ids::env3R, ids::env4R }; const char* c[4] = { ids::env1Curve, ids::env2Curve, ids::env3Curve, ids::env4Curve }; for (int i = 0; i < kNumEnvelopes; ++i) { envDisplays[(size_t) i].setParams (gv (a[i]), gv (d[i]), gv (s[i]), gv (r[i]), gv (c[i])); envDisplays[(size_t) i].repaint(); } // LFOs. const char* lshape[4] = { ids::lfo1Shape, ids::lfo2Shape, ids::lfo3Shape, ids::lfo4Shape }; for (int i = 0; i < kNumLfos; ++i) { lfoDisplays[(size_t) i].setShape ((int) std::llround (gv (lshape[i]) * 6.0f)); lfoDisplays[(size_t) i].setShapeData (processor.engine.getLfos()[(size_t) i].getShapeData(), processor.engine.getLfos()[(size_t) i].getShapeSteps()); lfoDisplays[(size_t) i].repaint(); } // RAVE state. raveButton.setToggleState (processor.isRaveEnabled()); // Scale change. if (processor.getUiScaleIndex() != currentScaleIndex) applyUiScale(); } void PluginEditor::updateModList() { juce::String text; const auto& cons = processor.engine.getMatrix().connections; for (const auto& c : cons) text += modSourceName (c.source) + " -> " + modTargetName (c.target) + " [" + juce::String (c.depth, 2) + (c.bipolar ? ", bipolar]" : "]") + "\n"; modList.setText (text, false); } void PluginEditor::updateMacroList() { juce::String text; for (int m = 0; m < kNumMacros; ++m) { text += MacroControls::macroName (m) + ":\n"; const auto& assigns = processor.engine.getMacros().assignments[(size_t) m]; if (assigns.empty()) text += " (none)\n"; for (const auto& a : assigns) text += " " + modTargetName (a.target) + " [" + juce::String (a.depth, 2) + "]\n"; } macroList.setText (text, false); } void PluginEditor::timerCallback() { updateVisuals(); updateModList(); updateMacroList(); } } // namespace serum