monostep/Source/PluginEditor.cpp
2026-08-05 23:12:17 +02:00

828 lines
28 KiB
C++

#include "PluginEditor.h"
#include "PluginProcessor.h"
#include "Theme.h"
#include "BuildInfo.h"
using namespace monostep;
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);
// 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);
g.setGradientFill (orangeGradient (bounds));
g.strokePath (valueArc, juce::PathStrokeType (2.5f));
}
// 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);
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();
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())
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);
}
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();
const int h = getHeight();
titleLabel.setBounds (0, 0, w, 14);
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());
}
//==============================================================================
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);
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);
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;
fineKnob.setBounds (knobX, knobY, 74, 94);
const int btnY = knobY + 102;
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);
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);
}
}
//==============================================================================
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"),
coarseKnob ("Coarse"),
coarseKnob2 ("Coarse"),
detuneKnob ("Detune"),
mixKnob ("Mix"),
fineKnob1 ("Fine"),
fineKnob2 ("Fine"),
cutoffKnob ("Cutoff"),
resKnob ("Res"),
glideKnob ("Glide"),
attackKnob ("Attack"),
decayKnob ("Decay"),
sustainKnob ("Sustain"),
releaseKnob ("Release"),
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)) + "%"; });
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"; });
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();
};
matrix.onAccentChanged = [this] (int stepIdx, bool accent)
{
processorRef.setStepAccent (stepIdx, accent);
stepPanel.refresh();
};
processorRef.onPatternChanged = [this] { refreshEditor(); };
startTimerHz (30);
refreshEditor();
}
MonostepAudioProcessorEditor::~MonostepAudioProcessorEditor()
{
stopTimer();
processorRef.onPatternChanged = nullptr;
setLookAndFeel (nullptr);
}
void MonostepAudioProcessorEditor::timerCallback()
{
const int step = processorRef.getCurrentStep();
if (step != lastPlayStep)
{
lastPlayStep = step;
matrix.setPlayPosition (step);
}
}
void MonostepAudioProcessorEditor::refreshEditor()
{
matrix.setSelectedStep (matrix.getSelectedStep());
stepPanel.refresh();
matrix.repaint();
}
void MonostepAudioProcessorEditor::paint (juce::Graphics& g)
{
g.fillAll (colours::bg);
}
void MonostepAudioProcessorEditor::paintContent (juce::Graphics& g)
{
g.setFont (font (22.0f));
g.setColour (colours::text);
g.drawText ("Monostep", 16, 12, 130, 24, juce::Justification::centredLeft);
g.setFont (font (9.0f));
g.setColour (colours::textDim);
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);
}
void MonostepAudioProcessorEditor::setZoomFactor (float newZoom)
{
zoomFactor = newZoom;
setSize (juce::roundToInt (baseWidth * newZoom), juce::roundToInt (baseHeight * newZoom));
}
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;
const int topBarH = 78;
const int bottomBarH = 96;
const int pad = 12;
const int topBarY = (topBarH - 26) / 2;
lengthKnob.setBounds (128, 2, 54, 74);
rateBar.setBounds (192, topBarY, 210, 26);
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;
const int knobGap = 60;
const int knobY = bottomY + 2;
const auto placeKnob = [&] (Knob& knob)
{
knob.setBounds (x, knobY, knobW, 82);
x += knobGap;
};
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);
}