mirror of
https://codeberg.org/armin/monostep.git
synced 2026-09-01 12:20:46 +02:00
Prototype
This commit is contained in:
parent
c117906a7f
commit
4271fc34bb
14 changed files with 950 additions and 169 deletions
|
|
@ -6,6 +6,19 @@
|
|||
|
||||
using namespace monostep;
|
||||
|
||||
namespace
|
||||
{
|
||||
juce::ColourGradient orangeGradient (const juce::Rectangle<float>& area)
|
||||
{
|
||||
const auto top = area.getTopLeft().translated (area.getWidth() * 0.5f, 0.0f);
|
||||
const auto bottom = area.getBottomLeft().translated (area.getWidth() * 0.5f, 0.0f);
|
||||
|
||||
return juce::ColourGradient (colours::accent, top,
|
||||
colours::accent.darker (0.35f), bottom,
|
||||
false);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
//==============================================================================
|
||||
EditorLookAndFeel::EditorLookAndFeel()
|
||||
{
|
||||
|
|
@ -33,6 +46,19 @@ void EditorLookAndFeel::drawRotarySlider (juce::Graphics& g, int x, int y, int w
|
|||
g.setColour (colours::gridLight);
|
||||
g.drawEllipse (centre.getX() - radius, centre.getY() - radius, radius * 2.0f, radius * 2.0f, 1.0f);
|
||||
|
||||
// Almost-vertical (-2°) shading overlay to give the dial a subtle 3D look.
|
||||
constexpr float degToRad = juce::MathConstants<float>::pi / 180.0f;
|
||||
const float tiltSin = std::sin (-2.0f * degToRad);
|
||||
const float tiltCos = std::cos (-2.0f * degToRad);
|
||||
|
||||
const auto topPoint = centre + juce::Point<float> ( radius * tiltSin, -radius * tiltCos);
|
||||
const auto bottomPoint = centre + juce::Point<float> (-radius * tiltSin, radius * tiltCos);
|
||||
|
||||
juce::ColourGradient shading (juce::Colours::white.withAlpha (0.07f), topPoint,
|
||||
juce::Colours::black.withAlpha (0.20f), bottomPoint, false);
|
||||
g.setGradientFill (shading);
|
||||
g.fillEllipse (centre.getX() - radius, centre.getY() - radius, radius * 2.0f, radius * 2.0f);
|
||||
|
||||
if (end - start < 6.0f)
|
||||
{
|
||||
juce::Path valueArc;
|
||||
|
|
@ -40,19 +66,19 @@ void EditorLookAndFeel::drawRotarySlider (juce::Graphics& g, int x, int y, int w
|
|||
radius * 0.85f, radius * 0.85f,
|
||||
0.0f, start, angle, true);
|
||||
|
||||
g.setColour (colours::accent);
|
||||
g.setGradientFill (orangeGradient (bounds));
|
||||
g.strokePath (valueArc, juce::PathStrokeType (2.5f));
|
||||
}
|
||||
|
||||
// JUCE measures rotary angles clockwise from 12 o'clock; the pointer is
|
||||
// drawn with the same convention so it matches the arc and mouse dragging.
|
||||
const float tickLen = radius * 0.62f;
|
||||
g.setColour (colours::accent);
|
||||
// The pointer reaches out to the value arc so it touches the arc.
|
||||
const float tickLen = radius * 0.85f + 1.0f;
|
||||
g.setGradientFill (orangeGradient (bounds));
|
||||
g.drawLine (centre.getX(), centre.getY(),
|
||||
centre.getX() + tickLen * std::sin (angle),
|
||||
centre.getY() - tickLen * std::cos (angle),
|
||||
2.5f);
|
||||
|
||||
g.setColour (colours::accent);
|
||||
g.fillEllipse (centre.getX() - 2.5f, centre.getY() - 2.5f, 5.0f, 5.0f);
|
||||
}
|
||||
|
||||
|
|
@ -61,7 +87,11 @@ void EditorLookAndFeel::drawToggleButton (juce::Graphics& g, juce::ToggleButton&
|
|||
{
|
||||
const auto bounds = button.getLocalBounds().toFloat();
|
||||
|
||||
g.setColour (button.getToggleState() ? colours::accent : colours::grid);
|
||||
if (button.getToggleState())
|
||||
g.setGradientFill (orangeGradient (bounds));
|
||||
else
|
||||
g.setColour (colours::grid);
|
||||
|
||||
g.fillRoundedRectangle (bounds, 4.0f);
|
||||
|
||||
g.setFont (font (11.0f));
|
||||
|
|
@ -75,7 +105,7 @@ void EditorLookAndFeel::drawButtonBackground (juce::Graphics& g, juce::Button& b
|
|||
const auto bounds = button.getLocalBounds().toFloat().reduced (0.5f);
|
||||
|
||||
if (button.getToggleState())
|
||||
g.setColour (colours::accent);
|
||||
g.setGradientFill (orangeGradient (bounds));
|
||||
else
|
||||
g.setColour (highlighted ? colours::gridLight : colours::grid);
|
||||
|
||||
|
|
@ -89,6 +119,81 @@ void EditorLookAndFeel::drawButtonText (juce::Graphics& g, juce::TextButton& but
|
|||
g.drawText (button.getButtonText(), button.getLocalBounds(), juce::Justification::centred);
|
||||
}
|
||||
|
||||
void EditorLookAndFeel::drawComboBox (juce::Graphics& g, int width, int height, bool,
|
||||
int, int, int, int, juce::ComboBox&)
|
||||
{
|
||||
const auto bounds = juce::Rectangle<float> (1.0f, 1.0f, (float) (width - 2), (float) (height - 2));
|
||||
|
||||
g.setColour (colours::grid);
|
||||
g.fillRoundedRectangle (bounds, 4.0f);
|
||||
g.setColour (colours::gridLight);
|
||||
g.drawRoundedRectangle (bounds, 4.0f, 1.0f);
|
||||
|
||||
const float cx = (float) (width - 15);
|
||||
const float cy = (float) height * 0.5f;
|
||||
|
||||
juce::Path arrow;
|
||||
arrow.addTriangle (cx - 5.0f, cy - 3.0f, cx + 5.0f, cy - 3.0f, cx, cy + 3.5f);
|
||||
g.setColour (colours::accent);
|
||||
g.fillPath (arrow);
|
||||
}
|
||||
|
||||
juce::Font EditorLookAndFeel::getComboBoxFont (juce::ComboBox&)
|
||||
{
|
||||
return font (11.0f);
|
||||
}
|
||||
|
||||
void EditorLookAndFeel::drawPopupMenuBackground (juce::Graphics& g, int width, int height)
|
||||
{
|
||||
g.setColour (colours::panel);
|
||||
g.fillRect (0, 0, width, height);
|
||||
g.setColour (colours::gridLight);
|
||||
g.drawRect (0.0f, 0.0f, (float) width, (float) height, 1.0f);
|
||||
}
|
||||
|
||||
void EditorLookAndFeel::drawPopupMenuItem (juce::Graphics& g, const juce::Rectangle<int>& area,
|
||||
bool isSeparator, bool isActive, bool isHighlighted,
|
||||
bool isTicked, bool hasSubMenu,
|
||||
const juce::String& text, const juce::String& shortcutKeyText,
|
||||
const juce::Drawable* icon, const juce::Colour* textColour)
|
||||
{
|
||||
(void) hasSubMenu;
|
||||
(void) icon;
|
||||
|
||||
if (isSeparator)
|
||||
{
|
||||
g.setColour (colours::grid);
|
||||
g.fillRect (area.reduced (10, 0).withY (area.getCentreY()).withHeight (1));
|
||||
return;
|
||||
}
|
||||
|
||||
g.setColour (isHighlighted ? colours::gridLight : colours::panel);
|
||||
g.fillRect (area);
|
||||
|
||||
if (isTicked)
|
||||
{
|
||||
g.setColour (colours::accent);
|
||||
g.fillEllipse ((float) (area.getRight() - 17), area.getCentreY() - 3.5f, 7.0f, 7.0f);
|
||||
}
|
||||
|
||||
g.setFont (font (11.0f));
|
||||
g.setColour (isActive ? (textColour != nullptr ? *textColour : colours::text)
|
||||
: colours::textDim);
|
||||
g.drawFittedText (text, area.withTrimmedLeft (12).withTrimmedRight (isTicked ? 28 : 10),
|
||||
juce::Justification::centredLeft, 1);
|
||||
|
||||
if (shortcutKeyText.isNotEmpty())
|
||||
{
|
||||
g.setColour (colours::textDim);
|
||||
g.drawFittedText (shortcutKeyText, area.withTrimmedRight (16), juce::Justification::centredRight, 1);
|
||||
}
|
||||
}
|
||||
|
||||
juce::Font EditorLookAndFeel::getPopupMenuFont()
|
||||
{
|
||||
return font (11.0f);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
Knob::Knob (const juce::String& title)
|
||||
{
|
||||
|
|
@ -124,10 +229,14 @@ Knob::Knob (const juce::String& title)
|
|||
void Knob::resized()
|
||||
{
|
||||
const int w = getWidth();
|
||||
const int h = getHeight();
|
||||
|
||||
titleLabel.setBounds (0, 0, w, 14);
|
||||
slider.setBounds (4, 15, w - 8, w - 8);
|
||||
valueLabel.setBounds (0, w - 8 + 15, w, 12);
|
||||
|
||||
const int valueH = 12;
|
||||
const int sliderH = juce::jmax (20, juce::jmin (w - 8, h - 15 - valueH));
|
||||
slider.setBounds (4, 15, w - 8, sliderH);
|
||||
valueLabel.setBounds (0, 15 + sliderH, w, valueH);
|
||||
}
|
||||
|
||||
void Knob::updateValueLabel()
|
||||
|
|
@ -234,7 +343,7 @@ void ChoiceBar::resized()
|
|||
}
|
||||
|
||||
//==============================================================================
|
||||
StepPanel::StepPanel (MonoStepAudioProcessor& processorRef)
|
||||
StepPanel::StepPanel (MonostepAudioProcessor& processorRef)
|
||||
: processor (processorRef),
|
||||
fineKnob ("Fine")
|
||||
{
|
||||
|
|
@ -284,6 +393,70 @@ StepPanel::StepPanel (MonoStepAudioProcessor& processorRef)
|
|||
clearButton.onClick = [this] { clearPattern(); };
|
||||
addAndMakeVisible (clearButton);
|
||||
|
||||
shiftLeftButton.setButtonText ("<<");
|
||||
shiftLeftButton.onClick = [this] { processor.shiftPattern (-1); };
|
||||
addAndMakeVisible (shiftLeftButton);
|
||||
|
||||
shiftRightButton.setButtonText (">>");
|
||||
shiftRightButton.onClick = [this] { processor.shiftPattern (1); };
|
||||
addAndMakeVisible (shiftRightButton);
|
||||
|
||||
toolsCaption.setText ("TOOLS", juce::dontSendNotification);
|
||||
toolsCaption.setJustificationType (juce::Justification::centredLeft);
|
||||
toolsCaption.setFont (font (9.0f));
|
||||
toolsCaption.setColour (juce::Label::textColourId, colours::textDim);
|
||||
addAndMakeVisible (toolsCaption);
|
||||
|
||||
presetCombo.setTextWhenNothingSelected ("Pick a preset");
|
||||
presetCombo.setColour (juce::ComboBox::textColourId, colours::text);
|
||||
presetCombo.setColour (juce::ComboBox::outlineColourId, colours::gridLight);
|
||||
presetCombo.setColour (juce::ComboBox::backgroundColourId, colours::grid);
|
||||
presetCombo.setColour (juce::ComboBox::arrowColourId, colours::accent);
|
||||
for (int i = 0; i < processor.getNumPresets(); ++i)
|
||||
presetCombo.addItem (processor.getPresetName (i), i + 1);
|
||||
|
||||
presetCombo.onChange = [this]
|
||||
{
|
||||
const int id = presetCombo.getSelectedId();
|
||||
if (id > 0)
|
||||
processor.applyPreset (id - 1);
|
||||
};
|
||||
|
||||
addAndMakeVisible (presetCombo);
|
||||
|
||||
randomizeButton.setButtonText ("Randomize");
|
||||
randomizeButton.onClick = [this]
|
||||
{
|
||||
if (randomizeToggles[RandomPattern].getToggleState())
|
||||
processor.randomizePattern();
|
||||
if (randomizeToggles[RandomOsc].getToggleState())
|
||||
processor.randomizeOsc();
|
||||
if (randomizeToggles[RandomFilter].getToggleState())
|
||||
processor.randomizeFilter();
|
||||
if (randomizeToggles[RandomEnv].getToggleState())
|
||||
processor.randomizeEnv();
|
||||
if (randomizeToggles[RandomSeq].getToggleState())
|
||||
processor.randomizeSeqSettings();
|
||||
if (randomizeToggles[RandomFx].getToggleState())
|
||||
processor.randomizeFx();
|
||||
};
|
||||
addAndMakeVisible (randomizeButton);
|
||||
|
||||
randomizeCaption.setText ("RANDOMIZE", juce::dontSendNotification);
|
||||
randomizeCaption.setJustificationType (juce::Justification::centredLeft);
|
||||
randomizeCaption.setFont (font (9.0f));
|
||||
randomizeCaption.setColour (juce::Label::textColourId, colours::textDim);
|
||||
addAndMakeVisible (randomizeCaption);
|
||||
|
||||
static const char* groupNames[] = { "Pattern", "Osc", "Filter", "Env", "Length", "FX" };
|
||||
|
||||
for (int i = 0; i < RandomGroupCount; ++i)
|
||||
{
|
||||
randomizeToggles[i].setButtonText (groupNames[i]);
|
||||
randomizeToggles[i].setToggleState (true, juce::dontSendNotification);
|
||||
addAndMakeVisible (randomizeToggles[i]);
|
||||
}
|
||||
|
||||
setSelectedStep (0);
|
||||
}
|
||||
|
||||
|
|
@ -328,6 +501,7 @@ void StepPanel::clearPattern()
|
|||
processor.setStepNote (i, 0);
|
||||
processor.setStepCents (i, 0.0f);
|
||||
processor.setStepSlide (i, false);
|
||||
processor.setStepAccent (i, false);
|
||||
}
|
||||
|
||||
refresh();
|
||||
|
|
@ -344,18 +518,37 @@ void StepPanel::resized()
|
|||
const int knobX = (w - 74) / 2;
|
||||
const int knobY = 68;
|
||||
|
||||
fineKnob.setBounds (knobX, knobY, 74, 74);
|
||||
fineKnob.setBounds (knobX, knobY, 74, 94);
|
||||
|
||||
const int btnY = knobY + 25;
|
||||
const int btnY = knobY + 102;
|
||||
prevButton.setBounds (knobX - 32, btnY, 24, 24);
|
||||
nextButton.setBounds (knobX + 74 + 8, btnY, 24, 24);
|
||||
|
||||
hintLabel.setBounds (8, knobY + 82, w - 16, 14);
|
||||
clearButton.setBounds (10, knobY + 106, w - 20, 26);
|
||||
hintLabel.setBounds (8, btnY + 30, w - 16, 14);
|
||||
|
||||
const int shiftW = 22;
|
||||
shiftLeftButton.setBounds (10, btnY + 50, shiftW, 26);
|
||||
clearButton.setBounds (10 + shiftW + 4, btnY + 50, w - 20 - shiftW * 2 - 8, 26);
|
||||
shiftRightButton.setBounds (10 + w - 20 - shiftW, btnY + 50, shiftW, 26);
|
||||
|
||||
toolsCaption.setBounds (10, btnY + 86, w - 20, 14);
|
||||
presetCombo.setBounds (10, btnY + 102, w - 20, 26);
|
||||
randomizeButton.setBounds (10, btnY + 134, w - 20, 26);
|
||||
randomizeCaption.setBounds (10, btnY + 166, w - 20, 12);
|
||||
|
||||
const int colW = (w - 20 - 4) / 2;
|
||||
const int rowH = 20;
|
||||
|
||||
for (int i = 0; i < RandomGroupCount; ++i)
|
||||
{
|
||||
const int col = i / 3;
|
||||
const int row = i % 3;
|
||||
randomizeToggles[i].setBounds (10 + col * (colW + 4), btnY + 180 + row * (rowH + 2), colW, rowH);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
MonoStepAudioProcessorEditor::MonoStepAudioProcessorEditor (MonoStepAudioProcessor& processor)
|
||||
MonostepAudioProcessorEditor::MonostepAudioProcessorEditor (MonostepAudioProcessor& processor)
|
||||
: AudioProcessorEditor (processor),
|
||||
processorRef (processor),
|
||||
content (*this),
|
||||
|
|
@ -365,8 +558,12 @@ MonoStepAudioProcessorEditor::MonoStepAudioProcessorEditor (MonoStepAudioProcess
|
|||
rootKnob ("Root"),
|
||||
gateKnob ("Gate"),
|
||||
masterKnob ("Master"),
|
||||
coarseKnob ("Coarse"),
|
||||
coarseKnob2 ("Coarse"),
|
||||
detuneKnob ("Detune"),
|
||||
mixKnob ("Mix"),
|
||||
fineKnob1 ("Fine"),
|
||||
fineKnob2 ("Fine"),
|
||||
cutoffKnob ("Cutoff"),
|
||||
resKnob ("Res"),
|
||||
glideKnob ("Glide"),
|
||||
|
|
@ -374,6 +571,9 @@ MonoStepAudioProcessorEditor::MonoStepAudioProcessorEditor (MonoStepAudioProcess
|
|||
decayKnob ("Decay"),
|
||||
sustainKnob ("Sustain"),
|
||||
releaseKnob ("Release"),
|
||||
driveKnob ("Drive"),
|
||||
ringKnob ("Ring"),
|
||||
accentKnob ("Accent"),
|
||||
rateBar (processor.getAPVTS(), "seqRate", { "1/4", "1/8", "1/8T", "1/16", "1/32" }),
|
||||
octaveBar (processor.getAPVTS(), "seqOctave", { "-2", "-1", "0", "+1", "+2" }),
|
||||
zoomBar ({ "100%", "125%", "150%", "175%", "200%" }, [] (int) {}),
|
||||
|
|
@ -414,6 +614,7 @@ MonoStepAudioProcessorEditor::MonoStepAudioProcessorEditor (MonoStepAudioProcess
|
|||
addKnob (rootKnob, "seqRoot", [] (float v) { return juce::String ((int) v); });
|
||||
addKnob (gateKnob, "seqGateLen", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; });
|
||||
addKnob (masterKnob, "master", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; });
|
||||
addKnob (coarseKnob, "osc2Coarse", [] (float v) { return juce::String (juce::roundToInt (v)) + " st"; });
|
||||
addKnob (detuneKnob, "detune", [] (float v) { return juce::String (juce::roundToInt (v)) + " ct"; });
|
||||
addKnob (mixKnob, "mix", [] (float v) { return juce::String (v, 2); });
|
||||
addKnob (cutoffKnob, "cutoff", [] (float v) { return v >= 1000.0f ? juce::String (juce::roundToInt (v / 100.0f) * 100) + " Hz" : juce::String (juce::roundToInt (v)) + " Hz"; });
|
||||
|
|
@ -423,6 +624,9 @@ MonoStepAudioProcessorEditor::MonoStepAudioProcessorEditor (MonoStepAudioProcess
|
|||
addKnob (decayKnob, "decay", [] (float v) { return juce::String (v, 2) + "s"; });
|
||||
addKnob (sustainKnob, "sustain", [] (float v) { return juce::String (v, 2); });
|
||||
addKnob (releaseKnob, "release", [] (float v) { return juce::String (v, 2) + "s"; });
|
||||
addKnob (driveKnob, "drive", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; });
|
||||
addKnob (ringKnob, "ringmod", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; });
|
||||
addKnob (accentKnob, "accent", [] (float v) { return juce::String (juce::roundToInt (v * 100.0f)) + "%"; });
|
||||
|
||||
content.addAndMakeVisible (matrix);
|
||||
content.addAndMakeVisible (stepPanel);
|
||||
|
|
@ -466,6 +670,12 @@ MonoStepAudioProcessorEditor::MonoStepAudioProcessorEditor (MonoStepAudioProcess
|
|||
stepPanel.refresh();
|
||||
};
|
||||
|
||||
matrix.onAccentChanged = [this] (int stepIdx, bool accent)
|
||||
{
|
||||
processorRef.setStepAccent (stepIdx, accent);
|
||||
stepPanel.refresh();
|
||||
};
|
||||
|
||||
processorRef.onPatternChanged = [this] { refreshEditor(); };
|
||||
|
||||
startTimerHz (30);
|
||||
|
|
@ -473,14 +683,14 @@ MonoStepAudioProcessorEditor::MonoStepAudioProcessorEditor (MonoStepAudioProcess
|
|||
refreshEditor();
|
||||
}
|
||||
|
||||
MonoStepAudioProcessorEditor::~MonoStepAudioProcessorEditor()
|
||||
MonostepAudioProcessorEditor::~MonostepAudioProcessorEditor()
|
||||
{
|
||||
stopTimer();
|
||||
processorRef.onPatternChanged = nullptr;
|
||||
setLookAndFeel (nullptr);
|
||||
}
|
||||
|
||||
void MonoStepAudioProcessorEditor::timerCallback()
|
||||
void MonostepAudioProcessorEditor::timerCallback()
|
||||
{
|
||||
const int step = processorRef.getCurrentStep();
|
||||
|
||||
|
|
@ -491,27 +701,27 @@ void MonoStepAudioProcessorEditor::timerCallback()
|
|||
}
|
||||
}
|
||||
|
||||
void MonoStepAudioProcessorEditor::refreshEditor()
|
||||
void MonostepAudioProcessorEditor::refreshEditor()
|
||||
{
|
||||
matrix.setSelectedStep (matrix.getSelectedStep());
|
||||
stepPanel.refresh();
|
||||
matrix.repaint();
|
||||
}
|
||||
|
||||
void MonoStepAudioProcessorEditor::paint (juce::Graphics& g)
|
||||
void MonostepAudioProcessorEditor::paint (juce::Graphics& g)
|
||||
{
|
||||
g.fillAll (colours::bg);
|
||||
}
|
||||
|
||||
void MonoStepAudioProcessorEditor::paintContent (juce::Graphics& g)
|
||||
void MonostepAudioProcessorEditor::paintContent (juce::Graphics& g)
|
||||
{
|
||||
g.setFont (font (22.0f));
|
||||
g.setColour (colours::text);
|
||||
g.drawText ("MonoStep", 16, 12, 130, 24, juce::Justification::centredLeft);
|
||||
g.drawText ("Monostep", 16, 12, 130, 24, juce::Justification::centredLeft);
|
||||
|
||||
g.setFont (font (9.0f));
|
||||
g.setColour (colours::textDim);
|
||||
g.drawText ("2 OSC - step sequencer", 16, 34, 130, 12, juce::Justification::centredLeft);
|
||||
g.drawText ("2 OSC Step-Sequencer Synth", 16, 34, 130, 12, juce::Justification::centredLeft);
|
||||
|
||||
const auto drawCaption = [&] (const juce::Component& component, const juce::String& text)
|
||||
{
|
||||
|
|
@ -538,14 +748,14 @@ void MonoStepAudioProcessorEditor::paintContent (juce::Graphics& g)
|
|||
g.drawText (buildinfo::footerText(), 16, footY + 4, baseWidth - 32, 12, juce::Justification::centredLeft);
|
||||
}
|
||||
|
||||
void MonoStepAudioProcessorEditor::setZoomFactor (float newZoom)
|
||||
void MonostepAudioProcessorEditor::setZoomFactor (float newZoom)
|
||||
{
|
||||
zoomFactor = newZoom;
|
||||
|
||||
setSize (juce::roundToInt (baseWidth * newZoom), juce::roundToInt (baseHeight * newZoom));
|
||||
}
|
||||
|
||||
void MonoStepAudioProcessorEditor::resized()
|
||||
void MonostepAudioProcessorEditor::resized()
|
||||
{
|
||||
const int targetW = juce::roundToInt (baseWidth * zoomFactor);
|
||||
const int targetH = juce::roundToInt (baseHeight * zoomFactor);
|
||||
|
|
@ -563,18 +773,19 @@ void MonoStepAudioProcessorEditor::resized()
|
|||
const int w = baseWidth;
|
||||
const int h = baseHeight;
|
||||
|
||||
const int topBarH = 58;
|
||||
const int topBarH = 78;
|
||||
const int bottomBarH = 96;
|
||||
const int pad = 12;
|
||||
const int topBarY = (topBarH - 26) / 2;
|
||||
|
||||
lengthKnob.setBounds (128, 2, 54, 56);
|
||||
rateBar.setBounds (192, 18, 210, 26);
|
||||
lengthKnob.setBounds (128, 2, 54, 74);
|
||||
rateBar.setBounds (192, topBarY, 210, 26);
|
||||
|
||||
swingKnob.setBounds (422, 2, 54, 56);
|
||||
rootKnob.setBounds (482, 2, 54, 56);
|
||||
octaveBar.setBounds (548, 18, 170, 26);
|
||||
zoomBar.setBounds (w - pad - 54 - 8 - 236, 18, 236, 26);
|
||||
masterKnob.setBounds (w - pad - 54, 2, 54, 56);
|
||||
swingKnob.setBounds (422, 2, 54, 74);
|
||||
rootKnob.setBounds (482, 2, 54, 74);
|
||||
octaveBar.setBounds (548, topBarY, 170, 26);
|
||||
zoomBar.setBounds (w - pad - 54 - 8 - 236, topBarY, 236, 26);
|
||||
masterKnob.setBounds (w - pad - 54, 2, 54, 74);
|
||||
|
||||
const int panelW = 200;
|
||||
const int midTop = topBarH;
|
||||
|
|
@ -592,7 +803,7 @@ void MonoStepAudioProcessorEditor::resized()
|
|||
wave2Bar.setBounds (x, bottomY + 30, barW, 24); x += barW + 16;
|
||||
|
||||
const int knobW = 56;
|
||||
const int knobGap = 72;
|
||||
const int knobGap = 60;
|
||||
const int knobY = bottomY + 2;
|
||||
|
||||
const auto placeKnob = [&] (Knob& knob)
|
||||
|
|
@ -601,6 +812,7 @@ void MonoStepAudioProcessorEditor::resized()
|
|||
x += knobGap;
|
||||
};
|
||||
|
||||
placeKnob (coarseKnob);
|
||||
placeKnob (detuneKnob);
|
||||
placeKnob (mixKnob);
|
||||
placeKnob (cutoffKnob);
|
||||
|
|
@ -610,4 +822,7 @@ void MonoStepAudioProcessorEditor::resized()
|
|||
placeKnob (decayKnob);
|
||||
placeKnob (sustainKnob);
|
||||
placeKnob (releaseKnob);
|
||||
placeKnob (driveKnob);
|
||||
placeKnob (ringKnob);
|
||||
placeKnob (accentKnob);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
#include <JuceHeader.h>
|
||||
#include "ui/SequencerMatrix.h"
|
||||
|
||||
class MonoStepAudioProcessor;
|
||||
class MonostepAudioProcessor;
|
||||
|
||||
class EditorLookAndFeel final : public juce::LookAndFeel_V4
|
||||
{
|
||||
|
|
@ -23,6 +23,17 @@ public:
|
|||
|
||||
void drawButtonText (juce::Graphics&, juce::TextButton&,
|
||||
bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown) override;
|
||||
|
||||
void drawComboBox (juce::Graphics&, int width, int height, bool isMouseButtonDown,
|
||||
int buttonX, int buttonY, int buttonW, int buttonH, juce::ComboBox&) override;
|
||||
juce::Font getComboBoxFont (juce::ComboBox&) override;
|
||||
|
||||
void drawPopupMenuBackground (juce::Graphics&, int width, int height) override;
|
||||
void drawPopupMenuItem (juce::Graphics&, const juce::Rectangle<int>& area,
|
||||
bool isSeparator, bool isActive, bool isHighlighted, bool isTicked, bool hasSubMenu,
|
||||
const juce::String& text, const juce::String& shortcutKeyText,
|
||||
const juce::Drawable* icon, const juce::Colour* textColour) override;
|
||||
juce::Font getPopupMenuFont() override;
|
||||
};
|
||||
|
||||
class Knob final : public juce::Component
|
||||
|
|
@ -93,7 +104,7 @@ private:
|
|||
class StepPanel final : public juce::Component
|
||||
{
|
||||
public:
|
||||
explicit StepPanel (MonoStepAudioProcessor& processor);
|
||||
explicit StepPanel (MonostepAudioProcessor& processor);
|
||||
|
||||
void setSelectedStep (int stepIdx);
|
||||
void refresh();
|
||||
|
|
@ -105,7 +116,7 @@ public:
|
|||
private:
|
||||
void clearPattern();
|
||||
|
||||
MonoStepAudioProcessor& processor;
|
||||
MonostepAudioProcessor& processor;
|
||||
|
||||
int selectedStep = 0;
|
||||
bool updating = false;
|
||||
|
|
@ -118,14 +129,34 @@ private:
|
|||
juce::TextButton nextButton;
|
||||
juce::Label hintLabel;
|
||||
juce::TextButton clearButton;
|
||||
juce::TextButton shiftLeftButton;
|
||||
juce::TextButton shiftRightButton;
|
||||
|
||||
juce::Label toolsCaption;
|
||||
juce::ComboBox presetCombo;
|
||||
juce::TextButton randomizeButton;
|
||||
juce::Label randomizeCaption;
|
||||
|
||||
enum RandomGroup
|
||||
{
|
||||
RandomPattern,
|
||||
RandomOsc,
|
||||
RandomFilter,
|
||||
RandomEnv,
|
||||
RandomSeq,
|
||||
RandomFx,
|
||||
RandomGroupCount
|
||||
};
|
||||
|
||||
juce::ToggleButton randomizeToggles[RandomGroupCount];
|
||||
};
|
||||
|
||||
class MonoStepAudioProcessorEditor final : public juce::AudioProcessorEditor,
|
||||
class MonostepAudioProcessorEditor final : public juce::AudioProcessorEditor,
|
||||
public juce::Timer
|
||||
{
|
||||
public:
|
||||
explicit MonoStepAudioProcessorEditor (MonoStepAudioProcessor& processor);
|
||||
~MonoStepAudioProcessorEditor() override;
|
||||
explicit MonostepAudioProcessorEditor (MonostepAudioProcessor& processor);
|
||||
~MonostepAudioProcessorEditor() override;
|
||||
|
||||
void paint (juce::Graphics&) override;
|
||||
void resized() override;
|
||||
|
|
@ -138,15 +169,15 @@ private:
|
|||
class ContentComponent final : public juce::Component
|
||||
{
|
||||
public:
|
||||
explicit ContentComponent (MonoStepAudioProcessorEditor& owner) : ownerRef (owner) {}
|
||||
explicit ContentComponent (MonostepAudioProcessorEditor& owner) : ownerRef (owner) {}
|
||||
|
||||
void paint (juce::Graphics& g) override { ownerRef.paintContent (g); }
|
||||
|
||||
private:
|
||||
MonoStepAudioProcessorEditor& ownerRef;
|
||||
MonostepAudioProcessorEditor& ownerRef;
|
||||
};
|
||||
|
||||
MonoStepAudioProcessor& processorRef;
|
||||
MonostepAudioProcessor& processorRef;
|
||||
|
||||
ContentComponent content;
|
||||
|
||||
|
|
@ -165,8 +196,12 @@ private:
|
|||
Knob rootKnob;
|
||||
Knob gateKnob;
|
||||
Knob masterKnob;
|
||||
Knob coarseKnob;
|
||||
Knob coarseKnob2;
|
||||
Knob detuneKnob;
|
||||
Knob mixKnob;
|
||||
Knob fineKnob1;
|
||||
Knob fineKnob2;
|
||||
Knob cutoffKnob;
|
||||
Knob resKnob;
|
||||
Knob glideKnob;
|
||||
|
|
@ -174,6 +209,9 @@ private:
|
|||
Knob decayKnob;
|
||||
Knob sustainKnob;
|
||||
Knob releaseKnob;
|
||||
Knob driveKnob;
|
||||
Knob ringKnob;
|
||||
Knob accentKnob;
|
||||
|
||||
ChoiceBar rateBar;
|
||||
ChoiceBar octaveBar;
|
||||
|
|
|
|||
|
|
@ -8,30 +8,35 @@ static const char* paramId (int index)
|
|||
{
|
||||
static const char* ids[] =
|
||||
{
|
||||
"osc1Wave", "osc2Wave", "detune", "mix",
|
||||
"osc1Wave", "osc2Wave", "osc1Coarse", "osc2Coarse", "detune", "mix",
|
||||
"cutoff", "res", "attack", "decay", "sustain", "release", "glide", "master",
|
||||
"seqLen", "seqRate", "seqRoot", "seqOctave", "seqSwing", "seqGateLen"
|
||||
"seqLen", "seqRate", "seqRoot", "seqOctave", "seqSwing", "seqGateLen",
|
||||
"drive", "ringmod", "accent", "fine1"
|
||||
};
|
||||
return ids[index];
|
||||
}
|
||||
|
||||
enum ParamIndex
|
||||
{
|
||||
pOsc1Wave, pOsc2Wave, pDetune, pMix,
|
||||
pOsc1Wave, pOsc2Wave, pOsc1Coarse, pOsc2Coarse, pDetune, pMix,
|
||||
pCutoff, pRes, pAttack, pDecay, pSustain, pRelease, pGlide, pMaster,
|
||||
pSeqLen, pSeqRate, pSeqRoot, pSeqOctave, pSeqSwing, pSeqGateLen,
|
||||
pDrive, pRingMod, pAccent, pFine1,
|
||||
numParams
|
||||
};
|
||||
|
||||
MonoStepAudioProcessor::MonoStepAudioProcessor()
|
||||
MonostepAudioProcessor::MonostepAudioProcessor()
|
||||
: AudioProcessor (BusesProperties()
|
||||
.withOutput ("Output", juce::AudioChannelSet::stereo(), true))
|
||||
, apvts (*this, nullptr, "PARAMS", createParameterLayout())
|
||||
{
|
||||
osc1WaveParam = apvts.getRawParameterValue (paramId (pOsc1Wave));
|
||||
osc2WaveParam = apvts.getRawParameterValue (paramId (pOsc2Wave));
|
||||
osc1CoarseParam = apvts.getRawParameterValue (paramId (pOsc1Coarse));
|
||||
osc2CoarseParam = apvts.getRawParameterValue (paramId (pOsc2Coarse));
|
||||
detuneParam = apvts.getRawParameterValue (paramId (pDetune));
|
||||
mixParam = apvts.getRawParameterValue (paramId (pMix));
|
||||
osc1PhaseParam = apvts.getRawParameterValue (paramId (pFine1));
|
||||
cutoffParam = apvts.getRawParameterValue (paramId (pCutoff));
|
||||
resParam = apvts.getRawParameterValue (paramId (pRes));
|
||||
attackParam = apvts.getRawParameterValue (paramId (pAttack));
|
||||
|
|
@ -46,16 +51,22 @@ MonoStepAudioProcessor::MonoStepAudioProcessor()
|
|||
seqOctaveParam = apvts.getRawParameterValue (paramId (pSeqOctave));
|
||||
seqSwingParam = apvts.getRawParameterValue (paramId (pSeqSwing));
|
||||
seqGateLenParam = apvts.getRawParameterValue (paramId (pSeqGateLen));
|
||||
driveParam = apvts.getRawParameterValue (paramId (pDrive));
|
||||
ringModParam = apvts.getRawParameterValue (paramId (pRingMod));
|
||||
accentParam = apvts.getRawParameterValue (paramId (pAccent));
|
||||
|
||||
setDefaultPattern();
|
||||
}
|
||||
|
||||
juce::AudioProcessorValueTreeState::ParameterLayout MonoStepAudioProcessor::createParameterLayout()
|
||||
juce::AudioProcessorValueTreeState::ParameterLayout MonostepAudioProcessor::createParameterLayout()
|
||||
{
|
||||
juce::AudioProcessorValueTreeState::ParameterLayout layout;
|
||||
|
||||
layout.add (std::make_unique<juce::AudioParameterInt> (paramId (pOsc1Wave), "Osc 1 Wave", 0, 3, 2));
|
||||
layout.add (std::make_unique<juce::AudioParameterInt> (paramId (pOsc2Wave), "Osc 2 Wave", 0, 3, 3));
|
||||
layout.add (std::make_unique<juce::AudioParameterInt> (paramId (pOsc1Wave), "Osc 1 Wave", 0, 7, 2));
|
||||
layout.add (std::make_unique<juce::AudioParameterInt> (paramId (pOsc2Wave), "Osc 2 Wave", 0, 7, 3));
|
||||
|
||||
layout.add (std::make_unique<juce::AudioParameterInt> (paramId (pOsc1Coarse), "Osc 1 Coarse", -12, 12, 0));
|
||||
layout.add (std::make_unique<juce::AudioParameterInt> (paramId (pOsc2Coarse), "Osc 2 Coarse", -12, 12, 0));
|
||||
|
||||
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pDetune),
|
||||
"Osc 2 Detune", juce::NormalisableRange<float> (-100.0f, 100.0f), 0.0f,
|
||||
|
|
@ -64,6 +75,10 @@ juce::AudioProcessorValueTreeState::ParameterLayout MonoStepAudioProcessor::crea
|
|||
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pMix),
|
||||
"Osc Mix", juce::NormalisableRange<float> (0.0f, 1.0f), 0.5f));
|
||||
|
||||
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pFine1),
|
||||
"Phase Offset", juce::NormalisableRange<float> (-1.0f, 1.0f), 0.0f,
|
||||
juce::AudioParameterFloatAttributes().withLabel ("±1")));
|
||||
|
||||
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pCutoff),
|
||||
"Filter Cutoff", juce::NormalisableRange<float> (40.0f, 16000.0f, 0.01f, 0.3f), 7000.0f,
|
||||
juce::AudioParameterFloatAttributes().withLabel ("Hz")));
|
||||
|
|
@ -109,10 +124,19 @@ juce::AudioProcessorValueTreeState::ParameterLayout MonoStepAudioProcessor::crea
|
|||
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pSeqGateLen),
|
||||
"Gate Length", juce::NormalisableRange<float> (0.05f, 1.0f), 0.8f));
|
||||
|
||||
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pDrive),
|
||||
"Distortion Drive", juce::NormalisableRange<float> (0.0f, 1.0f), 0.0f));
|
||||
|
||||
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pRingMod),
|
||||
"Ring Mod", juce::NormalisableRange<float> (0.0f, 1.0f), 0.0f));
|
||||
|
||||
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pAccent),
|
||||
"Accent", juce::NormalisableRange<float> (0.0f, 1.0f), 0.7f));
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
void MonoStepAudioProcessor::setDefaultPattern()
|
||||
void MonostepAudioProcessor::setDefaultPattern()
|
||||
{
|
||||
struct Def { int step; bool gate; int semitone; bool slide; };
|
||||
const Def defs[] =
|
||||
|
|
@ -136,9 +160,199 @@ void MonoStepAudioProcessor::setDefaultPattern()
|
|||
sequencer.step (d.step).semitone = d.semitone;
|
||||
sequencer.step (d.step).slide = d.slide;
|
||||
}
|
||||
|
||||
sequencer.step (0).accent = true;
|
||||
sequencer.step (4).accent = true;
|
||||
sequencer.step (8).accent = true;
|
||||
}
|
||||
|
||||
void MonoStepAudioProcessor::prepareToPlay (double sr, int samplesPerBlock)
|
||||
void MonostepAudioProcessor::applyParamValue (const juce::String& id, float value)
|
||||
{
|
||||
if (auto* param = apvts.getParameter (id))
|
||||
{
|
||||
param->setValueNotifyingHost (param->getNormalisableRange().convertTo0to1 (value));
|
||||
apvts.getParameterAsValue (id) = value;
|
||||
}
|
||||
}
|
||||
|
||||
int MonostepAudioProcessor::getNumPresets() const
|
||||
{
|
||||
return 18;
|
||||
}
|
||||
|
||||
const char* MonostepAudioProcessor::getPresetName (int index) const
|
||||
{
|
||||
static const char* names[] =
|
||||
{
|
||||
"Wet Tape",
|
||||
"Acid Wash",
|
||||
"Tin Can",
|
||||
"Bent Toy",
|
||||
"Neon Arcade",
|
||||
"Glitch Garden",
|
||||
"Underwater FM",
|
||||
"Robo Funk",
|
||||
"Lo-Fi Noir",
|
||||
"Spectral Sweep",
|
||||
"Broken Radio",
|
||||
"Plasma Organ",
|
||||
"Glass Bells",
|
||||
"Warm Pad",
|
||||
"Pulse Groove",
|
||||
"Steel Pluck",
|
||||
"Sub Rumble",
|
||||
"Tape Bounce"
|
||||
};
|
||||
|
||||
return names[juce::jlimit (0, getNumPresets() - 1, index)];
|
||||
}
|
||||
|
||||
void MonostepAudioProcessor::applyPreset (int index)
|
||||
{
|
||||
struct Preset
|
||||
{
|
||||
int osc1; int osc2; int coarse; float detune; float mix;
|
||||
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 drive; float ring; float accent;
|
||||
};
|
||||
|
||||
static const Preset presets[] =
|
||||
{
|
||||
// --- 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
|
||||
{ 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
|
||||
{ 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
|
||||
{ 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
|
||||
{ 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
|
||||
{ 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
|
||||
{ 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
|
||||
{ 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
|
||||
{ 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
|
||||
{ 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
|
||||
{ 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
|
||||
{ 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
|
||||
|
||||
// --- 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
|
||||
{ 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
|
||||
{ 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
|
||||
{ 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, 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
|
||||
{ 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
|
||||
};
|
||||
|
||||
const auto& pr = presets[juce::jlimit (0, getNumPresets() - 1, index)];
|
||||
|
||||
applyParamValue ("osc1Wave", (float) pr.osc1);
|
||||
applyParamValue ("osc2Wave", (float) pr.osc2);
|
||||
applyParamValue ("osc2Coarse", (float) pr.coarse);
|
||||
applyParamValue ("detune", pr.detune);
|
||||
applyParamValue ("mix", pr.mix);
|
||||
applyParamValue ("cutoff", pr.cutoff);
|
||||
applyParamValue ("res", pr.res);
|
||||
applyParamValue ("attack", pr.attack);
|
||||
applyParamValue ("decay", pr.decay);
|
||||
applyParamValue ("sustain", pr.sustain);
|
||||
applyParamValue ("release", pr.release);
|
||||
applyParamValue ("glide", pr.glide);
|
||||
applyParamValue ("seqGateLen", pr.gateLen);
|
||||
applyParamValue ("seqSwing", pr.swing);
|
||||
applyParamValue ("seqRate", (float) pr.rate);
|
||||
applyParamValue ("seqRoot", (float) pr.root);
|
||||
applyParamValue ("seqOctave", (float) pr.octave);
|
||||
applyParamValue ("drive", pr.drive);
|
||||
applyParamValue ("ringmod", pr.ring);
|
||||
applyParamValue ("accent", pr.accent);
|
||||
}
|
||||
|
||||
void MonostepAudioProcessor::randomizePattern()
|
||||
{
|
||||
const int length = juce::roundToInt (seqLenParam->load());
|
||||
int filled = 0;
|
||||
|
||||
for (int i = 0; i < monostep::numSteps; ++i)
|
||||
{
|
||||
auto& s = sequencer.step (i);
|
||||
|
||||
if (i >= length)
|
||||
{
|
||||
s.gate = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
const float chance = (i == 0) ? 0.9f : 0.45f;
|
||||
s.gate = random.nextFloat() < chance;
|
||||
|
||||
if (s.gate)
|
||||
{
|
||||
++filled;
|
||||
s.semitone = random.nextInt (monostep::maxSemitone - monostep::minSemitone + 1) + monostep::minSemitone;
|
||||
s.cents = random.nextFloat() * 100.0f - 50.0f;
|
||||
s.slide = random.nextFloat() < 0.25f;
|
||||
s.accent = random.nextFloat() < 0.3f;
|
||||
}
|
||||
}
|
||||
|
||||
if (filled == 0)
|
||||
{
|
||||
auto& s = sequencer.step (random.nextInt (length));
|
||||
s.gate = true;
|
||||
s.semitone = 0;
|
||||
}
|
||||
|
||||
if (onPatternChanged)
|
||||
onPatternChanged();
|
||||
}
|
||||
|
||||
void MonostepAudioProcessor::randomizeOsc()
|
||||
{
|
||||
applyParamValue ("osc1Wave", (float) random.nextInt (4));
|
||||
applyParamValue ("osc2Wave", (float) random.nextInt (4));
|
||||
applyParamValue ("osc2Coarse", (float) (random.nextInt (25) - 12));
|
||||
applyParamValue ("detune", (float) (random.nextInt (101) - 50));
|
||||
applyParamValue ("mix", 0.2f + random.nextFloat() * 0.6f);
|
||||
}
|
||||
|
||||
void MonostepAudioProcessor::randomizeFilter()
|
||||
{
|
||||
applyParamValue ("cutoff", 200.0f * std::pow (2.0f, random.nextFloat() * 5.0f));
|
||||
applyParamValue ("res", random.nextFloat() * 0.8f);
|
||||
}
|
||||
|
||||
void MonostepAudioProcessor::randomizeEnv()
|
||||
{
|
||||
applyParamValue ("attack", 0.001f * std::pow (500.0f, random.nextFloat()));
|
||||
applyParamValue ("decay", 0.05f * std::pow (30.0f, random.nextFloat()));
|
||||
applyParamValue ("sustain", random.nextFloat());
|
||||
applyParamValue ("release", 0.05f * std::pow (40.0f, random.nextFloat()));
|
||||
applyParamValue ("glide", random.nextFloat() * 0.5f);
|
||||
}
|
||||
|
||||
void MonostepAudioProcessor::randomizeSeqSettings()
|
||||
{
|
||||
applyParamValue ("seqGateLen", 0.3f + random.nextFloat() * 0.7f);
|
||||
applyParamValue ("seqSwing", random.nextFloat() * 0.4f);
|
||||
applyParamValue ("seqRate", (float) random.nextInt (5));
|
||||
applyParamValue ("seqLen", (float) (random.nextInt (13) + 4));
|
||||
}
|
||||
|
||||
void MonostepAudioProcessor::randomizeFx()
|
||||
{
|
||||
applyParamValue ("drive", random.nextFloat());
|
||||
applyParamValue ("ringmod", random.nextFloat());
|
||||
}
|
||||
|
||||
void MonostepAudioProcessor::randomizeParams()
|
||||
{
|
||||
randomizeOsc();
|
||||
randomizeFilter();
|
||||
randomizeEnv();
|
||||
randomizeSeqSettings();
|
||||
randomizeFx();
|
||||
}
|
||||
|
||||
void MonostepAudioProcessor::prepareToPlay (double sr, int samplesPerBlock)
|
||||
{
|
||||
sampleRate = sr;
|
||||
voice.prepare (sr);
|
||||
|
|
@ -151,17 +365,18 @@ void MonoStepAudioProcessor::prepareToPlay (double sr, int samplesPerBlock)
|
|||
lastHostPpq = -1.0;
|
||||
}
|
||||
|
||||
void MonoStepAudioProcessor::releaseResources()
|
||||
void MonostepAudioProcessor::releaseResources()
|
||||
{
|
||||
voice.reset();
|
||||
}
|
||||
|
||||
void MonoStepAudioProcessor::updateVoiceParams()
|
||||
void MonostepAudioProcessor::updateVoiceParams()
|
||||
{
|
||||
monostep::SynthVoice::Params p;
|
||||
p.waveA = (monostep::Waveform) (int) osc1WaveParam->load();
|
||||
p.waveB = (monostep::Waveform) (int) osc2WaveParam->load();
|
||||
p.detuneRatio = std::pow (2.0f, detuneParam->load() / 1200.0f);
|
||||
p.detuneRatio = std::pow (2.0f, osc2CoarseParam->load() / 12.0f)
|
||||
* std::pow (2.0f, detuneParam->load() / 1200.0f);
|
||||
p.mix = mixParam->load();
|
||||
p.cutoff = cutoffParam->load();
|
||||
p.resonance = resParam->load();
|
||||
|
|
@ -171,15 +386,17 @@ void MonoStepAudioProcessor::updateVoiceParams()
|
|||
p.release = releaseParam->load();
|
||||
p.glide = glideParam->load();
|
||||
p.master = masterParam->load();
|
||||
p.drive = driveParam->load();
|
||||
p.ringMod = ringModParam->load();
|
||||
voice.setParams (p);
|
||||
}
|
||||
|
||||
float MonoStepAudioProcessor::midiToFreq (float note) const
|
||||
float MonostepAudioProcessor::midiToFreq (float note) const
|
||||
{
|
||||
return 440.0f * std::pow (2.0f, (note - 69.0f) / 12.0f);
|
||||
}
|
||||
|
||||
void MonoStepAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
|
||||
void MonostepAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
|
||||
{
|
||||
juce::ScopedNoDenormals noDenormals;
|
||||
|
||||
|
|
@ -265,8 +482,12 @@ void MonoStepAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juc
|
|||
}
|
||||
else
|
||||
{
|
||||
if (lastGateApplied)
|
||||
voice.noteOff();
|
||||
|
||||
lastStep = -1;
|
||||
lastGateApplied = false;
|
||||
lastFreqApplied = -1.0f;
|
||||
playStep.store (-1);
|
||||
}
|
||||
|
||||
|
|
@ -277,6 +498,7 @@ void MonoStepAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juc
|
|||
bool seqGate = false;
|
||||
float seqFreq = 0.0f;
|
||||
bool seqSlide = false;
|
||||
bool seqAccent = false;
|
||||
|
||||
if (midiHeld)
|
||||
{
|
||||
|
|
@ -307,10 +529,21 @@ void MonoStepAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juc
|
|||
|
||||
const auto& s = sequencer.step (stepIdx);
|
||||
seqSlide = s.slide;
|
||||
seqAccent = s.accent;
|
||||
|
||||
// A step whose successor slides keeps its gate open until the next
|
||||
// step so the glide into it is seamless; otherwise the gate closes
|
||||
// after gateLen of the step.
|
||||
const int nextIdx = (stepIdx + 1) % seqLen;
|
||||
const bool slideIntoNext = sequencer.step (nextIdx).slide;
|
||||
const float nextStart = (stepIdx + 1) * stepLen
|
||||
+ (((stepIdx + 1) % 2) ? swing * stepLen : 0.0f);
|
||||
|
||||
// gate length: the step's note sustains for gateLen of the step
|
||||
const float ppqWithinStep = ppqInCycle - stepStart;
|
||||
seqGate = s.gate && (ppqWithinStep < stepLen * gateLen);
|
||||
const float gateClose = slideIntoNext ? (nextStart - stepStart)
|
||||
: stepLen * gateLen;
|
||||
|
||||
seqGate = s.gate && (ppqWithinStep < gateClose);
|
||||
|
||||
if (seqGate)
|
||||
{
|
||||
|
|
@ -332,6 +565,10 @@ void MonoStepAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juc
|
|||
wantLegato = seqSlide;
|
||||
}
|
||||
|
||||
// Accent tracks the current step (not just gate changes), so it holds for
|
||||
// the whole step even when the note repeats without retriggering.
|
||||
voice.setAccentLevel (seqAccent ? 1.0f + accentParam->load() * 4.0f : 1.0f);
|
||||
|
||||
if (wantGate != lastGateApplied)
|
||||
{
|
||||
if (wantGate)
|
||||
|
|
@ -357,7 +594,7 @@ void MonoStepAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juc
|
|||
buffer.addFrom (ch, 0, scratch, 0, 0, numSamples);
|
||||
}
|
||||
|
||||
void MonoStepAudioProcessor::setStepGate (int idx, bool gate)
|
||||
void MonostepAudioProcessor::setStepGate (int idx, bool gate)
|
||||
{
|
||||
if (idx < 0 || idx >= monostep::numSteps)
|
||||
return;
|
||||
|
|
@ -368,7 +605,7 @@ void MonoStepAudioProcessor::setStepGate (int idx, bool gate)
|
|||
onPatternChanged();
|
||||
}
|
||||
|
||||
void MonoStepAudioProcessor::setStepNote (int idx, int semitone)
|
||||
void MonostepAudioProcessor::setStepNote (int idx, int semitone)
|
||||
{
|
||||
if (idx < 0 || idx >= monostep::numSteps)
|
||||
return;
|
||||
|
|
@ -379,7 +616,7 @@ void MonoStepAudioProcessor::setStepNote (int idx, int semitone)
|
|||
onPatternChanged();
|
||||
}
|
||||
|
||||
void MonoStepAudioProcessor::setStepCents (int idx, float cents)
|
||||
void MonostepAudioProcessor::setStepCents (int idx, float cents)
|
||||
{
|
||||
if (idx < 0 || idx >= monostep::numSteps)
|
||||
return;
|
||||
|
|
@ -390,7 +627,7 @@ void MonoStepAudioProcessor::setStepCents (int idx, float cents)
|
|||
onPatternChanged();
|
||||
}
|
||||
|
||||
void MonoStepAudioProcessor::setStepSlide (int idx, bool slide)
|
||||
void MonostepAudioProcessor::setStepSlide (int idx, bool slide)
|
||||
{
|
||||
if (idx < 0 || idx >= monostep::numSteps)
|
||||
return;
|
||||
|
|
@ -401,7 +638,26 @@ void MonoStepAudioProcessor::setStepSlide (int idx, bool slide)
|
|||
onPatternChanged();
|
||||
}
|
||||
|
||||
juce::String MonoStepAudioProcessor::getStepNoteName (int idx) const
|
||||
void MonostepAudioProcessor::setStepAccent (int idx, bool accent)
|
||||
{
|
||||
if (idx < 0 || idx >= monostep::numSteps)
|
||||
return;
|
||||
|
||||
sequencer.step (idx).accent = accent;
|
||||
|
||||
if (onPatternChanged)
|
||||
onPatternChanged();
|
||||
}
|
||||
|
||||
void MonostepAudioProcessor::shiftPattern (int dir)
|
||||
{
|
||||
sequencer.shift (dir);
|
||||
|
||||
if (onPatternChanged)
|
||||
onPatternChanged();
|
||||
}
|
||||
|
||||
juce::String MonostepAudioProcessor::getStepNoteName (int idx) const
|
||||
{
|
||||
const int root = juce::roundToInt (seqRootParam->load());
|
||||
const int octave = juce::roundToInt (seqOctaveParam->load()) - 2;
|
||||
|
|
@ -414,12 +670,13 @@ juce::String MonoStepAudioProcessor::getStepNoteName (int idx) const
|
|||
return juce::MidiMessage::getMidiNoteName (note, true, true, 3);
|
||||
}
|
||||
|
||||
void MonoStepAudioProcessor::storeSequence (juce::ValueTree& seq) const
|
||||
void MonostepAudioProcessor::storeSequence (juce::ValueTree& seq) const
|
||||
{
|
||||
juce::String gates;
|
||||
juce::String notes;
|
||||
juce::String cents;
|
||||
juce::String slides;
|
||||
juce::String accents;
|
||||
|
||||
for (int i = 0; i < monostep::numSteps; ++i)
|
||||
{
|
||||
|
|
@ -429,15 +686,17 @@ void MonoStepAudioProcessor::storeSequence (juce::ValueTree& seq) const
|
|||
notes += juce::String (s.semitone) + ",";
|
||||
cents += juce::String (s.cents, 2) + ",";
|
||||
slides += s.slide ? "1" : "0";
|
||||
accents += s.accent ? "1" : "0";
|
||||
}
|
||||
|
||||
seq.setProperty ("gates", gates, nullptr);
|
||||
seq.setProperty ("notes", notes.dropLastCharacters (1), nullptr);
|
||||
seq.setProperty ("cents", cents.dropLastCharacters (1), nullptr);
|
||||
seq.setProperty ("slides", slides, nullptr);
|
||||
seq.setProperty ("accents", accents, nullptr);
|
||||
}
|
||||
|
||||
void MonoStepAudioProcessor::loadSequence (const juce::ValueTree& seq)
|
||||
void MonostepAudioProcessor::loadSequence (const juce::ValueTree& seq)
|
||||
{
|
||||
const auto splitFloats = [] (const juce::String& text)
|
||||
{
|
||||
|
|
@ -455,6 +714,7 @@ void MonoStepAudioProcessor::loadSequence (const juce::ValueTree& seq)
|
|||
const auto notes = splitFloats (seq.getProperty ("notes").toString());
|
||||
const auto cents = splitFloats (seq.getProperty ("cents").toString());
|
||||
const auto slides = seq.getProperty ("slides").toString();
|
||||
const auto accents = seq.getProperty ("accents").toString();
|
||||
|
||||
for (int i = 0; i < monostep::numSteps; ++i)
|
||||
{
|
||||
|
|
@ -471,12 +731,15 @@ void MonoStepAudioProcessor::loadSequence (const juce::ValueTree& seq)
|
|||
|
||||
if (i < slides.length())
|
||||
s.slide = slides[i] == '1';
|
||||
|
||||
if (i < accents.length())
|
||||
s.accent = accents[i] == '1';
|
||||
}
|
||||
}
|
||||
|
||||
void MonoStepAudioProcessor::getStateInformation (juce::MemoryBlock& destData)
|
||||
void MonostepAudioProcessor::getStateInformation (juce::MemoryBlock& destData)
|
||||
{
|
||||
juce::ValueTree state = apvts.state.createCopy();
|
||||
juce::ValueTree state = apvts.copyState();
|
||||
juce::ValueTree seq = state.getOrCreateChildWithName ("SEQ", nullptr);
|
||||
storeSequence (seq);
|
||||
|
||||
|
|
@ -484,7 +747,7 @@ void MonoStepAudioProcessor::getStateInformation (juce::MemoryBlock& destData)
|
|||
copyXmlToBinary (*xml, destData);
|
||||
}
|
||||
|
||||
void MonoStepAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
|
||||
void MonostepAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
|
||||
{
|
||||
std::unique_ptr<juce::XmlElement> xml (juce::AudioProcessor::getXmlFromBinary (data, sizeInBytes));
|
||||
|
||||
|
|
@ -501,10 +764,10 @@ void MonoStepAudioProcessor::setStateInformation (const void* data, int sizeInBy
|
|||
onPatternChanged();
|
||||
}
|
||||
|
||||
juce::AudioProcessorEditor* MonoStepAudioProcessor::createEditor()
|
||||
juce::AudioProcessorEditor* MonostepAudioProcessor::createEditor()
|
||||
{
|
||||
#ifndef MONOSTEP_HEADLESS
|
||||
return new MonoStepAudioProcessorEditor (*this);
|
||||
return new MonostepAudioProcessorEditor (*this);
|
||||
#else
|
||||
return nullptr;
|
||||
#endif
|
||||
|
|
@ -512,5 +775,5 @@ juce::AudioProcessorEditor* MonoStepAudioProcessor::createEditor()
|
|||
|
||||
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
|
||||
{
|
||||
return new MonoStepAudioProcessor();
|
||||
return new MonostepAudioProcessor();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@
|
|||
#include "dsp/SynthVoice.h"
|
||||
#include "dsp/StepSequencer.h"
|
||||
|
||||
class MonoStepAudioProcessor final : public juce::AudioProcessor
|
||||
class MonostepAudioProcessor final : public juce::AudioProcessor
|
||||
{
|
||||
public:
|
||||
MonoStepAudioProcessor();
|
||||
~MonoStepAudioProcessor() override = default;
|
||||
MonostepAudioProcessor();
|
||||
~MonostepAudioProcessor() override = default;
|
||||
|
||||
void prepareToPlay (double sampleRate, int samplesPerBlock) override;
|
||||
void releaseResources() override;
|
||||
|
|
@ -17,7 +17,7 @@ public:
|
|||
juce::AudioProcessorEditor* createEditor() override;
|
||||
bool hasEditor() const override { return true; }
|
||||
|
||||
const juce::String getName() const override { return "MonoStep"; }
|
||||
const juce::String getName() const override { return "Monostep"; }
|
||||
bool acceptsMidi() const override { return true; }
|
||||
bool producesMidi() const override { return false; }
|
||||
double getTailLengthSeconds() const override { return 0.0; }
|
||||
|
|
@ -43,6 +43,19 @@ public:
|
|||
void setStepNote (int idx, int semitone);
|
||||
void setStepCents (int idx, float cents);
|
||||
void setStepSlide (int idx, bool slide);
|
||||
void setStepAccent (int idx, bool accent);
|
||||
void shiftPattern (int dir);
|
||||
|
||||
int getNumPresets() const;
|
||||
const char* getPresetName (int index) const;
|
||||
void applyPreset (int index);
|
||||
void randomizePattern();
|
||||
void randomizeParams();
|
||||
void randomizeOsc();
|
||||
void randomizeFilter();
|
||||
void randomizeEnv();
|
||||
void randomizeSeqSettings();
|
||||
void randomizeFx();
|
||||
|
||||
juce::String getStepNoteName (int idx) const;
|
||||
|
||||
|
|
@ -52,6 +65,7 @@ private:
|
|||
static juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout();
|
||||
|
||||
void updateVoiceParams();
|
||||
void applyParamValue (const juce::String& id, float value);
|
||||
float midiToFreq (float note) const;
|
||||
void storeSequence (juce::ValueTree& seq) const;
|
||||
void loadSequence (const juce::ValueTree& seq);
|
||||
|
|
@ -61,8 +75,11 @@ private:
|
|||
|
||||
std::atomic<float>* osc1WaveParam = nullptr;
|
||||
std::atomic<float>* osc2WaveParam = nullptr;
|
||||
std::atomic<float>* osc1CoarseParam = nullptr;
|
||||
std::atomic<float>* osc2CoarseParam = nullptr;
|
||||
std::atomic<float>* detuneParam = nullptr;
|
||||
std::atomic<float>* mixParam = nullptr;
|
||||
std::atomic<float>* osc1PhaseParam = nullptr;
|
||||
std::atomic<float>* cutoffParam = nullptr;
|
||||
std::atomic<float>* resParam = nullptr;
|
||||
std::atomic<float>* attackParam = nullptr;
|
||||
|
|
@ -75,12 +92,16 @@ private:
|
|||
std::atomic<float>* seqRateParam = nullptr;
|
||||
std::atomic<float>* seqRootParam = nullptr;
|
||||
std::atomic<float>* seqOctaveParam = nullptr;
|
||||
std::atomic<float>* seqSwingParam = nullptr;
|
||||
std::atomic<float>* seqSwingParam = nullptr;
|
||||
std::atomic<float>* seqGateLenParam = nullptr;
|
||||
std::atomic<float>* driveParam = nullptr;
|
||||
std::atomic<float>* ringModParam = nullptr;
|
||||
std::atomic<float>* accentParam = nullptr;
|
||||
|
||||
monostep::StepSequencer sequencer;
|
||||
monostep::SynthVoice voice;
|
||||
juce::AudioBuffer<float> scratch;
|
||||
juce::Random random;
|
||||
|
||||
double sampleRate = 44100.0;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ namespace colours
|
|||
inline const juce::Colour text (0xFFC9CDD4);
|
||||
inline const juce::Colour textDim (0xFF6E7683);
|
||||
inline const juce::Colour accent (0xFFE8A33D);
|
||||
inline const juce::Colour accent2 (0xFF7FA6FF);
|
||||
inline const juce::Colour accent2 (0xFFE8A33D);
|
||||
inline const juce::Colour accentDim (0xFF7A5A26);
|
||||
inline const juce::Colour activeCell (0xFF3A6EA5);
|
||||
inline const juce::Colour selected (0xFFF0F4F8);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ struct Step
|
|||
int semitone = 0; // -12 .. +12
|
||||
float cents = 0.0f; // -50 .. +50
|
||||
bool slide = false;
|
||||
bool accent = false;
|
||||
};
|
||||
|
||||
class StepSequencer
|
||||
|
|
@ -31,6 +32,20 @@ public:
|
|||
s = Step{};
|
||||
}
|
||||
|
||||
void shift (int dir)
|
||||
{
|
||||
if (dir == 0)
|
||||
return;
|
||||
|
||||
std::array<Step, numSteps> rotated = steps;
|
||||
|
||||
for (int i = 0; i < numSteps; ++i)
|
||||
{
|
||||
const int from = (i - dir + numSteps) % numSteps;
|
||||
steps[i] = rotated[from];
|
||||
}
|
||||
}
|
||||
|
||||
int stepsPerBeatFromRate (int rateIndex) const
|
||||
{
|
||||
// 0: 1/4, 1: 1/8, 2: 1/8T, 3: 1/16, 4: 1/32
|
||||
|
|
|
|||
|
|
@ -13,8 +13,12 @@ public:
|
|||
{
|
||||
Waveform waveA = Waveform::saw;
|
||||
Waveform waveB = Waveform::square;
|
||||
float detuneRatio = 1.0f;
|
||||
float mix = 0.5f;
|
||||
float osc1Coarse = 0.0f;
|
||||
float osc2Coarse = 0.0f;
|
||||
float osc1Fine = 0.0f;
|
||||
float osc2Fine = 0.0f;
|
||||
float osc1Phase = 0.0f;
|
||||
float osc2Phase = 0.0f;
|
||||
float cutoff = 7000.0f;
|
||||
float resonance = 0.15f;
|
||||
float attack = 0.005f;
|
||||
|
|
@ -23,11 +27,22 @@ public:
|
|||
float release = 0.4f;
|
||||
float glide = 0.12f;
|
||||
float master = 0.9f;
|
||||
float drive = 0.0f;
|
||||
float ringMod = 0.0f;
|
||||
float detuneRatio = 1.0f;
|
||||
float mix = 0.5f;
|
||||
};
|
||||
|
||||
void addPhase (float phaseOffset)
|
||||
{
|
||||
phaseA = frac (phaseA + phaseOffset);
|
||||
phaseB = frac (phaseB + phaseOffset);
|
||||
}
|
||||
|
||||
void prepare (double sr)
|
||||
{
|
||||
sampleRate = (float) sr;
|
||||
accentSmoothCoef = 1.0f - std::exp (-1.0f / (0.003f * sampleRate));
|
||||
reset();
|
||||
}
|
||||
|
||||
|
|
@ -40,12 +55,16 @@ public:
|
|||
env = 0.0f;
|
||||
envStage = Stage::idle;
|
||||
gate = false;
|
||||
accentLevel = 1.0f;
|
||||
accentSmooth = 1.0f;
|
||||
}
|
||||
|
||||
void setParams (const Params& p) { params = p; }
|
||||
|
||||
bool isActive() const { return gate; }
|
||||
|
||||
void setAccentLevel (float level) { accentLevel = level; }
|
||||
|
||||
void noteOn (float freqHz, bool legato)
|
||||
{
|
||||
targetFreq = freqHz;
|
||||
|
|
@ -153,6 +172,9 @@ private:
|
|||
updateEnvelope();
|
||||
updateGlide();
|
||||
|
||||
// Smooth the accent gain so it ramps in/out instead of clicking.
|
||||
accentSmooth += (accentLevel - accentSmooth) * accentSmoothCoef;
|
||||
|
||||
const float incA = currentFreq / sampleRate;
|
||||
const float incB = (currentFreq * params.detuneRatio) / sampleRate;
|
||||
|
||||
|
|
@ -164,9 +186,24 @@ private:
|
|||
|
||||
float out = (1.0f - params.mix) * a + params.mix * b;
|
||||
|
||||
if (params.ringMod > 0.0f)
|
||||
out = (1.0f - params.ringMod) * out + params.ringMod * (a * b);
|
||||
|
||||
const float preFilter = out;
|
||||
out = processFilter (out);
|
||||
|
||||
return out * env * params.master;
|
||||
// Accented steps also get a touch of the unfiltered signal so they cut
|
||||
// through the mix (presence), not just a volume bump.
|
||||
if (accentSmooth > 1.0f)
|
||||
out += (accentSmooth - 1.0f) * 0.2f * preFilter;
|
||||
|
||||
if (params.drive > 0.0f)
|
||||
{
|
||||
const float wet = std::tanh (out * (1.0f + params.drive * 9.0f));
|
||||
out = (1.0f - params.drive) * out + params.drive * wet;
|
||||
}
|
||||
|
||||
return out * env * params.master * accentSmooth;
|
||||
}
|
||||
|
||||
float sampleRate = 44100.0f;
|
||||
|
|
@ -184,6 +221,9 @@ private:
|
|||
float env = 0.0f;
|
||||
Stage envStage = Stage::idle;
|
||||
bool gate = false;
|
||||
float accentLevel = 1.0f;
|
||||
float accentSmooth = 1.0f;
|
||||
float accentSmoothCoef = 0.01f;
|
||||
};
|
||||
|
||||
} // namespace monostep
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <random>
|
||||
|
||||
namespace monostep
|
||||
{
|
||||
|
|
@ -11,7 +12,11 @@ enum class Waveform : int
|
|||
sine = 0,
|
||||
triangle = 1,
|
||||
saw = 2,
|
||||
square = 3
|
||||
square = 3,
|
||||
pulse = 4,
|
||||
noise = 5,
|
||||
sub = 6,
|
||||
pluck = 7
|
||||
};
|
||||
|
||||
inline float frac (float x)
|
||||
|
|
@ -63,6 +68,31 @@ inline float renderWave (Waveform w, float phase, float inc)
|
|||
v -= polyBlep (frac (f + 0.5f), inc);
|
||||
return v;
|
||||
}
|
||||
|
||||
case Waveform::pulse:
|
||||
{
|
||||
const float f = frac (phase);
|
||||
return (f < 0.3f) ? 1.0f : -1.0f;
|
||||
}
|
||||
|
||||
case Waveform::sub:
|
||||
{
|
||||
const float f = frac (phase * 0.25f);
|
||||
return 4.0f * std::min (f, 1.0f - f) - 1.0f;
|
||||
}
|
||||
|
||||
case Waveform::noise:
|
||||
{
|
||||
thread_local static std::mt19937 rng { std::random_device {}() };
|
||||
thread_local static std::uniform_real_distribution<float> dist (-1.0f, 1.0f);
|
||||
return dist (rng);
|
||||
}
|
||||
|
||||
case Waveform::pluck:
|
||||
{
|
||||
const float f = frac (phase);
|
||||
return 2.0f * std::sin (f * 6.283185307179586f * 2.0f) * std::exp (-10.0f * f);
|
||||
}
|
||||
}
|
||||
|
||||
return 0.0f;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
using namespace monostep;
|
||||
|
||||
SequencerMatrix::SequencerMatrix (MonoStepAudioProcessor& processorRef)
|
||||
SequencerMatrix::SequencerMatrix (MonostepAudioProcessor& processorRef)
|
||||
: processor (processorRef)
|
||||
{
|
||||
}
|
||||
|
|
@ -22,13 +22,19 @@ int SequencerMatrix::semitoneForRow (int row) const
|
|||
|
||||
juce::Rectangle<int> SequencerMatrix::matrixBounds() const
|
||||
{
|
||||
const int w = getWidth();
|
||||
const int h = getHeight();
|
||||
|
||||
if (w > labelW && h > headerH)
|
||||
{
|
||||
cellW = (float) (w - labelW) / (float) numSteps;
|
||||
cellH = (float) (h - headerH) / (float) (numRows + 2);
|
||||
}
|
||||
|
||||
const int gridW = juce::roundToInt (cellW * numSteps);
|
||||
const int gridH = juce::roundToInt (cellH * (numRows + 1));
|
||||
const int gridH = juce::roundToInt (cellH * (numRows + 2));
|
||||
|
||||
const int x = juce::roundToInt (labelW + (getWidth() - labelW - gridW) / 2.0f);
|
||||
const int y = juce::roundToInt (headerH + (getHeight() - headerH - gridH) / 2.0f);
|
||||
|
||||
return { x, y, gridW, gridH };
|
||||
return { juce::roundToInt (labelW), juce::roundToInt (headerH), gridW, gridH };
|
||||
}
|
||||
|
||||
void SequencerMatrix::setSelectedStep (int stepIdx)
|
||||
|
|
@ -65,9 +71,16 @@ void SequencerMatrix::paint (juce::Graphics& g)
|
|||
g.fillRect (bounds.toFloat());
|
||||
|
||||
// selected column highlight
|
||||
g.setColour (colours::selected.withAlpha (0.07f));
|
||||
g.setColour (colours::selected.withAlpha (0.02f));
|
||||
g.fillRect (ox + selectedStep * cellW, oy, cellW, (float) bounds.getHeight());
|
||||
|
||||
// play position column highlight (drawn before cells so notes stay vivid)
|
||||
if (playPos >= 0)
|
||||
{
|
||||
g.setColour (colours::accent.withAlpha (0.04f));
|
||||
g.fillRect (ox + playPos * cellW, oy, cellW, (float) bounds.getHeight());
|
||||
}
|
||||
|
||||
// grid lines
|
||||
g.setColour (colours::grid);
|
||||
|
||||
|
|
@ -77,7 +90,7 @@ void SequencerMatrix::paint (juce::Graphics& g)
|
|||
g.drawVerticalLine (juce::roundToInt (x), oy, oy + (float) bounds.getHeight());
|
||||
}
|
||||
|
||||
for (int i = 0; i <= numRows + 1; ++i)
|
||||
for (int i = 0; i <= numRows + 2; ++i)
|
||||
{
|
||||
const float y = oy + i * cellH;
|
||||
g.drawHorizontalLine (juce::roundToInt (y), ox, ox + (float) bounds.getWidth());
|
||||
|
|
@ -107,14 +120,6 @@ void SequencerMatrix::paint (juce::Graphics& g)
|
|||
g.setColour ((i % 4 == 0 && i < seqLen) ? colours::text : colours::textDim);
|
||||
g.setFont (font (10.0f));
|
||||
g.drawText (juce::String (i + 1), juce::roundToInt (cx - 8.0f), juce::roundToInt (cy - 6.0f), 16, 12, juce::Justification::centred);
|
||||
|
||||
if (i == selectedStep)
|
||||
{
|
||||
g.setColour (colours::accent);
|
||||
juce::Path tri;
|
||||
tri.addTriangle (cx - 4.0f, cy - 8.0f, cx + 4.0f, cy - 8.0f, cx, cy);
|
||||
g.fillPath (tri);
|
||||
}
|
||||
}
|
||||
|
||||
// pitch labels
|
||||
|
|
@ -152,10 +157,10 @@ void SequencerMatrix::paint (juce::Graphics& g)
|
|||
const float radius = 3.0f;
|
||||
|
||||
// cell body
|
||||
juce::ColourGradient grad (isSelected ? colours::accent : colours::activeCell,
|
||||
cell.getTopLeft(),
|
||||
isSelected ? colours::accent2 : colours::activeCell.darker (0.6f),
|
||||
cell.getBottomRight(),
|
||||
juce::ColourGradient grad (colours::accent,
|
||||
cell.getTopLeft().translated (cell.getWidth() * 0.5f, 0.0f),
|
||||
isSelected ? colours::accent.brighter (0.12f) : colours::accent.darker (0.35f),
|
||||
cell.getBottomLeft().translated (cell.getWidth() * 0.5f, 0.0f),
|
||||
false);
|
||||
|
||||
g.setGradientFill (grad);
|
||||
|
|
@ -215,6 +220,39 @@ void SequencerMatrix::paint (juce::Graphics& g)
|
|||
g.fillPath (tri);
|
||||
}
|
||||
|
||||
// --- accent row (bottom row) ----------------------------------------------
|
||||
const float accentY = oy + (numRows + 1) * cellH;
|
||||
|
||||
g.setColour (colours::accentDim);
|
||||
g.drawHorizontalLine (juce::roundToInt (accentY - 1.0f), ox, ox + (float) bounds.getWidth());
|
||||
|
||||
g.setFont (font (9.0f));
|
||||
g.setColour (colours::textDim);
|
||||
g.drawText ("ACCENT",
|
||||
juce::roundToInt (labelW * 0.1f), juce::roundToInt (accentY + cellH * 0.5f - 6.0f),
|
||||
juce::roundToInt (labelW * 0.8f), 12,
|
||||
juce::Justification::centredRight);
|
||||
|
||||
for (int i = 0; i < numSteps; ++i)
|
||||
{
|
||||
if (! processor.getSequencer().step (i).accent)
|
||||
continue;
|
||||
|
||||
const auto cell = juce::Rectangle<float> (ox + i * cellW + 1.5f, accentY + 1.5f,
|
||||
cellW - 3.0f, cellH - 3.0f);
|
||||
|
||||
g.setColour (colours::accent2);
|
||||
g.fillRoundedRectangle (cell, 3.0f);
|
||||
|
||||
const float ax = cell.getX() + cell.getWidth() * 0.5f;
|
||||
const float ay = cell.getY() + cell.getHeight() * 0.5f;
|
||||
|
||||
g.setColour (colours::bg);
|
||||
juce::Path tri;
|
||||
tri.addTriangle (ax - 2.0f, ay - 2.5f, ax + 2.0f, ay - 2.5f, ax, ay + 2.5f);
|
||||
g.fillPath (tri);
|
||||
}
|
||||
|
||||
// --- dim columns beyond the pattern length ---------------------------------
|
||||
if (seqLen < numSteps)
|
||||
{
|
||||
|
|
@ -222,21 +260,6 @@ void SequencerMatrix::paint (juce::Graphics& g)
|
|||
g.fillRect (ox + seqLen * cellW, oy,
|
||||
(numSteps - seqLen) * cellW, (float) bounds.getHeight());
|
||||
}
|
||||
|
||||
// --- play position marker ---------------------------------------------------
|
||||
if (playPos >= 0)
|
||||
{
|
||||
const float px = ox + playPos * cellW + cellW * 0.5f;
|
||||
|
||||
g.setColour (colours::playMarker.withAlpha (0.65f));
|
||||
g.drawVerticalLine (juce::roundToInt (px - 1.0f), oy, oy + (float) bounds.getHeight());
|
||||
g.drawVerticalLine (juce::roundToInt (px), oy, oy + (float) bounds.getHeight());
|
||||
|
||||
g.setColour (colours::playMarker);
|
||||
juce::Path tri;
|
||||
tri.addTriangle (px - 3.0f, oy - headerH + 2.0f, px + 3.0f, oy - headerH + 2.0f, px, oy - 1.0f);
|
||||
g.fillPath (tri);
|
||||
}
|
||||
}
|
||||
|
||||
SequencerMatrix::CellRect SequencerMatrix::cellAt (const juce::MouseEvent& e) const
|
||||
|
|
@ -248,9 +271,9 @@ SequencerMatrix::CellRect SequencerMatrix::cellAt (const juce::MouseEvent& e) co
|
|||
|
||||
const bool hit = bounds.contains (e.getPosition())
|
||||
&& col >= 0 && col < numSteps
|
||||
&& row >= 0 && row < numRows + 1;
|
||||
&& row >= -1 && row < numRows + 2;
|
||||
|
||||
return { juce::jlimit (0, numSteps - 1, col), juce::jlimit (0, numRows, row), hit };
|
||||
return { juce::jlimit (0, numSteps - 1, col), juce::jlimit (0, numRows + 1, row), hit };
|
||||
}
|
||||
|
||||
void SequencerMatrix::mouseDown (const juce::MouseEvent& e)
|
||||
|
|
@ -260,11 +283,32 @@ void SequencerMatrix::mouseDown (const juce::MouseEvent& e)
|
|||
if (! cell.hit)
|
||||
return;
|
||||
|
||||
if (cell.row == -1)
|
||||
{
|
||||
selectedStep = cell.step;
|
||||
if (onStepSelected)
|
||||
onStepSelected (cell.step);
|
||||
return;
|
||||
}
|
||||
|
||||
selectedStep = cell.step;
|
||||
|
||||
if (onStepSelected)
|
||||
onStepSelected (cell.step);
|
||||
|
||||
if (cell.row == numRows + 1)
|
||||
{
|
||||
accentDragging = true;
|
||||
accentLastStep = cell.step;
|
||||
startedAccentActive = processor.getSequencer().step (cell.step).accent;
|
||||
|
||||
if (onAccentChanged)
|
||||
onAccentChanged (cell.step, ! startedAccentActive);
|
||||
|
||||
repaint();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cell.row == numRows)
|
||||
{
|
||||
slideDragging = true;
|
||||
|
|
@ -299,13 +343,12 @@ void SequencerMatrix::mouseDown (const juce::MouseEvent& e)
|
|||
|
||||
void SequencerMatrix::mouseDrag (const juce::MouseEvent& e)
|
||||
{
|
||||
const auto cell = cellAt (e);
|
||||
if (! cell.hit || cell.row == -1)
|
||||
return;
|
||||
|
||||
if (fineDragging)
|
||||
{
|
||||
const auto cell = cellAt (e);
|
||||
|
||||
if (! cell.hit)
|
||||
return;
|
||||
|
||||
// one matrix row (one semitone) maps to 4 cents of fine tuning
|
||||
const float cents = juce::jlimit (-50.0f, 50.0f,
|
||||
fineStartCents + (fineRefRow - cell.row) * 4.0f);
|
||||
|
|
@ -325,8 +368,6 @@ void SequencerMatrix::mouseDrag (const juce::MouseEvent& e)
|
|||
|
||||
if (slideDragging)
|
||||
{
|
||||
const auto cell = cellAt (e);
|
||||
|
||||
if (cell.hit && cell.row == numRows && cell.step != slideLastStep)
|
||||
{
|
||||
slideLastStep = cell.step;
|
||||
|
|
@ -340,10 +381,26 @@ void SequencerMatrix::mouseDrag (const juce::MouseEvent& e)
|
|||
return;
|
||||
}
|
||||
|
||||
if (accentDragging)
|
||||
{
|
||||
if (cell.hit && cell.row == numRows + 1 && cell.step != accentLastStep)
|
||||
{
|
||||
accentLastStep = cell.step;
|
||||
|
||||
if (onAccentChanged)
|
||||
onAccentChanged (cell.step, ! startedAccentActive);
|
||||
|
||||
repaint();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! dragging)
|
||||
return;
|
||||
|
||||
const auto cell = cellAt (e);
|
||||
if (cell.row >= numRows)
|
||||
return;
|
||||
|
||||
if (cell.hit && cell.row < numRows && cell.row != lastRow)
|
||||
{
|
||||
|
|
@ -352,6 +409,8 @@ void SequencerMatrix::mouseDrag (const juce::MouseEvent& e)
|
|||
|
||||
const int pitch = semitoneForRow (cell.row);
|
||||
|
||||
const auto& s = processor.getSequencer().step (cell.step);
|
||||
|
||||
if (startedActive)
|
||||
{
|
||||
if (onNoteChanged)
|
||||
|
|
@ -384,6 +443,12 @@ void SequencerMatrix::mouseUp (const juce::MouseEvent& e)
|
|||
return;
|
||||
}
|
||||
|
||||
if (accentDragging)
|
||||
{
|
||||
accentDragging = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (! dragging)
|
||||
return;
|
||||
|
||||
|
|
@ -391,7 +456,7 @@ void SequencerMatrix::mouseUp (const juce::MouseEvent& e)
|
|||
|
||||
const auto cell = cellAt (e);
|
||||
|
||||
if (! cell.hit || cell.step != dragStep || cell.row >= numRows)
|
||||
if (! cell.hit || cell.step != dragStep || cell.row >= numRows || cell.row == -1)
|
||||
return;
|
||||
|
||||
const int pitch = semitoneForRow (cell.row);
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
#include <JuceHeader.h>
|
||||
|
||||
class MonoStepAudioProcessor;
|
||||
class MonostepAudioProcessor;
|
||||
|
||||
class SequencerMatrix final : public juce::Component
|
||||
{
|
||||
public:
|
||||
explicit SequencerMatrix (MonoStepAudioProcessor& processor);
|
||||
explicit SequencerMatrix (MonostepAudioProcessor& processor);
|
||||
~SequencerMatrix() override = default;
|
||||
|
||||
std::function<void (int stepIdx)> onStepSelected;
|
||||
|
|
@ -15,6 +15,7 @@ public:
|
|||
std::function<void (int stepIdx, int semitone)> onNoteChanged;
|
||||
std::function<void (int stepIdx, float cents)> onFineChanged;
|
||||
std::function<void (int stepIdx, bool slide)> onSlideChanged;
|
||||
std::function<void (int stepIdx, bool accent)> onAccentChanged;
|
||||
|
||||
void setSelectedStep (int stepIdx);
|
||||
int getSelectedStep() const { return selectedStep; }
|
||||
|
|
@ -39,14 +40,15 @@ private:
|
|||
int rowForSemitone (int semitone) const;
|
||||
int semitoneForRow (int row) const;
|
||||
|
||||
MonoStepAudioProcessor& processor;
|
||||
MonostepAudioProcessor& processor;
|
||||
|
||||
float cellW = 24.0f;
|
||||
float cellH = 17.0f;
|
||||
float labelW = 40.0f;
|
||||
float headerH = 24.0f;
|
||||
float headerPad = 10.0f;
|
||||
|
||||
mutable float cellW = 24.0f;
|
||||
mutable float cellH = 17.0f;
|
||||
|
||||
int selectedStep = 0;
|
||||
int playPos = -1;
|
||||
|
||||
|
|
@ -64,4 +66,8 @@ private:
|
|||
bool slideDragging = false;
|
||||
bool startedSlideActive = false;
|
||||
int slideLastStep = -1;
|
||||
|
||||
bool accentDragging = false;
|
||||
bool startedAccentActive = false;
|
||||
int accentLastStep = -1;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue