mirror of
https://codeberg.org/armin/monostep.git
synced 2026-09-01 12:20:46 +02:00
Polish GUI, add pattern retrig option, and rework voice DSP
This commit is contained in:
parent
8ed279e84c
commit
4732240cc7
7 changed files with 546 additions and 129 deletions
|
|
@ -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 });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue