feat(modulation): add envelopes, LFOs and modulation matrix

This commit is contained in:
2026-09-08 14:55:21 +02:00
parent fc9d16b302
commit 44960b9acb
6 changed files with 458 additions and 0 deletions
+126
View File
@@ -0,0 +1,126 @@
#include "LFO.h"
namespace serum
{
void LFO::reset()
{
phase = 0.0;
value = 0.0f;
delayCounter = 0.0;
fadeCounter = 0.0;
fadeVal = 1.0f;
holdValue = 0.0f;
prevPhase = 0.0;
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;
}
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);
phase = juce::jlimit (0.0f, 1.0f, ph);
if (sync)
rateHz = maps::beatToMultiplier (beat) * (tempo / 60.0);
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;
}
}
void LFO::setShapeData (const std::vector<float>& data, int steps)
{
if (data.empty())
return;
shapeSteps = juce::jlimit (2, kShapePoints, steps);
shapeBuffer.assign (data.begin(), data.end());
shapeBuffer.resize ((size_t) kShapePoints, 0.0f);
}
float LFO::shapeValue() noexcept
{
const float p = (float) phase;
switch ((LfoShape) shape)
{
case LfoShape::Sine:
return std::sin (p * 6.28318530717958647692f);
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:
{
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));
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
{
// Start delay.
if (delayCounter > 0.0)
{
delayCounter -= 1.0;
value = 0.0f;
return 0.0f;
}
// Fade-in ramp.
if (fadeVal < 1.0f)
{
fadeCounter += 1.0;
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);
value = shapeValue() * fadeVal;
return value;
}
} // namespace serum