refactor(lfo): advance by sample count and separate phase offset

Add advance(numSamples) so the engine can step LFOs per sub-block
instead of calling process() in a sample loop. process() now
delegates to advance(1).

Split phaseOffset from phase so setParams doesn't overwrite the
running phase; getPhase() adds the offset and wraps. Fix sync mode
to divide by beat multiplier instead of multiplying. Move SampleHold
randomization into advance() based on cycle count. Validate shape
data in setShapeData with isfinite and jlimit.
This commit is contained in:
2026-09-09 14:33:13 +02:00
parent e2e66457c2
commit 40dc0d4105
2 changed files with 40 additions and 25 deletions
+36 -23
View File
@@ -10,14 +10,16 @@ void LFO::reset()
delayCounter = 0.0;
fadeCounter = 0.0;
fadeVal = 1.0f;
holdValue = 0.0f;
prevPhase = 0.0;
holdValue = rng.nextFloat() * 2.0f - 1.0f;
prevDelayParam = -1.0f;
prevFadeParam = -1.0f;
shapeBuffer.assign ((size_t) kShapePoints, 0.0f);
// default step sequence
for (int i = 0; i < kShapePoints; ++i)
shapeBuffer[(size_t) i] = (i % 2 == 0) ? 1.0f : -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,
@@ -26,10 +28,10 @@ void LFO::setParams (float rateNorm, bool s, float b, int shp, float ph,
sync = s;
beat = b;
shape = juce::jlimit (0, (int) LfoShape::Count - 1, shp);
phase = juce::jlimit (0.0f, 1.0f, ph);
phaseOffset = juce::jlimit (0.0f, 1.0f, ph);
if (sync)
rateHz = maps::beatToMultiplier (beat) * (tempo / 60.0);
rateHz = (tempo / 60.0) / maps::beatToMultiplier (beat);
else
rateHz = maps::rateToHz (rateNorm);
@@ -46,6 +48,7 @@ void LFO::setParams (float rateNorm, bool s, float b, int shp, float ph,
prevDelayParam = delay;
prevFadeParam = fade;
}
value = delayCounter > 0.0 ? 0.0f : shapeValue() * fadeVal;
}
void LFO::setShapeData (const std::vector<float>& data, int steps)
@@ -54,17 +57,18 @@ void LFO::setShapeData (const std::vector<float>& data, int steps)
return;
shapeSteps = juce::jlimit (2, kShapePoints, steps);
shapeBuffer.assign (data.begin(), data.end());
shapeBuffer.resize ((size_t) kShapePoints, 0.0f);
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 LFO::shapeValue() noexcept
{
const float p = (float) phase;
const float p = getPhase();
switch ((LfoShape) shape)
{
case LfoShape::Sine:
return std::sin (p * 6.28318530717958647692f);
return std::sin (p * juce::MathConstants<float>::twoPi);
case LfoShape::Triangle:
return 1.0f - 4.0f * std::abs (p - 0.5f);
case LfoShape::Saw:
@@ -72,11 +76,7 @@ float LFO::shapeValue() noexcept
case LfoShape::Square:
return (p < 0.5f) ? 1.0f : -1.0f;
case LfoShape::SampleHold:
{
if (phase < prevPhase)
holdValue = rng.nextFloat() * 2.0f - 1.0f;
return holdValue;
}
case LfoShape::StepSeq:
{
const int idx = juce::jlimit (0, shapeSteps - 1, (int) (p * shapeSteps));
@@ -97,27 +97,40 @@ float LFO::shapeValue() noexcept
float LFO::process() noexcept
{
return advance (1);
}
float LFO::advance (int numSamples) noexcept
{
if (numSamples <= 0)
return value;
// Start delay.
if (delayCounter > 0.0)
{
delayCounter -= 1.0;
value = 0.0f;
return 0.0f;
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 += 1.0;
fadeCounter += numSamples;
if (fadeSeconds > 0.0)
fadeVal = (float) juce::jlimit (0.0, 1.0, fadeCounter / (fadeSeconds * sr));
else
fadeVal = 1.0f;
}
prevPhase = phase;
phase += rateHz / sr;
phase -= std::floor (phase);
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;
+4 -2
View File
@@ -15,6 +15,7 @@ class LFO
public:
static constexpr int kShapePoints = 64;
LFO() { reset(); }
void prepare (double sampleRate) { sr = sampleRate; reset(); }
void reset();
@@ -24,7 +25,8 @@ public:
void setShapeData (const std::vector<float>& data, int steps);
float process() noexcept; // advance and return current value
float getPhase() const noexcept { return (float) phase; }
float advance (int numSamples) noexcept;
float getPhase() const noexcept { return (float) (phase + phaseOffset - std::floor (phase + phaseOffset)); }
float getValue() const noexcept { return value; }
const std::vector<float>& getShapeData() const noexcept { return shapeBuffer; }
@@ -35,6 +37,7 @@ private:
double tempo = 120.0;
double phase = 0.0;
double phaseOffset = 0.0;
double rateHz = 1.0;
float value = 0.0f;
@@ -55,7 +58,6 @@ private:
juce::Random rng;
float holdValue = 0.0f;
double prevPhase = 0.0;
float shapeValue() noexcept;
};