Polish GUI, add pattern retrig option, and rework voice DSP

This commit is contained in:
Armin 2026-08-06 15:51:56 +02:00
commit 4732240cc7
7 changed files with 546 additions and 129 deletions

View file

@ -8,6 +8,10 @@ using namespace monostep;
namespace namespace
{ {
constexpr int oscDropW = 96; // OSC 1 / OSC 2 dropdown width
constexpr int oscLabelW = 46; // label strip to the left of each dropdown
constexpr int oscGapW = 8; // gap between the dropdown column and the knobs
juce::ColourGradient orangeGradient (const juce::Rectangle<float>& area) juce::ColourGradient orangeGradient (const juce::Rectangle<float>& area)
{ {
const auto top = area.getTopLeft().translated (area.getWidth() * 0.5f, 0.0f); const auto top = area.getTopLeft().translated (area.getWidth() * 0.5f, 0.0f);
@ -40,11 +44,16 @@ void EditorLookAndFeel::drawRotarySlider (juce::Graphics& g, int x, int y, int w
const float end = rotaryEndAngle; const float end = rotaryEndAngle;
const float angle = start + sliderPos * (end - start); const float angle = start + sliderPos * (end - start);
g.setColour (colours::grid); const auto bodyRect = juce::Rectangle<float> (centre.getX() - radius, centre.getY() - radius,
g.fillEllipse (centre.getX() - radius, centre.getY() - radius, radius * 2.0f, radius * 2.0f); radius * 2.0f, radius * 2.0f);
g.setColour (colours::gridLight); // Raised body: lighter at the top edge, darker at the bottom.
g.drawEllipse (centre.getX() - radius, centre.getY() - radius, radius * 2.0f, radius * 2.0f, 1.0f); juce::ColourGradient bodyGrad (colours::gridLight.brighter (0.15f),
centre.getX(), bodyRect.getY(),
colours::grid.darker (0.18f),
centre.getX(), bodyRect.getBottom(), false);
g.setGradientFill (bodyGrad);
g.fillEllipse (bodyRect);
// Almost-vertical (-2°) shading overlay to give the dial a subtle 3D look. // Almost-vertical (-2°) shading overlay to give the dial a subtle 3D look.
constexpr float degToRad = juce::MathConstants<float>::pi / 180.0f; constexpr float degToRad = juce::MathConstants<float>::pi / 180.0f;
@ -54,10 +63,14 @@ void EditorLookAndFeel::drawRotarySlider (juce::Graphics& g, int x, int y, int w
const auto topPoint = centre + juce::Point<float> ( radius * tiltSin, -radius * tiltCos); const auto topPoint = centre + juce::Point<float> ( radius * tiltSin, -radius * tiltCos);
const auto bottomPoint = 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::ColourGradient shading (juce::Colours::black.withAlpha (0.10f), topPoint,
juce::Colours::black.withAlpha (0.20f), bottomPoint, false); juce::Colours::black.withAlpha (0.10f), bottomPoint, false);
g.setGradientFill (shading); g.setGradientFill (shading);
g.fillEllipse (centre.getX() - radius, centre.getY() - radius, radius * 2.0f, radius * 2.0f); g.fillEllipse (bodyRect);
// Outer rim.
g.setColour (colours::gridLight);
g.drawEllipse (bodyRect, 1.0f);
if (end - start < 6.0f) if (end - start < 6.0f)
{ {
@ -88,11 +101,27 @@ void EditorLookAndFeel::drawToggleButton (juce::Graphics& g, juce::ToggleButton&
const auto bounds = button.getLocalBounds().toFloat(); const auto bounds = button.getLocalBounds().toFloat();
if (button.getToggleState()) if (button.getToggleState())
{
g.setGradientFill (orangeGradient (bounds)); g.setGradientFill (orangeGradient (bounds));
else
g.setColour (colours::grid);
g.fillRoundedRectangle (bounds, 4.0f); g.fillRoundedRectangle (bounds, 4.0f);
}
else
{
// Raised, slightly-3D inactive button.
juce::ColourGradient offGrad (colours::gridLight.brighter (0.12f),
bounds.getX(), bounds.getY(),
colours::grid.darker (0.15f),
bounds.getX(), bounds.getBottom(), false);
g.setGradientFill (offGrad);
g.fillRoundedRectangle (bounds, 4.0f);
g.setColour (colours::gridLight.brighter (0.35f).withAlpha (0.45f));
g.drawHorizontalLine (juce::roundToInt (bounds.getY()) + 1,
bounds.getX() + 3.0f, bounds.getRight() - 3.0f);
g.setColour (colours::bg.withAlpha (0.30f));
g.drawRoundedRectangle (bounds.reduced (0.5f), 4.0f, 1.0f);
}
g.setFont (font (11.0f)); g.setFont (font (11.0f));
g.setColour (button.getToggleState() ? colours::bg : colours::text); g.setColour (button.getToggleState() ? colours::bg : colours::text);
@ -105,11 +134,25 @@ void EditorLookAndFeel::drawButtonBackground (juce::Graphics& g, juce::Button& b
const auto bounds = button.getLocalBounds().toFloat().reduced (0.5f); const auto bounds = button.getLocalBounds().toFloat().reduced (0.5f);
if (button.getToggleState()) if (button.getToggleState())
{
g.setGradientFill (orangeGradient (bounds)); g.setGradientFill (orangeGradient (bounds));
else
g.setColour (highlighted ? colours::gridLight : colours::grid);
g.fillRoundedRectangle (bounds, 4.0f); g.fillRoundedRectangle (bounds, 4.0f);
}
else
{
// Raised, slightly-3D inactive button.
const juce::Colour topColour = highlighted ? colours::gridLight.brighter (0.22f)
: colours::gridLight.brighter (0.10f);
juce::ColourGradient offGrad (topColour, bounds.getX(), bounds.getY(),
colours::grid.darker (0.15f),
bounds.getX(), bounds.getBottom(), false);
g.setGradientFill (offGrad);
g.fillRoundedRectangle (bounds, 4.0f);
g.setColour (colours::gridLight.brighter (0.35f).withAlpha (0.45f));
g.drawHorizontalLine (juce::roundToInt (bounds.getY()) + 1,
bounds.getX() + 3.0f, bounds.getRight() - 3.0f);
}
} }
void EditorLookAndFeel::drawButtonText (juce::Graphics& g, juce::TextButton& button, bool, bool) void EditorLookAndFeel::drawButtonText (juce::Graphics& g, juce::TextButton& button, bool, bool)
@ -124,11 +167,25 @@ void EditorLookAndFeel::drawComboBox (juce::Graphics& g, int width, int height,
{ {
const auto bounds = juce::Rectangle<float> (1.0f, 1.0f, (float) (width - 2), (float) (height - 2)); const auto bounds = juce::Rectangle<float> (1.0f, 1.0f, (float) (width - 2), (float) (height - 2));
g.setColour (colours::grid); // Subtle drop shadow under the box.
g.setColour (juce::Colours::black.withAlpha (0.30f));
g.fillRoundedRectangle (bounds.translated (0.0f, 1.5f), 4.0f);
// Raised box: lighter top edge, darker bottom.
juce::ColourGradient boxGrad (colours::gridLight.brighter (0.10f),
bounds.getX(), bounds.getY(),
colours::grid.darker (0.15f),
bounds.getX(), bounds.getBottom(), false);
g.setGradientFill (boxGrad);
g.fillRoundedRectangle (bounds, 4.0f); g.fillRoundedRectangle (bounds, 4.0f);
g.setColour (colours::gridLight); g.setColour (colours::gridLight);
g.drawRoundedRectangle (bounds, 4.0f, 1.0f); g.drawRoundedRectangle (bounds, 4.0f, 1.0f);
g.setColour (colours::gridLight.brighter (0.35f).withAlpha (0.45f));
g.drawHorizontalLine (juce::roundToInt (bounds.getY()) + 1,
bounds.getX() + 4.0f, bounds.getRight() - 4.0f);
const float cx = (float) (width - 15); const float cx = (float) (width - 15);
const float cy = (float) height * 0.5f; const float cy = (float) height * 0.5f;
@ -145,7 +202,10 @@ juce::Font EditorLookAndFeel::getComboBoxFont (juce::ComboBox&)
void EditorLookAndFeel::drawPopupMenuBackground (juce::Graphics& g, int width, int height) void EditorLookAndFeel::drawPopupMenuBackground (juce::Graphics& g, int width, int height)
{ {
g.setColour (colours::panel); // Slightly recessed panel with a gentle top-to-bottom gradient.
juce::ColourGradient bgGrad (colours::gridLight, 0.0f, 0.0f,
colours::panel.darker (0.06f), 0.0f, (float) height, false);
g.setGradientFill (bgGrad);
g.fillRect (0, 0, width, height); g.fillRect (0, 0, width, height);
g.setColour (colours::gridLight); g.setColour (colours::gridLight);
g.drawRect (0.0f, 0.0f, (float) width, (float) height, 1.0f); g.drawRect (0.0f, 0.0f, (float) width, (float) height, 1.0f);
@ -383,12 +443,6 @@ StepPanel::StepPanel (MonostepAudioProcessor& processorRef)
nextButton.onClick = [this] { setSelectedStep (selectedStep + 1); }; nextButton.onClick = [this] { setSelectedStep (selectedStep + 1); };
addAndMakeVisible (nextButton); 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.setButtonText ("Clear Pattern");
clearButton.onClick = [this] { clearPattern(); }; clearButton.onClick = [this] { clearPattern(); };
addAndMakeVisible (clearButton); addAndMakeVisible (clearButton);
@ -453,10 +507,23 @@ StepPanel::StepPanel (MonostepAudioProcessor& processorRef)
for (int i = 0; i < RandomGroupCount; ++i) for (int i = 0; i < RandomGroupCount; ++i)
{ {
randomizeToggles[i].setButtonText (groupNames[i]); randomizeToggles[i].setButtonText (groupNames[i]);
randomizeToggles[i].setToggleState (true, juce::dontSendNotification); randomizeToggles[i].setToggleState (i == RandomPattern, juce::dontSendNotification);
addAndMakeVisible (randomizeToggles[i]); addAndMakeVisible (randomizeToggles[i]);
} }
resetFineButton.setButtonText ("Reset Fine");
resetFineButton.onClick = [this]
{
for (int i = 0; i < numSteps; ++i)
processor.setStepCents (i, 0.0f);
};
addAndMakeVisible (resetFineButton);
retrigToggle.setButtonText ("Retrig");
retrigToggle.setTooltip ("Restart the pattern from step 1 on each new note trigger");
retrigAttachment = std::make_unique<juce::AudioProcessorValueTreeState::ButtonAttachment> (processor.getAPVTS(), "seqRetrig", retrigToggle);
addAndMakeVisible (retrigToggle);
setSelectedStep (0); setSelectedStep (0);
} }
@ -524,8 +591,6 @@ void StepPanel::resized()
prevButton.setBounds (knobX - 32, btnY, 24, 24); prevButton.setBounds (knobX - 32, btnY, 24, 24);
nextButton.setBounds (knobX + 74 + 8, btnY, 24, 24); nextButton.setBounds (knobX + 74 + 8, btnY, 24, 24);
hintLabel.setBounds (8, btnY + 30, w - 16, 14);
const int shiftW = 22; const int shiftW = 22;
shiftLeftButton.setBounds (10, btnY + 50, shiftW, 26); shiftLeftButton.setBounds (10, btnY + 50, shiftW, 26);
clearButton.setBounds (10 + shiftW + 4, btnY + 50, w - 20 - shiftW * 2 - 8, 26); clearButton.setBounds (10 + shiftW + 4, btnY + 50, w - 20 - shiftW * 2 - 8, 26);
@ -545,6 +610,10 @@ void StepPanel::resized()
const int row = i % 3; const int row = i % 3;
randomizeToggles[i].setBounds (10 + col * (colW + 4), btnY + 180 + row * (rowH + 2), colW, rowH); randomizeToggles[i].setBounds (10 + col * (colW + 4), btnY + 180 + row * (rowH + 2), colW, rowH);
} }
const int retrigW = 100;
retrigToggle.setBounds (10, btnY + 252, retrigW, 26);
resetFineButton.setBounds (10 + retrigW + 4, btnY + 252, w - 20 - retrigW - 4, 26);
} }
//============================================================================== //==============================================================================
@ -571,6 +640,11 @@ MonostepAudioProcessorEditor::MonostepAudioProcessorEditor (MonostepAudioProcess
decayKnob ("Decay"), decayKnob ("Decay"),
sustainKnob ("Sustain"), sustainKnob ("Sustain"),
releaseKnob ("Release"), releaseKnob ("Release"),
fAttackKnob ("Attack"),
fDecayKnob ("Decay"),
fSustainKnob ("Sustain"),
fReleaseKnob ("Release"),
fAmountKnob ("Amt"),
driveKnob ("Drive"), driveKnob ("Drive"),
ringKnob ("Ring"), ringKnob ("Ring"),
accentKnob ("Accent"), accentKnob ("Accent"),
@ -592,41 +666,58 @@ MonostepAudioProcessorEditor::MonostepAudioProcessorEditor (MonostepAudioProcess
content.addAndMakeVisible (octaveBar); content.addAndMakeVisible (octaveBar);
content.addAndMakeVisible (zoomBar); content.addAndMakeVisible (zoomBar);
static const juce::StringArray waveformChoices = const juce::StringArray waveformChoices =
{ {
"Sine", "Triangle", "Saw", "Square", "Sine", "Triangle", "Saw", "Square",
"Pulse", "Noise", "Sub", "Pluck", "Pulse", "Noise", "Sub", "Pluck",
"Super", "S&H", "Formant", "PWM" "Super", "S&H", "Formant", "PWM"
}; };
auto setupWaveCombo = [&apvts, &waveformChoices] (juce::ComboBox& combo, auto setupWaveCombo = [&waveformChoices] (juce::ComboBox& combo)
const juce::String& paramId)
{ {
combo.setColour (juce::ComboBox::textColourId, colours::text); combo.setColour (juce::ComboBox::textColourId, colours::text);
combo.setColour (juce::ComboBox::outlineColourId, colours::gridLight); combo.setColour (juce::ComboBox::outlineColourId, colours::gridLight);
combo.setColour (juce::ComboBox::backgroundColourId, colours::grid); combo.setColour (juce::ComboBox::backgroundColourId, colours::grid);
combo.setColour (juce::ComboBox::arrowColourId, colours::accent); combo.setColour (juce::ComboBox::arrowColourId, colours::accent);
combo.setFont (font (11.0f));
for (int i = 0; i < waveformChoices.size(); ++i) for (int i = 0; i < waveformChoices.size(); ++i)
combo.addItem (waveformChoices[i], i + 1); combo.addItem (waveformChoices[i], i + 1);
combo.selectId (static_cast<int> (apvts.getRawParameterValue (paramId)->load()) + 1);
}; };
setupWaveCombo (wave1Box, "osc1Wave"); setupWaveCombo (wave1Box);
setupWaveCombo (wave2Box, "osc2Wave"); setupWaveCombo (wave2Box);
wave1Attachment = std::make_unique<juce::AudioProcessorValueTreeState::ComboBoxAttachment> (apvts, "osc1Wave", wave1Box); wave1Attachment = std::make_unique<juce::AudioProcessorValueTreeState::ComboBoxAttachment> (apvts, "osc1Wave", wave1Box);
wave2Attachment = std::make_unique<juce::AudioProcessorValueTreeState::ComboBoxAttachment> (apvts, "osc2Wave", wave2Box); wave2Attachment = std::make_unique<juce::AudioProcessorValueTreeState::ComboBoxAttachment> (apvts, "osc2Wave", wave2Box);
// filter type dropdown (below the two OSC dropdowns)
const juce::StringArray filterTypeChoices =
{
"12 LP", "12 HP", "12 BP", "12 Notch",
"24 LP", "24 HP", "24 BP", "24 Notch",
"48 LP", "48 HP", "48 BP", "48 Notch"
};
for (int i = 0; i < filterTypeChoices.size(); ++i)
filterTypeBox.addItem (filterTypeChoices[i], i + 1);
filterTypeBox.setColour (juce::ComboBox::textColourId, colours::text);
filterTypeBox.setColour (juce::ComboBox::outlineColourId, colours::gridLight);
filterTypeBox.setColour (juce::ComboBox::backgroundColourId, colours::grid);
filterTypeBox.setColour (juce::ComboBox::arrowColourId, colours::accent);
filterTypeAttachment = std::make_unique<juce::AudioProcessorValueTreeState::ComboBoxAttachment> (apvts, "filterType", filterTypeBox);
content.addAndMakeVisible (wave1Box); content.addAndMakeVisible (wave1Box);
content.addAndMakeVisible (wave2Box); content.addAndMakeVisible (wave2Box);
content.addAndMakeVisible (filterTypeBox);
static constexpr float zoomValues[] = { 1.0f, 1.25f, 1.5f, 1.75f, 2.0f }; static constexpr float zoomValues[] = { 1.0f, 1.25f, 1.5f, 1.75f, 2.0f };
zoomBar.setManualHandler ([this] (int idx) zoomBar.setManualHandler ([this] (int idx)
{ {
setZoomFactor (zoomValues[juce::jlimit (0, 4, idx)]); idx = juce::jlimit (0, 4, idx);
processorRef.setZoomIndex (idx);
setZoomFactor (zoomValues[idx]);
}); });
auto addKnob = [&apvts, this] (Knob& knob, const juce::String& id, std::function<juce::String (float)> fmt) auto addKnob = [&apvts, this] (Knob& knob, const juce::String& id, std::function<juce::String (float)> fmt)
@ -651,10 +742,17 @@ MonostepAudioProcessorEditor::MonostepAudioProcessorEditor (MonostepAudioProcess
addKnob (decayKnob, "decay", [] (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 (sustainKnob, "sustain", [] (float v) { return juce::String (v, 2); });
addKnob (releaseKnob, "release", [] (float v) { return juce::String (v, 2) + "s"; }); addKnob (releaseKnob, "release", [] (float v) { return juce::String (v, 2) + "s"; });
addKnob (fAttackKnob, "filterAttack", [] (float v) { return juce::String (v, 2) + "s"; });
addKnob (fDecayKnob, "filterDecay", [] (float v) { return juce::String (v, 2) + "s"; });
addKnob (fSustainKnob, "filterSustain", [] (float v) { return juce::String (v, 2); });
addKnob (fReleaseKnob, "filterRelease", [] (float v) { return juce::String (v, 2) + "s"; });
addKnob (fAmountKnob, "filterEnvAmt", [] (float v) { return (v >= 0.0f ? "+" : "") + juce::String (v, 2) + " oct"; });
addKnob (driveKnob, "drive", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; }); 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 (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)) + "%"; }); addKnob (accentKnob, "accent", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; });
fAmountKnob.setDragSensitivity (800);
content.addAndMakeVisible (matrix); content.addAndMakeVisible (matrix);
content.addAndMakeVisible (stepPanel); content.addAndMakeVisible (stepPanel);
@ -708,6 +806,10 @@ MonostepAudioProcessorEditor::MonostepAudioProcessorEditor (MonostepAudioProcess
startTimerHz (30); startTimerHz (30);
refreshEditor(); refreshEditor();
const int savedZoom = processorRef.getZoomIndex();
zoomBar.setSelectedIndex (savedZoom);
setZoomFactor (zoomValues[juce::jlimit (0, 4, savedZoom)]);
} }
MonostepAudioProcessorEditor::~MonostepAudioProcessorEditor() MonostepAudioProcessorEditor::~MonostepAudioProcessorEditor()
@ -761,8 +863,58 @@ void MonostepAudioProcessorEditor::paintContent (juce::Graphics& g)
drawCaption (rateBar, "RATE"); drawCaption (rateBar, "RATE");
drawCaption (octaveBar, "OCTAVE"); drawCaption (octaveBar, "OCTAVE");
drawCaption (zoomBar, "ZOOM"); drawCaption (zoomBar, "ZOOM");
drawCaption (wave1Box, "OSC 1");
drawCaption (wave2Box, "OSC 2"); // Small labels inside the OSC section, left of the dropdowns.
g.setFont (font (8.5f));
g.setColour (colours::textDim);
g.drawText ("OSC 1", wave1Box.getX() - oscLabelW, wave1Box.getY() + 5, oscLabelW - 6, 14, juce::Justification::centredRight);
g.drawText ("OSC 2", wave2Box.getX() - oscLabelW, wave2Box.getY() + 5, oscLabelW - 6, 14, juce::Justification::centredRight);
g.setFont (font (9.0f));
// Union of two knob bounds, padded.
const auto groupBox = [] (const juce::Component& a, const juce::Component& b, float pad)
{
const auto ra = a.getBounds().toFloat();
const auto rb = b.getBounds().toFloat();
juce::Rectangle<float> r;
r.setX (juce::jmin (ra.getX(), rb.getX()) - pad);
r.setY (juce::jmin (ra.getY(), rb.getY()) - pad);
r.setRight (juce::jmax (ra.getRight(), rb.getRight()) + pad);
r.setBottom (juce::jmax (ra.getBottom(), rb.getBottom()) + pad);
return r;
};
// Draw a knob group: subtle gradient behind the title legend + a slight border
// around the knobs themselves. leftPad extends the frame to the left so it can
// enclose side labels.
auto drawGroup = [&] (const juce::Component& first, const juce::Component& last,
const juce::String& title, float pad, float leftPad = 0.0f)
{
const int capX = first.getX() - (int) leftPad;
const int capW = last.getRight() - capX;
const int capY = first.getY() - 13;
const juce::Rectangle<float> legend (capX - 6.0f, capY - 4.0f, (float) capW + 12.0f, 12.0f);
g.setGradientFill (orangeGradient (legend));
g.fillRoundedRectangle (legend, 3.0f);
g.setFont (font (9.0f).boldened());
g.setColour (colours::bg);
g.drawText (title, legend, juce::Justification::centred);
auto box = groupBox (first, last, pad);
box.setLeft (box.getX() - leftPad);
g.setColour (colours::accent.withAlpha (0.30f));
g.drawRoundedRectangle (box, 3.0f, 1.0f);
};
drawGroup (wave1Box, mixKnob, "OSC", 6.0f, (float) (oscLabelW - 21 + 9));
drawGroup (glideKnob, accentKnob, "NOTE", 6.0f);
drawGroup (driveKnob, ringKnob, "DIST", 6.0f);
drawGroup (filterTypeBox, resKnob, "FILTER", 6.0f, 6.0f);
drawGroup (attackKnob, releaseKnob, "AMP ENV", 6.0f);
drawGroup (fAttackKnob, fAmountKnob, "FILTER ENV", 6.0f);
const int footY = baseHeight - 20; const int footY = baseHeight - 20;
g.setColour (colours::grid); g.setColour (colours::grid);
@ -801,7 +953,7 @@ void MonostepAudioProcessorEditor::resized()
const int h = baseHeight; const int h = baseHeight;
const int topBarH = 78; const int topBarH = 78;
const int bottomBarH = 96; const int bottomBarH = 240;
const int pad = 12; const int pad = 12;
const int topBarY = (topBarH - 26) / 2; const int topBarY = (topBarH - 26) / 2;
@ -823,33 +975,60 @@ void MonostepAudioProcessorEditor::resized()
const int bottomY = h - bottomBarH - pad; const int bottomY = h - bottomBarH - pad;
int x = 16;
const int barW = 112;
wave1Box.setBounds (x, bottomY + 30, barW, 24); x += barW + 8;
wave2Box.setBounds (x, bottomY + 30, barW, 24); x += barW + 16;
const int knobW = 56; const int knobW = 56;
const int knobGap = 60; const int knobH = 82;
const int knobY = bottomY + 2; const int knobGap = 6;
const int step = knobW + knobGap; // left-edge advance per knob (62)
const auto placeKnob = [&] (Knob& knob) const int oscColW = oscLabelW + oscDropW + oscGapW; // labels + dropdowns + gap to knobs
// Both rows are centered over the full bottom bar width.
const int blockW = oscColW + 6 * step + knobW + 48; // +48 = the two group gaps
const int oscX = pad + ((w - 2 * pad) - blockW) / 2 + oscColW;
const int row1Y = bottomY + 24;
const int row2Y = row1Y + knobH + 34; // row 1 + a caption band
// OSC 1 / OSC 2 dropdowns stacked inside the OSC section, after the labels.
const int oscDropX = oscX - oscColW + oscLabelW;
wave1Box.setBounds (oscDropX, row1Y, oscDropW, 24);
wave2Box.setBounds (oscDropX, row1Y + 30, oscDropW, 24);
auto placeRow = [&] (int y, int startX, const std::initializer_list<Knob*> knobs)
{ {
knob.setBounds (x, knobY, knobW, 82); int cx = startX;
x += knobGap; for (auto* k : knobs)
{
k->setBounds (cx, y, knobW, knobH);
cx += step;
}
}; };
placeKnob (coarseKnob); // Row 1: coarse / detune / mix / glide / accent / drive / ring.
placeKnob (detuneKnob); // NOTE and DIST are pushed apart to clear the group frames.
placeKnob (mixKnob); placeRow (row1Y, oscX, { &coarseKnob, &detuneKnob, &mixKnob });
placeKnob (cutoffKnob); placeRow (row1Y, oscX + 3 * step + 24, { &glideKnob, &accentKnob });
placeKnob (resKnob); placeRow (row1Y, oscX + 5 * step + 48, { &driveKnob, &ringKnob });
placeKnob (glideKnob);
placeKnob (attackKnob); // Row 2: Filter (dropdown + cutoff, res) [gap] Amp Envelope [gap] Filter Env.
placeKnob (decayKnob); // The three sections are centered as one block with equal frame gaps.
placeKnob (sustainKnob); const int frameGap = 20;
placeKnob (releaseKnob); const int framePad = 5;
placeKnob (driveKnob); auto frameW = [&] (int n) { return n * knobW + (n - 1) * knobGap + 2 * framePad; };
placeKnob (ringKnob);
placeKnob (accentKnob); const int filterDropW = oscDropW; // FILTER TYPE dropdown width (matches the OSC dropdowns)
const int filterDropGap = 8; // gap between the dropdown and the filter knobs
const int filterSecW = filterDropW + filterDropGap + 2 * knobW + knobGap + 2 * framePad;
const int row2W = filterSecW + frameGap + frameW (4) + frameGap + frameW (5);
const int row2Left = pad + ((w - 2 * pad) - row2W) / 2;
const int filterDropX = row2Left + framePad;
const int filterKnobX = filterDropX + filterDropW + filterDropGap;
const int envStart = row2Left + filterSecW + frameGap + framePad;
const int fenvStart = row2Left + filterSecW + frameGap + frameW (4) + frameGap + framePad;
filterTypeBox.setBounds (filterDropX, row2Y, filterDropW, 24);
placeRow (row2Y, filterKnobX, { &cutoffKnob, &resKnob });
placeRow (row2Y, envStart, { &attackKnob, &decayKnob, &sustainKnob, &releaseKnob });
placeRow (row2Y, fenvStart, { &fAttackKnob, &fDecayKnob, &fSustainKnob, &fReleaseKnob, &fAmountKnob });
} }

View file

@ -59,6 +59,8 @@ public:
customOnValueChange = std::move (valueChangedFn); customOnValueChange = std::move (valueChangedFn);
} }
void setDragSensitivity (int pixels) { slider.setMouseDragSensitivity (pixels); }
void resized() override; void resized() override;
private: private:
@ -89,6 +91,13 @@ public:
void setManualHandler (std::function<void (int)> handler) { manualHandler = std::move (handler); } void setManualHandler (std::function<void (int)> handler) { manualHandler = std::move (handler); }
void setSelectedIndex (int index)
{
index = juce::jlimit (0, juce::jmax (0, buttons.size() - 1), index);
for (int i = 0; i < buttons.size(); ++i)
buttons[i]->setToggleState (i == index, juce::dontSendNotification);
}
void resized() override; void resized() override;
private: private:
@ -127,7 +136,6 @@ private:
Knob fineKnob; Knob fineKnob;
juce::TextButton prevButton; juce::TextButton prevButton;
juce::TextButton nextButton; juce::TextButton nextButton;
juce::Label hintLabel;
juce::TextButton clearButton; juce::TextButton clearButton;
juce::TextButton shiftLeftButton; juce::TextButton shiftLeftButton;
juce::TextButton shiftRightButton; juce::TextButton shiftRightButton;
@ -149,6 +157,11 @@ private:
}; };
juce::ToggleButton randomizeToggles[RandomGroupCount]; juce::ToggleButton randomizeToggles[RandomGroupCount];
juce::ToggleButton retrigToggle;
std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> retrigAttachment;
juce::TextButton resetFineButton;
}; };
class MonostepAudioProcessorEditor final : public juce::AudioProcessorEditor, class MonostepAudioProcessorEditor final : public juce::AudioProcessorEditor,
@ -182,7 +195,7 @@ private:
ContentComponent content; ContentComponent content;
static constexpr int baseWidth = 1060; static constexpr int baseWidth = 1060;
static constexpr int baseHeight = 640; static constexpr int baseHeight = 780;
float zoomFactor = 1.0f; float zoomFactor = 1.0f;
@ -209,6 +222,11 @@ private:
Knob decayKnob; Knob decayKnob;
Knob sustainKnob; Knob sustainKnob;
Knob releaseKnob; Knob releaseKnob;
Knob fAttackKnob;
Knob fDecayKnob;
Knob fSustainKnob;
Knob fReleaseKnob;
Knob fAmountKnob;
Knob driveKnob; Knob driveKnob;
Knob ringKnob; Knob ringKnob;
Knob accentKnob; Knob accentKnob;
@ -218,8 +236,10 @@ private:
ChoiceBar zoomBar; ChoiceBar zoomBar;
juce::ComboBox wave1Box; juce::ComboBox wave1Box;
juce::ComboBox wave2Box; juce::ComboBox wave2Box;
juce::ComboBox filterTypeBox;
std::unique_ptr<juce::AudioProcessorValueTreeState::ComboBoxAttachment> wave1Attachment; std::unique_ptr<juce::AudioProcessorValueTreeState::ComboBoxAttachment> wave1Attachment;
std::unique_ptr<juce::AudioProcessorValueTreeState::ComboBoxAttachment> wave2Attachment; std::unique_ptr<juce::AudioProcessorValueTreeState::ComboBoxAttachment> wave2Attachment;
std::unique_ptr<juce::AudioProcessorValueTreeState::ComboBoxAttachment> filterTypeAttachment;
SequencerMatrix matrix; SequencerMatrix matrix;
StepPanel stepPanel; StepPanel stepPanel;

