Prototype

This commit is contained in:
Armin 2026-08-05 23:12:17 +02:00
commit 4271fc34bb
14 changed files with 950 additions and 169 deletions

View file

@ -6,6 +6,19 @@
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()
{
@ -33,6 +46,19 @@ void EditorLookAndFeel::drawRotarySlider (juce::Graphics& g, int x, int y, int w
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;
@ -40,19 +66,19 @@ void EditorLookAndFeel::drawRotarySlider (juce::Graphics& g, int x, int y, int w
radius * 0.85f, radius * 0.85f,
0.0f, start, angle, true);
g.setColour (colours::accent);
g.setGradientFill (orangeGradient (bounds));
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);
// 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);
}
@ -61,7 +87,11 @@ void EditorLookAndFeel::drawToggleButton (juce::Graphics& g, juce::ToggleButton&
{
const auto bounds = button.getLocalBounds().toFloat();
g.setColour (button.getToggleState() ? colours::accent : colours::grid);
if (button.getToggleState())
g.setGradientFill (orangeGradient (bounds));
else
g.setColour (colours::grid);
g.fillRoundedRectangle (bounds, 4.0f);
g.setFont (font (11.0f));
@ -75,7 +105,7 @@ void EditorLookAndFeel::drawButtonBackground (juce::Graphics& g, juce::Button& b
const auto bounds = button.getLocalBounds().toFloat().reduced (0.5f);
if (button.getToggleState())
g.setColour (colours::accent);
g.setGradientFill (orangeGradient (bounds));
else
g.setColour (highlighted ? colours::gridLight : colours::grid);
@ -89,6 +119,81 @@ void EditorLookAndFeel::drawButtonText (juce::Graphics& g, juce::TextButton& but
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)
{
@ -124,10 +229,14 @@ Knob::Knob (const juce::String& title)
void Knob::resized()
{
const int w = getWidth();
const int h = getHeight();
titleLabel.setBounds (0, 0, w, 14);
slider.setBounds (4, 15, w - 8, w - 8);
valueLabel.setBounds (0, w - 8 + 15, w, 12);
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()
@ -234,7 +343,7 @@ void ChoiceBar::resized()
}
//==============================================================================
StepPanel::StepPanel (MonoStepAudioProcessor& processorRef)
StepPanel::StepPanel (MonostepAudioProcessor& processorRef)
: processor (processorRef),
fineKnob ("Fine")
{
@ -284,6 +393,70 @@ StepPanel::StepPanel (MonoStepAudioProcessor& processorRef)
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);
}
@ -328,6 +501,7 @@ void StepPanel::clearPattern()
processor.setStepNote (i, 0);
processor.setStepCents (i, 0.0f);
processor.setStepSlide (i, false);
processor.setStepAccent (i, false);
}
refresh();
@ -344,18 +518,37 @@ void StepPanel::resized()
const int knobX = (w - 74) / 2;
const int knobY = 68;
fineKnob.setBounds (knobX, knobY, 74, 74);
fineKnob.setBounds (knobX, knobY, 74, 94);
const int btnY = knobY + 25;
const int btnY = knobY + 102;
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);
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)
MonostepAudioProcessorEditor::MonostepAudioProcessorEditor (MonostepAudioProcessor& processor)
: AudioProcessorEditor (processor),
processorRef (processor),
content (*this),
@ -365,8 +558,12 @@ MonoStepAudioProcessorEditor::MonoStepAudioProcessorEditor (MonoStepAudioProcess
rootKnob ("Root"),
gateKnob ("Gate"),
masterKnob ("Master"),
coarseKnob ("Coarse"),
coarseKnob2 ("Coarse"),
detuneKnob ("Detune"),
mixKnob ("Mix"),
fineKnob1 ("Fine"),
fineKnob2 ("Fine"),
cutoffKnob ("Cutoff"),
resKnob ("Res"),
glideKnob ("Glide"),
@ -374,6 +571,9 @@ MonoStepAudioProcessorEditor::MonoStepAudioProcessorEditor (MonoStepAudioProcess
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) {}),
@ -414,6 +614,7 @@ MonoStepAudioProcessorEditor::MonoStepAudioProcessorEditor (MonoStepAudioProcess
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"; });
@ -423,6 +624,9 @@ MonoStepAudioProcessorEditor::MonoStepAudioProcessorEditor (MonoStepAudioProcess
addKnob (decayKnob, "decay", [] (float v) { return juce::String (v, 2) + "s"; });
addKnob (sustainKnob, "sustain", [] (float v) { return juce::String (v, 2); });
addKnob (releaseKnob, "release", [] (float v) { return juce::String (v, 2) + "s"; });
addKnob (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);
@ -466,6 +670,12 @@ MonoStepAudioProcessorEditor::MonoStepAudioProcessorEditor (MonoStepAudioProcess
stepPanel.refresh();
};
matrix.onAccentChanged = [this] (int stepIdx, bool accent)
{
processorRef.setStepAccent (stepIdx, accent);
stepPanel.refresh();
};
processorRef.onPatternChanged = [this] { refreshEditor(); };
startTimerHz (30);
@ -473,14 +683,14 @@ MonoStepAudioProcessorEditor::MonoStepAudioProcessorEditor (MonoStepAudioProcess
refreshEditor();
}
MonoStepAudioProcessorEditor::~MonoStepAudioProcessorEditor()
MonostepAudioProcessorEditor::~MonostepAudioProcessorEditor()
{
stopTimer();
processorRef.onPatternChanged = nullptr;
setLookAndFeel (nullptr);
}
void MonoStepAudioProcessorEditor::timerCallback()
void MonostepAudioProcessorEditor::timerCallback()
{
const int step = processorRef.getCurrentStep();
@ -491,27 +701,27 @@ void MonoStepAudioProcessorEditor::timerCallback()
}
}
void MonoStepAudioProcessorEditor::refreshEditor()
void MonostepAudioProcessorEditor::refreshEditor()
{
matrix.setSelectedStep (matrix.getSelectedStep());
stepPanel.refresh();
matrix.repaint();
}
void MonoStepAudioProcessorEditor::paint (juce::Graphics& g)
void MonostepAudioProcessorEditor::paint (juce::Graphics& g)
{
g.fillAll (colours::bg);
}
void MonoStepAudioProcessorEditor::paintContent (juce::Graphics& g)
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.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);
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)
{
@ -538,14 +748,14 @@ void MonoStepAudioProcessorEditor::paintContent (juce::Graphics& g)
g.drawText (buildinfo::footerText(), 16, footY + 4, baseWidth - 32, 12, juce::Justification::centredLeft);
}
void MonoStepAudioProcessorEditor::setZoomFactor (float newZoom)
void MonostepAudioProcessorEditor::setZoomFactor (float newZoom)
{
zoomFactor = newZoom;
setSize (juce::roundToInt (baseWidth * newZoom), juce::roundToInt (baseHeight * newZoom));
}
void MonoStepAudioProcessorEditor::resized()
void MonostepAudioProcessorEditor::resized()
{
const int targetW = juce::roundToInt (baseWidth * zoomFactor);
const int targetH = juce::roundToInt (baseHeight * zoomFactor);
@ -563,18 +773,19 @@ void MonoStepAudioProcessorEditor::resized()
const int w = baseWidth;
const int h = baseHeight;
const int topBarH = 58;
const int topBarH = 78;
const int bottomBarH = 96;
const int pad = 12;
const int topBarY = (topBarH - 26) / 2;
lengthKnob.setBounds (128, 2, 54, 56);
rateBar.setBounds (192, 18, 210, 26);
lengthKnob.setBounds (128, 2, 54, 74);
rateBar.setBounds (192, topBarY, 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);
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;
@ -592,7 +803,7 @@ void MonoStepAudioProcessorEditor::resized()
wave2Bar.setBounds (x, bottomY + 30, barW, 24); x += barW + 16;
const int knobW = 56;
const int knobGap = 72;
const int knobGap = 60;
const int knobY = bottomY + 2;
const auto placeKnob = [&] (Knob& knob)
@ -601,6 +812,7 @@ void MonoStepAudioProcessorEditor::resized()
x += knobGap;
};
placeKnob (coarseKnob);
placeKnob (detuneKnob);
placeKnob (mixKnob);
placeKnob (cutoffKnob);
@ -610,4 +822,7 @@ void MonoStepAudioProcessorEditor::resized()
placeKnob (decayKnob);
placeKnob (sustainKnob);
placeKnob (releaseKnob);
placeKnob (driveKnob);
placeKnob (ringKnob);
placeKnob (accentKnob);
}