refactor(source): move every implementation into its module directory

- 36 files move into the module whose AGENTS.md already claimed them:
  Engine/, Synth/, Modulation/, Params/, Plugin/, GUI/, EffectUnits/, Resources/
- Source/engine, modulation, params, plugin and synth become Engine,
  Modulation, Params, Plugin and Synth, matching the directories that already
  hold code
- ExactCompare.h stays at the Source/ root as the one shared header
- SERUMALT_INCLUDE_DIRS adds one include directory per module, so the 31
  parent-relative includes become plain names with no path prefix
- the module AGENTS.md files list owned files instead of describing a move
  that had not happened, and README.md and AGENTS.md describe the real tree

Verified: the harness passes CTest, the Linux VST3 and Standalone link, the
Windows cross-build passes its module checks, each configure exposes only its
own targets, no first-party warning line appears, and the generated Windows
manifest is unchanged.
This commit is contained in:
biggy committed 2026-10-06 09:17:49 +02:00
1 parent facd5c5b5e
commit 4b0db50271
70 files changed
+115 -111

No files matched your search

+166
View File
@@ -0,0 +1,166 @@
#include "Oscillator.h"
#include "ExactCompare.h"
namespace serum
{
void Oscillator::reset()
{
for (auto& v : voices)
v = SubVoice {};
activeUnison = 1;
initializedUnison = 0;
paramsValid = false;
}
void Oscillator::noteOn (const OscParams& p, juce::uint32 seed)
{
rng.setSeed (seed);
initializedUnison = 0;
paramsValid = false;
setParams (p);
}
void Oscillator::setParams (const OscParams& p) noexcept
{
const int uni = juce::jlimit (1, kMaxUnison, p.unison);
const bool countChanged = ! paramsValid || uni != activeUnison;
const bool detuneChanged = countChanged || changed (p.detune, cachedParams.detune);
const bool panChanged = countChanged || changed (p.pan, cachedParams.pan)
|| changed (p.spread, cachedParams.spread);
if (! detuneChanged && ! panChanged)
return;
const double basePhase = (double) p.phase * kTwoPi;
for (int v = initializedUnison; v < uni; ++v)
{
// Even phase spacing distributes the initial unison phases.
double offset = (uni > 1) ? ((double) v / (double) uni) * kTwoPi : 0.0;
double random = rng.nextFloat() * (double) p.randPhase * kTwoPi;
voices[(size_t) v].phase = basePhase + offset + random;
}
initializedUnison = juce::jmax (initializedUnison, uni);
const float unisonLevel = 1.0f / std::sqrt ((float) uni);
for (int v = 0; v < uni; ++v)
{
auto& sv = voices[(size_t) v];
if (detuneChanged)
{
// Detune: linear spread in cents, 0..50 cents at full depth.
double detuneCents = 0.0;
if (uni > 1)
detuneCents = (double) p.detune * 50.0 * ((double) (v - (uni - 1) / 2.0) / (double) ((uni - 1) / 2.0));
sv.detuneRatio = std::pow (2.0, detuneCents / 1200.0);
}
if (panChanged)
{
// Stereo spread.
float panPos = (uni > 1) ? ((float) v / (float) (uni - 1) - 0.5f) * 2.0f * p.spread : 0.0f;
panPos = juce::jlimit (-1.0f, 1.0f, panPos + p.pan);
// Constant-power pan.
const float panAngle = (panPos + 1.0f) * 0.5f * juce::MathConstants<float>::halfPi;
sv.panL = std::cos (panAngle);
sv.panR = std::sin (panAngle);
}
if (countChanged)
{
// Gain scaling with a centre emphasis for odd unison counts.
float lvl = unisonLevel;
if ((uni & 1) && v == uni / 2)
lvl *= 1.3f;
sv.level = lvl;
}
}
activeUnison = uni;
cachedParams = p;
paramsValid = true;
}
float Oscillator::warpPhase (float phase, const OscParams& p) const noexcept
{
const float amt = p.warpAmt;
switch ((WarpMode) p.warp)
{
case WarpMode::None: return phase;
case WarpMode::BendPlus: return phase + amt * 0.5f * std::sin (phase * (float) kTwoPi);
case WarpMode::BendMinus: return phase - amt * 0.5f * std::sin (phase * (float) kTwoPi);
case WarpMode::Sync: return std::fmod (phase * (1.0f + amt * 7.0f), 1.0f);
case WarpMode::Asym:
if (amt < 0.001f) return phase;
return std::pow (phase, 1.0f + amt * 3.0f);
case WarpMode::Mirror:
{
const float mirror = 2.0f * std::abs (phase - 0.5f);
return phase + (mirror - phase) * amt;
}
case WarpMode::PWM: return phase;
case WarpMode::Fold: return phase;
case WarpMode::Count: return phase;
default: return phase;
}
}
float Oscillator::warpSample (float sample, const OscParams& p) const noexcept
{
const float amt = p.warpAmt;
switch ((WarpMode) p.warp)
{
case WarpMode::PWM:
{
const float threshold = (2.0f * amt - 1.0f) * 0.9f;
return std::tanh ((sample - threshold) * 4.0f);
}
case WarpMode::Fold:
return std::sin (sample * (1.0f + amt * 5.0f) * 1.5707963267948966f);
case WarpMode::None:
case WarpMode::BendPlus:
case WarpMode::BendMinus:
case WarpMode::Sync:
case WarpMode::Asym:
case WarpMode::Mirror:
case WarpMode::Count:
default:
return sample;
}
}
void Oscillator::processAdd (const Wavetable& wt, const OscParams& p, double freqHz,
float& outL, float& outR) noexcept
{
if (! p.enabled || p.level <= 0.0f || freqHz <= 0.0)
return;
const int uni = activeUnison;
const float framePos = p.wtPos * 255.0f;
const double phaseInc = kTwoPi * freqHz / sr;
float accL = 0.0f;
float accR = 0.0f;
for (int v = 0; v < uni; ++v)
{
auto& sv = voices[(size_t) v];
sv.phase += phaseInc * sv.detuneRatio;
sv.phase -= std::floor (sv.phase * (1.0 / kTwoPi)) * kTwoPi;
if (sv.phase >= kTwoPi) sv.phase -= kTwoPi;
if (sv.phase < 0.0) sv.phase += kTwoPi;
float phase01 = (float) (sv.phase * (1.0 / kTwoPi));
float sample = wt.readSafe (framePos, warpPhase (phase01, p));
sample = warpSample (sample, p);
const float gain = sv.level * p.level;
accL += sample * gain * sv.panL;
accR += sample * gain * sv.panR;
}
outL += accL;
outR += accR;
}
} // namespace serum