#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& 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::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