diff --git a/Source/Envelope.cpp b/Source/Envelope.cpp new file mode 100644 index 0000000..9c1140e --- /dev/null +++ b/Source/Envelope.cpp @@ -0,0 +1,116 @@ +#include "Envelope.h" + +namespace serum +{ + +void Envelope::reset() +{ + stage = Stage::Idle; + value = 0.0f; + startValue = endValue = 0.0f; + elapsed = duration = 0.0; +} + +void Envelope::noteOn() +{ + stage = Stage::Attack; + elapsed = 0.0; + duration = attackSamples; + startValue = value; // retrigger from current level + endValue = 1.0f; +} + +void Envelope::noteOff() +{ + if (stage == Stage::Idle) + return; + + stage = Stage::Release; + elapsed = 0.0; + duration = releaseSamples; + startValue = value; +} + +void Envelope::setParams (float attackSec, float decaySec, float sustainLevel, float releaseSec, float curve) +{ + sustain = juce::jlimit (0.0f, 1.0f, sustainLevel); + curve = juce::jlimit (0.0f, 1.0f, curve); + + attackSamples = std::max (1.0, (double) attackSec * sr); + decaySamples = std::max (1.0, (double) decaySec * sr); + releaseSamples = std::max (1.0, (double) releaseSec * sr); + + // curve 0 -> fast attack / long release curve; curve 1 -> slow attack / fast decay + attackShape = 0.3 + curve * 2.7; // 0.3 .. 3.0 + decayShape = 3.0 - curve * 2.7; // 3.0 .. 0.3 + + shape.attack = attackSec; + shape.decay = decaySec; + shape.sustain = sustain; + shape.release = releaseSec; + shape.curve = curve; +} + +float Envelope::process() noexcept +{ + switch (stage) + { + case Stage::Idle: + value = 0.0f; + return 0.0f; + + case Stage::Attack: + elapsed += 1.0; + if (duration <= 1.0 || elapsed >= duration) + { + value = endValue; + stage = Stage::Decay; + elapsed = 0.0; + duration = decaySamples; + startValue = value; + endValue = sustain; + } + else + { + const double p = elapsed / duration; + value = startValue + (endValue - startValue) * (float) std::pow (p, attackShape); + } + return value; + + case Stage::Decay: + elapsed += 1.0; + if (duration <= 1.0 || elapsed >= duration) + { + value = sustain; + stage = Stage::Sustain; + } + else + { + const double p = elapsed / duration; + value = endValue + (startValue - endValue) * (float) std::pow (1.0 - p, decayShape); + } + return value; + + case Stage::Sustain: + value = sustain; + return value; + + case Stage::Release: + elapsed += 1.0; + if (duration <= 1.0 || elapsed >= duration) + { + value = 0.0f; + stage = Stage::Idle; + } + else + { + const double p = elapsed / duration; + value = startValue * (float) std::pow (1.0 - p, decayShape); + } + return value; + } + + return 0.0f; +} + +} // namespace serum diff --git a/Source/Envelope.h b/Source/Envelope.h new file mode 100644 index 0000000..13aa2fc --- /dev/null +++ b/Source/Envelope.h @@ -0,0 +1,48 @@ +#pragma once + +#include + +namespace serum +{ + +// =========================================================================== +// ADSR envelope with curve tension. Durations are in seconds; the sustain +// level is 0..1. Retriggering (noteOn while active) starts from the current +// value for click-free legato behaviour. +// =========================================================================== +class Envelope +{ +public: + void prepare (double sampleRate) { sr = sampleRate; reset(); } + void reset(); + + void noteOn(); + void noteOff(); + + void setParams (float attackSec, float decaySec, float sustain, float releaseSec, float curve); + + float process() noexcept; // advance one sample and return the value + float getValue() const noexcept { return value; } + bool isActive() const noexcept { return stage != Stage::Idle; } + + // Snapshot for GUI rendering. + struct Shape { float attack = 0, decay = 0, sustain = 0, release = 0, curve = 0.5f; }; + Shape getShape() const noexcept { return shape; } + +private: + enum class Stage { Idle, Attack, Decay, Sustain, Release }; + + Stage stage = Stage::Idle; + double sr = 44100.0; + + float value = 0.0f; + float startValue = 0.0f, endValue = 0.0f; + double elapsed = 0.0, duration = 0.0; + double attackShape = 1.0, decayShape = 1.0; + + float sustain = 0.7f; + double attackSamples = 0.0, decaySamples = 0.0, releaseSamples = 0.0; + Shape shape; +}; + +} // namespace serum diff --git a/Source/LFO.cpp b/Source/LFO.cpp new file mode 100644 index 0000000..67573fb --- /dev/null +++ b/Source/LFO.cpp @@ -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& 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 diff --git a/Source/LFO.h b/Source/LFO.h new file mode 100644 index 0000000..ad57a53 --- /dev/null +++ b/Source/LFO.h @@ -0,0 +1,63 @@ +#pragma once + +#include +#include "Params.h" + +namespace serum +{ + +// =========================================================================== +// Free-running LFO with tempo sync, seven shapes (sine/tri/saw/square/S&H/ +// step-sequencer/freehand), adjustable phase, fade-in and start delay. +// =========================================================================== +class LFO +{ +public: + static constexpr int kShapePoints = 64; + + void prepare (double sampleRate) { sr = sampleRate; reset(); } + void reset(); + + void setParams (float rateNorm, bool sync, float beat, int shape, float phase, + float fade, float delay); + void setTempo (double bpm) noexcept { tempo = bpm; } + void setShapeData (const std::vector& data, int steps); + + float process() noexcept; // advance and return current value + float getPhase() const noexcept { return (float) phase; } + float getValue() const noexcept { return value; } + + const std::vector& getShapeData() const noexcept { return shapeBuffer; } + int getShapeSteps() const noexcept { return shapeSteps; } + +private: + double sr = 44100.0; + double tempo = 120.0; + + double phase = 0.0; + double rateHz = 1.0; + float value = 0.0f; + + int shape = 0; + bool sync = false; + float beat = 0.25f; + + double delaySeconds = 0.0; + double fadeSeconds = 0.0; + double delayCounter = 0.0; // samples remaining before start + double fadeCounter = 0.0; // samples elapsed in fade + float fadeVal = 1.0f; + float prevDelayParam = -1.0f; + float prevFadeParam = -1.0f; + + std::vector shapeBuffer; + int shapeSteps = 16; + + juce::Random rng; + float holdValue = 0.0f; + double prevPhase = 0.0; + + float shapeValue() noexcept; +}; + +} // namespace serum diff --git a/Source/ModulationMatrix.cpp b/Source/ModulationMatrix.cpp new file mode 100644 index 0000000..146ad13 --- /dev/null +++ b/Source/ModulationMatrix.cpp @@ -0,0 +1,62 @@ +#include "ModulationMatrix.h" + +namespace serum +{ + +bool ModulationMatrix::addConnection (ModSource source, ModTarget target, float depth, bool bipolar) +{ + if (target == ModTarget::None || (int) connections.size() >= kMaxConnections) + return false; + connections.push_back ({ source, target, depth, bipolar }); + return true; +} + +void ModulationMatrix::removeConnection (int index) +{ + if (index >= 0 && index < (int) connections.size()) + connections.erase (connections.begin() + index); +} + +void ModulationMatrix::removeAllWithTarget (ModTarget target) +{ + connections.erase (std::remove_if (connections.begin(), connections.end(), + [target] (const ModConnection& c) { return c.target == target; }), + connections.end()); +} + +juce::ValueTree ModulationMatrix::toValueTree() const +{ + juce::ValueTree tree ("MODMATRIX"); + for (const auto& c : connections) + { + juce::ValueTree con ("CONNECTION"); + con.setProperty ("source", modSourceToString (c.source), nullptr); + con.setProperty ("target", modTargetToString (c.target), nullptr); + con.setProperty ("depth", c.depth, nullptr); + con.setProperty ("bipolar", c.bipolar, nullptr); + tree.appendChild (con, nullptr); + } + return tree; +} + +void ModulationMatrix::fromValueTree (const juce::ValueTree& tree) +{ + connections.clear(); + if (! tree.isValid()) + return; + + for (const auto& con : tree) + { + if (! con.hasType ("CONNECTION")) + continue; + ModConnection c; + c.source = modSourceFromString (con.getProperty ("source").toString()); + c.target = modTargetFromString (con.getProperty ("target").toString()); + c.depth = (float) con.getProperty ("depth", 0.0); + c.bipolar = (bool) con.getProperty ("bipolar", false); + if (c.target != ModTarget::None && (int) connections.size() < kMaxConnections) + connections.push_back (c); + } +} + +} // namespace serum diff --git a/Source/ModulationMatrix.h b/Source/ModulationMatrix.h new file mode 100644 index 0000000..cd1969d --- /dev/null +++ b/Source/ModulationMatrix.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include "Params.h" + +namespace serum +{ + +// =========================================================================== +// A single modulation connection: source -> target with a static depth and a +// bipolar flag. One level of modulation only (no modulation of modulation). +// =========================================================================== +struct ModConnection +{ + ModSource source = ModSource::Lfo1; + ModTarget target = ModTarget::None; + float depth = 0.0f; + bool bipolar = false; +}; + +// =========================================================================== +// Ordered collection of modulation connections. Serialisable to/from a +// juce::ValueTree so that connections survive preset save/load. +// =========================================================================== +class ModulationMatrix +{ +public: + static constexpr int kMaxConnections = 32; + + std::vector connections; + + bool addConnection (ModSource source, ModTarget target, float depth, bool bipolar); + void removeConnection (int index); + void removeAllWithTarget (ModTarget target); + void clear() { connections.clear(); } + + int size() const noexcept { return (int) connections.size(); } + + juce::ValueTree toValueTree() const; + void fromValueTree (const juce::ValueTree& tree); +}; + +} // namespace serum