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

+43
View File
@@ -0,0 +1,43 @@
# plugin module
## Owned files
- PluginProcessor.h, PluginProcessor.cpp
## Rules
- Keep this the JUCE `AudioProcessor` boundary: APVTS, engine, RAVE, presets and state.
- Declare the full parameter layout in `createParameterLayout()`.
- Use `juce::ScopedNoDenormals` in `processBlock`.
- Guard `getRawParameterValue` and `getParameter` results before dereferencing them.
- Return silently on null or invalid state XML and invalid ValueTrees.
- Use raw `new` only for JUCE-owned objects (`createEditor`, `createPluginFilter`).
- Hold the engine control lock across preset and state changes; preserve parameter IDs and normalized values for saved-project compatibility.
- Factory presets reset sound parameters before applying overrides, while preserving UI scale.
- Save one copy of each custom state section. LFO shape arrays are stored as JSON text inside XML.
- RAVE is an ordinary parameter that enables a render-time boost; it does not rewrite other parameters.
## IF-THEN
- IF state XML is null or invalid THEN return without touching the APVTS.
- IF a parameter is missing from a preset load THEN skip it with an `if (auto* param = ...)` guard.
## Examples
```cpp
// BAD: dereferences a possibly null XML result
auto xml = getXmlFromBinary (data, size);
auto state = juce::ValueTree::fromXml (*xml);
```
```cpp
// GOOD: null guard, silent return
std::unique_ptr<juce::XmlElement> xml (getXmlFromBinary (data, size));
if (xml == nullptr)
return;
juce::ValueTree state = juce::ValueTree::fromXml (*xml);
if (! state.isValid())
return;
```
This file overrides /AGENTS.md where they conflict.
+453
View File
@@ -0,0 +1,453 @@
#include "PluginProcessor.h"
#include "PluginEditor.h"
#include "Presets/FactoryPresets.h"
#include "ExactCompare.h"
namespace serum
{
namespace
{
std::unique_ptr<juce::AudioParameterFloat> f (const char* id, const juce::String& name, float def)
{
return std::make_unique<juce::AudioParameterFloat> (juce::ParameterID { id, 1 }, name,
juce::NormalisableRange<float> (0.0f, 1.0f), def);
}
}
// ---------------------------------------------------------------------------
SerumAltAudioProcessor::SerumAltAudioProcessor()
: AudioProcessor (BusesProperties().withInput ("Input", juce::AudioChannelSet::stereo(), false)
.withOutput ("Output", juce::AudioChannelSet::stereo(), true)),
parameters (*this, nullptr, juce::Identifier ("SerumAlt"), createParameterLayout())
{
}
SerumAltAudioProcessor::~SerumAltAudioProcessor() = default;
juce::AudioProcessorValueTreeState::ParameterLayout SerumAltAudioProcessor::createParameterLayout()
{
std::vector<std::unique_ptr<juce::RangedAudioParameter>> params;
// Global
params.push_back (f (ids::master, "Master", 0.8f));
params.push_back (f (ids::uiScale, "UI Scale", 0.25f));
params.push_back (f (ids::rave, "RAVE", 0.0f));
// Oscillator A
params.push_back (f (ids::oscAOn, "Osc A On", 1.0f));
params.push_back (f (ids::oscAWave, "Osc A Wave", 0.0f));
params.push_back (f (ids::oscAWtPos, "Osc A WT Pos", 0.0f));
params.push_back (f (ids::oscAWarp, "Osc A Warp", 0.0f));
params.push_back (f (ids::oscAWarpAmt, "Osc A Warp Amt", 0.0f));
params.push_back (f (ids::oscACoarse, "Osc A Coarse", 0.5f));
params.push_back (f (ids::oscAFine, "Osc A Fine", 0.5f));
params.push_back (f (ids::oscALevel, "Osc A Level", 0.8f));
params.push_back (f (ids::oscAPan, "Osc A Pan", 0.5f));
params.push_back (f (ids::oscAUnison, "Osc A Unison", 0.0f));
params.push_back (f (ids::oscADetune, "Osc A Detune", 0.0f));
params.push_back (f (ids::oscASpread, "Osc A Spread", 0.0f));
params.push_back (f (ids::oscAPhase, "Osc A Phase", 0.0f));
params.push_back (f (ids::oscARandPh, "Osc A Rand Phase", 0.0f));
// Oscillator B
params.push_back (f (ids::oscBOn, "Osc B On", 0.0f));
params.push_back (f (ids::oscBWave, "Osc B Wave", 1.0f / 9.0f));
params.push_back (f (ids::oscBWtPos, "Osc B WT Pos", 0.0f));
params.push_back (f (ids::oscBWarp, "Osc B Warp", 0.0f));
params.push_back (f (ids::oscBWarpAmt, "Osc B Warp Amt", 0.0f));
params.push_back (f (ids::oscBCoarse, "Osc B Coarse", 0.5f));
params.push_back (f (ids::oscBFine, "Osc B Fine", 0.5f));
params.push_back (f (ids::oscBLevel, "Osc B Level", 0.5f));
params.push_back (f (ids::oscBPan, "Osc B Pan", 0.5f));
params.push_back (f (ids::oscBUnison, "Osc B Unison", 0.0f));
params.push_back (f (ids::oscBDetune, "Osc B Detune", 0.0f));
params.push_back (f (ids::oscBSpread, "Osc B Spread", 0.0f));
params.push_back (f (ids::oscBPhase, "Osc B Phase", 0.0f));
params.push_back (f (ids::oscBRandPh, "Osc B Rand Phase", 0.0f));
// Sub
params.push_back (f (ids::subOn, "Sub On", 0.0f));
params.push_back (f (ids::subShape, "Sub Shape", 0.0f));
params.push_back (f (ids::subOct, "Sub Octave", 1.0f / 2.0f));
params.push_back (f (ids::subLevel, "Sub Level", 0.5f));
// Noise
params.push_back (f (ids::noiseOn, "Noise On", 0.0f));
params.push_back (f (ids::noiseType, "Noise Type", 0.0f));
params.push_back (f (ids::noiseLevel, "Noise Level", 0.5f));
// Filter 1
params.push_back (f (ids::f1On, "Filter 1 On", 1.0f));
params.push_back (f (ids::f1Type, "Filter 1 Type", 0.0f));
params.push_back (f (ids::f1Cutoff, "Filter 1 Cutoff", 0.65f));
params.push_back (f (ids::f1Res, "Filter 1 Res", 0.05f));
params.push_back (f (ids::f1Drive, "Filter 1 Drive", 0.0f));
params.push_back (f (ids::f1Key, "Filter 1 Keytrack", 0.0f));
params.push_back (f (ids::f1Slope, "Filter 1 Slope", 1.0f));
// Filter 2
params.push_back (f (ids::f2On, "Filter 2 On", 0.0f));
params.push_back (f (ids::f2Type, "Filter 2 Type", 0.0f));
params.push_back (f (ids::f2Cutoff, "Filter 2 Cutoff", 0.5f));
params.push_back (f (ids::f2Res, "Filter 2 Res", 0.0f));
params.push_back (f (ids::f2Drive, "Filter 2 Drive", 0.0f));
params.push_back (f (ids::f2Key, "Filter 2 Keytrack", 0.0f));
params.push_back (f (ids::f2Slope, "Filter 2 Slope", 1.0f));
params.push_back (f (ids::fRoute, "Filter Route", 0.0f));
params.push_back (f (ids::fMix, "Filter Mix", 0.5f));
params.push_back (f (ids::fOut, "Filter Out", 0.667f));
// Envelopes 1..4 (IDs come from the shared paramIds groups)
constexpr auto& envA = paramIds::envAttack;
constexpr auto& envD = paramIds::envDecay;
constexpr auto& envS = paramIds::envSustain;
constexpr auto& envR = paramIds::envRelease;
constexpr auto& envC = paramIds::envCurve;
const float envADef[4] = { 0.05f, 0.1f, 0.2f, 0.2f };
const float envDDef[4] = { 0.25f, 0.3f, 0.3f, 0.3f };
const float envSDef[4] = { 0.8f, 0.5f, 0.5f, 0.5f };
const float envRDef[4] = { 0.3f, 0.3f, 0.4f, 0.4f };
for (int i = 0; i < 4; ++i)
{
params.push_back (f (envA[i], juce::String ("Env ") + juce::String (i + 1) + " Attack", envADef[i]));
params.push_back (f (envD[i], juce::String ("Env ") + juce::String (i + 1) + " Decay", envDDef[i]));
params.push_back (f (envS[i], juce::String ("Env ") + juce::String (i + 1) + " Sustain", envSDef[i]));
params.push_back (f (envR[i], juce::String ("Env ") + juce::String (i + 1) + " Release", envRDef[i]));
params.push_back (f (envC[i], juce::String ("Env ") + juce::String (i + 1) + " Curve", 0.5f));
}
// LFOs 1..4 (IDs come from the shared paramIds groups)
constexpr auto& lfoRate = paramIds::lfoRate;
constexpr auto& lfoSync = paramIds::lfoSync;
constexpr auto& lfoBeat = paramIds::lfoBeat;
constexpr auto& lfoShape = paramIds::lfoShape;
constexpr auto& lfoPhase = paramIds::lfoPhase;
constexpr auto& lfoFade = paramIds::lfoFade;
constexpr auto& lfoDelay = paramIds::lfoDelay;
const float lfoShapeDef[4] = { 0.0f, 1.0f / 6.0f, 2.0f / 6.0f, 3.0f / 6.0f };
for (int i = 0; i < 4; ++i)
{
const juce::String n = juce::String ("LFO ") + juce::String (i + 1);
params.push_back (f (lfoRate[i], n + " Rate", 0.5f));
params.push_back (f (lfoSync[i], n + " Sync", 0.0f));
params.push_back (f (lfoBeat[i], n + " Beat", 0.5f));
params.push_back (f (lfoShape[i], n + " Shape", lfoShapeDef[i]));
params.push_back (f (lfoPhase[i], n + " Phase", 0.0f));
params.push_back (f (lfoFade[i], n + " Fade", 0.0f));
params.push_back (f (lfoDelay[i], n + " Delay", 0.0f));
}
// FX slots 1..8 (IDs come from the shared paramIds groups)
constexpr auto& fxType = paramIds::fxType;
constexpr auto& fxMix = paramIds::fxMix;
const char* const fxP[4][8] = { { paramIds::fxP1[0], paramIds::fxP1[1], paramIds::fxP1[2], paramIds::fxP1[3],
paramIds::fxP1[4], paramIds::fxP1[5], paramIds::fxP1[6], paramIds::fxP1[7] },
{ paramIds::fxP2[0], paramIds::fxP2[1], paramIds::fxP2[2], paramIds::fxP2[3],
paramIds::fxP2[4], paramIds::fxP2[5], paramIds::fxP2[6], paramIds::fxP2[7] },
{ paramIds::fxP3[0], paramIds::fxP3[1], paramIds::fxP3[2], paramIds::fxP3[3],
paramIds::fxP3[4], paramIds::fxP3[5], paramIds::fxP3[6], paramIds::fxP3[7] },
{ paramIds::fxP4[0], paramIds::fxP4[1], paramIds::fxP4[2], paramIds::fxP4[3],
paramIds::fxP4[4], paramIds::fxP4[5], paramIds::fxP4[6], paramIds::fxP4[7] } };
for (int i = 0; i < 8; ++i)
{
const juce::String n = juce::String ("FX ") + juce::String (i + 1);
params.push_back (f (fxType[i], n + " Type", 0.0f));
params.push_back (f (fxMix[i], n + " Mix", 0.5f));
params.push_back (f (fxP[0][i], n + " P1", 0.5f));
params.push_back (f (fxP[1][i], n + " P2", 0.5f));
params.push_back (f (fxP[2][i], n + " P3", 0.5f));
params.push_back (f (fxP[3][i], n + " P4", 0.5f));
}
// Macros (display names come from MacroControls::macroName, the single source)
for (int i = 0; i < kNumMacros; ++i)
params.push_back (f (paramIds::macros[i], MacroControls::macroName (i), 0.0f));
return { params.begin(), params.end() };
}
// ---------------------------------------------------------------------------
void SerumAltAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
engine.prepare (sampleRate, samplesPerBlock, parameters);
}
void SerumAltAudioProcessor::releaseResources()
{
engine.reset();
}
void SerumAltAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midi)
{
juce::ScopedNoDenormals noDenormals;
engine.processBlock (buffer, midi, parameters, getPlayHead());
}
juce::AudioProcessorEditor* SerumAltAudioProcessor::createEditor()
{
return new PluginEditor (*this);
}
// ---------------------------------------------------------------------------
// Programs / presets
// ---------------------------------------------------------------------------
int SerumAltAudioProcessor::getNumPrograms()
{
return (int) getFactoryPresets().size();
}
int SerumAltAudioProcessor::getCurrentProgram()
{
return currentProgram.load();
}
void SerumAltAudioProcessor::setCurrentProgram (int index)
{
if (getNumPrograms() > 0)
loadFactoryPreset (juce::jlimit (0, getNumPrograms() - 1, index));
}
const juce::String SerumAltAudioProcessor::getProgramName (int index)
{
const auto& presets = getFactoryPresets();
if (index >= 0 && index < (int) presets.size())
return presets[(size_t) index].name;
return {};
}
void SerumAltAudioProcessor::changeProgramName (int, const juce::String&)
{
}
int SerumAltAudioProcessor::getNumFactoryPresets() const
{
return (int) getFactoryPresets().size();
}
void SerumAltAudioProcessor::loadFactoryPreset (int index)
{
const auto& presets = getFactoryPresets();
if (index < 0 || index >= (int) presets.size())
return;
const juce::ScopedLock lock (engine.getControlLock());
const FactoryPreset& preset = presets[(size_t) index];
const auto* uiScaleParam = parameters.getParameter (ids::uiScale);
for (auto* param : getParameters())
if (param != nullptr && param != uiScaleParam)
param->setValueNotifyingHost (param->getDefaultValue());
for (const auto& kv : preset.params)
if (auto* param = parameters.getParameter (kv.first))
if (param != uiScaleParam && std::isfinite (kv.second))
param->setValueNotifyingHost (juce::jlimit (0.0f, 1.0f, kv.second));
// RAVE should start off for a freshly loaded preset.
if (auto* raveParam = parameters.getParameter (ids::rave))
raveParam->setValueNotifyingHost (0.0f);
auto& matrix = engine.getMatrix();
matrix.clear();
for (const auto& mod : preset.mods)
if (! matrix.addConnection (mod.source, mod.target, mod.depth, mod.bipolar))
continue;
auto& macros = engine.getMacros();
macros.clear();
for (const auto& ma : preset.macroAssigns)
if (! macros.addAssignment (ma.macro, ma.target, ma.depth))
continue;
restoreLfoShapesFromState ({});
// A preset starts from clean DSP state, so the previous preset's filter,
// delay and reverb tails cannot bleed into the new one.
engine.requestReset();
currentProgram.store (index);
}
// ---------------------------------------------------------------------------
// RAVE / UI scale
// ---------------------------------------------------------------------------
void SerumAltAudioProcessor::setRaveEnabled (bool enabled)
{
const juce::ScopedLock lock (engine.getControlLock());
if (auto* raveParam = parameters.getParameter (ids::rave))
{
raveParam->beginChangeGesture();
raveParam->setValueNotifyingHost (enabled ? 1.0f : 0.0f);
raveParam->endChangeGesture();
}
}
bool SerumAltAudioProcessor::isRaveEnabled() const
{
if (auto* raveParam = parameters.getRawParameterValue (ids::rave))
return raveParam->load() > 0.5f;
return false;
}
int SerumAltAudioProcessor::getUiScaleIndex() const
{
if (auto* p = parameters.getRawParameterValue (ids::uiScale))
{
const float value = p->load();
if (std::isfinite (value))
return (int) std::llround (juce::jlimit (0.0f, 1.0f, value) * 4.0f);
}
return 1;
}
void SerumAltAudioProcessor::setUiScaleIndex (int index)
{
const juce::ScopedLock lock (engine.getControlLock());
index = juce::jlimit (0, 4, index);
if (auto* p = parameters.getParameter (ids::uiScale))
p->setValueNotifyingHost ((float) index / 4.0f);
}
float SerumAltAudioProcessor::getUiScale() const
{
static constexpr float scales[5] = { 0.75f, 1.0f, 1.25f, 1.5f, 2.0f };
return scales[getUiScaleIndex()];
}
// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------
void SerumAltAudioProcessor::getStateInformation (juce::MemoryBlock& destData)
{
const juce::ScopedLock lock (engine.getControlLock());
auto state = parameters.copyState();
for (int i = state.getNumChildren(); --i >= 0;)
{
const auto child = state.getChild (i);
if (child.hasType ("MODMATRIX") || child.hasType ("MACROS") || child.hasType ("LFOSHAPES"))
state.removeChild (i, nullptr);
}
state.setProperty ("currentProgram", currentProgram.load(), nullptr);
state.appendChild (engine.getMatrix().toValueTree(), nullptr);
state.appendChild (engine.getMacros().toValueTree(), nullptr);
saveLfoShapesToState (state);
std::unique_ptr<juce::XmlElement> xml (state.createXml());
if (xml != nullptr)
copyXmlToBinary (*xml, destData);
}
void SerumAltAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
{
if (data == nullptr || sizeInBytes <= 0)
return;
std::unique_ptr<juce::XmlElement> xml (getXmlFromBinary (data, sizeInBytes));
if (xml == nullptr || ! xml->hasTagName ("SerumAlt"))
return;
juce::ValueTree state = juce::ValueTree::fromXml (*xml);
if (! state.hasType ("SerumAlt"))
return;
const juce::ScopedLock lock (engine.getControlLock());
// A persisted RAVE toggle is rendered non-destructively, so retain it.
parameters.replaceState (state);
engine.getMatrix().fromValueTree (state.getChildWithName ("MODMATRIX"));
engine.getMacros().fromValueTree (state.getChildWithName ("MACROS"));
restoreLfoShapesFromState (state);
// Loading a saved project is a patch change too: start from clean state.
engine.requestReset();
const double savedProgram = (double) state.getProperty ("currentProgram", 0);
currentProgram.store (std::isfinite (savedProgram)
? (int) juce::jlimit (0.0, (double) juce::jmax (0, getNumPrograms() - 1), savedProgram) : 0);
}
void SerumAltAudioProcessor::saveLfoShapesToState (juce::ValueTree& state) const
{
const juce::ScopedLock lock (engine.getControlLock());
juce::ValueTree tree ("LFOSHAPES");
for (int i = 0; i < kNumLfos; ++i)
{
const auto& source = engine.getLfos()[(size_t) i];
const auto& data = source.getShapeData();
juce::ValueTree lfo ("LFO");
lfo.setProperty ("index", i, nullptr);
lfo.setProperty ("steps", juce::jlimit (2, LFO::kShapePoints, source.getShapeSteps()), nullptr);
juce::Array<juce::var> arr;
for (int point = 0; point < juce::jmin (LFO::kShapePoints, (int) data.size()); ++point)
{
const float value = data[(size_t) point];
arr.add (std::isfinite (value) ? juce::jlimit (-1.0f, 1.0f, value) : 0.0f);
}
lfo.setProperty ("data", juce::JSON::toString (juce::var (arr), true), nullptr);
tree.appendChild (lfo, nullptr);
}
state.appendChild (tree, nullptr);
}
void SerumAltAudioProcessor::restoreLfoShapesFromState (const juce::ValueTree& state)
{
const juce::ScopedLock lock (engine.getControlLock());
std::vector<float> defaultShape ((size_t) LFO::kShapePoints);
for (int point = 0; point < LFO::kShapePoints; ++point)
defaultShape[(size_t) point] = (point % 2 == 0) ? 1.0f : -1.0f;
for (int index = 0; index < kNumLfos; ++index)
engine.setLfoShapeData (index, defaultShape, 16);
const juce::ValueTree tree = state.getChildWithName ("LFOSHAPES");
if (! tree.isValid())
return;
std::array<bool, kNumLfos> restored {};
for (const auto& lfo : tree)
{
if (! lfo.hasType ("LFO"))
continue;
const double savedIndex = (double) lfo.getProperty ("index", -1);
if (! std::isfinite (savedIndex) || savedIndex < 0.0 || savedIndex >= kNumLfos
|| ! isWholeNumber (savedIndex))
continue;
const int index = (int) savedIndex;
if (restored[(size_t) index])
continue;
const double savedSteps = (double) lfo.getProperty ("steps", 16);
const int steps = std::isfinite (savedSteps)
? (int) juce::jlimit (2.0, (double) LFO::kShapePoints, savedSteps) : 16;
juce::var shape = lfo.getProperty ("data");
if (shape.isString())
{
const auto text = shape.toString();
if (text.length() > 8192)
continue;
shape = juce::JSON::parse (text);
}
const auto* arr = shape.getArray();
if (arr == nullptr || arr->isEmpty())
continue;
const int numPoints = juce::jmin (LFO::kShapePoints, arr->size());
std::vector<float> data;
data.reserve ((size_t) numPoints);
for (int point = 0; point < numPoints; ++point)
{
const auto& savedValue = arr->getReference (point);
const double value = (savedValue.isDouble() || savedValue.isInt() || savedValue.isInt64())
? (double) savedValue : 0.0;
data.push_back (std::isfinite (value) ? (float) juce::jlimit (-1.0, 1.0, value) : 0.0f);
}
engine.setLfoShapeData (index, data, steps);
restored[(size_t) index] = true;
}
}
} // namespace serum
// ===========================================================================
// Plugin entry point (required by the JUCE plugin clients).
// ===========================================================================
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
{
return new serum::SerumAltAudioProcessor();
}
+72
View File
@@ -0,0 +1,72 @@
#pragma once
#include <JuceHeader.h>
#include <atomic>
#include "Params.h"
#include "Engine.h"
namespace serum
{
// ===========================================================================
// SerumAlt — a wavetable synthesiser (VST3 / AU). Hosts the APVTS, the audio
// engine, preset management and the RAVE toggle.
// ===========================================================================
class SerumAltAudioProcessor : public juce::AudioProcessor
{
public:
SerumAltAudioProcessor();
~SerumAltAudioProcessor() override;
// --- AudioProcessor ---
void prepareToPlay (double sampleRate, int samplesPerBlock) override;
void releaseResources() override;
void processBlock (juce::AudioBuffer<float>&, juce::MidiBuffer&) override;
// Keeps the base class double-precision overload visible, so it is not hidden.
using juce::AudioProcessor::processBlock;
juce::AudioProcessorEditor* createEditor() override;
bool hasEditor() const override { return true; }
const juce::String getName() const override { return "SerumAlt"; }
bool acceptsMidi() const override { return true; }
bool producesMidi() const override { return false; }
bool isMidiEffect() const override { return false; }
double getTailLengthSeconds() const override { return 2.0; }
int getNumPrograms() override;
int getCurrentProgram() override;
void setCurrentProgram (int index) override;
const juce::String getProgramName (int index) override;
void changeProgramName (int index, const juce::String& newName) override;
void getStateInformation (juce::MemoryBlock& destData) override;
void setStateInformation (const void* data, int sizeInBytes) override;
// --- SerumAlt ---
void setRaveEnabled (bool enabled);
bool isRaveEnabled() const;
int getUiScaleIndex() const;
void setUiScaleIndex (int index);
float getUiScale() const;
void loadFactoryPreset (int index);
int getNumFactoryPresets() const;
// Public control state (hold the engine control lock for matrix/macros/LFOs).
juce::AudioProcessorValueTreeState parameters;
Engine engine;
static juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout();
private:
std::atomic<int> currentProgram { 0 };
void restoreLfoShapesFromState (const juce::ValueTree& state);
void saveLfoShapesToState (juce::ValueTree& state) const;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SerumAltAudioProcessor)
};
} // namespace serum