From 40dc0d410545670bf1e56e1228a4e8b9e70ba095 Mon Sep 17 00:00:00 2001 From: Igor Barcik Date: Wed, 9 Sep 2026 14:33:13 +0200 Subject: [PATCH] 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. --- Source/LFO.cpp | 59 ++++++++++++++++++++++++++++++-------------------- Source/LFO.h | 6 +++-- 2 files changed, 40 insertions(+), 25 deletions(-) diff --git a/Source/LFO.cpp b/Source/LFO.cpp index 67573fb..9530a24 100644 --- a/Source/LFO.cpp +++ b/Source/LFO.cpp @@ -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& data, int steps) @@ -54,17 +57,18 @@ void LFO::setShapeData (const std::vector& 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::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; diff --git a/Source/LFO.h b/Source/LFO.h index ad57a53..2974e9c 100644 --- a/Source/LFO.h +++ b/Source/LFO.h @@ -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& 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& 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; };