- 36 files move into the module whose AGENTS.md already claimed them: Engine/, Synth/, Modulation/, Params/, Plugin/, GUI/, EffectUnits/, Resources/ - Source/engine, modulation, params, plugin and synth become Engine, Modulation, Params, Plugin and Synth, matching the directories that already hold code - ExactCompare.h stays at the Source/ root as the one shared header - SERUMALT_INCLUDE_DIRS adds one include directory per module, so the 31 parent-relative includes become plain names with no path prefix - the module AGENTS.md files list owned files instead of describing a move that had not happened, and README.md and AGENTS.md describe the real tree Verified: the harness passes CTest, the Linux VST3 and Standalone link, the Windows cross-build passes its module checks, each configure exposes only its own targets, no first-party warning line appears, and the generated Windows manifest is unchanged.
131 lines
4.3 KiB
C++
131 lines
4.3 KiB
C++
#include "LFODisplay.h"
|
|
#include "ExactCompare.h"
|
|
|
|
namespace serum
|
|
{
|
|
|
|
void LFODisplay::setShapeData (const std::vector<float>& data, int s)
|
|
{
|
|
const int newSteps = juce::jlimit (2, LFO::kShapePoints, s);
|
|
const auto dataSize = (std::ptrdiff_t) std::min (data.size(), size_t (LFO::kShapePoints));
|
|
if (steps == newSteps && (int) shapeData.size() == LFO::kShapePoints
|
|
&& std::equal (data.begin(), data.begin() + dataSize, shapeData.begin())
|
|
&& std::all_of (shapeData.begin() + dataSize, shapeData.end(), [] (float v) { return exactly (v, 0.0f); }))
|
|
return;
|
|
|
|
steps = newSteps;
|
|
shapeData.assign (data.begin(), data.begin() + dataSize);
|
|
shapeData.resize ((size_t) LFO::kShapePoints, 0.0f);
|
|
repaint();
|
|
}
|
|
|
|
float LFODisplay::valueAt (float phase) const noexcept
|
|
{
|
|
return lfoShapeValue (phase, (LfoShape) shape, steps,
|
|
shapeData.data(), (int) shapeData.size(), kSampleHoldLevel);
|
|
}
|
|
|
|
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));
|
|
}
|
|
}
|
|
|
|
// Sample and hold is stateful in the audio path, so the preview draws the
|
|
// held level as a flat schematic with its hold steps marked, and labels it,
|
|
// rather than inventing a sequence the synth will never play.
|
|
if (shape == (int) LfoShape::SampleHold)
|
|
{
|
|
const float stepW = (b.getWidth() - 8.0f) / (float) steps;
|
|
g.setColour (theme::outline);
|
|
for (int i = 1; i < steps; ++i)
|
|
{
|
|
const float x = b.getX() + 4.0f + stepW * (float) i;
|
|
g.drawLine (x, midY - b.getHeight() * 0.28f, x, midY + b.getHeight() * 0.28f, 1.0f);
|
|
}
|
|
|
|
g.setColour (theme::textDim);
|
|
g.setFont (10.0f);
|
|
g.drawText ("S&H schematic",
|
|
juce::Rectangle<float> (b.getX(), b.getY(), b.getWidth(), 14.0f).toNearestInt(),
|
|
juce::Justification::centred, false);
|
|
}
|
|
|
|
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 ((size_t) LFO::kShapePoints, 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
|