mirror of
https://codeberg.org/armin/monostep.git
synced 2026-09-01 04:10:46 +02:00
613 lines
20 KiB
C++
613 lines
20 KiB
C++
|
|
#include "PluginEditor.h"
|
||
|
|
|
||
|
|
#include "PluginProcessor.h"
|
||
|
|
#include "Theme.h"
|
||
|
|
#include "BuildInfo.h"
|
||
|
|
|
||
|
|
using namespace monostep;
|
||
|
|
|
||
|
|
//==============================================================================
|
||
|
|
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);
|
||
|
|
|
||
|
|
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.setColour (colours::accent);
|
||
|
|
g.strokePath (valueArc, juce::PathStrokeType (2.5f));
|
||
|
|
}
|
||
|
|
|
||
|
|
// JUCE measures rotary angles clockwise from 12 o'clock; the pointer is
|
||
|
|
// drawn with the same convention so it matches the arc and mouse dragging.
|
||
|
|
const float tickLen = radius * 0.62f;
|
||
|
|
g.setColour (colours::accent);
|
||
|
|
g.drawLine (centre.getX(), centre.getY(),
|
||
|
|
centre.getX() + tickLen * std::sin (angle),
|
||
|
|
centre.getY() - tickLen * std::cos (angle),
|
||
|
|
2.5f);
|
||
|
|
|
||
|
|
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();
|
||
|
|
|
||
|
|
g.setColour (button.getToggleState() ? colours::accent : 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.setColour (colours::accent);
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
|
||
|
|
//==============================================================================
|
||
|
|
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();
|
||
|
|
|
||
|
|
titleLabel.setBounds (0, 0, w, 14);
|
||
|
|
slider.setBounds (4, 15, w - 8, w - 8);
|
||
|
|
valueLabel.setBounds (0, w - 8 + 15, w, 12);
|
||
|
|
}
|
||
|
|
|
||
|
|
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);
|
||
|
|
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
|
||
|
|
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, 74);
|
||
|
|
|
||
|
|
const int btnY = knobY + 25;
|
||
|
|
prevButton.setBounds (knobX - 32, btnY, 24, 24);
|
||
|
|
nextButton.setBounds (knobX + 74 + 8, btnY, 24, 24);
|
||
|
|
|
||
|
|
hintLabel.setBounds (8, knobY + 82, w - 16, 14);
|
||
|
|
clearButton.setBounds (10, knobY + 106, w - 20, 26);
|
||
|
|
}
|
||
|
|
|
||
|
|
//==============================================================================
|
||
|
|
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"),
|
||
|
|
detuneKnob ("Detune"),
|
||
|
|
mixKnob ("Mix"),
|
||
|
|
cutoffKnob ("Cutoff"),
|
||
|
|
resKnob ("Res"),
|
||
|
|
glideKnob ("Glide"),
|
||
|
|
attackKnob ("Attack"),
|
||
|
|
decayKnob ("Decay"),
|
||
|
|
sustainKnob ("Sustain"),
|
||
|
|
releaseKnob ("Release"),
|
||
|
|
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 (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"; });
|
||
|
|
|
||
|
|
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();
|
||
|
|
};
|
||
|
|
|
||
|
|
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", 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 = 58;
|
||
|
|
const int bottomBarH = 96;
|
||
|
|
const int pad = 12;
|
||
|
|
|
||
|
|
lengthKnob.setBounds (128, 2, 54, 56);
|
||
|
|
rateBar.setBounds (192, 18, 210, 26);
|
||
|
|
|
||
|
|
swingKnob.setBounds (422, 2, 54, 56);
|
||
|
|
rootKnob.setBounds (482, 2, 54, 56);
|
||
|
|
octaveBar.setBounds (548, 18, 170, 26);
|
||
|
|
zoomBar.setBounds (w - pad - 54 - 8 - 236, 18, 236, 26);
|
||
|
|
masterKnob.setBounds (w - pad - 54, 2, 54, 56);
|
||
|
|
|
||
|
|
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 = 72;
|
||
|
|
const int knobY = bottomY + 2;
|
||
|
|
|
||
|
|
const auto placeKnob = [&] (Knob& knob)
|
||
|
|
{
|
||
|
|
knob.setBounds (x, knobY, knobW, 82);
|
||
|
|
x += knobGap;
|
||
|
|
};
|
||
|
|
|
||
|
|
placeKnob (detuneKnob);
|
||
|
|
placeKnob (mixKnob);
|
||
|
|
placeKnob (cutoffKnob);
|
||
|
|
placeKnob (resKnob);
|
||
|
|
placeKnob (glideKnob);
|
||
|
|
placeKnob (attackKnob);
|
||
|
|
placeKnob (decayKnob);
|
||
|
|
placeKnob (sustainKnob);
|
||
|
|
placeKnob (releaseKnob);
|
||
|
|
}
|