#include "PluginEditor.h" #include "PluginProcessor.h" // Small helpers to set APVTS params safely (gesture + normalized) namespace { inline void setFloatParam (juce::AudioProcessorValueTreeState& apvts, const juce::String& id, float value) { if (auto* p = apvts.getParameter (id)) if (auto* rp = dynamic_cast (p)) { auto norm = rp->getNormalisableRange().convertTo0to1 (value); p->beginChangeGesture(); p->setValueNotifyingHost (juce::jlimit (0.0f, 1.0f, norm)); p->endChangeGesture(); } } inline void setChoiceParam (juce::AudioProcessorValueTreeState& apvts, const juce::String& id, int index) { if (auto* p = apvts.getParameter (id)) if (auto* cp = dynamic_cast (p)) { const int n = cp->choices.size(); index = juce::jlimit (0, juce::jmax (0, n - 1), index); float norm = (n > 1 ? (float) index / (float) (n - 1) : 0.0f); p->beginChangeGesture(); p->setValueNotifyingHost (juce::jlimit (0.0f, 1.0f, norm)); p->endChangeGesture(); } } } //============================================================================== // Editor TwoOscAudioProcessorEditor::TwoOscAudioProcessorEditor (TwoOscAudioProcessor& p) : AudioProcessorEditor (&p), processor (p) { // Smaller overall size setSize (860, 460); // Header title title.setText ("Samedi Dimanche AV-60", juce::dontSendNotification); title.setJustificationType (juce::Justification::centredLeft); title.setFont (juce::Font (juce::FontOptions (20.0f, juce::Font::bold))); // slightly smaller addAndMakeVisible (title); // Preset display + Browse button inside a framed "preset screen" presetDisplay.setJustificationType (juce::Justification::centred); presetDisplay.setFont (juce::Font (juce::FontOptions (13.0f))); presetDisplay.setColour (juce::Label::textColourId, juce::Colours::white.withAlpha (0.85f)); addAndMakeVisible (presetDisplay); browseBtn.setButtonText ("Browse..."); browseBtn.onClick = [this]{ showPresetPopup(); }; addAndMakeVisible (browseBtn); // Controls oscA.addItemList ({"Sine","Saw","Square","Tri"}, 1); oscB.addItemList ({"Sine","Saw","Square","Tri"}, 1); mix.setRange (0,1); detune.setRange (-24,24,0.01); cutoff.setRange (20,20000,1); cutoff.setSkewFactorFromMidPoint (500.0); reso.setRange (0.1,1.0,0.0); envAmt.setRange (-1,1,0.0); aA.setRange (0.001,2.0,0.0); dA.setRange (0.001,2.0,0.0); sA.setRange (0,1); rA.setRange (0.001,4.0,0.0); aF.setRange (0.001,2.0,0.0); dF.setRange (0.001,2.0,0.0); sF.setRange (0,1); rF.setRange (0.001,4.0,0.0); masterGain.setRange (-36.0, 12.0, 0.1); // dB // LFO GUI ranges (matching APVTS) lfoRate.setRange (0.0, 20.0, 0.01); // Hz lfoDepth.setRange (-4.0, 4.0, 0.0); // octaves (mapped to lfoToCut) for (auto* s : { &mix,&detune,&cutoff,&reso,&envAmt, &aA,&dA,&sA,&rA,&aF,&dF,&sF,&rF,&masterGain, &lfoRate,&lfoDepth }) styleKnob (*s); for (auto* l : { &lOscA,&lOscB,&lMix,&lDetune,&lCutoff,&lReso,&lEnv, &lAA,&lDA,&lSA,&lRA,&lAF,&lDF,&lSF,&lRF,&lMaster, &lLfoRate,&lLfoDepth }) styleLabel (*l); // Add with captions addLabeled (oscA, lOscA, "Osc A"); addLabeled (oscB, lOscB, "Osc B"); addLabeled (lfoRate, lLfoRate, "LFO Rate (Hz)"); addLabeled (lfoDepth, lLfoDepth, "LFO Depth (oct)"); addLabeled (mix, lMix, "A/B Mix"); addLabeled (detune, lDetune, "Detune (c)"); addLabeled (cutoff, lCutoff, "Cutoff"); addLabeled (reso, lReso, "Resonance"); addLabeled (aF, lAF, "Filt A"); addLabeled (dF, lDF, "Filt D"); addLabeled (sF, lSF, "Filt S"); addLabeled (rF, lRF, "Filt R"); addLabeled (envAmt, lEnv, "Filter Env"); addLabeled (aA, lAA, "Amp A"); addLabeled (dA, lDA, "Amp D"); addLabeled (sA, lSA, "Amp S"); addLabeled (rA, lRA, "Amp R"); addLabeled (masterGain, lMaster, "Master"); // Attach to APVTS auto& apvts = processor.apvts; aOscA.reset (new CA (apvts, "oscA", oscA)); aOscB.reset (new CA (apvts, "oscB", oscB)); aMix.reset (new SA (apvts, "mix", mix)); aDetune.reset (new SA (apvts, "detune", detune)); aCutoff.reset (new SA (apvts, "cutoff", cutoff)); aReso.reset (new SA (apvts, "reso", reso)); aEnv.reset (new SA (apvts, "envAmt", envAmt)); aAA.reset (new SA (apvts, "aA", aA)); aDA.reset (new SA (apvts, "dA", dA)); aSA.reset (new SA (apvts, "sA", sA)); aRA.reset (new SA (apvts, "rA", rA)); aAF.reset (new SA (apvts, "aF", aF)); aDF.reset (new SA (apvts, "dF", dF)); aSF.reset (new SA (apvts, "sF", sF)); aRF.reset (new SA (apvts, "rF", rF)); aMaster.reset (new SA (apvts, "masterGain", masterGain)); // LFO attachments aLfoRate .reset (new SA (apvts, "lfoRate", lfoRate)); aLfoDepth.reset (new SA (apvts, "lfoToCut", lfoDepth)); updatePresetDisplay(); // show the current preset name on load } TwoOscAudioProcessorEditor::~TwoOscAudioProcessorEditor() { for (auto* s : { &mix,&detune,&cutoff,&reso,&envAmt, &aA,&dA,&sA,&rA,&aF,&dF,&sF,&rF,&masterGain, &lfoRate,&lfoDepth }) s->setLookAndFeel (nullptr); } //------------------------------------------------------------------------------ // Styling void TwoOscAudioProcessorEditor::styleKnob (juce::Slider& s) { s.setLookAndFeel (&retroLNF); s.setSliderStyle (juce::Slider::RotaryVerticalDrag); s.setTextBoxStyle (juce::Slider::NoTextBox, false, 0, 0); // SW -> SE 270° sweep const float start = juce::MathConstants::pi * 0.75f; // 135° const float sweep = juce::MathConstants::pi * 1.5f; // 270° s.setRotaryParameters (start, start + sweep, true); } void TwoOscAudioProcessorEditor::styleLabel (juce::Label& l) { l.setJustificationType (juce::Justification::centred); l.setColour (juce::Label::textColourId, juce::Colours::white.withAlpha (0.85f)); l.setFont (juce::Font (juce::FontOptions (11.0f, juce::Font::plain))); // a touch smaller addAndMakeVisible (l); } void TwoOscAudioProcessorEditor::addLabeled (juce::Component& c, juce::Label& label, const juce::String& text) { addAndMakeVisible (c); label.setText (text, juce::dontSendNotification); } //------------------------------------------------------------------------------ // Layout & painting void TwoOscAudioProcessorEditor::paint (juce::Graphics& g) { g.fillAll (juce::Colours::black); const int corner = 10; auto chrome = getLocalBounds().reduced (8); // Header band auto header = chrome.removeFromTop (headerH); g.setColour (juce::Colours::white.withAlpha (0.06f)); g.fillRoundedRectangle (header.toFloat(), (float) corner); g.setColour (juce::Colours::white.withAlpha (0.10f)); g.drawRoundedRectangle (header.toFloat(), (float) corner, 1.0f); // Body panel g.setColour (juce::Colours::white.withAlpha (0.08f)); g.fillRoundedRectangle (chrome.toFloat(), (float) corner); g.setColour (juce::Colours::white.withAlpha (0.12f)); g.drawRoundedRectangle (chrome.toFloat(), (float) corner, 1.0f); // Preset "screen" if (! presetBarBounds.isEmpty()) { auto r = presetBarBounds.toFloat(); const float innerCorner = 6.0f; g.setColour (juce::Colours::white.withAlpha (0.06f)); g.fillRoundedRectangle (r, innerCorner); g.setColour (juce::Colours::grey.withAlpha (0.45f)); g.drawRoundedRectangle (r, innerCorner, 1.0f); } // --- Group backgrounds --- auto drawGroup = [&g](juce::Rectangle bounds, juce::Colour fill, juce::Colour stroke) { if (bounds.isEmpty()) return; auto rf = bounds.toFloat(); const float r = 8.0f; g.setColour (fill); g.fillRoundedRectangle (rf, r); g.setColour (stroke); g.drawRoundedRectangle (rf, r, 1.0f); }; // Neutral soft grey const auto softFill = juce::Colours::white.withAlpha (0.10f); const auto softEdge = juce::Colours::white.withAlpha (0.16f); // Amp ADSR: darker + teal const auto ampFill = juce::Colour::fromRGB (34, 50, 54).withAlpha (0.40f); const auto ampEdge = juce::Colour::fromRGB (70, 110, 118).withAlpha (0.50f); drawGroup (groupCutResBounds, softFill, softEdge); drawGroup (groupMixDetBounds, softFill, softEdge); drawGroup (groupLfoBounds, softFill, softEdge); // LFO block drawGroup (groupAmpBounds, ampFill, ampEdge); } void TwoOscAudioProcessorEditor::resized() { auto r = getLocalBounds().reduced (10); // ---------- Header ---------- auto header = r.removeFromTop (headerH); { auto titleArea = header.removeFromLeft (260).reduced (12, 8); title.setBounds (titleArea); auto screen = header.reduced (12, 8); presetBarBounds = screen; const int lineH = 30; // slightly smaller const int btnW = 98; auto inner = screen.withHeight (lineH) .withY (screen.getY() + (screen.getHeight() - lineH) / 2) .reduced (8, 0); auto lbl = inner.removeFromLeft (inner.getWidth() - btnW - 8); presetDisplay.setBounds (lbl); inner.removeFromLeft (8); browseBtn.setBounds (inner.removeFromLeft (btnW)); } r.removeFromTop (6); const auto bodyClamp = r; // ---------- helpers (tight captions) ---------- auto putMenu = [&](juce::ComboBox& box, juce::Label& lab, juce::Rectangle cell, int menuH = 30) { const int labelH = 14; lab.setBounds (cell.removeFromTop (labelH)); cell.removeFromTop (2); auto b = cell.withHeight (menuH); b.setY (cell.getY() + (cell.getHeight() - menuH) / 2); box.setBounds (b); }; auto putKnob = [&](juce::Slider& s, juce::Label& lab, juce::Rectangle cell) { const int labelH = 14; lab.setBounds (cell.removeFromTop (labelH)); cell.removeFromTop (2); auto k = juce::Rectangle (knobSize, knobSize) .withCentre ({ cell.getCentreX(), cell.getY() + knobSize / 2 }); s.setBounds (k); }; auto rowCells = [&](juce::Rectangle row, int cols) { juce::Array> cells; const int totalGap = gap * (cols - 1); const int colW = (row.getWidth() - totalGap) / cols; for (int c = 0; c < cols; ++c) { auto cell = row.removeFromLeft (colW); if (c < cols - 1) row.removeFromLeft (gap); cells.add (cell); } return cells; }; const int rowH = knobSize + 14 + 6 + 12; // a little tighter // ---------- Top row ---------- auto topRowArea = r.removeFromTop (rowH); auto top = rowCells (topRowArea, 6); { putMenu (oscA, lOscA, top[0].reduced (6)); putMenu (oscB, lOscB, top[1].reduced (6)); // LFO knobs use the middle two cells putKnob (lfoRate, lLfoRate, top[2]); putKnob (lfoDepth, lLfoDepth, top[3]); putKnob (mix, lMix, top[4]); putKnob (detune, lDetune, top[5]); // SAFE padding so backgrounds never overlap (use at most half the inter-column gap) const int safePad = juce::jmax (2, gap / 2 - 2); // with gap=10, pad=3 groupLfoBounds = top[2].getUnion (top[3]).expanded (safePad) .getIntersection (topRowArea).getIntersection (bodyClamp); groupMixDetBounds = top[4].getUnion (top[5]).expanded (safePad) .getIntersection (topRowArea).getIntersection (bodyClamp); } r.removeFromTop (6); // ---------- Middle row ---------- auto midRowArea = r.removeFromTop (rowH); auto mid = rowCells (midRowArea, 6); { putKnob (cutoff, lCutoff, mid[0]); putKnob (reso, lReso, mid[1]); putKnob (aF, lAF, mid[2]); putKnob (dF, lDF, mid[3]); putKnob (sF, lSF, mid[4]); putKnob (rF, lRF, mid[5]); const int pad = 12; groupCutResBounds = mid[0].getUnion (mid[1]).expanded (pad) .getIntersection (midRowArea).getIntersection (bodyClamp); } r.removeFromTop (6); // ---------- Bottom row ---------- auto botRowArea = r.removeFromTop (rowH); auto bot = rowCells (botRowArea, 6); { putKnob (envAmt, lEnv, bot[0]); putKnob (aA, lAA, bot[1]); putKnob (dA, lDA, bot[2]); putKnob (sA, lSA, bot[3]); putKnob (rA, lRA, bot[4]); putKnob (masterGain, lMaster, bot[5]); const int pad = 14; groupAmpBounds = bot[1].getUnion (bot[2]).getUnion (bot[3]).getUnion (bot[4]).expanded (pad) .getIntersection (botRowArea).getIntersection (bodyClamp); } } //------------------------------------------------------------------------------ // Preset UI void TwoOscAudioProcessorEditor::updatePresetDisplay() { const auto label = processor.getCurrentPresetLabel(); // "Category - Name" presetDisplay.setText (label, juce::dontSendNotification); } void TwoOscAudioProcessorEditor::showPresetPopup() { juce::PopupMenu root; const juce::StringArray cats = processor.getPresetCategories(); for (int c = 0; c < cats.size(); ++c) { juce::PopupMenu sub; const auto indices = processor.getPresetIndicesForCategory (cats[c]); for (int j = 0; j < indices.size(); ++j) { const int idx = indices[j]; const juce::String nm = processor.getPresetLabel (idx); sub.addItem (1000 + idx, nm); } root.addSubMenu (cats[c], sub, sub.getNumItems() > 0); } root.addSeparator(); root.addItem (999, "Init patch"); root.showMenuAsync (juce::PopupMenu::Options(), [this](int res) { if (res == 999) { // "Init" = simple neutral patch setChoiceParam (processor.apvts, "oscA", 0); setChoiceParam (processor.apvts, "oscB", 0); setFloatParam (processor.apvts, "mix", 0.5f); setFloatParam (processor.apvts, "detune", 0.0f); setFloatParam (processor.apvts, "cutoff", 1000.0f); setFloatParam (processor.apvts, "reso", 0.24f); setFloatParam (processor.apvts, "envAmt", 0.30f); setFloatParam (processor.apvts, "aA", 0.01f); setFloatParam (processor.apvts, "dA", 0.25f); setFloatParam (processor.apvts, "sA", 0.85f); setFloatParam (processor.apvts, "rA", 0.40f); setFloatParam (processor.apvts, "aF", 0.05f); setFloatParam (processor.apvts, "dF", 0.30f); setFloatParam (processor.apvts, "sF", 0.40f); setFloatParam (processor.apvts, "rF", 0.40f); setFloatParam (processor.apvts, "masterGain", -18.0f); // Reset LFO for Init setFloatParam (processor.apvts, "lfoRate", 0.0f); setFloatParam (processor.apvts, "lfoToCut", 0.0f); setFloatParam (processor.apvts, "lfoToPitch", 0.0f); presetDisplay.setText ("Init", juce::dontSendNotification); return; } if (res >= 1000) { const int patchIndex = res - 1000; if (auto* prm = dynamic_cast (processor.apvts.getParameter ("presetIndex"))) { const int n = prm->choices.size(); const float norm = (n > 1 ? (float) patchIndex / (float) (n - 1) : 0.0f); prm->beginChangeGesture(); prm->setValueNotifyingHost (juce::jlimit (0.0f, 1.0f, norm)); prm->endChangeGesture(); } updatePresetDisplay(); } }); }