From 6e4d399a04285b4002b871ae9cd0666d057cf372 Mon Sep 17 00:00:00 2001 From: Igor Barcik Date: Tue, 8 Sep 2026 14:55:22 +0200 Subject: [PATCH] feat(plugin): add plugin processor and editor --- Source/PluginEditor.cpp | 711 +++++++++++++++++++++++++++++++++++++ Source/PluginEditor.h | 125 +++++++ Source/PluginProcessor.cpp | 381 ++++++++++++++++++++ Source/PluginProcessor.h | 71 ++++ 4 files changed, 1288 insertions(+) create mode 100644 Source/PluginEditor.cpp create mode 100644 Source/PluginEditor.h create mode 100644 Source/PluginProcessor.cpp create mode 100644 Source/PluginProcessor.h diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp new file mode 100644 index 0000000..825755b --- /dev/null +++ b/Source/PluginEditor.cpp @@ -0,0 +1,711 @@ +#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 diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h new file mode 100644 index 0000000..19f3732 --- /dev/null +++ b/Source/PluginEditor.h @@ -0,0 +1,125 @@ +#pragma once + +#include +#include "PluginProcessor.h" +#include "GUI/SerumLookAndFeel.h" +#include "GUI/Knob.h" +#include "GUI/Slider.h" +#include "GUI/ToggleButton.h" +#include "GUI/Display.h" +#include "GUI/Panel.h" +#include "GUI/WaveformDisplay.h" +#include "GUI/FilterDisplay.h" +#include "GUI/EnvelopeDisplay.h" +#include "GUI/LFODisplay.h" + +namespace serum +{ + +// =========================================================================== +// SerumAlt editor: dark vector GUI with a tabbed layout, real-time +// visualisations, RAVE, macro controls and preset-scale (75%..200%) scaling. +// =========================================================================== +class PluginEditor : public juce::AudioProcessorEditor, + public juce::Timer +{ +public: + explicit PluginEditor (SerumAltAudioProcessor& p); + ~PluginEditor() override; + + void paint (juce::Graphics&) override; + void resized() override; + void timerCallback() override; + + void setTab (int index); + +private: + SerumAltAudioProcessor& processor; + SerumLookAndFeel laf; + + static constexpr int kBaseW = 1120; + static constexpr int kBaseH = 740; + + juce::Component root; + + // --- top bar --- + std::unique_ptr logo; + juce::ComboBox presetCombo; + juce::ComboBox scaleCombo; + ToggleButton raveButton; + Knob masterKnob; + Display masterDisplay; + + // --- tabs --- + juce::TextButton tabOsc, tabFilter, tabMod, tabFx, tabMacro; + juce::Component oscView, filterView, modView, fxView, macroView; + int currentTab = 0; + + // --- OSC --- + Panel oscAPanel { "Oscillator A" }, oscBPanel { "Oscillator B" }; + Panel subPanel { "Sub" }, noisePanel { "Noise" }; + WaveformDisplay oscAWave, oscBWave; + std::vector oscAComponents, oscBComponents; + + // --- FILTER --- + Panel filter1Panel { "Filter 1" }, filter2Panel { "Filter 2" }, routingPanel { "Routing" }; + FilterDisplay filter1Display, filter2Display; + + // --- MOD --- + std::array envPanels { { Panel ("Env 1 (Amp)"), Panel ("Env 2 (Filter)"), + Panel ("Env 3"), Panel ("Env 4") } }; + std::array envDisplays; + std::array lfoPanels { { Panel ("LFO 1"), Panel ("LFO 2"), Panel ("LFO 3"), Panel ("LFO 4") } }; + std::array lfoDisplays; + + Panel matrixPanel { "Modulation Matrix" }; + juce::ComboBox modSourceCombo, modTargetCombo; + Knob modDepthKnob { "Depth" }; + ToggleButton modBipolarToggle; + juce::TextButton modAddButton, modRemoveButton, modClearButton; + juce::TextEditor modList; + + // --- FX --- + std::array fxPanels; + std::array fxTypeCombos; + std::array, kNumFxSlots> fxKnobs; + std::array fxUp, fxDown; + + // --- MACRO --- + std::array macroPanels; + std::array macroKnobs; + Panel macroAssignPanel { "Macro Assignments" }; + juce::ComboBox macroAssignTarget; + juce::ComboBox macroAssignIndex; + Knob macroDepthKnob { "Depth" }; + juce::TextButton macroAssignButton, macroClearButton; + juce::TextEditor macroList; + + // --- attachments --- + std::vector> sliderAttachments; + std::vector> comboAttachments; + + int currentScaleIndex = -1; + + // --- helpers --- + Knob* makeKnob (juce::Component* parent, const juce::String& name, const juce::String& paramId, + std::function fmt = {}); + juce::ComboBox* makeCombo (juce::Component* parent, const juce::String& paramId, const juce::StringArray& items); + ToggleButton* makeToggle (juce::Component* parent, const juce::String& label, const juce::String& paramId); + + void buildTopBar(); + void buildOscTab(); + void buildFilterTab(); + void buildModTab(); + void buildFxTab(); + void buildMacroTab(); + void swapFxSlots (int a, int b); + void applyUiScale(); + void updateVisuals(); + void updateModList(); + void updateMacroList(); + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PluginEditor) +}; + +} // namespace serum diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp new file mode 100644 index 0000000..b4ccd4c --- /dev/null +++ b/Source/PluginProcessor.cpp @@ -0,0 +1,381 @@ +#include "PluginProcessor.h" +#include "PluginEditor.h" +#include "Presets/FactoryPresets.h" + +namespace serum +{ + +namespace +{ + std::unique_ptr f (const char* id, const juce::String& name, float def) + { + return std::make_unique (juce::ParameterID { id, 1 }, name, + juce::NormalisableRange (0.0f, 1.0f), def); + } +} + +// --------------------------------------------------------------------------- +SerumAltAudioProcessor::SerumAltAudioProcessor() + : AudioProcessor (BusesProperties().withInput ("Input", juce::AudioChannelSet::stereo(), false) + .withOutput ("Output", juce::AudioChannelSet::stereo(), true)), + parameters (*this, nullptr, juce::Identifier ("SerumAlt"), createParameterLayout()) +{ +} + +SerumAltAudioProcessor::~SerumAltAudioProcessor() = default; + +juce::AudioProcessorValueTreeState::ParameterLayout SerumAltAudioProcessor::createParameterLayout() +{ + std::vector> params; + + // Global + params.push_back (f (ids::master, "Master", 0.8f)); + params.push_back (f (ids::uiScale, "UI Scale", 0.25f)); + params.push_back (f (ids::rave, "RAVE", 0.0f)); + + // Oscillator A + params.push_back (f (ids::oscAOn, "Osc A On", 1.0f)); + params.push_back (f (ids::oscAWave, "Osc A Wave", 0.0f)); + params.push_back (f (ids::oscAWtPos, "Osc A WT Pos", 0.0f)); + params.push_back (f (ids::oscAWarp, "Osc A Warp", 0.0f)); + params.push_back (f (ids::oscAWarpAmt, "Osc A Warp Amt", 0.0f)); + params.push_back (f (ids::oscACoarse, "Osc A Coarse", 0.5f)); + params.push_back (f (ids::oscAFine, "Osc A Fine", 0.5f)); + params.push_back (f (ids::oscALevel, "Osc A Level", 0.8f)); + params.push_back (f (ids::oscAPan, "Osc A Pan", 0.5f)); + params.push_back (f (ids::oscAUnison, "Osc A Unison", 0.0f)); + params.push_back (f (ids::oscADetune, "Osc A Detune", 0.0f)); + params.push_back (f (ids::oscASpread, "Osc A Spread", 0.0f)); + params.push_back (f (ids::oscAPhase, "Osc A Phase", 0.0f)); + params.push_back (f (ids::oscARandPh, "Osc A Rand Phase", 0.0f)); + + // Oscillator B + params.push_back (f (ids::oscBOn, "Osc B On", 0.0f)); + params.push_back (f (ids::oscBWave, "Osc B Wave", 1.0f / 9.0f)); + params.push_back (f (ids::oscBWtPos, "Osc B WT Pos", 0.0f)); + params.push_back (f (ids::oscBWarp, "Osc B Warp", 0.0f)); + params.push_back (f (ids::oscBWarpAmt, "Osc B Warp Amt", 0.0f)); + params.push_back (f (ids::oscBCoarse, "Osc B Coarse", 0.5f)); + params.push_back (f (ids::oscBFine, "Osc B Fine", 0.5f)); + params.push_back (f (ids::oscBLevel, "Osc B Level", 0.5f)); + params.push_back (f (ids::oscBPan, "Osc B Pan", 0.5f)); + params.push_back (f (ids::oscBUnison, "Osc B Unison", 0.0f)); + params.push_back (f (ids::oscBDetune, "Osc B Detune", 0.0f)); + params.push_back (f (ids::oscBSpread, "Osc B Spread", 0.0f)); + params.push_back (f (ids::oscBPhase, "Osc B Phase", 0.0f)); + params.push_back (f (ids::oscBRandPh, "Osc B Rand Phase", 0.0f)); + + // Sub + params.push_back (f (ids::subOn, "Sub On", 0.0f)); + params.push_back (f (ids::subShape, "Sub Shape", 0.0f)); + params.push_back (f (ids::subOct, "Sub Octave", 1.0f / 2.0f)); + params.push_back (f (ids::subLevel, "Sub Level", 0.5f)); + + // Noise + params.push_back (f (ids::noiseOn, "Noise On", 0.0f)); + params.push_back (f (ids::noiseType, "Noise Type", 0.0f)); + params.push_back (f (ids::noiseLevel, "Noise Level", 0.5f)); + + // Filter 1 + params.push_back (f (ids::f1On, "Filter 1 On", 1.0f)); + params.push_back (f (ids::f1Type, "Filter 1 Type", 0.0f)); + params.push_back (f (ids::f1Cutoff, "Filter 1 Cutoff", 0.65f)); + params.push_back (f (ids::f1Res, "Filter 1 Res", 0.05f)); + params.push_back (f (ids::f1Drive, "Filter 1 Drive", 0.0f)); + params.push_back (f (ids::f1Key, "Filter 1 Keytrack", 0.0f)); + params.push_back (f (ids::f1Slope, "Filter 1 Slope", 1.0f)); + + // Filter 2 + params.push_back (f (ids::f2On, "Filter 2 On", 0.0f)); + params.push_back (f (ids::f2Type, "Filter 2 Type", 0.0f)); + params.push_back (f (ids::f2Cutoff, "Filter 2 Cutoff", 0.5f)); + params.push_back (f (ids::f2Res, "Filter 2 Res", 0.0f)); + params.push_back (f (ids::f2Drive, "Filter 2 Drive", 0.0f)); + params.push_back (f (ids::f2Key, "Filter 2 Keytrack", 0.0f)); + params.push_back (f (ids::f2Slope, "Filter 2 Slope", 1.0f)); + + params.push_back (f (ids::fRoute, "Filter Route", 0.0f)); + params.push_back (f (ids::fMix, "Filter Mix", 0.5f)); + params.push_back (f (ids::fOut, "Filter Out", 0.667f)); + + // Envelopes 1..4 + const char* envA[4] = { ids::env1A, ids::env2A, ids::env3A, ids::env4A }; + const char* envD[4] = { ids::env1D, ids::env2D, ids::env3D, ids::env4D }; + const char* envS[4] = { ids::env1S, ids::env2S, ids::env3S, ids::env4S }; + const char* envR[4] = { ids::env1R, ids::env2R, ids::env3R, ids::env4R }; + const char* envC[4] = { ids::env1Curve, ids::env2Curve, ids::env3Curve, ids::env4Curve }; + const float envADef[4] = { 0.05f, 0.1f, 0.2f, 0.2f }; + const float envDDef[4] = { 0.25f, 0.3f, 0.3f, 0.3f }; + const float envSDef[4] = { 0.8f, 0.5f, 0.5f, 0.5f }; + const float envRDef[4] = { 0.3f, 0.3f, 0.4f, 0.4f }; + for (int i = 0; i < 4; ++i) + { + params.push_back (f (envA[i], juce::String ("Env ") + juce::String (i + 1) + " Attack", envADef[i])); + params.push_back (f (envD[i], juce::String ("Env ") + juce::String (i + 1) + " Decay", envDDef[i])); + params.push_back (f (envS[i], juce::String ("Env ") + juce::String (i + 1) + " Sustain", envSDef[i])); + params.push_back (f (envR[i], juce::String ("Env ") + juce::String (i + 1) + " Release", envRDef[i])); + params.push_back (f (envC[i], juce::String ("Env ") + juce::String (i + 1) + " Curve", 0.5f)); + } + + // LFOs 1..4 + const char* lfoRate[4] = { ids::lfo1Rate, ids::lfo2Rate, ids::lfo3Rate, ids::lfo4Rate }; + const char* lfoSync[4] = { ids::lfo1Sync, ids::lfo2Sync, ids::lfo3Sync, ids::lfo4Sync }; + const char* lfoBeat[4] = { ids::lfo1Beat, ids::lfo2Beat, ids::lfo3Beat, ids::lfo4Beat }; + const char* lfoShape[4] = { ids::lfo1Shape, ids::lfo2Shape, ids::lfo3Shape, ids::lfo4Shape }; + const char* lfoPhase[4] = { ids::lfo1Phase, ids::lfo2Phase, ids::lfo3Phase, ids::lfo4Phase }; + const char* lfoFade[4] = { ids::lfo1Fade, ids::lfo2Fade, ids::lfo3Fade, ids::lfo4Fade }; + const char* lfoDelay[4] = { ids::lfo1Delay, ids::lfo2Delay, ids::lfo3Delay, ids::lfo4Delay }; + const float lfoShapeDef[4] = { 0.0f, 1.0f / 6.0f, 2.0f / 6.0f, 3.0f / 6.0f }; + for (int i = 0; i < 4; ++i) + { + const juce::String n = juce::String ("LFO ") + juce::String (i + 1); + params.push_back (f (lfoRate[i], n + " Rate", 0.5f)); + params.push_back (f (lfoSync[i], n + " Sync", 0.0f)); + params.push_back (f (lfoBeat[i], n + " Beat", 0.5f)); + params.push_back (f (lfoShape[i], n + " Shape", lfoShapeDef[i])); + params.push_back (f (lfoPhase[i], n + " Phase", 0.0f)); + params.push_back (f (lfoFade[i], n + " Fade", 0.0f)); + params.push_back (f (lfoDelay[i], n + " Delay", 0.0f)); + } + + // FX slots 1..8 + const char* fxType[8] = { ids::fx1Type, ids::fx2Type, ids::fx3Type, ids::fx4Type, + ids::fx5Type, ids::fx6Type, ids::fx7Type, ids::fx8Type }; + const char* fxMix[8] = { ids::fx1Mix, ids::fx2Mix, ids::fx3Mix, ids::fx4Mix, + ids::fx5Mix, ids::fx6Mix, ids::fx7Mix, ids::fx8Mix }; + const char* fxP[8][4] = { + { ids::fx1P1, ids::fx1P2, ids::fx1P3, ids::fx1P4 }, + { ids::fx2P1, ids::fx2P2, ids::fx2P3, ids::fx2P4 }, + { ids::fx3P1, ids::fx3P2, ids::fx3P3, ids::fx3P4 }, + { ids::fx4P1, ids::fx4P2, ids::fx4P3, ids::fx4P4 }, + { ids::fx5P1, ids::fx5P2, ids::fx5P3, ids::fx5P4 }, + { ids::fx6P1, ids::fx6P2, ids::fx6P3, ids::fx6P4 }, + { ids::fx7P1, ids::fx7P2, ids::fx7P3, ids::fx7P4 }, + { ids::fx8P1, ids::fx8P2, ids::fx8P3, ids::fx8P4 } + }; + for (int i = 0; i < 8; ++i) + { + const juce::String n = juce::String ("FX ") + juce::String (i + 1); + params.push_back (f (fxType[i], n + " Type", 0.0f)); + params.push_back (f (fxMix[i], n + " Mix", 0.5f)); + params.push_back (f (fxP[i][0], n + " P1", 0.5f)); + params.push_back (f (fxP[i][1], n + " P2", 0.5f)); + params.push_back (f (fxP[i][2], n + " P3", 0.5f)); + params.push_back (f (fxP[i][3], n + " P4", 0.5f)); + } + + // Macros + params.push_back (f (ids::macro1, "Macro 1", 0.0f)); + params.push_back (f (ids::macro2, "Macro 2", 0.0f)); + params.push_back (f (ids::macro3, "Macro 3", 0.0f)); + params.push_back (f (ids::macro4, "Macro 4", 0.0f)); + + return { params.begin(), params.end() }; +} + +// --------------------------------------------------------------------------- +void SerumAltAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock) +{ + engine.prepare (sampleRate, samplesPerBlock); +} + +void SerumAltAudioProcessor::releaseResources() +{ + engine.reset(); +} + +void SerumAltAudioProcessor::processBlock (juce::AudioBuffer& buffer, juce::MidiBuffer& midi) +{ + juce::ScopedNoDenormals noDenormals; + engine.processBlock (buffer, midi, parameters, getPlayHead()); +} + +juce::AudioProcessorEditor* SerumAltAudioProcessor::createEditor() +{ + return new PluginEditor (*this); +} + +// --------------------------------------------------------------------------- +// Programs / presets +// --------------------------------------------------------------------------- +int SerumAltAudioProcessor::getNumPrograms() +{ + return (int) getFactoryPresets().size(); +} + +int SerumAltAudioProcessor::getCurrentProgram() +{ + return currentProgram; +} + +void SerumAltAudioProcessor::setCurrentProgram (int index) +{ + index = juce::jlimit (0, getNumPrograms() - 1, index); + loadFactoryPreset (index); + currentProgram = index; +} + +const juce::String SerumAltAudioProcessor::getProgramName (int index) +{ + const auto& presets = getFactoryPresets(); + if (index >= 0 && index < (int) presets.size()) + return presets[(size_t) index].name; + return {}; +} + +void SerumAltAudioProcessor::changeProgramName (int, const juce::String&) +{ +} + +int SerumAltAudioProcessor::getNumFactoryPresets() const +{ + return (int) getFactoryPresets().size(); +} + +void SerumAltAudioProcessor::loadFactoryPreset (int index) +{ + const auto& presets = getFactoryPresets(); + if (index < 0 || index >= (int) presets.size()) + return; + + const FactoryPreset& preset = presets[(size_t) index]; + + // RAVE should start off for a freshly loaded preset. + rave.resetSnapshot(); + if (auto* raveParam = parameters.getParameter (ids::rave)) + raveParam->setValueNotifyingHost (0.0f); + + for (const auto& kv : preset.params) + if (auto* param = parameters.getParameter (kv.first)) + param->setValueNotifyingHost (kv.second); + + engine.getMatrix().clear(); + for (const auto& mod : preset.mods) + engine.getMatrix().addConnection (mod.source, mod.target, mod.depth, mod.bipolar); + + engine.getMacros().clear(); + for (const auto& ma : preset.macroAssigns) + engine.getMacros().addAssignment (ma.macro, ma.target, ma.depth); +} + +// --------------------------------------------------------------------------- +// RAVE / UI scale +// --------------------------------------------------------------------------- +void SerumAltAudioProcessor::setRaveEnabled (bool enabled) +{ + rave.setEnabled (enabled, parameters); + if (auto* raveParam = parameters.getParameter (ids::rave)) + raveParam->setValueNotifyingHost (enabled ? 1.0f : 0.0f); +} + +bool SerumAltAudioProcessor::isRaveEnabled() const +{ + return rave.isEnabled(); +} + +int SerumAltAudioProcessor::getUiScaleIndex() const +{ + if (auto* p = parameters.getRawParameterValue (ids::uiScale)) + return juce::jlimit (0, 4, (int) std::llround (p->load() * 4.0f)); + return 1; +} + +void SerumAltAudioProcessor::setUiScaleIndex (int index) +{ + index = juce::jlimit (0, 4, index); + if (auto* p = parameters.getParameter (ids::uiScale)) + p->setValueNotifyingHost ((float) index / 4.0f); +} + +float SerumAltAudioProcessor::getUiScale() const +{ + static constexpr float scales[5] = { 0.75f, 1.0f, 1.25f, 1.5f, 2.0f }; + return scales[getUiScaleIndex()]; +} + +// --------------------------------------------------------------------------- +// State +// --------------------------------------------------------------------------- +void SerumAltAudioProcessor::getStateInformation (juce::MemoryBlock& destData) +{ + auto state = parameters.copyState(); + state.appendChild (engine.getMatrix().toValueTree(), nullptr); + state.appendChild (engine.getMacros().toValueTree(), nullptr); + saveLfoShapesToState (state); + + std::unique_ptr xml (state.createXml()); + if (xml != nullptr) + copyXmlToBinary (*xml, destData); +} + +void SerumAltAudioProcessor::setStateInformation (const void* data, int sizeInBytes) +{ + std::unique_ptr xml (getXmlFromBinary (data, sizeInBytes)); + if (xml == nullptr) + return; + + juce::ValueTree state = juce::ValueTree::fromXml (*xml); + if (! state.isValid()) + return; + + parameters.replaceState (state); + engine.getMatrix().fromValueTree (state.getChildWithName ("MODMATRIX")); + engine.getMacros().fromValueTree (state.getChildWithName ("MACROS")); + restoreLfoShapesFromState (state); + + // A persisted RAVE toggle has no live snapshot, so start it off. + rave.resetSnapshot(); + if (auto* raveParam = parameters.getParameter (ids::rave)) + raveParam->setValueNotifyingHost (0.0f); +} + +void SerumAltAudioProcessor::saveLfoShapesToState (juce::ValueTree& state) const +{ + juce::ValueTree tree ("LFOSHAPES"); + for (int i = 0; i < kNumLfos; ++i) + { + const auto& data = engine.getLfos()[(size_t) i].getShapeData(); + juce::ValueTree lfo ("LFO"); + lfo.setProperty ("index", i, nullptr); + lfo.setProperty ("steps", engine.getLfos()[(size_t) i].getShapeSteps(), nullptr); + + juce::Array arr; + for (float vv : data) + arr.add (vv); + lfo.setProperty ("data", juce::var (arr), nullptr); + tree.appendChild (lfo, nullptr); + } + state.appendChild (tree, nullptr); +} + +void SerumAltAudioProcessor::restoreLfoShapesFromState (const juce::ValueTree& state) +{ + const juce::ValueTree tree = state.getChildWithName ("LFOSHAPES"); + if (! tree.isValid()) + return; + + for (const auto& lfo : tree) + { + if (! lfo.hasType ("LFO")) + continue; + const int index = juce::jlimit (0, kNumLfos - 1, (int) lfo.getProperty ("index", 0)); + const int steps = (int) lfo.getProperty ("steps", 16); + + std::vector data; + if (auto* arr = lfo.getProperty ("data").getArray()) + for (const auto& vv : *arr) + data.push_back ((float) vv); + + engine.setLfoShapeData (index, data, steps); + } +} + +} // namespace serum + +// =========================================================================== +// Plugin entry point (required by the JUCE plugin clients). +// =========================================================================== +juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() +{ + return new serum::SerumAltAudioProcessor(); +} diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h new file mode 100644 index 0000000..9176ea5 --- /dev/null +++ b/Source/PluginProcessor.h @@ -0,0 +1,71 @@ +#pragma once + +#include +#include "Params.h" +#include "Engine.h" +#include "RAVEButton.h" + +namespace serum +{ + +// =========================================================================== +// SerumAlt — a wavetable synthesiser (VST3 / AU). Hosts the APVTS, the audio +// engine, preset management and the RAVE controller. +// =========================================================================== +class SerumAltAudioProcessor : public juce::AudioProcessor +{ +public: + SerumAltAudioProcessor(); + ~SerumAltAudioProcessor() override; + + // --- AudioProcessor --- + void prepareToPlay (double sampleRate, int samplesPerBlock) override; + void releaseResources() override; + void processBlock (juce::AudioBuffer&, juce::MidiBuffer&) override; + + juce::AudioProcessorEditor* createEditor() override; + bool hasEditor() const override { return true; } + + const juce::String getName() const override { return "SerumAlt"; } + bool acceptsMidi() const override { return true; } + bool producesMidi() const override { return false; } + bool isMidiEffect() const override { return false; } + double getTailLengthSeconds() const override { return 2.0; } + + int getNumPrograms() override; + int getCurrentProgram() override; + void setCurrentProgram (int index) override; + const juce::String getProgramName (int index) override; + void changeProgramName (int index, const juce::String& newName) override; + + void getStateInformation (juce::MemoryBlock& destData) override; + void setStateInformation (const void* data, int sizeInBytes) override; + + // --- SerumAlt --- + void setRaveEnabled (bool enabled); + bool isRaveEnabled() const; + + int getUiScaleIndex() const; + void setUiScaleIndex (int index); + float getUiScale() const; + + void loadFactoryPreset (int index); + int getNumFactoryPresets() const; + + // Public DSP state (read/write from the GUI thread). + juce::AudioProcessorValueTreeState parameters; + Engine engine; + RaveController rave; + + static juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout(); + +private: + int currentProgram = 0; + + void restoreLfoShapesFromState (const juce::ValueTree& state); + void saveLfoShapesToState (juce::ValueTree& state) const; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SerumAltAudioProcessor) +}; + +} // namespace serum