Files
biggy 4b0db50271 refactor(source): move every implementation into its module directory
- 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.
2026-10-06 09:17:49 +02:00

153 lines
4.5 KiB
C++

#include "LFO.h"
#include "ExactCompare.h"
namespace serum
{
void LFO::reset()
{
phase = 0.0;
value = 0.0f;
delayCounter = 0.0;
fadeCounter = 0.0;
fadeVal = 1.0f;
holdValue = rng.nextFloat() * 2.0f - 1.0f;
prevDelayParam = -1.0f;
prevFadeParam = -1.0f;
if (shapeBuffer.empty())
{
shapeBuffer.resize ((size_t) kShapePoints);
// default step sequence
for (int i = 0; i < kShapePoints; ++i)
shapeBuffer[(size_t) i] = (i % 2 == 0) ? 1.0f : -1.0f;
}
}
void LFO::setParams (float rateNorm, bool s, float b, int shp, float ph,
float fade, float delay)
{
sync = s;
beat = b;
shape = juce::jlimit (0, (int) LfoShape::Count - 1, shp);
phaseOffset = juce::jlimit (0.0f, 1.0f, ph);
if (sync)
rateHz = (tempo / 60.0) / maps::beatToMultiplier (beat);
else
rateHz = maps::rateToHz (rateNorm);
delaySeconds = juce::jlimit (0.0f, 1.0f, delay) * 4.0;
fadeSeconds = juce::jlimit (0.0f, 1.0f, fade) * 8.0;
// Only restart the delay/fade timing when those knobs actually change,
// so repeated block-rate setParams calls don't keep resetting the LFO.
if (changed (delay, prevDelayParam) || changed (fade, prevFadeParam))
{
delayCounter = delaySeconds * sr;
fadeCounter = 0.0;
fadeVal = (fadeSeconds <= 0.0) ? 1.0f : 0.0f;
prevDelayParam = delay;
prevFadeParam = fade;
}
value = delayCounter > 0.0 ? 0.0f : shapeValue() * fadeVal;
}
void LFO::setShapeData (const std::vector<float>& data, int steps)
{
if (data.empty())
return;
shapeSteps = juce::jlimit (2, kShapePoints, steps);
for (size_t i = 0; i < shapeBuffer.size(); ++i)
shapeBuffer[i] = i < data.size() && std::isfinite (data[i])
? juce::jlimit (-1.0f, 1.0f, data[i]) : 0.0f;
}
float lfoShapeValue (float phase, LfoShape shape, int steps,
const float* data, int dataSize, float sampleHoldValue) noexcept
{
const int numSteps = juce::jlimit (2, LFO::kShapePoints, steps);
const int numData = data != nullptr ? juce::jlimit (0, LFO::kShapePoints, dataSize) : 0;
auto point = [data, numData] (int index) noexcept
{
return numData > 0 ? data[(size_t) juce::jlimit (0, numData - 1, index)] : 0.0f;
};
switch (shape)
{
case LfoShape::Sine:
return std::sin (phase * juce::MathConstants<float>::twoPi);
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:
return sampleHoldValue;
case LfoShape::StepSeq:
return point (juce::jlimit (0, numSteps - 1, (int) (phase * (float) numSteps)));
case LfoShape::Freehand:
{
const float pos = phase * (float) (numSteps - 1);
const int i0 = (int) pos;
const int i1 = juce::jmin (i0 + 1, numSteps - 1);
const float frac = pos - (float) i0;
return point (i0) * (1.0f - frac) + point (i1) * frac;
}
case LfoShape::Count:
default:
return 0.0f;
}
}
float LFO::shapeValue() noexcept
{
return lfoShapeValue (getPhase(), (LfoShape) shape, shapeSteps,
shapeBuffer.data(), (int) shapeBuffer.size(), holdValue);
}
float LFO::process() noexcept
{
return advance (1);
}
float LFO::advance (int numSamples) noexcept
{
if (numSamples <= 0)
return value;
// Start delay.
if (delayCounter > 0.0)
{
const int skipped = (int) juce::jmin ((double) numSamples, std::ceil (delayCounter));
delayCounter = juce::jmax (0.0, delayCounter - skipped);
numSamples -= skipped;
if (numSamples == 0)
return value = 0.0f;
}
// Fade-in ramp.
if (fadeVal < 1.0f)
{
fadeCounter += numSamples;
if (fadeSeconds > 0.0)
fadeVal = (float) juce::jlimit (0.0, 1.0, fadeCounter / (fadeSeconds * sr));
else
fadeVal = 1.0f;
}
phase += rateHz * numSamples / sr;
const double cycles = std::floor (phase);
phase -= cycles;
if (shape == (int) LfoShape::SampleHold)
for (int i = 0; i < (int) cycles; ++i)
holdValue = rng.nextFloat() * 2.0f - 1.0f;
value = shapeValue() * fadeVal;
return value;
}
} // namespace serum