mirror of
https://codeberg.org/armin/monostep.git
synced 2026-09-01 04:10:46 +02:00
Polish GUI, add pattern retrig option, and rework voice DSP
This commit is contained in:
parent
8ed279e84c
commit
4732240cc7
7 changed files with 546 additions and 129 deletions
|
|
@ -8,6 +8,10 @@ using namespace monostep;
|
|||
|
||||
namespace
|
||||
{
|
||||
constexpr int oscDropW = 96; // OSC 1 / OSC 2 dropdown width
|
||||
constexpr int oscLabelW = 46; // label strip to the left of each dropdown
|
||||
constexpr int oscGapW = 8; // gap between the dropdown column and the knobs
|
||||
|
||||
juce::ColourGradient orangeGradient (const juce::Rectangle<float>& area)
|
||||
{
|
||||
const auto top = area.getTopLeft().translated (area.getWidth() * 0.5f, 0.0f);
|
||||
|
|
@ -40,11 +44,16 @@ void EditorLookAndFeel::drawRotarySlider (juce::Graphics& g, int x, int y, int w
|
|||
const float end = rotaryEndAngle;
|
||||
const float angle = start + sliderPos * (end - start);
|
||||
|
||||
g.setColour (colours::grid);
|
||||
g.fillEllipse (centre.getX() - radius, centre.getY() - radius, radius * 2.0f, radius * 2.0f);
|
||||
const auto bodyRect = juce::Rectangle<float> (centre.getX() - radius, centre.getY() - radius,
|
||||
radius * 2.0f, radius * 2.0f);
|
||||
|
||||
g.setColour (colours::gridLight);
|
||||
g.drawEllipse (centre.getX() - radius, centre.getY() - radius, radius * 2.0f, radius * 2.0f, 1.0f);
|
||||
// Raised body: lighter at the top edge, darker at the bottom.
|
||||
juce::ColourGradient bodyGrad (colours::gridLight.brighter (0.15f),
|
||||
centre.getX(), bodyRect.getY(),
|
||||
colours::grid.darker (0.18f),
|
||||
centre.getX(), bodyRect.getBottom(), false);
|
||||
g.setGradientFill (bodyGrad);
|
||||
g.fillEllipse (bodyRect);
|
||||
|
||||
// Almost-vertical (-2°) shading overlay to give the dial a subtle 3D look.
|
||||
constexpr float degToRad = juce::MathConstants<float>::pi / 180.0f;
|
||||
|
|
@ -54,10 +63,14 @@ void EditorLookAndFeel::drawRotarySlider (juce::Graphics& g, int x, int y, int w
|
|||
const auto topPoint = centre + juce::Point<float> ( radius * tiltSin, -radius * tiltCos);
|
||||
const auto bottomPoint = centre + juce::Point<float> (-radius * tiltSin, radius * tiltCos);
|
||||
|
||||
juce::ColourGradient shading (juce::Colours::white.withAlpha (0.07f), topPoint,
|
||||
juce::Colours::black.withAlpha (0.20f), bottomPoint, false);
|
||||
juce::ColourGradient shading (juce::Colours::black.withAlpha (0.10f), topPoint,
|
||||
juce::Colours::black.withAlpha (0.10f), bottomPoint, false);
|
||||
g.setGradientFill (shading);
|
||||
g.fillEllipse (centre.getX() - radius, centre.getY() - radius, radius * 2.0f, radius * 2.0f);
|
||||
g.fillEllipse (bodyRect);
|
||||
|
||||
// Outer rim.
|
||||
g.setColour (colours::gridLight);
|
||||
g.drawEllipse (bodyRect, 1.0f);
|
||||
|
||||
if (end - start < 6.0f)
|
||||
{
|
||||
|
|
@ -88,11 +101,27 @@ void EditorLookAndFeel::drawToggleButton (juce::Graphics& g, juce::ToggleButton&
|
|||
const auto bounds = button.getLocalBounds().toFloat();
|
||||
|
||||
if (button.getToggleState())
|
||||
{
|
||||
g.setGradientFill (orangeGradient (bounds));
|
||||
g.fillRoundedRectangle (bounds, 4.0f);
|
||||
}
|
||||
else
|
||||
g.setColour (colours::grid);
|
||||
{
|
||||
// Raised, slightly-3D inactive button.
|
||||
juce::ColourGradient offGrad (colours::gridLight.brighter (0.12f),
|
||||
bounds.getX(), bounds.getY(),
|
||||
colours::grid.darker (0.15f),
|
||||
bounds.getX(), bounds.getBottom(), false);
|
||||
g.setGradientFill (offGrad);
|
||||
g.fillRoundedRectangle (bounds, 4.0f);
|
||||
|
||||
g.fillRoundedRectangle (bounds, 4.0f);
|
||||
g.setColour (colours::gridLight.brighter (0.35f).withAlpha (0.45f));
|
||||
g.drawHorizontalLine (juce::roundToInt (bounds.getY()) + 1,
|
||||
bounds.getX() + 3.0f, bounds.getRight() - 3.0f);
|
||||
|
||||
g.setColour (colours::bg.withAlpha (0.30f));
|
||||
g.drawRoundedRectangle (bounds.reduced (0.5f), 4.0f, 1.0f);
|
||||
}
|
||||
|
||||
g.setFont (font (11.0f));
|
||||
g.setColour (button.getToggleState() ? colours::bg : colours::text);
|
||||
|
|
@ -105,11 +134,25 @@ void EditorLookAndFeel::drawButtonBackground (juce::Graphics& g, juce::Button& b
|
|||
const auto bounds = button.getLocalBounds().toFloat().reduced (0.5f);
|
||||
|
||||
if (button.getToggleState())
|
||||
{
|
||||
g.setGradientFill (orangeGradient (bounds));
|
||||
g.fillRoundedRectangle (bounds, 4.0f);
|
||||
}
|
||||
else
|
||||
g.setColour (highlighted ? colours::gridLight : colours::grid);
|
||||
{
|
||||
// Raised, slightly-3D inactive button.
|
||||
const juce::Colour topColour = highlighted ? colours::gridLight.brighter (0.22f)
|
||||
: colours::gridLight.brighter (0.10f);
|
||||
juce::ColourGradient offGrad (topColour, bounds.getX(), bounds.getY(),
|
||||
colours::grid.darker (0.15f),
|
||||
bounds.getX(), bounds.getBottom(), false);
|
||||
g.setGradientFill (offGrad);
|
||||
g.fillRoundedRectangle (bounds, 4.0f);
|
||||
|
||||
g.fillRoundedRectangle (bounds, 4.0f);
|
||||
g.setColour (colours::gridLight.brighter (0.35f).withAlpha (0.45f));
|
||||
g.drawHorizontalLine (juce::roundToInt (bounds.getY()) + 1,
|
||||
bounds.getX() + 3.0f, bounds.getRight() - 3.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void EditorLookAndFeel::drawButtonText (juce::Graphics& g, juce::TextButton& button, bool, bool)
|
||||
|
|
@ -124,11 +167,25 @@ void EditorLookAndFeel::drawComboBox (juce::Graphics& g, int width, int height,
|
|||
{
|
||||
const auto bounds = juce::Rectangle<float> (1.0f, 1.0f, (float) (width - 2), (float) (height - 2));
|
||||
|
||||
g.setColour (colours::grid);
|
||||
// Subtle drop shadow under the box.
|
||||
g.setColour (juce::Colours::black.withAlpha (0.30f));
|
||||
g.fillRoundedRectangle (bounds.translated (0.0f, 1.5f), 4.0f);
|
||||
|
||||
// Raised box: lighter top edge, darker bottom.
|
||||
juce::ColourGradient boxGrad (colours::gridLight.brighter (0.10f),
|
||||
bounds.getX(), bounds.getY(),
|
||||
colours::grid.darker (0.15f),
|
||||
bounds.getX(), bounds.getBottom(), false);
|
||||
g.setGradientFill (boxGrad);
|
||||
g.fillRoundedRectangle (bounds, 4.0f);
|
||||
|
||||
g.setColour (colours::gridLight);
|
||||
g.drawRoundedRectangle (bounds, 4.0f, 1.0f);
|
||||
|
||||
g.setColour (colours::gridLight.brighter (0.35f).withAlpha (0.45f));
|
||||
g.drawHorizontalLine (juce::roundToInt (bounds.getY()) + 1,
|
||||
bounds.getX() + 4.0f, bounds.getRight() - 4.0f);
|
||||
|
||||
const float cx = (float) (width - 15);
|
||||
const float cy = (float) height * 0.5f;
|
||||
|
||||
|
|
@ -145,7 +202,10 @@ juce::Font EditorLookAndFeel::getComboBoxFont (juce::ComboBox&)
|
|||
|
||||
void EditorLookAndFeel::drawPopupMenuBackground (juce::Graphics& g, int width, int height)
|
||||
{
|
||||
g.setColour (colours::panel);
|
||||
// Slightly recessed panel with a gentle top-to-bottom gradient.
|
||||
juce::ColourGradient bgGrad (colours::gridLight, 0.0f, 0.0f,
|
||||
colours::panel.darker (0.06f), 0.0f, (float) height, false);
|
||||
g.setGradientFill (bgGrad);
|
||||
g.fillRect (0, 0, width, height);
|
||||
g.setColour (colours::gridLight);
|
||||
g.drawRect (0.0f, 0.0f, (float) width, (float) height, 1.0f);
|
||||
|
|
@ -383,12 +443,6 @@ StepPanel::StepPanel (MonostepAudioProcessor& processorRef)
|
|||
nextButton.onClick = [this] { setSelectedStep (selectedStep + 1); };
|
||||
addAndMakeVisible (nextButton);
|
||||
|
||||
hintLabel.setText ("click a column in the grid or use the arrows to pick the step to fine-tune", juce::dontSendNotification);
|
||||
hintLabel.setJustificationType (juce::Justification::centred);
|
||||
hintLabel.setFont (font (8.5f));
|
||||
hintLabel.setColour (juce::Label::textColourId, colours::textDim);
|
||||
addAndMakeVisible (hintLabel);
|
||||
|
||||
clearButton.setButtonText ("Clear Pattern");
|
||||
clearButton.onClick = [this] { clearPattern(); };
|
||||
addAndMakeVisible (clearButton);
|
||||
|
|
@ -453,10 +507,23 @@ StepPanel::StepPanel (MonostepAudioProcessor& processorRef)
|
|||
for (int i = 0; i < RandomGroupCount; ++i)
|
||||
{
|
||||
randomizeToggles[i].setButtonText (groupNames[i]);
|
||||
randomizeToggles[i].setToggleState (true, juce::dontSendNotification);
|
||||
randomizeToggles[i].setToggleState (i == RandomPattern, juce::dontSendNotification);
|
||||
addAndMakeVisible (randomizeToggles[i]);
|
||||
}
|
||||
|
||||
resetFineButton.setButtonText ("Reset Fine");
|
||||
resetFineButton.onClick = [this]
|
||||
{
|
||||
for (int i = 0; i < numSteps; ++i)
|
||||
processor.setStepCents (i, 0.0f);
|
||||
};
|
||||
addAndMakeVisible (resetFineButton);
|
||||
|
||||
retrigToggle.setButtonText ("Retrig");
|
||||
retrigToggle.setTooltip ("Restart the pattern from step 1 on each new note trigger");
|
||||
retrigAttachment = std::make_unique<juce::AudioProcessorValueTreeState::ButtonAttachment> (processor.getAPVTS(), "seqRetrig", retrigToggle);
|
||||
addAndMakeVisible (retrigToggle);
|
||||
|
||||
setSelectedStep (0);
|
||||
}
|
||||
|
||||
|
|
@ -524,8 +591,6 @@ void StepPanel::resized()
|
|||
prevButton.setBounds (knobX - 32, btnY, 24, 24);
|
||||
nextButton.setBounds (knobX + 74 + 8, btnY, 24, 24);
|
||||
|
||||
hintLabel.setBounds (8, btnY + 30, w - 16, 14);
|
||||
|
||||
const int shiftW = 22;
|
||||
shiftLeftButton.setBounds (10, btnY + 50, shiftW, 26);
|
||||
clearButton.setBounds (10 + shiftW + 4, btnY + 50, w - 20 - shiftW * 2 - 8, 26);
|
||||
|
|
@ -545,6 +610,10 @@ void StepPanel::resized()
|
|||
const int row = i % 3;
|
||||
randomizeToggles[i].setBounds (10 + col * (colW + 4), btnY + 180 + row * (rowH + 2), colW, rowH);
|
||||
}
|
||||
|
||||
const int retrigW = 100;
|
||||
retrigToggle.setBounds (10, btnY + 252, retrigW, 26);
|
||||
resetFineButton.setBounds (10 + retrigW + 4, btnY + 252, w - 20 - retrigW - 4, 26);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
|
@ -571,6 +640,11 @@ MonostepAudioProcessorEditor::MonostepAudioProcessorEditor (MonostepAudioProcess
|
|||
decayKnob ("Decay"),
|
||||
sustainKnob ("Sustain"),
|
||||
releaseKnob ("Release"),
|
||||
fAttackKnob ("Attack"),
|
||||
fDecayKnob ("Decay"),
|
||||
fSustainKnob ("Sustain"),
|
||||
fReleaseKnob ("Release"),
|
||||
fAmountKnob ("Amt"),
|
||||
driveKnob ("Drive"),
|
||||
ringKnob ("Ring"),
|
||||
accentKnob ("Accent"),
|
||||
|
|
@ -592,41 +666,58 @@ MonostepAudioProcessorEditor::MonostepAudioProcessorEditor (MonostepAudioProcess
|
|||
content.addAndMakeVisible (octaveBar);
|
||||
content.addAndMakeVisible (zoomBar);
|
||||
|
||||
static const juce::StringArray waveformChoices =
|
||||
const juce::StringArray waveformChoices =
|
||||
{
|
||||
"Sine", "Triangle", "Saw", "Square",
|
||||
"Pulse", "Noise", "Sub", "Pluck",
|
||||
"Super", "S&H", "Formant", "PWM"
|
||||
};
|
||||
|
||||
auto setupWaveCombo = [&apvts, &waveformChoices] (juce::ComboBox& combo,
|
||||
const juce::String& paramId)
|
||||
auto setupWaveCombo = [&waveformChoices] (juce::ComboBox& combo)
|
||||
{
|
||||
combo.setColour (juce::ComboBox::textColourId, colours::text);
|
||||
combo.setColour (juce::ComboBox::outlineColourId, colours::gridLight);
|
||||
combo.setColour (juce::ComboBox::backgroundColourId, colours::grid);
|
||||
combo.setColour (juce::ComboBox::arrowColourId, colours::accent);
|
||||
combo.setFont (font (11.0f));
|
||||
|
||||
for (int i = 0; i < waveformChoices.size(); ++i)
|
||||
combo.addItem (waveformChoices[i], i + 1);
|
||||
|
||||
combo.selectId (static_cast<int> (apvts.getRawParameterValue (paramId)->load()) + 1);
|
||||
};
|
||||
|
||||
setupWaveCombo (wave1Box, "osc1Wave");
|
||||
setupWaveCombo (wave2Box, "osc2Wave");
|
||||
setupWaveCombo (wave1Box);
|
||||
setupWaveCombo (wave2Box);
|
||||
|
||||
wave1Attachment = std::make_unique<juce::AudioProcessorValueTreeState::ComboBoxAttachment> (apvts, "osc1Wave", wave1Box);
|
||||
wave2Attachment = std::make_unique<juce::AudioProcessorValueTreeState::ComboBoxAttachment> (apvts, "osc2Wave", wave2Box);
|
||||
|
||||
// filter type dropdown (below the two OSC dropdowns)
|
||||
const juce::StringArray filterTypeChoices =
|
||||
{
|
||||
"12 LP", "12 HP", "12 BP", "12 Notch",
|
||||
"24 LP", "24 HP", "24 BP", "24 Notch",
|
||||
"48 LP", "48 HP", "48 BP", "48 Notch"
|
||||
};
|
||||
|
||||
for (int i = 0; i < filterTypeChoices.size(); ++i)
|
||||
filterTypeBox.addItem (filterTypeChoices[i], i + 1);
|
||||
|
||||
filterTypeBox.setColour (juce::ComboBox::textColourId, colours::text);
|
||||
filterTypeBox.setColour (juce::ComboBox::outlineColourId, colours::gridLight);
|
||||
filterTypeBox.setColour (juce::ComboBox::backgroundColourId, colours::grid);
|
||||
filterTypeBox.setColour (juce::ComboBox::arrowColourId, colours::accent);
|
||||
|
||||
filterTypeAttachment = std::make_unique<juce::AudioProcessorValueTreeState::ComboBoxAttachment> (apvts, "filterType", filterTypeBox);
|
||||
|
||||
content.addAndMakeVisible (wave1Box);
|
||||
content.addAndMakeVisible (wave2Box);
|
||||
content.addAndMakeVisible (filterTypeBox);
|
||||
|
||||
static constexpr float zoomValues[] = { 1.0f, 1.25f, 1.5f, 1.75f, 2.0f };
|
||||
zoomBar.setManualHandler ([this] (int idx)
|
||||
{
|
||||
setZoomFactor (zoomValues[juce::jlimit (0, 4, idx)]);
|
||||
idx = juce::jlimit (0, 4, idx);
|
||||
processorRef.setZoomIndex (idx);
|
||||
setZoomFactor (zoomValues[idx]);
|
||||
});
|
||||
|
||||
auto addKnob = [&apvts, this] (Knob& knob, const juce::String& id, std::function<juce::String (float)> fmt)
|
||||
|
|
@ -651,10 +742,17 @@ MonostepAudioProcessorEditor::MonostepAudioProcessorEditor (MonostepAudioProcess
|
|||
addKnob (decayKnob, "decay", [] (float v) { return juce::String (v, 2) + "s"; });
|
||||
addKnob (sustainKnob, "sustain", [] (float v) { return juce::String (v, 2); });
|
||||
addKnob (releaseKnob, "release", [] (float v) { return juce::String (v, 2) + "s"; });
|
||||
addKnob (fAttackKnob, "filterAttack", [] (float v) { return juce::String (v, 2) + "s"; });
|
||||
addKnob (fDecayKnob, "filterDecay", [] (float v) { return juce::String (v, 2) + "s"; });
|
||||
addKnob (fSustainKnob, "filterSustain", [] (float v) { return juce::String (v, 2); });
|
||||
addKnob (fReleaseKnob, "filterRelease", [] (float v) { return juce::String (v, 2) + "s"; });
|
||||
addKnob (fAmountKnob, "filterEnvAmt", [] (float v) { return (v >= 0.0f ? "+" : "") + juce::String (v, 2) + " oct"; });
|
||||
addKnob (driveKnob, "drive", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; });
|
||||
addKnob (ringKnob, "ringmod", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; });
|
||||
addKnob (accentKnob, "accent", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; });
|
||||
|
||||
fAmountKnob.setDragSensitivity (800);
|
||||
|
||||
content.addAndMakeVisible (matrix);
|
||||
content.addAndMakeVisible (stepPanel);
|
||||
|
||||
|
|
@ -708,6 +806,10 @@ MonostepAudioProcessorEditor::MonostepAudioProcessorEditor (MonostepAudioProcess
|
|||
startTimerHz (30);
|
||||
|
||||
refreshEditor();
|
||||
|
||||
const int savedZoom = processorRef.getZoomIndex();
|
||||
zoomBar.setSelectedIndex (savedZoom);
|
||||
setZoomFactor (zoomValues[juce::jlimit (0, 4, savedZoom)]);
|
||||
}
|
||||
|
||||
MonostepAudioProcessorEditor::~MonostepAudioProcessorEditor()
|
||||
|
|
@ -761,8 +863,58 @@ void MonostepAudioProcessorEditor::paintContent (juce::Graphics& g)
|
|||
drawCaption (rateBar, "RATE");
|
||||
drawCaption (octaveBar, "OCTAVE");
|
||||
drawCaption (zoomBar, "ZOOM");
|
||||
drawCaption (wave1Box, "OSC 1");
|
||||
drawCaption (wave2Box, "OSC 2");
|
||||
|
||||
// Small labels inside the OSC section, left of the dropdowns.
|
||||
g.setFont (font (8.5f));
|
||||
g.setColour (colours::textDim);
|
||||
g.drawText ("OSC 1", wave1Box.getX() - oscLabelW, wave1Box.getY() + 5, oscLabelW - 6, 14, juce::Justification::centredRight);
|
||||
g.drawText ("OSC 2", wave2Box.getX() - oscLabelW, wave2Box.getY() + 5, oscLabelW - 6, 14, juce::Justification::centredRight);
|
||||
|
||||
g.setFont (font (9.0f));
|
||||
|
||||
// Union of two knob bounds, padded.
|
||||
const auto groupBox = [] (const juce::Component& a, const juce::Component& b, float pad)
|
||||
{
|
||||
const auto ra = a.getBounds().toFloat();
|
||||
const auto rb = b.getBounds().toFloat();
|
||||
juce::Rectangle<float> r;
|
||||
r.setX (juce::jmin (ra.getX(), rb.getX()) - pad);
|
||||
r.setY (juce::jmin (ra.getY(), rb.getY()) - pad);
|
||||
r.setRight (juce::jmax (ra.getRight(), rb.getRight()) + pad);
|
||||
r.setBottom (juce::jmax (ra.getBottom(), rb.getBottom()) + pad);
|
||||
return r;
|
||||
};
|
||||
|
||||
// Draw a knob group: subtle gradient behind the title legend + a slight border
|
||||
// around the knobs themselves. leftPad extends the frame to the left so it can
|
||||
// enclose side labels.
|
||||
auto drawGroup = [&] (const juce::Component& first, const juce::Component& last,
|
||||
const juce::String& title, float pad, float leftPad = 0.0f)
|
||||
{
|
||||
const int capX = first.getX() - (int) leftPad;
|
||||
const int capW = last.getRight() - capX;
|
||||
const int capY = first.getY() - 13;
|
||||
|
||||
const juce::Rectangle<float> legend (capX - 6.0f, capY - 4.0f, (float) capW + 12.0f, 12.0f);
|
||||
g.setGradientFill (orangeGradient (legend));
|
||||
g.fillRoundedRectangle (legend, 3.0f);
|
||||
|
||||
g.setFont (font (9.0f).boldened());
|
||||
g.setColour (colours::bg);
|
||||
g.drawText (title, legend, juce::Justification::centred);
|
||||
|
||||
auto box = groupBox (first, last, pad);
|
||||
box.setLeft (box.getX() - leftPad);
|
||||
g.setColour (colours::accent.withAlpha (0.30f));
|
||||
g.drawRoundedRectangle (box, 3.0f, 1.0f);
|
||||
};
|
||||
|
||||
drawGroup (wave1Box, mixKnob, "OSC", 6.0f, (float) (oscLabelW - 21 + 9));
|
||||
drawGroup (glideKnob, accentKnob, "NOTE", 6.0f);
|
||||
drawGroup (driveKnob, ringKnob, "DIST", 6.0f);
|
||||
drawGroup (filterTypeBox, resKnob, "FILTER", 6.0f, 6.0f);
|
||||
drawGroup (attackKnob, releaseKnob, "AMP ENV", 6.0f);
|
||||
drawGroup (fAttackKnob, fAmountKnob, "FILTER ENV", 6.0f);
|
||||
|
||||
const int footY = baseHeight - 20;
|
||||
g.setColour (colours::grid);
|
||||
|
|
@ -801,7 +953,7 @@ void MonostepAudioProcessorEditor::resized()
|
|||
const int h = baseHeight;
|
||||
|
||||
const int topBarH = 78;
|
||||
const int bottomBarH = 96;
|
||||
const int bottomBarH = 240;
|
||||
const int pad = 12;
|
||||
const int topBarY = (topBarH - 26) / 2;
|
||||
|
||||
|
|
@ -823,33 +975,60 @@ void MonostepAudioProcessorEditor::resized()
|
|||
|
||||
const int bottomY = h - bottomBarH - pad;
|
||||
|
||||
int x = 16;
|
||||
const int barW = 112;
|
||||
|
||||
wave1Box.setBounds (x, bottomY + 30, barW, 24); x += barW + 8;
|
||||
wave2Box.setBounds (x, bottomY + 30, barW, 24); x += barW + 16;
|
||||
|
||||
const int knobW = 56;
|
||||
const int knobGap = 60;
|
||||
const int knobY = bottomY + 2;
|
||||
const int knobH = 82;
|
||||
const int knobGap = 6;
|
||||
const int step = knobW + knobGap; // left-edge advance per knob (62)
|
||||
|
||||
const auto placeKnob = [&] (Knob& knob)
|
||||
const int oscColW = oscLabelW + oscDropW + oscGapW; // labels + dropdowns + gap to knobs
|
||||
|
||||
// Both rows are centered over the full bottom bar width.
|
||||
const int blockW = oscColW + 6 * step + knobW + 48; // +48 = the two group gaps
|
||||
const int oscX = pad + ((w - 2 * pad) - blockW) / 2 + oscColW;
|
||||
const int row1Y = bottomY + 24;
|
||||
const int row2Y = row1Y + knobH + 34; // row 1 + a caption band
|
||||
|
||||
// OSC 1 / OSC 2 dropdowns stacked inside the OSC section, after the labels.
|
||||
const int oscDropX = oscX - oscColW + oscLabelW;
|
||||
wave1Box.setBounds (oscDropX, row1Y, oscDropW, 24);
|
||||
wave2Box.setBounds (oscDropX, row1Y + 30, oscDropW, 24);
|
||||
|
||||
auto placeRow = [&] (int y, int startX, const std::initializer_list<Knob*> knobs)
|
||||
{
|
||||
knob.setBounds (x, knobY, knobW, 82);
|
||||
x += knobGap;
|
||||
int cx = startX;
|
||||
for (auto* k : knobs)
|
||||
{
|
||||
k->setBounds (cx, y, knobW, knobH);
|
||||
cx += step;
|
||||
}
|
||||
};
|
||||
|
||||
placeKnob (coarseKnob);
|
||||
placeKnob (detuneKnob);
|
||||
placeKnob (mixKnob);
|
||||
placeKnob (cutoffKnob);
|
||||
placeKnob (resKnob);
|
||||
placeKnob (glideKnob);
|
||||
placeKnob (attackKnob);
|
||||
placeKnob (decayKnob);
|
||||
placeKnob (sustainKnob);
|
||||
placeKnob (releaseKnob);
|
||||
placeKnob (driveKnob);
|
||||
placeKnob (ringKnob);
|
||||
placeKnob (accentKnob);
|
||||
// Row 1: coarse / detune / mix / glide / accent / drive / ring.
|
||||
// NOTE and DIST are pushed apart to clear the group frames.
|
||||
placeRow (row1Y, oscX, { &coarseKnob, &detuneKnob, &mixKnob });
|
||||
placeRow (row1Y, oscX + 3 * step + 24, { &glideKnob, &accentKnob });
|
||||
placeRow (row1Y, oscX + 5 * step + 48, { &driveKnob, &ringKnob });
|
||||
|
||||
// Row 2: Filter (dropdown + cutoff, res) [gap] Amp Envelope [gap] Filter Env.
|
||||
// The three sections are centered as one block with equal frame gaps.
|
||||
const int frameGap = 20;
|
||||
const int framePad = 5;
|
||||
auto frameW = [&] (int n) { return n * knobW + (n - 1) * knobGap + 2 * framePad; };
|
||||
|
||||
const int filterDropW = oscDropW; // FILTER TYPE dropdown width (matches the OSC dropdowns)
|
||||
const int filterDropGap = 8; // gap between the dropdown and the filter knobs
|
||||
const int filterSecW = filterDropW + filterDropGap + 2 * knobW + knobGap + 2 * framePad;
|
||||
|
||||
const int row2W = filterSecW + frameGap + frameW (4) + frameGap + frameW (5);
|
||||
const int row2Left = pad + ((w - 2 * pad) - row2W) / 2;
|
||||
|
||||
const int filterDropX = row2Left + framePad;
|
||||
const int filterKnobX = filterDropX + filterDropW + filterDropGap;
|
||||
const int envStart = row2Left + filterSecW + frameGap + framePad;
|
||||
const int fenvStart = row2Left + filterSecW + frameGap + frameW (4) + frameGap + framePad;
|
||||
|
||||
filterTypeBox.setBounds (filterDropX, row2Y, filterDropW, 24);
|
||||
placeRow (row2Y, filterKnobX, { &cutoffKnob, &resKnob });
|
||||
placeRow (row2Y, envStart, { &attackKnob, &decayKnob, &sustainKnob, &releaseKnob });
|
||||
placeRow (row2Y, fenvStart, { &fAttackKnob, &fDecayKnob, &fSustainKnob, &fReleaseKnob, &fAmountKnob });
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue