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.
140 lines
3.9 KiB
C++
140 lines
3.9 KiB
C++
#include "LFO.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 (delay != prevDelayParam || 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 LFO::shapeValue() noexcept
|
|
{
|
|
const float p = getPhase();
|
|
switch ((LfoShape) shape)
|
|
{
|
|
case LfoShape::Sine:
|
|
return std::sin (p * juce::MathConstants<float>::twoPi);
|
|
case LfoShape::Triangle:
|
|
return 1.0f - 4.0f * std::abs (p - 0.5f);
|
|
case LfoShape::Saw:
|
|
return 2.0f * p - 1.0f;
|
|
case LfoShape::Square:
|
|
return (p < 0.5f) ? 1.0f : -1.0f;
|
|
case LfoShape::SampleHold:
|
|
return holdValue;
|
|
case LfoShape::StepSeq:
|
|
{
|
|
const int idx = juce::jlimit (0, shapeSteps - 1, (int) (p * shapeSteps));
|
|
return shapeBuffer[(size_t) idx];
|
|
}
|
|
case LfoShape::Freehand:
|
|
{
|
|
const float pos = p * (float) (shapeSteps - 1);
|
|
const int i0 = (int) pos;
|
|
const int i1 = juce::jmin (i0 + 1, shapeSteps - 1);
|
|
const float frac = pos - (float) i0;
|
|
return shapeBuffer[(size_t) i0] * (1.0f - frac) + shapeBuffer[(size_t) i1] * frac;
|
|
}
|
|
default:
|
|
return 0.0f;
|
|
}
|
|
}
|
|
|
|
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
|