monostep/Source/PluginEditor.cpp

828 lines
28 KiB
C++
Raw Normal View History

#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
//==============================================================================
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);
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));
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));
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);
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);
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));
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);
}
//==============================================================================
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();
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);
}
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)
: 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]);
}
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);
}
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 23:12:17 +02:00
const int btnY = knobY + 102;
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 23:12:17 +02:00
MonostepAudioProcessorEditor::MonostepAudioProcessorEditor (MonostepAudioProcessor& processor)
: 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"),
detuneKnob ("Detune"),
mixKnob ("Mix"),
2026-08-05 23:12:17 +02:00
fineKnob1 ("Fine"),
fineKnob2 ("Fine"),
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"),
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"; });
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)) + "%"; });
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();
};
processorRef.onPatternChanged = [this] { refreshEditor(); };
startTimerHz (30);
refreshEditor();
}
2026-08-05 23:12:17 +02:00
MonostepAudioProcessorEditor::~MonostepAudioProcessorEditor()
{
stopTimer();
processorRef.onPatternChanged = nullptr;
setLookAndFeel (nullptr);
}
2026-08-05 23:12:17 +02:00
void MonostepAudioProcessorEditor::timerCallback()
{
const int step = processorRef.getCurrentStep();
if (step != lastPlayStep)
{
lastPlayStep = step;
matrix.setPlayPosition (step);
}
}
2026-08-05 23:12:17 +02:00
void MonostepAudioProcessorEditor::refreshEditor()
{
matrix.setSelectedStep (matrix.getSelectedStep());
stepPanel.refresh();
matrix.repaint();
}
2026-08-05 23:12:17 +02:00
void MonostepAudioProcessorEditor::paint (juce::Graphics& g)
{
g.fillAll (colours::bg);
}
2026-08-05 23:12:17 +02:00
void MonostepAudioProcessorEditor::paintContent (juce::Graphics& g)
{
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);
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);
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)
{
zoomFactor = newZoom;
setSize (juce::roundToInt (baseWidth * newZoom), juce::roundToInt (baseHeight * newZoom));
}
2026-08-05 23:12:17 +02:00
void MonostepAudioProcessorEditor::resized()
{
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;
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 23:12:17 +02:00
lengthKnob.setBounds (128, 2, 54, 74);
rateBar.setBounds (192, topBarY, 210, 26);
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);
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;
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);
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);
}