View file

@ -9,9 +9,11 @@ static const char* paramId (int index)
static const char* ids[] = static const char* ids[] =
{ {
"osc1Wave", "osc2Wave", "osc1Coarse", "osc2Coarse", "detune", "mix", "osc1Wave", "osc2Wave", "osc1Coarse", "osc2Coarse", "detune", "mix",
"cutoff", "res", "attack", "decay", "sustain", "release", "glide", "master", "cutoff", "res", "filterType", "attack", "decay", "sustain", "release", "glide", "master",
"seqLen", "seqRate", "seqRoot", "seqOctave", "seqSwing", "seqGateLen", "seqLen", "seqRate", "seqRoot", "seqOctave", "seqSwing", "seqGateLen",
"drive", "ringmod", "accent", "fine1" "drive", "ringmod", "accent", "fine1",
"filterAttack", "filterDecay", "filterSustain", "filterRelease", "filterEnvAmt",
"seqRetrig"
}; };
return ids[index]; return ids[index];
} }
@ -19,9 +21,11 @@ static const char* paramId (int index)
enum ParamIndex enum ParamIndex
{ {
pOsc1Wave, pOsc2Wave, pOsc1Coarse, pOsc2Coarse, pDetune, pMix, pOsc1Wave, pOsc2Wave, pOsc1Coarse, pOsc2Coarse, pDetune, pMix,
pCutoff, pRes, pAttack, pDecay, pSustain, pRelease, pGlide, pMaster, pCutoff, pRes, pFilterType, pAttack, pDecay, pSustain, pRelease, pGlide, pMaster,
pSeqLen, pSeqRate, pSeqRoot, pSeqOctave, pSeqSwing, pSeqGateLen, pSeqLen, pSeqRate, pSeqRoot, pSeqOctave, pSeqSwing, pSeqGateLen,
pDrive, pRingMod, pAccent, pFine1, pDrive, pRingMod, pAccent, pFine1,
pFilterAttack, pFilterDecay, pFilterSustain, pFilterRelease, pFilterEnvAmt,
pSeqRetrig,
numParams numParams
}; };
@ -39,10 +43,16 @@ MonostepAudioProcessor::MonostepAudioProcessor()
osc1PhaseParam = apvts.getRawParameterValue (paramId (pFine1)); osc1PhaseParam = apvts.getRawParameterValue (paramId (pFine1));
cutoffParam = apvts.getRawParameterValue (paramId (pCutoff)); cutoffParam = apvts.getRawParameterValue (paramId (pCutoff));
resParam = apvts.getRawParameterValue (paramId (pRes)); resParam = apvts.getRawParameterValue (paramId (pRes));
filterTypeParam = apvts.getRawParameterValue (paramId (pFilterType));
attackParam = apvts.getRawParameterValue (paramId (pAttack)); attackParam = apvts.getRawParameterValue (paramId (pAttack));
decayParam = apvts.getRawParameterValue (paramId (pDecay)); decayParam = apvts.getRawParameterValue (paramId (pDecay));
sustainParam = apvts.getRawParameterValue (paramId (pSustain)); sustainParam = apvts.getRawParameterValue (paramId (pSustain));
releaseParam = apvts.getRawParameterValue (paramId (pRelease)); releaseParam = apvts.getRawParameterValue (paramId (pRelease));
filterAttackParam = apvts.getRawParameterValue (paramId (pFilterAttack));
filterDecayParam = apvts.getRawParameterValue (paramId (pFilterDecay));
filterSustainParam = apvts.getRawParameterValue (paramId (pFilterSustain));
filterReleaseParam = apvts.getRawParameterValue (paramId (pFilterRelease));
filterEnvAmtParam = apvts.getRawParameterValue (paramId (pFilterEnvAmt));
glideParam = apvts.getRawParameterValue (paramId (pGlide)); glideParam = apvts.getRawParameterValue (paramId (pGlide));
masterParam = apvts.getRawParameterValue (paramId (pMaster)); masterParam = apvts.getRawParameterValue (paramId (pMaster));
seqLenParam = apvts.getRawParameterValue (paramId (pSeqLen)); seqLenParam = apvts.getRawParameterValue (paramId (pSeqLen));
@ -54,6 +64,7 @@ MonostepAudioProcessor::MonostepAudioProcessor()
driveParam = apvts.getRawParameterValue (paramId (pDrive)); driveParam = apvts.getRawParameterValue (paramId (pDrive));
ringModParam = apvts.getRawParameterValue (paramId (pRingMod)); ringModParam = apvts.getRawParameterValue (paramId (pRingMod));
accentParam = apvts.getRawParameterValue (paramId (pAccent)); accentParam = apvts.getRawParameterValue (paramId (pAccent));
seqRetrigParam = apvts.getRawParameterValue (paramId (pSeqRetrig));
setDefaultPattern(); setDefaultPattern();
} }
@ -86,6 +97,12 @@ juce::AudioProcessorValueTreeState::ParameterLayout MonostepAudioProcessor::crea
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pRes), layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pRes),
"Filter Res", juce::NormalisableRange<float> (0.0f, 1.0f), 0.15f)); "Filter Res", juce::NormalisableRange<float> (0.0f, 1.0f), 0.15f));
layout.add (std::make_unique<juce::AudioParameterInt> (paramId (pFilterType),
"Filter Type", 0,
(int) monostep::SynthVoice::FilterType::numFilterTypes - 1,
0));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pAttack), layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pAttack),
"Attack", juce::NormalisableRange<float> (0.001f, 2.0f, 0.001f, 0.3f), 0.005f, "Attack", juce::NormalisableRange<float> (0.001f, 2.0f, 0.001f, 0.3f), 0.005f,
juce::AudioParameterFloatAttributes().withLabel ("s"))); juce::AudioParameterFloatAttributes().withLabel ("s")));
@ -101,6 +118,25 @@ juce::AudioProcessorValueTreeState::ParameterLayout MonostepAudioProcessor::crea
"Release", juce::NormalisableRange<float> (0.01f, 4.0f, 0.001f, 0.3f), 0.4f, "Release", juce::NormalisableRange<float> (0.01f, 4.0f, 0.001f, 0.3f), 0.4f,
juce::AudioParameterFloatAttributes().withLabel ("s"))); juce::AudioParameterFloatAttributes().withLabel ("s")));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pFilterAttack),
"Filter Env Attack", juce::NormalisableRange<float> (0.001f, 2.0f, 0.001f, 0.3f), 0.005f,
juce::AudioParameterFloatAttributes().withLabel ("s")));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pFilterDecay),
"Filter Env Decay", juce::NormalisableRange<float> (0.001f, 3.0f, 0.001f, 0.3f), 0.3f,
juce::AudioParameterFloatAttributes().withLabel ("s")));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pFilterSustain),
"Filter Env Sustain", juce::NormalisableRange<float> (0.0f, 1.0f), 0.7f));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pFilterRelease),
"Filter Env Release", juce::NormalisableRange<float> (0.01f, 4.0f, 0.001f, 0.3f), 0.4f,
juce::AudioParameterFloatAttributes().withLabel ("s")));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pFilterEnvAmt),
"Filter Env Amount", juce::NormalisableRange<float> (-4.0f, 4.0f), 1.0f,
juce::AudioParameterFloatAttributes().withLabel ("oct")));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pGlide), layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pGlide),
"Glide", juce::NormalisableRange<float> (0.0f, 1.0f, 0.001f, 0.5f), 0.12f, "Glide", juce::NormalisableRange<float> (0.0f, 1.0f, 0.001f, 0.5f), 0.12f,
juce::AudioParameterFloatAttributes().withLabel ("s"))); juce::AudioParameterFloatAttributes().withLabel ("s")));
@ -124,6 +160,9 @@ juce::AudioProcessorValueTreeState::ParameterLayout MonostepAudioProcessor::crea
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pSeqGateLen), layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pSeqGateLen),
"Gate Length", juce::NormalisableRange<float> (0.05f, 1.0f), 0.8f)); "Gate Length", juce::NormalisableRange<float> (0.05f, 1.0f), 0.8f));
layout.add (std::make_unique<juce::AudioParameterBool> (paramId (pSeqRetrig),
"Restart Pattern on Note", false));
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pDrive), layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pDrive),
"Distortion Drive", juce::NormalisableRange<float> (0.0f, 1.0f), 0.0f)); "Distortion Drive", juce::NormalisableRange<float> (0.0f, 1.0f), 0.0f));
@ -215,31 +254,32 @@ void MonostepAudioProcessor::applyPreset (int index)
float cutoff; float res; float attack; float decay; float sustain; float release; float cutoff; float res; float attack; float decay; float sustain; float release;
float glide; float gateLen; float swing; int rate; int root; int octave; float glide; float gateLen; float swing; int rate; int root; int octave;
float drive; float ring; float accent; float drive; float ring; float accent;
float fAttack; float fDecay; float fSustain; float fRelease; float fAmt;
}; };
static const Preset presets[] = static const Preset presets[] =
{ {
// --- gritty / glidy ----------------------------------------------------- // --- gritty / glidy -----------------------------------------------------
{ 0, 1, 0, 15.0f, 0.60f, 4200.0f, 0.15f, 0.02f, 0.25f, 0.70f, 0.35f, 0.25f, 0.80f, 0.10f, 2, 0, 3, 0.55f, 0.00f, 0.5f }, // Wet Tape { 0, 1, 0, 15.0f, 0.60f, 4200.0f, 0.15f, 0.02f, 0.25f, 0.70f, 0.35f, 0.25f, 0.80f, 0.10f, 2, 0, 3, 0.55f, 0.00f, 0.5f, 0.02f, 0.30f, 0.60f, 0.25f, 1.5f }, // Wet Tape
{ 2, 0, -12, -8.0f, 0.45f, 1100.0f, 0.65f, 0.003f, 0.40f, 0.10f, 0.08f, 0.12f, 0.90f, 0.00f, 3, 0, 3, 0.85f, 0.00f, 0.4f }, // Acid Wash { 2, 0, -12, -8.0f, 0.45f, 1100.0f, 0.65f, 0.003f, 0.40f, 0.10f, 0.08f, 0.12f, 0.90f, 0.00f, 3, 0, 3, 0.85f, 0.00f, 0.4f, 0.005f, 0.35f, 0.10f, 0.10f, 3.0f }, // Acid Wash
{ 3, 3, 0, 5.0f, 0.50f, 380.0f, 0.30f, 0.005f, 0.10f, 0.40f, 0.10f, 0.20f, 0.95f, 0.05f, 3, 0, 3, 0.60f, 0.00f, 0.3f }, // Tin Can { 3, 3, 0, 5.0f, 0.50f, 380.0f, 0.30f, 0.005f, 0.10f, 0.40f, 0.10f, 0.20f, 0.95f, 0.05f, 3, 0, 3, 0.60f, 0.00f, 0.3f, 0.005f, 0.15f, 0.30f, 0.15f, 0.5f }, // Tin Can
{ 0, 3, 7, 25.0f, 0.35f, 2500.0f, 0.20f, 0.04f, 0.20f, 0.30f, 0.60f, 0.45f, 0.85f, 0.25f, 2, 0, 3, 0.40f, 0.00f, 0.5f }, // Bent Toy { 0, 3, 7, 25.0f, 0.35f, 2500.0f, 0.20f, 0.04f, 0.20f, 0.30f, 0.60f, 0.45f, 0.85f, 0.25f, 2, 0, 3, 0.40f, 0.00f, 0.5f, 0.03f, 0.25f, 0.50f, 0.40f, 1.0f }, // Bent Toy
{ 2, 3, 0, 0.0f, 0.55f, 650.0f, 0.45f, 0.002f, 0.15f, 0.20f, 0.05f, 0.10f, 0.90f, 0.10f, 3, 0, 3, 0.95f, 0.60f, 0.3f }, // Broken Radio { 2, 3, 0, 0.0f, 0.55f, 650.0f, 0.45f, 0.002f, 0.15f, 0.20f, 0.05f, 0.10f, 0.90f, 0.10f, 3, 0, 3, 0.95f, 0.60f, 0.3f, 0.005f, 0.20f, 0.40f, 0.15f, 1.5f }, // Broken Radio
{ 1, 2, 0, 20.0f, 0.50f, 6000.0f, 0.10f, 0.003f, 0.15f, 0.80f, 0.20f, 0.10f, 0.90f, 0.15f, 3, 0, 3, 0.20f, 0.00f, 0.6f }, // Neon Arcade { 1, 2, 0, 20.0f, 0.50f, 6000.0f, 0.10f, 0.003f, 0.15f, 0.80f, 0.20f, 0.10f, 0.90f, 0.15f, 3, 0, 3, 0.20f, 0.00f, 0.6f, 0.01f, 0.30f, 0.70f, 0.30f, 1.0f }, // Neon Arcade
{ 0, 1, -12, -35.0f, 0.55f, 1800.0f, 0.40f, 0.10f, 0.35f, 0.20f, 0.80f, 0.50f, 0.70f, 0.20f, 2, 0, 3, 0.00f, 0.00f, 0.5f }, // Glitch Garden { 0, 1, -12, -35.0f, 0.55f, 1800.0f, 0.40f, 0.10f, 0.35f, 0.20f, 0.80f, 0.50f, 0.70f, 0.20f, 2, 0, 3, 0.00f, 0.00f, 0.5f, 0.01f, 0.40f, 0.50f, 0.20f, -1.5f }, // Glitch Garden
{ 2, 2, 5, 10.0f, 0.40f, 3200.0f, 0.35f, 0.002f, 0.20f, 0.60f, 0.12f, 0.15f, 0.95f, 0.40f, 3, 0, 3, 0.70f, 0.20f, 0.5f }, // Robo Funk { 2, 2, 5, 10.0f, 0.40f, 3200.0f, 0.35f, 0.002f, 0.20f, 0.60f, 0.12f, 0.15f, 0.95f, 0.40f, 3, 0, 3, 0.70f, 0.20f, 0.5f, 0.01f, 0.25f, 0.60f, 0.20f, 2.0f }, // Robo Funk
{ 1, 1, -7, -15.0f, 0.50f, 2600.0f, 0.20f, 0.03f, 0.30f, 0.35f, 0.60f, 0.60f, 0.80f, 0.05f, 2, 0, 3, 0.10f, 0.00f, 0.4f }, // Lo-Fi Noir { 1, 1, -7, -15.0f, 0.50f, 2600.0f, 0.20f, 0.03f, 0.30f, 0.35f, 0.60f, 0.60f, 0.80f, 0.05f, 2, 0, 3, 0.10f, 0.00f, 0.4f, 0.02f, 0.30f, 0.50f, 0.30f, -0.5f }, // Lo-Fi Noir
{ 0, 2, 0, 8.0f, 0.60f, 7000.0f, 0.08f, 0.01f, 0.20f, 0.90f, 0.25f, 0.30f, 0.75f, 0.00f, 3, 0, 3, 0.35f, 0.35f, 0.6f }, // Spectral Sweep { 0, 2, 0, 8.0f, 0.60f, 7000.0f, 0.08f, 0.01f, 0.20f, 0.90f, 0.25f, 0.30f, 0.75f, 0.00f, 3, 0, 3, 0.35f, 0.35f, 0.6f, 0.01f, 0.30f, 0.20f, 0.30f, 3.5f }, // Spectral Sweep
{ 3, 0, -12, 12.0f, 0.45f, 5000.0f, 0.25f, 0.02f, 0.50f, 0.70f, 0.30f, 0.40f, 0.60f, 0.20f, 2, 0, 3, 0.30f, 0.15f, 0.5f }, // Plasma Organ { 3, 0, -12, 12.0f, 0.45f, 5000.0f, 0.25f, 0.02f, 0.50f, 0.70f, 0.30f, 0.40f, 0.60f, 0.20f, 2, 0, 3, 0.30f, 0.15f, 0.5f, 0.02f, 0.40f, 0.80f, 0.30f, 0.5f }, // Plasma Organ
{ 0, 0, 0, 0.0f, 0.50f, 900.0f, 0.50f, 0.05f, 0.30f, 0.50f, 0.50f, 0.35f, 0.85f, 0.30f, 2, 0, 3, 0.25f, 0.55f, 0.3f }, // Underwater FM { 0, 0, 0, 0.0f, 0.50f, 900.0f, 0.50f, 0.05f, 0.30f, 0.50f, 0.50f, 0.35f, 0.85f, 0.30f, 2, 0, 3, 0.25f, 0.55f, 0.3f, 0.05f, 0.30f, 0.50f, 0.40f, -2.0f }, // Underwater FM
// --- clean / no glide / no drive --------------------------------------- // --- clean / no glide / no drive ---------------------------------------
{ 0, 0, 0, 0.0f, 0.50f, 7000.0f, 0.10f, 0.002f, 0.25f, 0.20f, 0.30f, 0.00f, 0.35f, 0.00f, 3, 0, 4, 0.00f, 0.00f, 0.6f }, // Glass Bells { 0, 0, 0, 0.0f, 0.50f, 7000.0f, 0.10f, 0.002f, 0.25f, 0.20f, 0.30f, 0.00f, 0.35f, 0.00f, 3, 0, 4, 0.00f, 0.00f, 0.6f, 0.005f, 0.20f, 0.30f, 0.40f, 0.3f }, // Glass Bells
{ 1, 1, 0, 12.0f, 0.60f, 1200.0f, 0.25f, 0.30f, 1.20f, 0.80f, 1.50f, 0.20f, 1.00f, 0.10f, 2, 0, 2, 0.00f, 0.00f, 0.2f }, // Warm Pad { 1, 1, 0, 12.0f, 0.60f, 1200.0f, 0.25f, 0.30f, 1.20f, 0.80f, 1.50f, 0.20f, 1.00f, 0.10f, 2, 0, 2, 0.00f, 0.00f, 0.2f, 0.50f, 1.00f, 0.70f, 1.20f, 1.0f }, // Warm Pad
{ 2, 2, 0, 5.0f, 0.50f, 2500.0f, 0.15f, 0.002f, 0.20f, 0.30f, 0.12f, 0.00f, 0.50f, 0.30f, 3, 0, 3, 0.00f, 0.00f, 0.5f }, // Pulse Groove { 2, 2, 0, 5.0f, 0.50f, 2500.0f, 0.15f, 0.002f, 0.20f, 0.30f, 0.12f, 0.00f, 0.50f, 0.30f, 3, 0, 3, 0.00f, 0.00f, 0.5f, 0.01f, 0.25f, 0.40f, 0.20f, 1.5f }, // Pulse Groove
{ 0, 3, 12, 0.0f, 0.40f, 1500.0f, 0.60f, 0.001f, 0.35f, 0.15f, 0.20f, 0.00f, 0.45f, 0.00f, 3, 0, 4, 0.00f, 0.35f, 0.7f }, // Steel Pluck { 0, 3, 12, 0.0f, 0.40f, 1500.0f, 0.60f, 0.001f, 0.35f, 0.15f, 0.20f, 0.00f, 0.45f, 0.00f, 3, 0, 4, 0.00f, 0.35f, 0.7f, 0.002f, 0.30f, 0.20f, 0.25f, 2.5f }, // Steel Pluck
{ 0, 0, 0, -5.0f, 0.40f, 500.0f, 0.20f, 0.01f, 0.40f, 0.90f, 0.30f, 0.00f, 0.90f, 0.00f, 2, 0, 1, 0.00f, 0.00f, 0.3f }, // Sub Rumble { 0, 0, 0, -5.0f, 0.40f, 500.0f, 0.20f, 0.01f, 0.40f, 0.90f, 0.30f, 0.00f, 0.90f, 0.00f, 2, 0, 1, 0.00f, 0.00f, 0.3f, 0.01f, 0.30f, 0.60f, 0.30f, -1.0f }, // Sub Rumble
{ 1, 0, 0, 8.0f, 0.55f, 3000.0f, 0.20f, 0.005f, 0.30f, 0.60f, 0.25f, 0.30f, 0.70f, 0.15f, 2, 0, 3, 0.25f, 0.00f, 0.5f }, // Tape Bounce { 1, 0, 0, 8.0f, 0.55f, 3000.0f, 0.20f, 0.005f, 0.30f, 0.60f, 0.25f, 0.30f, 0.70f, 0.15f, 2, 0, 3, 0.25f, 0.00f, 0.5f, 0.01f, 0.30f, 0.50f, 0.30f, 1.0f }, // Tape Bounce
}; };
const auto& pr = presets[juce::jlimit (0, getNumPresets() - 1, index)]; const auto& pr = presets[juce::jlimit (0, getNumPresets() - 1, index)];
@ -264,6 +304,11 @@ void MonostepAudioProcessor::applyPreset (int index)
applyParamValue ("drive", pr.drive); applyParamValue ("drive", pr.drive);
applyParamValue ("ringmod", pr.ring); applyParamValue ("ringmod", pr.ring);
applyParamValue ("accent", pr.accent); applyParamValue ("accent", pr.accent);
applyParamValue ("filterAttack", pr.fAttack);
applyParamValue ("filterDecay", pr.fDecay);
applyParamValue ("filterSustain", pr.fSustain);
applyParamValue ("filterRelease", pr.fRelease);
applyParamValue ("filterEnvAmt", pr.fAmt);
} }
void MonostepAudioProcessor::randomizePattern() void MonostepAudioProcessor::randomizePattern()
@ -318,6 +363,11 @@ void MonostepAudioProcessor::randomizeFilter()
{ {
applyParamValue ("cutoff", 200.0f * std::pow (2.0f, random.nextFloat() * 5.0f)); applyParamValue ("cutoff", 200.0f * std::pow (2.0f, random.nextFloat() * 5.0f));
applyParamValue ("res", random.nextFloat() * 0.8f); applyParamValue ("res", random.nextFloat() * 0.8f);
applyParamValue ("filterEnvAmt", -3.0f + random.nextFloat() * 6.0f);
applyParamValue ("filterAttack", 0.001f * std::pow (500.0f, random.nextFloat()));
applyParamValue ("filterDecay", 0.05f * std::pow (30.0f, random.nextFloat()));
applyParamValue ("filterSustain", random.nextFloat());
applyParamValue ("filterRelease", 0.05f * std::pow (40.0f, random.nextFloat()));
} }
void MonostepAudioProcessor::randomizeEnv() void MonostepAudioProcessor::randomizeEnv()
@ -375,15 +425,22 @@ void MonostepAudioProcessor::updateVoiceParams()
monostep::SynthVoice::Params p; monostep::SynthVoice::Params p;
p.waveA = (monostep::Waveform) (int) osc1WaveParam->load(); p.waveA = (monostep::Waveform) (int) osc1WaveParam->load();
p.waveB = (monostep::Waveform) (int) osc2WaveParam->load(); p.waveB = (monostep::Waveform) (int) osc2WaveParam->load();
p.detuneRatio = std::pow (2.0f, osc2CoarseParam->load() / 12.0f) p.detuneRatioA = std::pow (2.0f, detuneParam->load() / 1200.0f); // OSC1: follows the knob
* std::pow (2.0f, detuneParam->load() / 1200.0f); p.detuneRatio = std::pow (2.0f, osc2CoarseParam->load() / 12.0f) // OSC2: coarse
* std::pow (2.0f, -detuneParam->load() / 1200.0f); // OSC2: detune inverted
p.mix = mixParam->load(); p.mix = mixParam->load();
p.cutoff = cutoffParam->load(); p.cutoff = cutoffParam->load();
p.resonance = resParam->load(); p.resonance = resParam->load();
p.filterType = (monostep::SynthVoice::FilterType) (int) filterTypeParam->load();
p.attack = attackParam->load(); p.attack = attackParam->load();
p.decay = decayParam->load(); p.decay = decayParam->load();
p.sustain = sustainParam->load(); p.sustain = sustainParam->load();
p.release = releaseParam->load(); p.release = releaseParam->load();
p.fAttack = filterAttackParam->load();
p.fDecay = filterDecayParam->load();
p.fSustain = filterSustainParam->load();
p.fRelease = filterReleaseParam->load();
p.fAmount = filterEnvAmtParam->load();
p.glide = glideParam->load(); p.glide = glideParam->load();
p.master = masterParam->load(); p.master = masterParam->load();
p.drive = driveParam->load(); p.drive = driveParam->load();
@ -419,6 +476,7 @@ void MonostepAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juc
const float cyclePpq = (float) seqLen * stepLen; const float cyclePpq = (float) seqLen * stepLen;
// --- MIDI notes (gate/trigger for the sequencer) ------------------------- // --- MIDI notes (gate/trigger for the sequencer) -------------------------
bool noteOnSeen = false;
for (const auto metadata : midiMessages) for (const auto metadata : midiMessages)
{ {
const auto msg = metadata.getMessage(); const auto msg = metadata.getMessage();
@ -426,6 +484,7 @@ void MonostepAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juc
if (msg.isNoteOn()) if (msg.isNoteOn())
{ {
midiHeld = true; midiHeld = true;
noteOnSeen = true;
lastMidiNote = msg.getNoteNumber(); lastMidiNote = msg.getNoteNumber();
} }
else if (msg.isNoteOff()) else if (msg.isNoteOff())
@ -477,6 +536,16 @@ void MonostepAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juc
lastGateApplied = false; lastGateApplied = false;
} }
// Optional retrigger: a fresh note-on restarts the pattern from
// step 1. Only honoured in free-run mode; while the host transport
// is running, the pattern follows the host grid instead.
if (noteOnSeen && seqRetrigParam->load() >= 0.5f)
{
freePpq = 0.0;
lastStep = -1;
lastGateApplied = false;
}
freePpq += ppqPerSample * numSamples; freePpq += ppqPerSample * numSamples;
} }
} }
@ -742,6 +811,7 @@ void MonostepAudioProcessor::getStateInformation (juce::MemoryBlock& destData)
juce::ValueTree state = apvts.copyState(); juce::ValueTree state = apvts.copyState();
juce::ValueTree seq = state.getOrCreateChildWithName ("SEQ", nullptr); juce::ValueTree seq = state.getOrCreateChildWithName ("SEQ", nullptr);
storeSequence (seq); storeSequence (seq);
state.setProperty ("zoom", zoomIndex, nullptr);
std::unique_ptr<juce::XmlElement> xml (state.createXml()); std::unique_ptr<juce::XmlElement> xml (state.createXml());
copyXmlToBinary (*xml, destData); copyXmlToBinary (*xml, destData);
@ -760,6 +830,8 @@ void MonostepAudioProcessor::setStateInformation (const void* data, int sizeInBy
if (auto seq = state.getChildWithName ("SEQ"); seq.isValid()) if (auto seq = state.getChildWithName ("SEQ"); seq.isValid())
loadSequence (seq); loadSequence (seq);
zoomIndex = juce::jlimit (0, 4, (int) state.getProperty ("zoom", 0));
if (onPatternChanged) if (onPatternChanged)
onPatternChanged(); onPatternChanged();
} }

