Initial commit
This commit is contained in:
431
PluginEditor.cpp
Normal file
431
PluginEditor.cpp
Normal file
@@ -0,0 +1,431 @@
|
||||
#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<juce::RangedAudioParameter*> (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<juce::AudioParameterChoice*> (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<float>::pi * 0.75f; // 135°
|
||||
const float sweep = juce::MathConstants<float>::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<int> 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<int> 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<int> cell)
|
||||
{
|
||||
const int labelH = 14;
|
||||
lab.setBounds (cell.removeFromTop (labelH));
|
||||
cell.removeFromTop (2);
|
||||
auto k = juce::Rectangle<int> (knobSize, knobSize)
|
||||
.withCentre ({ cell.getCentreX(), cell.getY() + knobSize / 2 });
|
||||
s.setBounds (k);
|
||||
};
|
||||
|
||||
auto rowCells = [&](juce::Rectangle<int> row, int cols)
|
||||
{
|
||||
juce::Array<juce::Rectangle<int>> 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<juce::AudioParameterChoice*> (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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user