Add change detection to Display, WaveformDisplay, FilterDisplay, EnvelopeDisplay, and LFODisplay so repaint() is only called when values actually change. Gate updateVisuals() by currentTab so only the visible tab's displays are updated each timer tick. Similarly gate updateModList/updateMacroList to their respective tabs. Manage dynamically created controls via ownedControls unique_ptr vector instead of raw new leaks. Null-check parameter lookups before creating attachments. Hold engine control lock when reading matrix/macros/LFO shape data. Fix envelope display curve directions (attack rising from bottom, decay falling toward sustain, release falling toward bottom). Add tooltip to FilterDisplay noting the approximation. Replace ToggleButton raw param pointer with ParameterAttachment for proper gesture handling. Sync preset and scale combos to processor state.
135 lines
4.3 KiB
C++
135 lines
4.3 KiB
C++
#include "LFODisplay.h"
|
|
|
|
namespace serum
|
|
{
|
|
|
|
void LFODisplay::setShapeData (const std::vector<float>& data, int s)
|
|
{
|
|
const int newSteps = juce::jlimit (2, 64, s);
|
|
const size_t dataSize = std::min (data.size(), size_t (64));
|
|
if (steps == newSteps && shapeData.size() == 64
|
|
&& std::equal (data.begin(), data.begin() + dataSize, shapeData.begin())
|
|
&& std::all_of (shapeData.begin() + dataSize, shapeData.end(), [] (float v) { return v == 0.0f; }))
|
|
return;
|
|
|
|
steps = newSteps;
|
|
shapeData.assign (data.begin(), data.begin() + dataSize);
|
|
shapeData.resize (64, 0.0f);
|
|
repaint();
|
|
}
|
|
|
|
float LFODisplay::valueAt (float phase) const noexcept
|
|
{
|
|
switch ((LfoShape) shape)
|
|
{
|
|
case LfoShape::Sine: return std::sin (phase * 6.2831853f);
|
|
case LfoShape::Triangle: return 1.0f - 4.0f * std::abs (phase - 0.5f);
|
|
case LfoShape::Saw: return 2.0f * phase - 1.0f;
|
|
case LfoShape::Square: return (phase < 0.5f) ? 1.0f : -1.0f;
|
|
case LfoShape::SampleHold:
|
|
{
|
|
const int step = juce::jlimit (0, 15, (int) (phase * 16.0f));
|
|
return ((step * 2654435761u) & 0xffffu) / 32768.0f - 1.0f;
|
|
}
|
|
case LfoShape::StepSeq:
|
|
{
|
|
const int idx = juce::jlimit (0, steps - 1, (int) (phase * steps));
|
|
return shapeData.empty() ? 0.0f : shapeData[(size_t) idx];
|
|
}
|
|
case LfoShape::Freehand:
|
|
{
|
|
if (shapeData.empty()) return 0.0f;
|
|
const float pos = phase * (float) (steps - 1);
|
|
const int i0 = (int) pos;
|
|
const int i1 = juce::jmin (i0 + 1, steps - 1);
|
|
const float frac = pos - (float) i0;
|
|
return shapeData[(size_t) i0] * (1.0f - frac) + shapeData[(size_t) i1] * frac;
|
|
}
|
|
default: return 0.0f;
|
|
}
|
|
}
|
|
|
|
void LFODisplay::paint (juce::Graphics& g)
|
|
{
|
|
const auto b = getLocalBounds().toFloat();
|
|
g.setColour (juce::Colours::black.withAlpha (0.4f));
|
|
g.fillRoundedRectangle (b, 4.0f);
|
|
g.setColour (theme::outline);
|
|
g.drawRoundedRectangle (b.reduced (0.5f), 4.0f, 1.0f);
|
|
|
|
const float midY = b.getCentreY();
|
|
g.setColour (theme::outline);
|
|
g.drawLine (b.getX() + 4.0f, midY, b.getRight() - 4.0f, midY, 1.0f);
|
|
|
|
// Editable shapes show step markers.
|
|
if (shape == (int) LfoShape::StepSeq)
|
|
{
|
|
const float stepW = (b.getWidth() - 8.0f) / (float) steps;
|
|
for (int i = 0; i < steps; ++i)
|
|
{
|
|
const float v = shapeData.empty() ? 0.0f : shapeData[(size_t) i];
|
|
const float x = b.getX() + 4.0f + stepW * i;
|
|
const float y = midY - v * (b.getHeight() * 0.42f);
|
|
g.setColour (theme::amber);
|
|
g.fillRect (x + 1.0f, juce::jmin (y, midY), stepW - 2.0f, std::abs (midY - y));
|
|
}
|
|
}
|
|
|
|
juce::Path path;
|
|
path.startNewSubPath (b.getX() + 4.0f, midY);
|
|
const int n = 256;
|
|
for (int i = 0; i <= n; ++i)
|
|
{
|
|
const float phase = (float) i / (float) n;
|
|
const float v = valueAt (phase);
|
|
const float x = b.getX() + 4.0f + phase * (b.getWidth() - 8.0f);
|
|
const float y = midY - v * (b.getHeight() * 0.42f);
|
|
if (i == 0) path.startNewSubPath (x, y);
|
|
else path.lineTo (x, y);
|
|
}
|
|
g.setColour (theme::green);
|
|
g.strokePath (path, juce::PathStrokeType (1.5f));
|
|
}
|
|
|
|
void LFODisplay::editAt (float x, float y)
|
|
{
|
|
const auto b = getLocalBounds().toFloat();
|
|
const float midY = b.getCentreY();
|
|
const float normX = juce::jlimit (0.0f, 1.0f, (x - b.getX() - 4.0f) / (b.getWidth() - 8.0f));
|
|
const float v = juce::jlimit (-1.0f, 1.0f, (midY - y) / (b.getHeight() * 0.42f));
|
|
|
|
if (shapeData.empty())
|
|
shapeData.assign (64, 0.0f);
|
|
|
|
if (shape == (int) LfoShape::StepSeq)
|
|
{
|
|
const int idx = juce::jlimit (0, steps - 1, (int) (normX * steps));
|
|
shapeData[(size_t) idx] = v;
|
|
}
|
|
else if (shape == (int) LfoShape::Freehand)
|
|
{
|
|
const int idx = juce::jlimit (0, steps - 1, (int) (normX * steps));
|
|
shapeData[(size_t) idx] = v;
|
|
}
|
|
else
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (onEdited)
|
|
onEdited (shapeData, steps);
|
|
repaint();
|
|
}
|
|
|
|
void LFODisplay::mouseDown (const juce::MouseEvent& e)
|
|
{
|
|
editAt (e.x, e.y);
|
|
}
|
|
|
|
void LFODisplay::mouseDrag (const juce::MouseEvent& e)
|
|
{
|
|
editAt (e.x, e.y);
|
|
}
|
|
|
|
} // namespace serum
|