mirror of
https://codeberg.org/armin/monostep.git
synced 2026-09-01 04:10:46 +02:00
412 lines
11 KiB
C++
412 lines
11 KiB
C++
|
|
#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);
|
||
|
|
}
|
||
|
|
}
|