View file

@ -32,6 +32,8 @@ public:
void setStateInformation (const void*, int) override; void setStateInformation (const void*, int) override;
juce::AudioProcessorValueTreeState& getAPVTS() { return apvts; } juce::AudioProcessorValueTreeState& getAPVTS() { return apvts; }
void setZoomIndex (int index) { zoomIndex = juce::jlimit (0, 4, index); }
int getZoomIndex() const { return zoomIndex; }
monostep::StepSequencer& getSequencer() { return sequencer; } monostep::StepSequencer& getSequencer() { return sequencer; }
const monostep::StepSequencer& getSequencer() const { return sequencer; } const monostep::StepSequencer& getSequencer() const { return sequencer; }
@ -82,10 +84,16 @@ private:
std::atomic<float>* osc1PhaseParam = nullptr; std::atomic<float>* osc1PhaseParam = nullptr;
std::atomic<float>* cutoffParam = nullptr; std::atomic<float>* cutoffParam = nullptr;
std::atomic<float>* resParam = nullptr; std::atomic<float>* resParam = nullptr;
std::atomic<float>* filterTypeParam = nullptr;
std::atomic<float>* attackParam = nullptr; std::atomic<float>* attackParam = nullptr;
std::atomic<float>* decayParam = nullptr; std::atomic<float>* decayParam = nullptr;
std::atomic<float>* sustainParam = nullptr; std::atomic<float>* sustainParam = nullptr;
std::atomic<float>* releaseParam = nullptr; std::atomic<float>* releaseParam = nullptr;
std::atomic<float>* filterAttackParam = nullptr;
std::atomic<float>* filterDecayParam = nullptr;
std::atomic<float>* filterSustainParam = nullptr;
std::atomic<float>* filterReleaseParam = nullptr;
std::atomic<float>* filterEnvAmtParam = nullptr;
std::atomic<float>* glideParam = nullptr; std::atomic<float>* glideParam = nullptr;
std::atomic<float>* masterParam = nullptr; std::atomic<float>* masterParam = nullptr;
std::atomic<float>* seqLenParam = nullptr; std::atomic<float>* seqLenParam = nullptr;
@ -97,6 +105,7 @@ private:
std::atomic<float>* driveParam = nullptr; std::atomic<float>* driveParam = nullptr;
std::atomic<float>* ringModParam = nullptr; std::atomic<float>* ringModParam = nullptr;
std::atomic<float>* accentParam = nullptr; std::atomic<float>* accentParam = nullptr;
std::atomic<float>* seqRetrigParam = nullptr;
monostep::StepSequencer sequencer; monostep::StepSequencer sequencer;
monostep::SynthVoice voice; monostep::SynthVoice voice;
@ -105,6 +114,8 @@ private:
double sampleRate = 44100.0; double sampleRate = 44100.0;
int zoomIndex = 0;
int lastStep = -1; int lastStep = -1;
std::atomic<int> playStep { -1 }; std::atomic<int> playStep { -1 };
bool lastGateApplied = false; bool lastGateApplied = false;

View file

@ -9,6 +9,14 @@ namespace monostep
class SynthVoice class SynthVoice
{ {
public: public:
enum class FilterType
{
lp12 = 0, hp12, bp12, notch12,
lp24, hp24, bp24, notch24,
lp48, hp48, bp48, notch48, // 48 dB/octave = "2 x 24"
numFilterTypes
};
struct Params struct Params
{ {
Waveform waveA = Waveform::saw; Waveform waveA = Waveform::saw;
@ -21,15 +29,22 @@ public:
float osc2Phase = 0.0f; float osc2Phase = 0.0f;
float cutoff = 7000.0f; float cutoff = 7000.0f;
float resonance = 0.15f; float resonance = 0.15f;
FilterType filterType = FilterType::lp12;
float attack = 0.005f; float attack = 0.005f;
float decay = 0.3f; float decay = 0.3f;
float sustain = 0.7f; float sustain = 0.7f;
float release = 0.4f; float release = 0.4f;
float fAttack = 0.005f;
float fDecay = 0.3f;
float fSustain = 0.7f;
float fRelease = 0.4f;
float fAmount = 1.0f; // filter-env cutoff modulation in octaves
float glide = 0.12f; float glide = 0.12f;
float master = 0.9f; float master = 0.9f;
float drive = 0.0f; float drive = 0.0f;
float ringMod = 0.0f; float ringMod = 0.0f;
float detuneRatio = 1.0f; float detuneRatio = 1.0f; // OSC2 (inverted detune)
float detuneRatioA = 1.0f; // OSC1 (direct detune)
float mix = 0.5f; float mix = 0.5f;
}; };
@ -42,7 +57,8 @@ public:
void prepare (double sr) void prepare (double sr)
{ {
sampleRate = (float) sr; sampleRate = (float) sr;
accentSmoothCoef = 1.0f - std::exp (-1.0f / (0.003f * sampleRate)); accentAttackCoef = 1.0f - std::exp (-1.0f / (0.003f * sampleRate));
accentReleaseCoef = 1.0f - std::exp (-1.0f / (0.050f * sampleRate));
reset(); reset();
} }
@ -51,9 +67,15 @@ public:
phaseA = phaseB = 0.0f; phaseA = phaseB = 0.0f;
currentFreq = targetFreq = 440.0f; currentFreq = targetFreq = 440.0f;
smoothing = 0.0f; smoothing = 0.0f;
filterLow = filterBand = 0.0f; for (int i = 0; i < 4; ++i)
{
filterLow[i] = 0.0f;
filterBand[i] = 0.0f;
}
env = 0.0f; env = 0.0f;
envStage = Stage::idle; envStage = Stage::idle;
fenv = 0.0f;
fenvStage = Stage::idle;
gate = false; gate = false;
accentLevel = 1.0f; accentLevel = 1.0f;
accentSmooth = 1.0f; accentSmooth = 1.0f;
@ -92,6 +114,9 @@ public:
if (envStage != Stage::idle && envStage != Stage::release) if (envStage != Stage::idle && envStage != Stage::release)
envStage = Stage::release; envStage = Stage::release;
if (fenvStage != Stage::idle && fenvStage != Stage::release)
fenvStage = Stage::release;
} }
} }
@ -109,6 +134,37 @@ private:
void retriggerEnvelope() void retriggerEnvelope()
{ {
envStage = Stage::attack; envStage = Stage::attack;
fenvStage = Stage::attack;
}
void updateFilterEnvelope()
{
const float sr = sampleRate;
switch (fenvStage)
{
case Stage::attack:
fenv += 1.0f / (params.fAttack * sr);
if (fenv >= 1.0f) { fenv = 1.0f; fenvStage = Stage::decay; }
break;
case Stage::decay:
fenv -= (1.0f - params.fSustain) / (params.fDecay * sr);
if (fenv <= params.fSustain) { fenv = params.fSustain; fenvStage = Stage::sustain; }
break;
case Stage::sustain:
fenv = params.fSustain;
break;
case Stage::release:
fenv -= 1.0f / (params.fRelease * sr);
if (fenv <= 0.0f) { fenv = 0.0f; fenvStage = Stage::idle; }
break;
case Stage::idle:
break;
}
} }
void updateEnvelope() void updateEnvelope()
@ -155,27 +211,83 @@ private:
} }
} }
float processFilter (float input) float processFilter (float input, float cutoff)
{ {
const float f = 2.0f * std::sin (juce::MathConstants<float>::pi * params.cutoff / sampleRate); const int numStages = numFilterStages();
const float q = 1.0f / (1.0f + params.resonance * 9.0f); const float f = 2.0f * std::sin (juce::MathConstants<float>::pi * cutoff / sampleRate);
// Distribute resonance across the pole stages (each stage gets res/stages) so the
// composite resonance matches the 12 dB case instead of stacking into self-oscillation
// near Nyquist. The 12 dB case (one stage) is unchanged.
const float q = 1.0f / (1.0f + params.resonance * 9.0f / (float) numStages);
filterLow += f * filterBand; // Always cascade the LP response (low -> low): this keeps the integrator chain
const float high = input - filterLow - q * filterBand; // unconditionally stable for every pole count. The requested response (LP/HP/BP/Notch)
filterBand += f * high; // is then tapped from the final stage, which preserves the 12/24/48 slope while
// avoiding the ringing/unstability of cascading raw high/band signals.
float sig = input;
float low = 0.0f, high = 0.0f, band = 0.0f;
return filterLow; for (int s = 0; s < numStages; ++s)
{
low = filterLow[s] + f * filterBand[s]; // new low (old band)
high = sig - low - q * filterBand[s]; // new high (new low, old band)
band = filterBand[s] + f * high; // new band
// Safety clamp: prevents the resonant state from diverging into NaN when a
// high pole count is driven at max resonance near Nyquist. Normal signals
// (accents peak ~[1 + res*4]) never reach this bound.
band = juce::jlimit (-16.0f, 16.0f, band);
low = juce::jlimit (-16.0f, 16.0f, low);
filterLow[s] = low;
filterBand[s] = band;
sig = low; // cascade LP response
}
return filterTap (low, high, band); // tap from last stage
}
int numFilterStages() const
{
switch (params.filterType)
{
case FilterType::lp12: case FilterType::hp12:
case FilterType::bp12: case FilterType::notch12: return 1;
case FilterType::lp24: case FilterType::hp24:
case FilterType::bp24: case FilterType::notch24: return 2;
case FilterType::lp48: case FilterType::hp48:
case FilterType::bp48: case FilterType::notch48: return 4;
default: return 1;
}
return 1;
}
float filterTap (float low, float high, float band) const
{
switch (params.filterType)
{
case FilterType::lp12: case FilterType::lp24: case FilterType::lp48: return low;
case FilterType::hp12: case FilterType::hp24: case FilterType::hp48: return high;
case FilterType::bp12: case FilterType::bp24: case FilterType::bp48: return band;
case FilterType::notch12: case FilterType::notch24: case FilterType::notch48: return low + high;
default: return low;
}
return low;
} }
float renderSample() float renderSample()
{ {
updateEnvelope(); updateEnvelope();
updateFilterEnvelope();
updateGlide(); updateGlide();
// Smooth the accent gain so it ramps in/out instead of clicking. // Accent gains fast attack / slow release, so the boost fades out smoothly
accentSmooth += (accentLevel - accentSmooth) * accentSmoothCoef; // at the tail of an accented note instead of cutting off hard-edged.
accentSmooth += (accentLevel - accentSmooth)
* (accentLevel > accentSmooth ? accentAttackCoef : accentReleaseCoef);
const float incA = currentFreq / sampleRate; const float incA = (currentFreq * params.detuneRatioA) / sampleRate;
const float incB = (currentFreq * params.detuneRatio) / sampleRate; const float incB = (currentFreq * params.detuneRatio) / sampleRate;
const float a = renderWave (params.waveA, phaseA, incA); const float a = renderWave (params.waveA, phaseA, incA);
@ -190,7 +302,9 @@ private:
out = (1.0f - params.ringMod) * out + params.ringMod * (a * b); out = (1.0f - params.ringMod) * out + params.ringMod * (a * b);
const float preFilter = out; const float preFilter = out;
out = processFilter (out); const float envCutoff = juce::jlimit (20.0f, 20000.0f,
params.cutoff * std::pow (2.0f, params.fAmount * fenv));
out = processFilter (out, envCutoff);
// Accented steps also get a touch of the unfiltered signal so they cut // Accented steps also get a touch of the unfiltered signal so they cut
// through the mix (presence), not just a volume bump. // through the mix (presence), not just a volume bump.
@ -215,15 +329,18 @@ private:
float targetFreq = 440.0f; float targetFreq = 440.0f;
float smoothing = 0.0f; float smoothing = 0.0f;
float filterLow = 0.0f; float filterLow[4] = {}; // SVF stage states (up to 4 poles for 48 dB)
float filterBand = 0.0f; float filterBand[4] = {};
float env = 0.0f; float env = 0.0f;
Stage envStage = Stage::idle; Stage envStage = Stage::idle;
float fenv = 0.0f;
Stage fenvStage = Stage::idle;
bool gate = false; bool gate = false;
float accentLevel = 1.0f; float accentLevel = 1.0f;
float accentSmooth = 1.0f; float accentSmooth = 1.0f;
float accentSmoothCoef = 0.01f; float accentAttackCoef = 0.0f; // fast charge when an accent starts
float accentReleaseCoef = 0.01f; // slow release -> boost fades out at note end
}; };
} // namespace monostep } // namespace monostep

View file

@ -103,7 +103,6 @@ inline float renderWave (Waveform w, float phase, float inc)
case Waveform::super: case Waveform::super:
{ {
// A small detuned stack of band-limited saws -> fat, chorus-like lead.
float sum = 0.0f; float sum = 0.0f;
const float detunes[3] = { -0.01f, 0.0f, 0.01f }; const float detunes[3] = { -0.01f, 0.0f, 0.01f };
@ -118,8 +117,6 @@ inline float renderWave (Waveform w, float phase, float inc)
case Waveform::sah: case Waveform::sah:
{ {
// Sample & hold: a stepped random level per fraction of the cycle.
// Deterministic (no shared state) so each oscillator is independent.
const int steps = 16; const int steps = 16;
const float f = frac (phase); const float f = frac (phase);
int s = static_cast<int> (std::floor (f * (float) steps)); int s = static_cast<int> (std::floor (f * (float) steps));
@ -135,7 +132,6 @@ inline float renderWave (Waveform w, float phase, float inc)
case Waveform::formant: case Waveform::formant:
{ {
// Two formant-like bands via slow AM sidebands (fixed ratios).
const float w = phase * 6.283185307179586f; const float w = phase * 6.283185307179586f;
const float env = 0.5f + 0.3f * std::cos (w * 0.5f) + 0.2f * std::cos (w * 0.25f); const float env = 0.5f + 0.3f * std::cos (w * 0.5f) + 0.2f * std::cos (w * 0.25f);
return std::sin (w) * env; return std::sin (w) * env;
@ -143,7 +139,6 @@ inline float renderWave (Waveform w, float phase, float inc)
case Waveform::pwm: case Waveform::pwm:
{ {
// Fixed 20 % pulse, band-limited at both edges.
const float width = 0.2f; const float width = 0.2f;
const float f = frac (phase); const float f = frac (phase);
float v = (f < width) ? 1.0f : -1.0f; float v = (f < width) ? 1.0f : -1.0f;

View file

@ -66,6 +66,22 @@ void SequencerMatrix::paint (juce::Graphics& g)
const float ox = (float) bounds.getX(); const float ox = (float) bounds.getX();
const float oy = (float) bounds.getY(); const float oy = (float) bounds.getY();
// Slightly-3D header band above the grid.
juce::ColourGradient headerGrad (colours::gridLight,
0.0f, 0.0f,
colours::panel.darker (0.10f),
0.0f, headerH, false);
g.setGradientFill (headerGrad);
g.fillRect (0.0f, 0.0f, (float) getWidth(), headerH);
g.setColour (colours::gridLight.brighter (0.3f).withAlpha (0.4f));
g.fillRect (0.0f, 0.0f, (float) getWidth(), 1.0f);
g.setColour (colours::grid);
g.drawHorizontalLine (juce::roundToInt (headerH) - 1, 0, (float) getWidth());
g.setColour (colours::bg);
g.drawHorizontalLine (juce::roundToInt (headerH), 0, (float) getWidth());
// grid background // grid background
g.setColour (colours::bg); g.setColour (colours::bg);
g.fillRect (bounds.toFloat()); g.fillRect (bounds.toFloat());
@ -156,28 +172,21 @@ void SequencerMatrix::paint (juce::Graphics& g)
const bool isSelected = (i == selectedStep); const bool isSelected = (i == selectedStep);
const float radius = 3.0f; const float radius = 3.0f;
// cell body // cell body: keep a visible top->bottom gradient in both states. The selected
juce::ColourGradient grad (colours::accent, // note is brighter overall, but still holds its gradient (a flat bright fill
// would read as "no gradient").
const juce::Colour noteTop = isSelected ? colours::accent.brighter (0.2f) : colours::accent;
const juce::Colour noteBottom = colours::accent.darker (0.3f);
juce::ColourGradient grad (noteTop,
cell.getTopLeft().translated (cell.getWidth() * 0.5f, 0.0f), cell.getTopLeft().translated (cell.getWidth() * 0.5f, 0.0f),
isSelected ? colours::accent.brighter (0.12f) : colours::accent.darker (0.35f), noteBottom,
cell.getBottomLeft().translated (cell.getWidth() * 0.5f, 0.0f), cell.getBottomLeft().translated (cell.getWidth() * 0.5f, 0.0f),
false); false);
g.setGradientFill (grad); g.setGradientFill (grad);
g.fillRoundedRectangle (cell, radius); g.fillRoundedRectangle (cell, radius);
// slide arrow
if (s.slide)
{
const float ax = cell.getX() + cell.getWidth() * 0.5f;
const float ay = cell.getY() + cell.getHeight() - 4.0f;
g.setColour (colours::selected);
juce::Path tri;
tri.addTriangle (ax - 3.5f, ay - 1.5f, ax + 3.5f, ay - 1.5f, ax, ay + 3.5f);
g.fillPath (tri);
}
// fine-tune indicator // fine-tune indicator
if (std::fabs (s.cents) > 0.5f) if (std::fabs (s.cents) > 0.5f)
{ {
@ -208,7 +217,13 @@ void SequencerMatrix::paint (juce::Graphics& g)
const auto cell = juce::Rectangle<float> (ox + i * cellW + 1.5f, slideY + 1.5f, const auto cell = juce::Rectangle<float> (ox + i * cellW + 1.5f, slideY + 1.5f,
cellW - 3.0f, cellH - 3.0f); cellW - 3.0f, cellH - 3.0f);
g.setColour (colours::accent); // match the gradient style of the note cells
juce::ColourGradient grad (colours::accent,
cell.getTopLeft().translated (cell.getWidth() * 0.5f, 0.0f),
colours::accent.darker (0.3f),
cell.getBottomLeft().translated (cell.getWidth() * 0.5f, 0.0f),
false);
g.setGradientFill (grad);
g.fillRoundedRectangle (cell, 3.0f); g.fillRoundedRectangle (cell, 3.0f);
const float ax = cell.getX() + cell.getWidth() * 0.5f; const float ax = cell.getX() + cell.getWidth() * 0.5f;
@ -241,7 +256,15 @@ void SequencerMatrix::paint (juce::Graphics& g)
const auto cell = juce::Rectangle<float> (ox + i * cellW + 1.5f, accentY + 1.5f, const auto cell = juce::Rectangle<float> (ox + i * cellW + 1.5f, accentY + 1.5f,
cellW - 3.0f, cellH - 3.0f); cellW - 3.0f, cellH - 3.0f);
g.setColour (colours::accent2); const bool isSelected = (i == selectedStep);
juce::ColourGradient agrad (isSelected ? colours::accent2.brighter (0.2f) : colours::accent2,
cell.getTopLeft().translated (cell.getWidth() * 0.5f, 0.0f),
colours::accent2.darker (0.3f),
cell.getBottomLeft().translated (cell.getWidth() * 0.5f, 0.0f),
false);
g.setGradientFill (agrad);
g.fillRoundedRectangle (cell, 3.0f); g.fillRoundedRectangle (cell, 3.0f);
const float ax = cell.getX() + cell.getWidth() * 0.5f; const float ax = cell.getX() + cell.getWidth() * 0.5f;