2026-08-05 19:55:57 +02:00
|
|
|
#include "PluginEditor.h"
|
|
|
|
|
|
|
|
|
|
#include "PluginProcessor.h"
|
|
|
|
|
#include "Theme.h"
|
|
|
|
|
#include "BuildInfo.h"
|
|
|
|
|
|
|
|
|
|
using namespace monostep;
|
|
|
|
|
|
2026-08-05 23:12:17 +02:00
|
|
|
namespace
|
|
|
|
|
{
|
|
|
|
|
juce::ColourGradient orangeGradient (const juce::Rectangle<float>& area)
|
|
|
|
|
{
|
|
|
|
|
const auto top = area.getTopLeft().translated (area.getWidth() * 0.5f, 0.0f);
|
|
|
|
|
const auto bottom = area.getBottomLeft().translated (area.getWidth() * 0.5f, 0.0f);
|
|
|
|
|
|
|
|
|
|
return juce::ColourGradient (colours::accent, top,
|
|
|
|
|
colours::accent.darker (0.35f), bottom,
|
|
|
|
|
false);
|
|
|
|
|
}
|
|
|
|
|
} // namespace
|
|
|
|
|
|
2026-08-05 19:55:57 +02:00
|
|
|
//==============================================================================
|
|
|
|
|
EditorLookAndFeel::EditorLookAndFeel()
|
|
|
|
|
{
|
|
|
|
|
setColour (juce::Slider::textBoxBackgroundColourId, colours::panel);
|
|
|
|
|
setColour (juce::Slider::textBoxTextColourId, colours::text);
|
|
|
|
|
setColour (juce::Slider::textBoxOutlineColourId, colours::grid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void EditorLookAndFeel::drawRotarySlider (juce::Graphics& g, int x, int y, int w, int h,
|
|
|
|
|
float sliderPos, float rotaryStartAngle,
|
|
|
|
|
float rotaryEndAngle, juce::Slider&)
|
|
|
|
|
{
|
|
|
|
|
const auto bounds = juce::Rectangle<float> ((float) x, (float) y, (float) w, (float) h);
|
|
|
|
|
const auto centre = bounds.getCentre();
|
|
|
|
|
|
|
|
|
|
const float radius = juce::jmin (bounds.getWidth(), bounds.getHeight()) * 0.5f;
|
|
|
|
|
|
|
|
|
|
const float start = rotaryStartAngle;
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
g.setColour (colours::gridLight);
|
|
|
|
|
g.drawEllipse (centre.getX() - radius, centre.getY() - radius, radius * 2.0f, radius * 2.0f, 1.0f);
|
|
|
|
|
|
2026-08-05 23:12:17 +02:00
|
|
|
// Almost-vertical (-2°) shading overlay to give the dial a subtle 3D look.
|
|
|
|
|
constexpr float degToRad = juce::MathConstants<float>::pi / 180.0f;
|
|
|
|
|
const float tiltSin = std::sin (-2.0f * degToRad);
|
|
|
|
|
const float tiltCos = std::cos (-2.0f * degToRad);
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
g.setGradientFill (shading);
|
|
|
|
|
g.fillEllipse (centre.getX() - radius, centre.getY() - radius, radius * 2.0f, radius * 2.0f);
|
|
|
|
|
|
2026-08-05 19:55:57 +02:00
|
|
|
if (end - start < 6.0f)
|
|
|
|
|
{
|
|
|
|
|
juce::Path valueArc;
|
|
|
|
|
valueArc.addCentredArc (centre.getX(), centre.getY(),
|
|
|
|
|
radius * 0.85f, radius * 0.85f,
|
|
|
|
|
0.0f, start, angle, true);
|
|
|
|
|
|
2026-08-05 23:12:17 +02:00
|
|
|
g.setGradientFill (orangeGradient (bounds));
|
2026-08-05 19:55:57 +02:00
|
|
|
g.strokePath (valueArc, juce::PathStrokeType (2.5f));
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-05 23:12:17 +02:00
|
|
|
// The pointer reaches out to the value arc so it touches the arc.
|
|
|
|
|
const float tickLen = radius * 0.85f + 1.0f;
|
|
|
|
|
g.setGradientFill (orangeGradient (bounds));
|
2026-08-05 19:55:57 +02:00
|
|
|
g.drawLine (centre.getX(), centre.getY(),
|
|
|
|
|
centre.getX() + tickLen * std::sin (angle),
|
|
|
|
|
centre.getY() - tickLen * std::cos (angle),
|
|
|
|
|
2.5f);
|
|
|
|
|
|
2026-08-05 23:12:17 +02:00
|
|
|
g.setColour (colours::accent);
|
2026-08-05 19:55:57 +02:00
|
|
|
g.fillEllipse (centre.getX() - 2.5f, centre.getY() - 2.5f, 5.0f, 5.0f);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void EditorLookAndFeel::drawToggleButton (juce::Graphics& g, juce::ToggleButton& button,
|
|
|
|
|
bool, bool)
|
|
|
|
|
{
|
|
|
|
|
const auto bounds = button.getLocalBounds().toFloat();
|
|
|
|
|
|
2026-08-05 23:12:17 +02:00
|
|
|
if (button.getToggleState())
|
|
|
|
|
g.setGradientFill (orangeGradient (bounds));
|
|
|
|
|
else
|
|
|
|
|
g.setColour (colours::grid);
|
|
|
|
|
|
2026-08-05 19:55:57 +02:00
|
|
|
g.fillRoundedRectangle (bounds, 4.0f);
|
|
|
|
|
|
|
|
|
|
g.setFont (font (11.0f));
|
|
|
|
|
g.setColour (button.getToggleState() ? colours::bg : colours::text);
|
|
|
|
|
g.drawText (button.getButtonText(), bounds, juce::Justification::centred);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void EditorLookAndFeel::drawButtonBackground (juce::Graphics& g, juce::Button& button,
|
|
|
|
|
const juce::Colour&, bool highlighted, bool)
|
|
|
|
|
{
|
|
|
|
|
const auto bounds = button.getLocalBounds().toFloat().reduced (0.5f);
|
|
|
|
|
|
|
|
|
|
if (button.getToggleState())
|
2026-08-05 23:12:17 +02:00
|
|
|
g.setGradientFill (orangeGradient (bounds));
|
2026-08-05 19:55:57 +02:00
|
|
|
else
|
|
|
|
|
g.setColour (highlighted ? colours::gridLight : colours::grid);
|
|
|
|
|
|
|
|
|
|
g.fillRoundedRectangle (bounds, 4.0f);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void EditorLookAndFeel::drawButtonText (juce::Graphics& g, juce::TextButton& button, bool, bool)
|
|
|
|
|
{
|
|
|
|
|
g.setFont (font (10.5f));
|
|
|
|
|
g.setColour (button.getToggleState() ? colours::bg : colours::text);
|
|
|
|
|
g.drawText (button.getButtonText(), button.getLocalBounds(), juce::Justification::centred);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-05 23:12:17 +02:00
|
|
|
void EditorLookAndFeel::drawComboBox (juce::Graphics& g, int width, int height, bool,
|
|
|
|
|
int, int, int, int, juce::ComboBox&)
|
|
|
|
|
{
|
|
|
|
|
const auto bounds = juce::Rectangle<float> (1.0f, 1.0f, (float) (width - 2), (float) (height - 2));
|
|
|
|
|
|
|
|
|
|
g.setColour (colours::grid);
|
|
|
|
|
g.fillRoundedRectangle (bounds, 4.0f);
|
|
|
|
|
g.setColour (colours::gridLight);
|
|
|
|
|
g.drawRoundedRectangle (bounds, 4.0f, 1.0f);
|
|
|
|
|
|
|
|
|
|
const float cx = (float) (width - 15);
|
|
|
|
|
const float cy = (float) height * 0.5f;
|
|
|
|
|
|
|
|
|
|
juce::Path arrow;
|
|
|
|
|
arrow.addTriangle (cx - 5.0f, cy - 3.0f, cx + 5.0f, cy - 3.0f, cx, cy + 3.5f);
|
|
|
|
|
g.setColour (colours::accent);
|
|
|
|
|
g.fillPath (arrow);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
juce::Font EditorLookAndFeel::getComboBoxFont (juce::ComboBox&)
|
|
|
|
|
{
|
|
|
|
|
return font (11.0f);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void EditorLookAndFeel::drawPopupMenuBackground (juce::Graphics& g, int width, int height)
|
|
|
|
|
{
|
|
|
|
|
g.setColour (colours::panel);
|
|
|
|
|
g.fillRect (0, 0, width, height);
|
|
|
|
|
g.setColour (colours::gridLight);
|
|
|
|
|
g.drawRect (0.0f, 0.0f, (float) width, (float) height, 1.0f);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void EditorLookAndFeel::drawPopupMenuItem (juce::Graphics& g, const juce::Rectangle<int>& area,
|
|
|
|
|
bool isSeparator, bool isActive, bool isHighlighted,
|
|
|
|
|
bool isTicked, bool hasSubMenu,
|
|
|
|
|
const juce::String& text, const juce::String& shortcutKeyText,
|
|
|
|
|
const juce::Drawable* icon, const juce::Colour* textColour)
|
|
|
|
|
{
|
|
|
|
|
(void) hasSubMenu;
|
|
|
|
|
(void) icon;
|
|
|
|
|
|
|
|
|
|
if (isSeparator)
|
|
|
|
|
{
|
|
|
|
|
g.setColour (colours::grid);
|
|
|
|
|
g.fillRect (area.reduced (10, 0).withY (area.getCentreY()).withHeight (1));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
g.setColour (isHighlighted ? colours::gridLight : colours::panel);
|
|
|
|
|
g.fillRect (area);
|
|
|
|
|
|
|
|
|
|
if (isTicked)
|
|
|
|
|
{
|
|
|
|
|
g.setColour (colours::accent);
|
|
|
|
|
g.fillEllipse ((float) (area.getRight() - 17), area.getCentreY() - 3.5f, 7.0f, 7.0f);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
g.setFont (font (11.0f));
|
|
|
|
|
g.setColour (isActive ? (textColour != nullptr ? *textColour : colours::text)
|
|
|
|
|
: colours::textDim);
|
|
|
|
|
g.drawFittedText (text, area.withTrimmedLeft (12).withTrimmedRight (isTicked ? 28 : 10),
|
|
|
|
|
juce::Justification::centredLeft, 1);
|
|
|
|
|
|
|
|
|
|
if (shortcutKeyText.isNotEmpty())
|
|
|
|
|
{
|
|
|
|
|
g.setColour (colours::textDim);
|
|
|
|
|
g.drawFittedText (shortcutKeyText, area.withTrimmedRight (16), juce::Justification::centredRight, 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
juce::Font EditorLookAndFeel::getPopupMenuFont()
|
|
|
|
|
{
|
|
|
|
|
return font (11.0f);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-05 19:55:57 +02:00
|
|
|
//==============================================================================
|
|
|
|
|
Knob::Knob (const juce::String& title)
|
|
|
|
|
{
|
|
|
|
|
slider.setSliderStyle (juce::Slider::RotaryVerticalDrag);
|
|
|
|
|
slider.setRotaryParameters (juce::MathConstants<float>::pi * 1.25f,
|
|
|
|
|
juce::MathConstants<float>::pi * 2.75f,
|
|
|
|
|
true);
|
|
|
|
|
slider.setTextBoxStyle (juce::Slider::NoTextBox, false, 0, 0);
|
|
|
|
|
slider.onValueChange = [this]
|
|
|
|
|
{
|
|
|
|
|
updateValueLabel();
|
|
|
|
|
|
|
|
|
|
if (customOnValueChange != nullptr)
|
|
|
|
|
customOnValueChange ((float) slider.getValue());
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
addAndMakeVisible (slider);
|
|
|
|
|
|
|
|
|
|
titleLabel.setText (title, juce::dontSendNotification);
|
|
|
|
|
titleLabel.setJustificationType (juce::Justification::centred);
|
|
|
|
|
titleLabel.setFont (font (10.0f));
|
|
|
|
|
titleLabel.setColour (juce::Label::textColourId, colours::text);
|
|
|
|
|
addAndMakeVisible (titleLabel);
|
|
|
|
|
|
|
|
|
|
valueLabel.setJustificationType (juce::Justification::centred);
|
|
|
|
|
valueLabel.setFont (font (9.0f));
|
|
|
|
|
valueLabel.setColour (juce::Label::textColourId, colours::accent);
|
|
|
|
|
addAndMakeVisible (valueLabel);
|
|
|
|
|
|
|
|
|
|
updateValueLabel();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Knob::resized()
|
|
|
|
|
{
|
|
|
|
|
const int w = getWidth();
|
2026-08-05 23:12:17 +02:00
|
|
|
const int h = getHeight();
|
2026-08-05 19:55:57 +02:00
|
|
|
|
|
|
|
|
titleLabel.setBounds (0, 0, w, 14);
|
2026-08-05 23:12:17 +02:00
|
|
|
|
|
|
|
|
const int valueH = 12;
|
|
|
|
|
const int sliderH = juce::jmax (20, juce::jmin (w - 8, h - 15 - valueH));
|
|
|
|
|
slider.setBounds (4, 15, w - 8, sliderH);
|
|
|
|
|
valueLabel.setBounds (0, 15 + sliderH, w, valueH);
|
2026-08-05 19:55:57 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Knob::updateValueLabel()
|
|
|
|
|
{
|
|
|
|
|
const float value = slider.getValue();
|
|
|
|
|
|
|
|
|
|
if (format)
|
|
|
|
|
valueLabel.setText (format (value), juce::dontSendNotification);
|
|
|
|
|
else
|
|
|
|
|
valueLabel.setText (juce::String (value, 2), juce::dontSendNotification);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//==============================================================================
|
|
|
|
|
ChoiceBar::ChoiceBar (juce::AudioProcessorValueTreeState& apvtsRef,
|
|
|
|
|
const juce::String& id,
|
|
|
|
|
const juce::StringArray& choices)
|
|
|
|
|
: apvts (&apvtsRef), paramId (id)
|
|
|
|
|
{
|
|
|
|
|
for (int i = 0; i < choices.size(); ++i)
|
|
|
|
|
{
|
|
|
|
|
auto* button = buttons.add (new juce::TextButton (choices[i]));
|
|
|
|
|
|
|
|
|
|
button->setClickingTogglesState (true);
|
|
|
|
|
button->setRadioGroupId (1);
|
|
|
|
|
button->onClick = [this, i] { setIndex (i); };
|
|
|
|
|
|
|
|
|
|
addAndMakeVisible (button);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
apvts->addParameterListener (paramId, this);
|
|
|
|
|
refreshFromParam();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ChoiceBar::ChoiceBar (const juce::StringArray& choices,
|
|
|
|
|
std::function<void (int)> onSelected)
|
|
|
|
|
: manualHandler (std::move (onSelected))
|
|
|
|
|
{
|
|
|
|
|
for (int i = 0; i < choices.size(); ++i)
|
|
|
|
|
{
|
|
|
|
|
auto* button = buttons.add (new juce::TextButton (choices[i]));
|
|
|
|
|
|
|
|
|
|
button->setClickingTogglesState (true);
|
|
|
|
|
button->setRadioGroupId (1);
|
|
|
|
|
button->onClick = [this, i] { setIndex (i); };
|
|
|
|
|
|
|
|
|
|
addAndMakeVisible (button);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
buttons.getFirst()->setToggleState (true, juce::dontSendNotification);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ChoiceBar::~ChoiceBar()
|
|
|
|
|
{
|
|
|
|
|
if (apvts != nullptr)
|
|
|
|
|
apvts->removeParameterListener (paramId, this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ChoiceBar::parameterChanged (const juce::String&, float)
|
|
|
|
|
{
|
|
|
|
|
juce::MessageManager::callAsync ([this] { refreshFromParam(); });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ChoiceBar::refreshFromParam()
|
|
|
|
|
{
|
|
|
|
|
if (apvts == nullptr)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (auto* parameter = apvts->getParameter (paramId))
|
|
|
|
|
{
|
|
|
|
|
const int index = juce::roundToInt (parameter->convertFrom0to1 (parameter->getValue()));
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < buttons.size(); ++i)
|
|
|
|
|
buttons[i]->setToggleState (i == index, juce::dontSendNotification);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ChoiceBar::setIndex (int index)
|
|
|
|
|
{
|
|
|
|
|
if (apvts != nullptr)
|
|
|
|
|
{
|
|
|
|
|
if (auto* parameter = apvts->getParameter (paramId))
|
|
|
|
|
{
|
|
|
|
|
parameter->beginChangeGesture();
|
|
|
|
|
parameter->setValueNotifyingHost (parameter->convertTo0to1 ((float) index));
|
|
|
|
|
parameter->endChangeGesture();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (manualHandler != nullptr)
|
|
|
|
|
manualHandler (index);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ChoiceBar::resized()
|
|
|
|
|
{
|
|
|
|
|
const int count = buttons.size();
|
|
|
|
|
const float spacing = 3.0f;
|
|
|
|
|
const float w = ((float) getWidth() - spacing * (count - 1)) / (float) count;
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < count; ++i)
|
|
|
|
|
buttons[i]->setBounds (juce::roundToInt (i * (w + spacing)), 0,
|
|
|
|
|
juce::roundToInt (w), getHeight());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//==============================================================================
|
2026-08-05 23:12:17 +02:00
|
|
|
StepPanel::StepPanel (MonostepAudioProcessor& processorRef)
|
2026-08-05 19:55:57 +02:00
|
|
|
: processor (processorRef),
|
|
|
|
|
fineKnob ("Fine")
|
|
|
|
|
{
|
|
|
|
|
titleLabel.setText ("Step", juce::dontSendNotification);
|
|
|
|
|
titleLabel.setJustificationType (juce::Justification::centred);
|
|
|
|
|
titleLabel.setFont (font (12.0f));
|
|
|
|
|
titleLabel.setColour (juce::Label::textColourId, colours::text);
|
|
|
|
|
addAndMakeVisible (titleLabel);
|
|
|
|
|
|
|
|
|
|
noteLabel.setJustificationType (juce::Justification::centred);
|
|
|
|
|
noteLabel.setFont (font (20.0f));
|
|
|
|
|
noteLabel.setColour (juce::Label::textColourId, colours::accent);
|
|
|
|
|
addAndMakeVisible (noteLabel);
|
|
|
|
|
|
|
|
|
|
gateLabel.setJustificationType (juce::Justification::centred);
|
|
|
|
|
gateLabel.setFont (font (10.0f));
|
|
|
|
|
gateLabel.setColour (juce::Label::textColourId, colours::textDim);
|
|
|
|
|
addAndMakeVisible (gateLabel);
|
|
|
|
|
|
|
|
|
|
auto& fineSlider = fineKnob.getSlider();
|
|
|
|
|
fineSlider.setRange (-50.0, 50.0, 1.0);
|
|
|
|
|
fineSlider.setValue (0.0, juce::dontSendNotification);
|
|
|
|
|
fineKnob.setFormat ([] (float v) { return juce::String (v, 0) + " ct"; });
|
|
|
|
|
fineKnob.setOnValueChanged ([this] (float v)
|
|
|
|
|
{
|
|
|
|
|
if (! updating)
|
|
|
|
|
processor.setStepCents (selectedStep, v);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
addAndMakeVisible (fineKnob);
|
|
|
|
|
|
|
|
|
|
prevButton.setButtonText ("<");
|
|
|
|
|
prevButton.onClick = [this] { setSelectedStep (selectedStep - 1); };
|
|
|
|
|
addAndMakeVisible (prevButton);
|
|
|
|
|
|
|
|
|
|
nextButton.setButtonText (">");
|
|
|
|
|
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);
|
|
|
|
|
|
2026-08-05 23:12:17 +02:00
|
|
|
shiftLeftButton.setButtonText ("<<");
|
|
|
|
|
shiftLeftButton.onClick = [this] { processor.shiftPattern (-1); };
|
|
|
|
|
addAndMakeVisible (shiftLeftButton);
|
|
|
|
|
|
|
|
|
|
shiftRightButton.setButtonText (">>");
|
|
|
|
|
shiftRightButton.onClick = [this] { processor.shiftPattern (1); };
|
|
|
|
|
addAndMakeVisible (shiftRightButton);
|
|
|
|
|
|
|
|
|
|
toolsCaption.setText ("TOOLS", juce::dontSendNotification);
|
|
|
|
|
toolsCaption.setJustificationType (juce::Justification::centredLeft);
|
|
|
|
|
toolsCaption.setFont (font (9.0f));
|
|
|
|
|
toolsCaption.setColour (juce::Label::textColourId, colours::textDim);
|
|
|
|
|
addAndMakeVisible (toolsCaption);
|
|
|
|
|
|
|
|
|
|
presetCombo.setTextWhenNothingSelected ("Pick a preset");
|
|
|
|
|
presetCombo.setColour (juce::ComboBox::textColourId, colours::text);
|
|
|
|
|
presetCombo.setColour (juce::ComboBox::outlineColourId, colours::gridLight);
|
|
|
|
|
presetCombo.setColour (juce::ComboBox::backgroundColourId, colours::grid);
|
|
|
|
|
presetCombo.setColour (juce::ComboBox::arrowColourId, colours::accent);
|
|
|
|
|
for (int i = 0; i < processor.getNumPresets(); ++i)
|
|
|
|
|
presetCombo.addItem (processor.getPresetName (i), i + 1);
|
|
|
|
|
|
|
|
|
|
presetCombo.onChange = [this]
|
|
|
|
|
{
|
|
|
|
|
const int id = presetCombo.getSelectedId();
|
|
|
|
|
if (id > 0)
|
|
|
|
|
processor.applyPreset (id - 1);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
addAndMakeVisible (presetCombo);
|
|
|
|
|
|
|
|
|
|
randomizeButton.setButtonText ("Randomize");
|
|
|
|
|
randomizeButton.onClick = [this]
|
|
|
|
|
{
|
|
|
|
|
if (randomizeToggles[RandomPattern].getToggleState())
|
|
|
|
|
processor.randomizePattern();
|
|
|
|
|
if (randomizeToggles[RandomOsc].getToggleState())
|
|
|
|
|
processor.randomizeOsc();
|
|
|
|
|
if (randomizeToggles[RandomFilter].getToggleState())
|
|
|
|
|
processor.randomizeFilter();
|
|
|
|
|
if (randomizeToggles[RandomEnv].getToggleState())
|
|
|
|
|
processor.randomizeEnv();
|
|
|
|
|
if (randomizeToggles[RandomSeq].getToggleState())
|
|
|
|
|
processor.randomizeSeqSettings();
|
|
|
|
|
if (randomizeToggles[RandomFx].getToggleState())
|
|
|
|
|
processor.randomizeFx();
|
|
|
|
|
};
|
|
|
|
|
addAndMakeVisible (randomizeButton);
|
|
|
|
|
|
|
|
|
|
randomizeCaption.setText ("RANDOMIZE", juce::dontSendNotification);
|
|
|
|
|
randomizeCaption.setJustificationType (juce::Justification::centredLeft);
|
|
|
|
|
randomizeCaption.setFont (font (9.0f));
|
|
|
|
|
randomizeCaption.setColour (juce::Label::textColourId, colours::textDim);
|
|
|
|
|
addAndMakeVisible (randomizeCaption);
|
|
|
|
|
|
|
|
|
|
static const char* groupNames[] = { "Pattern", "Osc", "Filter", "Env", "Length", "FX" };
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < RandomGroupCount; ++i)
|
|
|
|
|
{
|
|
|
|
|
randomizeToggles[i].setButtonText (groupNames[i]);
|
|
|
|
|
randomizeToggles[i].setToggleState (true, juce::dontSendNotification);
|
|
|
|
|
addAndMakeVisible (randomizeToggles[i]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-05 19:55:57 +02:00
|
|
|
setSelectedStep (0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void StepPanel::setSelectedStep (int stepIdx)
|
|
|
|
|
{
|
|
|
|
|
if (stepIdx < 0 || stepIdx >= numSteps)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (stepIdx != selectedStep)
|
|
|
|
|
{
|
|
|
|
|
selectedStep = stepIdx;
|
|
|
|
|
|
|
|
|
|
if (onStepSelected)
|
|
|
|
|
onStepSelected (stepIdx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
refresh();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void StepPanel::refresh()
|
|
|
|
|
{
|
|
|
|
|
const auto& s = processor.getSequencer().step (selectedStep);
|
|
|
|
|
|
|
|
|
|
updating = true;
|
|
|
|
|
|
|
|
|
|
titleLabel.setText ("Step " + juce::String (selectedStep + 1), juce::dontSendNotification);
|
|
|
|
|
noteLabel.setText (processor.getStepNoteName (selectedStep)
|
|
|
|
|
+ (std::fabs (s.cents) > 0.5f ? " (" + formatCents (s.cents) + ")" : ""),
|
|
|
|
|
juce::dontSendNotification);
|
|
|
|
|
gateLabel.setText (s.gate ? "gate: on" : "gate: off", juce::dontSendNotification);
|
|
|
|
|
|
|
|
|
|
fineKnob.getSlider().setValue (s.cents, juce::dontSendNotification);
|
|
|
|
|
|
|
|
|
|
updating = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void StepPanel::clearPattern()
|
|
|
|
|
{
|
|
|
|
|
for (int i = 0; i < numSteps; ++i)
|
|
|
|
|
{
|
|
|
|
|
processor.setStepGate (i, false);
|
|
|
|
|
processor.setStepNote (i, 0);
|
|
|
|
|
processor.setStepCents (i, 0.0f);
|
|
|
|
|
processor.setStepSlide (i, false);
|
2026-08-05 23:12:17 +02:00
|
|
|
processor.setStepAccent (i, false);
|
2026-08-05 19:55:57 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
refresh();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void StepPanel::resized()
|
|
|
|
|
{
|
|
|
|
|
const int w = getWidth();
|
|
|
|
|
|
|
|
|
|
titleLabel.setBounds (0, 4, w, 16);
|
|
|
|
|
noteLabel.setBounds (0, 22, w, 28);
|
|
|
|
|
gateLabel.setBounds (0, 50, w, 14);
|
|
|
|
|
|
|
|
|
|
const int knobX = (w - 74) / 2;
|
|
|
|
|
const int knobY = 68;
|
|
|
|
|
|
2026-08-05 23:12:17 +02:00
|
|
|
fineKnob.setBounds (knobX, knobY, 74, 94);
|
2026-08-05 19:55:57 +02:00
|
|
|
|
2026-08-05 23:12:17 +02:00
|
|
|
const int btnY = knobY + 102;
|
2026-08-05 19:55:57 +02:00
|
|
|
prevButton.setBounds (knobX - 32, btnY, 24, 24);
|
|
|
|
|
nextButton.setBounds (knobX + 74 + 8, btnY, 24, 24);
|
|
|
|
|
|
2026-08-05 23:12:17 +02:00
|
|
|
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);
|
|
|
|
|
shiftRightButton.setBounds (10 + w - 20 - shiftW, btnY + 50, shiftW, 26);
|
|
|
|
|
|
|
|
|
|
toolsCaption.setBounds (10, btnY + 86, w - 20, 14);
|
|
|
|
|
presetCombo.setBounds (10, btnY + 102, w - 20, 26);
|
|
|
|
|
randomizeButton.setBounds (10, btnY + 134, w - 20, 26);
|
|
|
|
|
randomizeCaption.setBounds (10, btnY + 166, w - 20, 12);
|
|
|
|
|
|
|
|
|
|
const int colW = (w - 20 - 4) / 2;
|
|
|
|
|
const int rowH = 20;
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < RandomGroupCount; ++i)
|
|
|
|
|
{
|
|
|
|
|
const int col = i / 3;
|
|
|
|
|
const int row = i % 3;
|
|
|
|
|
randomizeToggles[i].setBounds (10 + col * (colW + 4), btnY + 180 + row * (rowH + 2), colW, rowH);
|
|
|
|
|
}
|
2026-08-05 19:55:57 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//==============================================================================
|
2026-08-05 23:12:17 +02:00
|
|
|
MonostepAudioProcessorEditor::MonostepAudioProcessorEditor (MonostepAudioProcessor& processor)
|
2026-08-05 19:55:57 +02:00
|
|
|
: AudioProcessorEditor (processor),
|
|
|
|
|
processorRef (processor),
|
|
|
|
|
content (*this),
|
|
|
|
|
lnf (std::make_unique<EditorLookAndFeel>()),
|
|
|
|
|
lengthKnob ("Length"),
|
|
|
|
|
swingKnob ("Swing"),
|
|
|
|
|
rootKnob ("Root"),
|
|
|
|
|
gateKnob ("Gate"),
|
|
|
|
|
masterKnob ("Master"),
|
2026-08-05 23:12:17 +02:00
|
|
|
coarseKnob ("Coarse"),
|
|
|
|
|
coarseKnob2 ("Coarse"),
|
2026-08-05 19:55:57 +02:00
|
|
|
detuneKnob ("Detune"),
|
|
|
|
|
mixKnob ("Mix"),
|
2026-08-05 23:12:17 +02:00
|
|
|
fineKnob1 ("Fine"),
|
|
|
|
|
fineKnob2 ("Fine"),
|
2026-08-05 19:55:57 +02:00
|
|
|
cutoffKnob ("Cutoff"),
|
|
|
|
|
resKnob ("Res"),
|
|
|
|
|
glideKnob ("Glide"),
|
|
|
|
|
attackKnob ("Attack"),
|
|
|
|
|
decayKnob ("Decay"),
|
|
|
|
|
sustainKnob ("Sustain"),
|
|
|
|
|
releaseKnob ("Release"),
|
2026-08-05 23:12:17 +02:00
|
|
|
driveKnob ("Drive"),
|
|
|
|
|
ringKnob ("Ring"),
|
|
|
|
|
accentKnob ("Accent"),
|
2026-08-05 19:55:57 +02:00
|
|
|
rateBar (processor.getAPVTS(), "seqRate", { "1/4", "1/8", "1/8T", "1/16", "1/32" }),
|
|
|
|
|
octaveBar (processor.getAPVTS(), "seqOctave", { "-2", "-1", "0", "+1", "+2" }),
|
|
|
|
|
zoomBar ({ "100%", "125%", "150%", "175%", "200%" }, [] (int) {}),
|
|
|
|
|
wave1Bar (processor.getAPVTS(), "osc1Wave", { "Sin", "Tri", "Saw", "Sq" }),
|
|
|
|
|
wave2Bar (processor.getAPVTS(), "osc2Wave", { "Sin", "Tri", "Saw", "Sq" }),
|
|
|
|
|
matrix (processor),
|
|
|
|
|
stepPanel (processor)
|
|
|
|
|
{
|
|
|
|
|
setLookAndFeel (lnf.get());
|
|
|
|
|
|
|
|
|
|
setSize (baseWidth, baseHeight);
|
|
|
|
|
|
|
|
|
|
auto& apvts = processor.getAPVTS();
|
|
|
|
|
|
|
|
|
|
addAndMakeVisible (content);
|
|
|
|
|
|
|
|
|
|
content.addAndMakeVisible (rateBar);
|
|
|
|
|
content.addAndMakeVisible (octaveBar);
|
|
|
|
|
content.addAndMakeVisible (zoomBar);
|
|
|
|
|
content.addAndMakeVisible (wave1Bar);
|
|
|
|
|
content.addAndMakeVisible (wave2Bar);
|
|
|
|
|
|
|
|
|
|
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)]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
auto addKnob = [&apvts, this] (Knob& knob, const juce::String& id, std::function<juce::String (float)> fmt)
|
|
|
|
|
{
|
|
|
|
|
content.addAndMakeVisible (knob);
|
|
|
|
|
knob.attach (apvts, id);
|
|
|
|
|
knob.setFormat (std::move (fmt));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
addKnob (lengthKnob, "seqLen", [] (float v) { return juce::String ((int) v); });
|
|
|
|
|
addKnob (swingKnob, "seqSwing", [] (float v) { return juce::String (v, 2); });
|
|
|
|
|
addKnob (rootKnob, "seqRoot", [] (float v) { return juce::String ((int) v); });
|
|
|
|
|
addKnob (gateKnob, "seqGateLen", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; });
|
|
|
|
|
addKnob (masterKnob, "master", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; });
|
2026-08-05 23:12:17 +02:00
|
|
|
addKnob (coarseKnob, "osc2Coarse", [] (float v) { return juce::String (juce::roundToInt (v)) + " st"; });
|
2026-08-05 19:55:57 +02:00
|
|
|
addKnob (detuneKnob, "detune", [] (float v) { return juce::String (juce::roundToInt (v)) + " ct"; });
|
|
|
|
|
addKnob (mixKnob, "mix", [] (float v) { return juce::String (v, 2); });
|
|
|
|
|
addKnob (cutoffKnob, "cutoff", [] (float v) { return v >= 1000.0f ? juce::String (juce::roundToInt (v / 100.0f) * 100) + " Hz" : juce::String (juce::roundToInt (v)) + " Hz"; });
|
|
|
|
|
addKnob (resKnob, "res", [] (float v) { return juce::String (v, 2); });
|
|
|
|
|
addKnob (glideKnob, "glide", [] (float v) { return juce::String (v, 2) + "s"; });
|
|
|
|
|
addKnob (attackKnob, "attack", [] (float v) { return juce::String (v, 2) + "s"; });
|
|
|
|
|
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"; });
|
2026-08-05 23:12:17 +02:00
|
|
|
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)) + "%"; });
|
2026-08-05 19:55:57 +02:00
|
|
|
|
|
|
|
|
content.addAndMakeVisible (matrix);
|
|
|
|
|
content.addAndMakeVisible (stepPanel);
|
|
|
|
|
|
|
|
|
|
matrix.onStepSelected = [this] (int stepIdx)
|
|
|
|
|
{
|
|
|
|
|
stepPanel.setSelectedStep (stepIdx);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
stepPanel.onStepSelected = [this] (int stepIdx)
|
|
|
|
|
{
|
|
|
|
|
matrix.setSelectedStep (stepIdx);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
lengthKnob.setOnValueChanged ([this] (float)
|
|
|
|
|
{
|
|
|
|
|
matrix.repaint();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
matrix.onGateChanged = [this] (int stepIdx, bool gate)
|
|
|
|
|
{
|
|
|
|
|
processorRef.setStepGate (stepIdx, gate);
|
|
|
|
|
stepPanel.refresh();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
matrix.onNoteChanged = [this] (int stepIdx, int semitone)
|
|
|
|
|
{
|
|
|
|
|
processorRef.setStepNote (stepIdx, semitone);
|
|
|
|
|
stepPanel.refresh();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
matrix.onFineChanged = [this] (int stepIdx, float cents)
|
|
|
|
|
{
|
|
|
|
|
processorRef.setStepCents (stepIdx, cents);
|
|
|
|
|
stepPanel.refresh();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
matrix.onSlideChanged = [this] (int stepIdx, bool slide)
|
|
|
|
|
{
|
|
|
|
|
processorRef.setStepSlide (stepIdx, slide);
|
|
|
|
|
stepPanel.refresh();
|
|
|
|
|
};
|
|
|
|
|
|
2026-08-05 23:12:17 +02:00
|
|
|
matrix.onAccentChanged = [this] (int stepIdx, bool accent)
|
|
|
|
|
{
|
|
|
|
|
processorRef.setStepAccent (stepIdx, accent);
|
|
|
|
|
stepPanel.refresh();
|
|
|
|
|
};
|
|
|
|
|
|
2026-08-05 19:55:57 +02:00
|
|
|
processorRef.onPatternChanged = [this] { refreshEditor(); };
|
|
|
|
|
|
|
|
|
|
startTimerHz (30);
|
|
|
|
|
|
|
|
|
|
refreshEditor();
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-05 23:12:17 +02:00
|
|
|
MonostepAudioProcessorEditor::~MonostepAudioProcessorEditor()
|
2026-08-05 19:55:57 +02:00
|
|
|
{
|
|
|
|
|
stopTimer();
|
|
|
|
|
processorRef.onPatternChanged = nullptr;
|
|
|
|
|
setLookAndFeel (nullptr);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-05 23:12:17 +02:00
|
|
|
void MonostepAudioProcessorEditor::timerCallback()
|
2026-08-05 19:55:57 +02:00
|
|
|
{
|
|
|
|
|
const int step = processorRef.getCurrentStep();
|
|
|
|
|
|
|
|
|
|
if (step != lastPlayStep)
|
|
|
|
|
{
|
|
|
|
|
lastPlayStep = step;
|
|
|
|
|
matrix.setPlayPosition (step);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-05 23:12:17 +02:00
|
|
|
void MonostepAudioProcessorEditor::refreshEditor()
|
2026-08-05 19:55:57 +02:00
|
|
|
{
|
|
|
|
|
matrix.setSelectedStep (matrix.getSelectedStep());
|
|
|
|
|
stepPanel.refresh();
|
|
|
|
|
matrix.repaint();
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-05 23:12:17 +02:00
|
|
|
void MonostepAudioProcessorEditor::paint (juce::Graphics& g)
|
2026-08-05 19:55:57 +02:00
|
|
|
{
|
|
|
|
|
g.fillAll (colours::bg);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-05 23:12:17 +02:00
|
|
|
void MonostepAudioProcessorEditor::paintContent (juce::Graphics& g)
|
2026-08-05 19:55:57 +02:00
|
|
|
{
|
|
|
|
|
g.setFont (font (22.0f));
|
|
|
|
|
g.setColour (colours::text);
|
2026-08-05 23:12:17 +02:00
|
|
|
g.drawText ("Monostep", 16, 12, 130, 24, juce::Justification::centredLeft);
|
2026-08-05 19:55:57 +02:00
|
|
|
|
|
|
|
|
g.setFont (font (9.0f));
|
|
|
|
|
g.setColour (colours::textDim);
|
2026-08-05 23:12:17 +02:00
|
|
|
g.drawText ("2 OSC Step-Sequencer Synth", 16, 34, 130, 12, juce::Justification::centredLeft);
|
2026-08-05 19:55:57 +02:00
|
|
|
|
|
|
|
|
const auto drawCaption = [&] (const juce::Component& component, const juce::String& text)
|
|
|
|
|
{
|
|
|
|
|
g.drawText (text,
|
|
|
|
|
component.getX(), component.getY() - 13,
|
|
|
|
|
component.getWidth(), 12,
|
|
|
|
|
juce::Justification::centred);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
drawCaption (rateBar, "RATE");
|
|
|
|
|
drawCaption (octaveBar, "OCTAVE");
|
|
|
|
|
drawCaption (zoomBar, "ZOOM");
|
|
|
|
|
drawCaption (wave1Bar, "OSC 1");
|
|
|
|
|
drawCaption (wave2Bar, "OSC 2");
|
|
|
|
|
|
|
|
|
|
const int footY = baseHeight - 20;
|
|
|
|
|
g.setColour (colours::grid);
|
|
|
|
|
g.fillRect (0, footY, baseWidth, 20);
|
|
|
|
|
g.setColour (colours::panel);
|
|
|
|
|
g.drawHorizontalLine (footY, 0, (float) baseWidth);
|
|
|
|
|
|
|
|
|
|
g.setFont (font (9.0f));
|
|
|
|
|
g.setColour (colours::textDim);
|
|
|
|
|
g.drawText (buildinfo::footerText(), 16, footY + 4, baseWidth - 32, 12, juce::Justification::centredLeft);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-05 23:12:17 +02:00
|
|
|
void MonostepAudioProcessorEditor::setZoomFactor (float newZoom)
|
2026-08-05 19:55:57 +02:00
|
|
|
{
|
|
|
|
|
zoomFactor = newZoom;
|
|
|
|
|
|
|
|
|
|
setSize (juce::roundToInt (baseWidth * newZoom), juce::roundToInt (baseHeight * newZoom));
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-05 23:12:17 +02:00
|
|
|
void MonostepAudioProcessorEditor::resized()
|
2026-08-05 19:55:57 +02:00
|
|
|
{
|
|
|
|
|
const int targetW = juce::roundToInt (baseWidth * zoomFactor);
|
|
|
|
|
const int targetH = juce::roundToInt (baseHeight * zoomFactor);
|
|
|
|
|
|
|
|
|
|
if (getWidth() != targetW || getHeight() != targetH)
|
|
|
|
|
{
|
|
|
|
|
setSize (targetW, targetH);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
content.setBounds (0, 0, baseWidth, baseHeight);
|
|
|
|
|
content.setTransform (juce::AffineTransform::scale (zoomFactor));
|
|
|
|
|
|
|
|
|
|
// All layout below happens in design units; the content transform scales it.
|
|
|
|
|
const int w = baseWidth;
|
|
|
|
|
const int h = baseHeight;
|
|
|
|
|
|
2026-08-05 23:12:17 +02:00
|
|
|
const int topBarH = 78;
|
2026-08-05 19:55:57 +02:00
|
|
|
const int bottomBarH = 96;
|
|
|
|
|
const int pad = 12;
|
2026-08-05 23:12:17 +02:00
|
|
|
const int topBarY = (topBarH - 26) / 2;
|
2026-08-05 19:55:57 +02:00
|
|
|
|
2026-08-05 23:12:17 +02:00
|
|
|
lengthKnob.setBounds (128, 2, 54, 74);
|
|
|
|
|
rateBar.setBounds (192, topBarY, 210, 26);
|
2026-08-05 19:55:57 +02:00
|
|
|
|
2026-08-05 23:12:17 +02:00
|
|
|
swingKnob.setBounds (422, 2, 54, 74);
|
|
|
|
|
rootKnob.setBounds (482, 2, 54, 74);
|
|
|
|
|
octaveBar.setBounds (548, topBarY, 170, 26);
|
|
|
|
|
zoomBar.setBounds (w - pad - 54 - 8 - 236, topBarY, 236, 26);
|
|
|
|
|
masterKnob.setBounds (w - pad - 54, 2, 54, 74);
|
2026-08-05 19:55:57 +02:00
|
|
|
|
|
|
|
|
const int panelW = 200;
|
|
|
|
|
const int midTop = topBarH;
|
|
|
|
|
const int midH = h - topBarH - bottomBarH - pad;
|
|
|
|
|
|
|
|
|
|
stepPanel.setBounds (w - pad - panelW, midTop, panelW, midH);
|
|
|
|
|
matrix.setBounds (pad, midTop, w - pad * 2 - panelW - pad, midH);
|
|
|
|
|
|
|
|
|
|
const int bottomY = h - bottomBarH - pad;
|
|
|
|
|
|
|
|
|
|
int x = 16;
|
|
|
|
|
const int barW = 112;
|
|
|
|
|
|
|
|
|
|
wave1Bar.setBounds (x, bottomY + 30, barW, 24); x += barW + 8;
|
|
|
|
|
wave2Bar.setBounds (x, bottomY + 30, barW, 24); x += barW + 16;
|
|
|
|
|
|
|
|
|
|
const int knobW = 56;
|
2026-08-05 23:12:17 +02:00
|
|
|
const int knobGap = 60;
|
2026-08-05 19:55:57 +02:00
|
|
|
const int knobY = bottomY + 2;
|
|
|
|
|
|
|
|
|
|
const auto placeKnob = [&] (Knob& knob)
|
|
|
|
|
{
|
|
|
|
|
knob.setBounds (x, knobY, knobW, 82);
|
|
|
|
|
x += knobGap;
|
|
|
|
|
};
|
|
|
|
|
|
2026-08-05 23:12:17 +02:00
|
|
|
placeKnob (coarseKnob);
|
2026-08-05 19:55:57 +02:00
|
|
|
placeKnob (detuneKnob);
|
|
|
|
|
placeKnob (mixKnob);
|
|
|
|
|
placeKnob (cutoffKnob);
|
|
|
|
|
placeKnob (resKnob);
|
|
|
|
|
placeKnob (glideKnob);
|
|
|
|
|
placeKnob (attackKnob);
|
|
|
|
|
placeKnob (decayKnob);
|
|
|
|
|
placeKnob (sustainKnob);
|
|
|
|
|
placeKnob (releaseKnob);
|
2026-08-05 23:12:17 +02:00
|
|
|
placeKnob (driveKnob);
|
|
|
|
|
placeKnob (ringKnob);
|
|
|
|
|
placeKnob (accentKnob);
|
2026-08-05 19:55:57 +02:00
|
|
|
}
|