mirror of
https://codeberg.org/armin/monostep.git
synced 2026-09-01 04:10:46 +02:00
MonoStep: monophonic 2-oscillator step-sequencer synth
This commit is contained in:
commit
e52e5cb161
14 changed files with 2674 additions and 0 deletions
412
Source/ui/SequencerMatrix.cpp
Normal file
412
Source/ui/SequencerMatrix.cpp
Normal file
|
|
@ -0,0 +1,412 @@
|
|||
#include "SequencerMatrix.h"
|
||||
|
||||
#include "../PluginProcessor.h"
|
||||
#include "../Theme.h"
|
||||
|
||||
using namespace monostep;
|
||||
|
||||
SequencerMatrix::SequencerMatrix (MonoStepAudioProcessor& processorRef)
|
||||
: processor (processorRef)
|
||||
{
|
||||
}
|
||||
|
||||
int SequencerMatrix::rowForSemitone (int semitone) const
|
||||
{
|
||||
return maxSemitone - semitone;
|
||||
}
|
||||
|
||||
int SequencerMatrix::semitoneForRow (int row) const
|
||||
{
|
||||
return maxSemitone - row;
|
||||
}
|
||||
|
||||
juce::Rectangle<int> SequencerMatrix::matrixBounds() const
|
||||
{
|
||||
const int gridW = juce::roundToInt (cellW * numSteps);
|
||||
const int gridH = juce::roundToInt (cellH * (numRows + 1));
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
void SequencerMatrix::setSelectedStep (int stepIdx)
|
||||
{
|
||||
if (stepIdx < 0 || stepIdx >= numSteps)
|
||||
return;
|
||||
|
||||
selectedStep = stepIdx;
|
||||
repaint();
|
||||
}
|
||||
|
||||
void SequencerMatrix::setPlayPosition (int stepIdx)
|
||||
{
|
||||
if (stepIdx < -1 || stepIdx >= numSteps)
|
||||
return;
|
||||
|
||||
if (stepIdx == playPos)
|
||||
return;
|
||||
|
||||
playPos = stepIdx;
|
||||
repaint();
|
||||
}
|
||||
|
||||
void SequencerMatrix::paint (juce::Graphics& g)
|
||||
{
|
||||
g.fillAll (colours::panel);
|
||||
|
||||
const auto bounds = matrixBounds();
|
||||
const float ox = (float) bounds.getX();
|
||||
const float oy = (float) bounds.getY();
|
||||
|
||||
// grid background
|
||||
g.setColour (colours::bg);
|
||||
g.fillRect (bounds.toFloat());
|
||||
|
||||
// selected column highlight
|
||||
g.setColour (colours::selected.withAlpha (0.07f));
|
||||
g.fillRect (ox + selectedStep * cellW, oy, cellW, (float) bounds.getHeight());
|
||||
|
||||
// grid lines
|
||||
g.setColour (colours::grid);
|
||||
|
||||
for (int i = 0; i <= numSteps; ++i)
|
||||
{
|
||||
const float x = ox + i * cellW;
|
||||
g.drawVerticalLine (juce::roundToInt (x), oy, oy + (float) bounds.getHeight());
|
||||
}
|
||||
|
||||
for (int i = 0; i <= numRows + 1; ++i)
|
||||
{
|
||||
const float y = oy + i * cellH;
|
||||
g.drawHorizontalLine (juce::roundToInt (y), ox, ox + (float) bounds.getWidth());
|
||||
}
|
||||
|
||||
// accent C rows (semitone multiple of 12)
|
||||
for (int r = 0; r < numRows; ++r)
|
||||
{
|
||||
const int st = semitoneForRow (r);
|
||||
|
||||
if (st % 12 == 0)
|
||||
{
|
||||
g.setColour (colours::gridLight);
|
||||
const float y = oy + r * cellH;
|
||||
g.drawHorizontalLine (juce::roundToInt (y), ox, ox + (float) bounds.getWidth());
|
||||
}
|
||||
}
|
||||
|
||||
// step header numbers + selected marker
|
||||
const int seqLen = processor.getPatternLength();
|
||||
|
||||
for (int i = 0; i < numSteps; ++i)
|
||||
{
|
||||
const float cx = ox + i * cellW + cellW * 0.5f;
|
||||
const float cy = oy - headerH * 0.5f;
|
||||
|
||||
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
|
||||
for (int r = 0; r < numRows; ++r)
|
||||
{
|
||||
const int st = semitoneForRow (r);
|
||||
const float ly = oy + r * cellH + cellH * 0.5f;
|
||||
|
||||
g.setFont (font (8.5f));
|
||||
g.setColour (colours::textDim);
|
||||
g.drawText (juce::String (st > 0 ? "+" : "") + juce::String (st),
|
||||
juce::roundToInt (labelW * 0.1f), juce::roundToInt (ly - 5.0f), juce::roundToInt (labelW * 0.6f), 10,
|
||||
juce::Justification::centredRight);
|
||||
|
||||
g.setColour (st % 12 == 0 ? colours::accent : colours::text);
|
||||
g.drawText (noteNameForOffset (st),
|
||||
juce::roundToInt (labelW * 0.62f), juce::roundToInt (ly - 5.0f), juce::roundToInt (labelW * 0.4f), 10,
|
||||
juce::Justification::centredRight);
|
||||
}
|
||||
|
||||
// active cells
|
||||
for (int i = 0; i < numSteps; ++i)
|
||||
{
|
||||
const auto& s = processor.getSequencer().step (i);
|
||||
|
||||
if (! s.gate)
|
||||
continue;
|
||||
|
||||
const int r = rowForSemitone (s.semitone);
|
||||
|
||||
const auto cell = juce::Rectangle<float> (ox + i * cellW + 1.5f, oy + r * cellH + 1.5f,
|
||||
cellW - 3.0f, cellH - 3.0f);
|
||||
|
||||
const bool isSelected = (i == selectedStep);
|
||||
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(),
|
||||
false);
|
||||
|
||||
g.setGradientFill (grad);
|
||||
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
|
||||
if (std::fabs (s.cents) > 0.5f)
|
||||
{
|
||||
g.setColour (colours::bg.withAlpha (0.85f));
|
||||
g.setFont (font (7.5f));
|
||||
g.drawText (formatCents (s.cents), cell.toNearestInt().reduced (1, 1), juce::Justification::topLeft);
|
||||
}
|
||||
}
|
||||
|
||||
// --- slide row (bottom row) ----------------------------------------------
|
||||
const float slideY = oy + numRows * cellH;
|
||||
|
||||
g.setColour (colours::accentDim);
|
||||
g.drawHorizontalLine (juce::roundToInt (slideY - 1.0f), ox, ox + (float) bounds.getWidth());
|
||||
|
||||
g.setFont (font (9.0f));
|
||||
g.setColour (colours::textDim);
|
||||
g.drawText ("SLIDE",
|
||||
juce::roundToInt (labelW * 0.1f), juce::roundToInt (slideY + 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).slide)
|
||||
continue;
|
||||
|
||||
const auto cell = juce::Rectangle<float> (ox + i * cellW + 1.5f, slideY + 1.5f,
|
||||
cellW - 3.0f, cellH - 3.0f);
|
||||
|
||||
g.setColour (colours::accent);
|
||||
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 - 3.0f, ay - 2.0f, ax - 3.0f, ay + 2.0f, ax + 3.5f, ay);
|
||||
g.fillPath (tri);
|
||||
}
|
||||
|
||||
// --- dim columns beyond the pattern length ---------------------------------
|
||||
if (seqLen < numSteps)
|
||||
{
|
||||
g.setColour (colours::bg.withAlpha (0.55f));
|
||||
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
|
||||
{
|
||||
const auto bounds = matrixBounds();
|
||||
|
||||
const int col = (int) std::floor ((e.getPosition().getX() - bounds.getX()) / cellW);
|
||||
const int row = (int) std::floor ((e.getPosition().getY() - bounds.getY()) / cellH);
|
||||
|
||||
const bool hit = bounds.contains (e.getPosition())
|
||||
&& col >= 0 && col < numSteps
|
||||
&& row >= 0 && row < numRows + 1;
|
||||
|
||||
return { juce::jlimit (0, numSteps - 1, col), juce::jlimit (0, numRows, row), hit };
|
||||
}
|
||||
|
||||
void SequencerMatrix::mouseDown (const juce::MouseEvent& e)
|
||||
{
|
||||
const auto cell = cellAt (e);
|
||||
|
||||
if (! cell.hit)
|
||||
return;
|
||||
|
||||
selectedStep = cell.step;
|
||||
|
||||
if (onStepSelected)
|
||||
onStepSelected (cell.step);
|
||||
|
||||
if (cell.row == numRows)
|
||||
{
|
||||
slideDragging = true;
|
||||
slideLastStep = cell.step;
|
||||
startedSlideActive = processor.getSequencer().step (cell.step).slide;
|
||||
|
||||
if (onSlideChanged)
|
||||
onSlideChanged (cell.step, ! startedSlideActive);
|
||||
|
||||
repaint();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.mods.isAltDown())
|
||||
{
|
||||
fineDragging = true;
|
||||
fineRefRow = cell.row;
|
||||
fineStartCents = processor.getSequencer().step (cell.step).cents;
|
||||
fineLastCents = -1000.0f;
|
||||
repaint();
|
||||
return;
|
||||
}
|
||||
|
||||
dragging = true;
|
||||
moved = false;
|
||||
dragStep = cell.step;
|
||||
lastRow = cell.row;
|
||||
|
||||
const auto& s = processor.getSequencer().step (cell.step);
|
||||
startedActive = (s.gate && s.semitone == semitoneForRow (cell.row));
|
||||
}
|
||||
|
||||
void SequencerMatrix::mouseDrag (const juce::MouseEvent& e)
|
||||
{
|
||||
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);
|
||||
|
||||
if (std::fabs (cents - fineLastCents) >= 0.5f)
|
||||
{
|
||||
fineLastCents = cents;
|
||||
|
||||
if (onFineChanged)
|
||||
onFineChanged (selectedStep, cents);
|
||||
|
||||
repaint();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (slideDragging)
|
||||
{
|
||||
const auto cell = cellAt (e);
|
||||
|
||||
if (cell.hit && cell.row == numRows && cell.step != slideLastStep)
|
||||
{
|
||||
slideLastStep = cell.step;
|
||||
|
||||
if (onSlideChanged)
|
||||
onSlideChanged (cell.step, ! startedSlideActive);
|
||||
|
||||
repaint();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! dragging)
|
||||
return;
|
||||
|
||||
const auto cell = cellAt (e);
|
||||
|
||||
if (cell.hit && cell.row < numRows && cell.row != lastRow)
|
||||
{
|
||||
lastRow = cell.row;
|
||||
moved = true;
|
||||
|
||||
const int pitch = semitoneForRow (cell.row);
|
||||
|
||||
if (startedActive)
|
||||
{
|
||||
if (onNoteChanged)
|
||||
onNoteChanged (dragStep, pitch);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (onGateChanged)
|
||||
onGateChanged (dragStep, true);
|
||||
|
||||
if (onNoteChanged)
|
||||
onNoteChanged (dragStep, pitch);
|
||||
}
|
||||
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
|
||||
void SequencerMatrix::mouseUp (const juce::MouseEvent& e)
|
||||
{
|
||||
if (fineDragging)
|
||||
{
|
||||
fineDragging = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (slideDragging)
|
||||
{
|
||||
slideDragging = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (! dragging)
|
||||
return;
|
||||
|
||||
dragging = false;
|
||||
|
||||
const auto cell = cellAt (e);
|
||||
|
||||
if (! cell.hit || cell.step != dragStep || cell.row >= numRows)
|
||||
return;
|
||||
|
||||
const int pitch = semitoneForRow (cell.row);
|
||||
|
||||
if (startedActive && ! moved)
|
||||
{
|
||||
if (onGateChanged)
|
||||
onGateChanged (cell.step, false);
|
||||
}
|
||||
else if (! startedActive && ! moved)
|
||||
{
|
||||
if (onGateChanged)
|
||||
onGateChanged (cell.step, true);
|
||||
|
||||
if (onNoteChanged)
|
||||
onNoteChanged (cell.step, pitch);
|
||||
}
|
||||
}
|
||||
67
Source/ui/SequencerMatrix.h
Normal file
67
Source/ui/SequencerMatrix.h
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
#pragma once
|
||||
|
||||
#include <JuceHeader.h>
|
||||
|
||||
class MonoStepAudioProcessor;
|
||||
|
||||
class SequencerMatrix final : public juce::Component
|
||||
{
|
||||
public:
|
||||
explicit SequencerMatrix (MonoStepAudioProcessor& processor);
|
||||
~SequencerMatrix() override = default;
|
||||
|
||||
std::function<void (int stepIdx)> onStepSelected;
|
||||
std::function<void (int stepIdx, bool gate)> onGateChanged;
|
||||
std::function<void (int stepIdx, int semitone)> onNoteChanged;
|
||||
std::function<void (int stepIdx, float cents)> onFineChanged;
|
||||
std::function<void (int stepIdx, bool slide)> onSlideChanged;
|
||||
|
||||
void setSelectedStep (int stepIdx);
|
||||
int getSelectedStep() const { return selectedStep; }
|
||||
|
||||
void setPlayPosition (int stepIdx);
|
||||
|
||||
void paint (juce::Graphics&) override;
|
||||
void mouseDown (const juce::MouseEvent&) override;
|
||||
void mouseDrag (const juce::MouseEvent&) override;
|
||||
void mouseUp (const juce::MouseEvent&) override;
|
||||
|
||||
private:
|
||||
struct CellRect
|
||||
{
|
||||
int step;
|
||||
int row;
|
||||
bool hit;
|
||||
};
|
||||
|
||||
CellRect cellAt (const juce::MouseEvent&) const;
|
||||
juce::Rectangle<int> matrixBounds() const;
|
||||
int rowForSemitone (int semitone) const;
|
||||
int semitoneForRow (int row) const;
|
||||
|
||||
MonoStepAudioProcessor& processor;
|
||||
|
||||
float cellW = 24.0f;
|
||||
float cellH = 17.0f;
|
||||
float labelW = 40.0f;
|
||||
float headerH = 24.0f;
|
||||
float headerPad = 10.0f;
|
||||
|
||||
int selectedStep = 0;
|
||||
int playPos = -1;
|
||||
|
||||
bool dragging = false;
|
||||
bool startedActive = false;
|
||||
bool moved = false;
|
||||
int lastRow = 0;
|
||||
int dragStep = 0;
|
||||
|
||||
bool fineDragging = false;
|
||||
int fineRefRow = 0;
|
||||
float fineStartCents = 0.0f;
|
||||
float fineLastCents = -1000.0f;
|
||||
|
||||
bool slideDragging = false;
|
||||
bool startedSlideActive = false;
|
||||
int slideLastStep = -1;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue