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

+31
View File
@@ -0,0 +1,31 @@
# modulation module
## Owned files
- ModulationMatrix.h, ModulationMatrix.cpp
- MacroControls.h, MacroControls.cpp
- RAVEButton.h, RAVEButton.cpp, which define `RaveController`
## Rules
- Use `bool` return codes for bounded inserts (`addConnection`, `addAssignment`).
- Require a cap on connection and assignment counts (`kMaxConnections`, `kMaxAssignments`).
- Serialise and restore state through `juce::ValueTree` so presets survive save and load.
- Never accept a `ModTarget::None` connection; reject it and return false.
- RaveController applies a non-destructive boost to render settings. It does not write APVTS parameters or keep a restoration snapshot.
- Keep the existing RAVEButton filenames unless a rename is explicitly in scope.
## GUI/audio state
Hold `Engine::getControlLock()` while reading or editing the control-side matrix,
macro assignments, or LFO shapes. The audio thread tries that lock once per block
and copies into reserved audio-side storage. If the lock is busy, it keeps the
previous settings and continues rendering. Never iterate the control-side vectors
from the render path.
Master and FX-mix modulation use global sources directly. Per-voice sources for
those destinations come from the newest active voice.
If `addConnection` or `addAssignment` returns false, handle the failed insert.
This file refines the repository-root AGENTS.md for the related sources.
+100
View File
@@ -0,0 +1,100 @@
#include "MacroControls.h"
#include "ExactCompare.h"
namespace serum
{
bool MacroControls::addAssignment (int macro, ModTarget target, float depth)
{
if (macro < 0 || macro >= kNumMacros
|| (int) target < 0 || (int) target >= kNumModTargets
|| ! std::isfinite (depth) || (int) assignments[(size_t) macro].size() >= kMaxAssignments)
return false;
assignments[(size_t) macro].push_back ({ target, juce::jlimit (-1.0f, 1.0f, depth) });
return true;
}
void MacroControls::removeAssignment (int macro, int index)
{
macro = juce::jlimit (0, kNumMacros - 1, macro);
auto& vec = assignments[(size_t) macro];
if (index >= 0 && index < (int) vec.size())
vec.erase (vec.begin() + index);
}
void MacroControls::clear()
{
for (auto& a : assignments)
a.clear();
}
juce::String MacroControls::macroName (int index)
{
switch (index)
{
case 0: return "Energy";
case 1: return "Width";
case 2: return "Drive";
case 3: return "Atmosphere";
default: return {};
}
}
juce::ValueTree MacroControls::toValueTree() const
{
MacroControls validated;
for (int m = 0; m < kNumMacros; ++m)
for (const auto& a : assignments[(size_t) m])
{
if ((int) validated.assignments[(size_t) m].size() >= kMaxAssignments)
break;
if (! validated.addAssignment (m, a.target, a.depth))
continue;
}
juce::ValueTree tree ("MACROS");
for (int m = 0; m < kNumMacros; ++m)
{
juce::ValueTree mac ("MACRO");
mac.setProperty ("index", m, nullptr);
for (const auto& a : validated.assignments[(size_t) m])
{
juce::ValueTree asg ("ASSIGN");
asg.setProperty ("target", modTargetToString (a.target), nullptr);
asg.setProperty ("depth", a.depth, nullptr);
mac.appendChild (asg, nullptr);
}
tree.appendChild (mac, nullptr);
}
return tree;
}
void MacroControls::fromValueTree (const juce::ValueTree& tree)
{
clear();
if (! tree.hasType ("MACROS"))
return;
for (const auto& mac : tree)
{
if (! mac.hasType ("MACRO"))
continue;
const double savedIndex = (double) mac.getProperty ("index", -1);
if (! std::isfinite (savedIndex) || savedIndex < 0.0 || savedIndex >= kNumMacros
|| ! isWholeNumber (savedIndex))
continue;
const int index = (int) savedIndex;
for (const auto& asg : mac)
{
if ((int) assignments[(size_t) index].size() >= kMaxAssignments)
break;
if (! asg.hasType ("ASSIGN"))
continue;
if (! addAssignment (index,
modTargetFromString (asg.getProperty ("target").toString()),
(float) asg.getProperty ("depth", 0.0)))
continue;
}
}
}
} // namespace serum
+40
View File
@@ -0,0 +1,40 @@
#pragma once
#include <JuceHeader.h>
#include "Params.h"
namespace serum
{
// ===========================================================================
// One assignable destination for a macro knob.
// ===========================================================================
struct MacroAssignment
{
ModTarget target = ModTarget::None;
float depth = 0.0f;
};
// ===========================================================================
// Four macros (Energy, Width, Drive, Atmosphere). Macro *values* are APVTS
// parameters; this class stores the assignable destinations and serialises
// them with the preset.
// ===========================================================================
class MacroControls
{
public:
static constexpr int kMaxAssignments = 8;
std::array<std::vector<MacroAssignment>, kNumMacros> assignments;
bool addAssignment (int macro, ModTarget target, float depth);
void removeAssignment (int macro, int index);
void clear();
juce::ValueTree toValueTree() const;
void fromValueTree (const juce::ValueTree& tree);
static juce::String macroName (int index);
};
} // namespace serum
+76
View File
@@ -0,0 +1,76 @@
#include "ModulationMatrix.h"
namespace serum
{
bool ModulationMatrix::addConnection (ModSource source, ModTarget target, float depth, bool bipolar)
{
if ((int) source < 0 || (int) source >= kNumModSources
|| (int) target < 0 || (int) target >= kNumModTargets
|| ! std::isfinite (depth) || (int) connections.size() >= kMaxConnections)
return false;
connections.push_back ({ source, target, juce::jlimit (-1.0f, 1.0f, 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
{
ModulationMatrix validated;
for (const auto& c : connections)
{
if (validated.size() >= kMaxConnections)
break;
if (! validated.addConnection (c.source, c.target, c.depth, c.bipolar))
continue;
}
juce::ValueTree tree ("MODMATRIX");
for (const auto& c : validated.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.hasType ("MODMATRIX"))
return;
for (const auto& con : tree)
{
if ((int) connections.size() >= kMaxConnections)
break;
if (! con.hasType ("CONNECTION"))
continue;
const auto sourceName = con.getProperty ("source").toString();
const auto source = modSourceFromString (sourceName);
if (sourceName.isEmpty() || modSourceToString (source) != sourceName)
continue;
if (! addConnection (source,
modTargetFromString (con.getProperty ("target").toString()),
(float) con.getProperty ("depth", 0.0),
(bool) con.getProperty ("bipolar", false)))
continue;
}
}
} // namespace serum
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include <JuceHeader.h>
#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<ModConnection> 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
+38
View File
@@ -0,0 +1,38 @@
#include "RAVEButton.h"
#include "SynthVoice.h"
#include "FXProcessor.h"
namespace serum
{
void RaveController::apply (RenderContext& context, FxSlotParams* slots, int numSlots) noexcept
{
// Boost the static rendering parameters.
context.oscA.unison = juce::jlimit (8, kMaxUnison, context.oscA.unison);
context.oscB.unison = juce::jlimit (8, kMaxUnison, context.oscB.unison);
context.oscA.spread = 1.0f;
context.oscB.spread = 1.0f;
context.oscA.detune = 1.0f;
context.oscB.detune = 0.7f;
// Apply drive boosts.
context.filters.f1Drive = 0.6f;
context.filters.f2Drive = 0.6f;
// Boost FX-specific params (Hyper intensity / Reverb mix).
if (slots == nullptr)
return;
for (int i = 0; i < juce::jlimit (0, kNumFxSlots, numSlots); ++i)
{
auto& slot = slots[i];
if (slot.type == (int) FxType::Hyper)
slot.p[0] = 1.0f; // OTT intensity 100%
else if (slot.type == (int) FxType::Reverb)
{
const float mix = std::isfinite (slot.mix) ? slot.mix : 0.0f;
slot.mix = juce::jlimit (0.0f, 1.0f, mix + 0.4f); // reverb send +6dB-ish
}
}
}
} // namespace serum
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include <JuceHeader.h>
namespace serum
{
struct RenderContext;
struct FxSlotParams;
// ===========================================================================
// RAVE — non-destructive "make it huge" control. While enabled, rendering
// boosts unison, width, drive, OTT and reverb in the current block's snapshots;
// the underlying parameters remain unchanged when toggling on or off.
// ===========================================================================
class RaveController
{
public:
static void apply (RenderContext& context, FxSlotParams* slots, int numSlots) noexcept;
};
} // namespace serum