Add root AGENT.md with project conventions, build verification steps, source layout, style rules, and real-time/concurrency requirements. Add per-module AGENT.md files for each existing and proposed source subdirectory. Add AUDIT_REPORT.md as a historical Phase 1 snapshot documenting memory management, error handling, concurrency model, naming conventions, and anti-pattern catalog.
29 KiB
SerumAlt codebase audit (Phase 1)
Historical snapshot, taken before the platform/harness build separation. Build commands, defaults, target locations and line numbers below describe that earlier state. Use README.md's Building section and the current CMake files for builds. The proposed source moves are suggestions, not an approved implementation plan.
Scope: the SerumAlt C++/JUCE VST3 + Standalone synthesiser. This report records the
implicit standards, anti-patterns, memory choices, error handling, concurrency model and
module boundaries found by reading the real repository files. No source, build or
documentation files were modified. All findings are cited with path:line references read
during this task.
Repository note: at the start of this audit the working tree already had uncommitted
changes to CMakeLists.txt, several Source/ files and several third_party/JUCE/ files,
plus untracked build/, build_windows/ and SerumAlt_Windows/ directories. Those
changes predate this audit and are not analysed here except where the current on-disk
content is the source of truth.
1. Build & test setup
Project metadata and targets
- Project name and version:
project(SerumAlt VERSION 1.0.0 LANGUAGES CXX)atCMakeLists.txt:3. - C++ standard: C++17, required, extensions off at
CMakeLists.txt:8-10(CMAKE_CXX_STANDARD 17,CMAKE_CXX_STANDARD_REQUIRED ON,CMAKE_CXX_EXTENSIONS OFF). - The plugin target is
SerumAlt, declared withjuce_add_plugin(...)atCMakeLists.txt:33-46. Formats areVST3 AU StandaloneatCMakeLists.txt:44. JUCE generates the concrete per-format targets (SerumAlt_VST3,SerumAlt_Standalone, andSerumAlt_AUon macOS). There is no separatePluginProcessor/PluginEditortarget; those are ordinary classes compiled intoSerumAlt. - A console test target
SerumAltTestis added atCMakeLists.txt:126-140viajuce_add_console_app. It compiles the entire${SERUMALT_SOURCES}list plusSource/Tests/TestMain.cpp(CMakeLists.txt:128).
Source list
Sources are an explicit list, not a glob. set(SERUMALT_SOURCES ...) spans
CMakeLists.txt:50-87 and is consumed by target_sources(SerumAlt PRIVATE ${SERUMALT_SOURCES}) at CMakeLists.txt:89. The list enumerates every .cpp used by the
plugin, including the subdirectory files Source/EffectUnits/*.cpp, Source/GUI/*.cpp
and Source/Presets/FactoryPresets.cpp.
Two header-only files are intentionally absent from the list because they have no
translation unit: Source/EffectUnits/Biquad.h and Source/GUI/SerumLookAndFeel.h.
Consequence for later phases: any file move or rename must update this list, because there is no glob to pick files up automatically.
Custom flags and linkage
- Compile definitions at
CMakeLists.txt:91-95:JUCE_WEB_BROWSER=0,JUCE_USE_CURL=0,JUCE_VST3_CAN_REPLACE_VST2=0. - Link libraries at
CMakeLists.txt:97-103:juce::juce_audio_utilsandjuce::juce_dsp(private),juce::juce_recommended_config_flagsandjuce::juce_recommended_warning_flags(public). - Warning flags at
CMakeLists.txt:142-146:/W4under MSVC,-Wall -Wextraotherwise. - MinGW cross-build static runtime at
CMakeLists.txt:113-118: whenCMAKE_CROSSCOMPILING,SerumAlt_VST3gets-static-libgcc -static-libstdc++ -staticso the DLL is self-contained. - VST3 manifest handling at
CMakeLists.txt:23-31:SERUMALT_VST3_AUTO_MANIFESTisFALSEwhen cross-compiling (because JUCE 7.0.12 builds the manifest helper with the same toolchain, producing a Windows.exethat cannot run on the Linux host). The manifest is instead injected bybuild_windows.sh.
Windows cross-build (build_windows.sh)
build_windows.sh is the canonical Windows build path. Exact steps, in order:
- Verify
x86_64-w64-mingw32-g++exists, else exit with an error (build_windows.sh:16-21). - Regenerate
mingw64-toolchain.cmakefrom a heredoc inside the script (build_windows.sh:26-42). This is identical in content to the checked-inmingw64-toolchain.cmake(see below), so the toolchain file is defined in two places. - Configure:
cmake -S . -B build_windows -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=mingw64-toolchain.cmake -DSERUMALT_BUILD_TESTS=OFF(build_windows.sh:47-50). Note the test harness is explicitly disabled here. - Build:
cmake --build build_windows --target SerumAlt_VST3(build_windows.sh:53). - Inject
Contents/Resources/moduleinfo.jsoninto the.vst3bundle by writing the file directly, because the automatic manifest step was disabled (build_windows.sh:63-126). - Copy the bundle to
SerumAlt_Windows/SerumAlt.vst3(build_windows.sh:129-131).
The toolchain (mingw64-toolchain.cmake:1-15) sets CMAKE_SYSTEM_NAME Windows,
CMAKE_SYSTEM_PROCESSOR x86_64, the x86_64-w64-mingw32-* compilers/tools, and
CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32 with PROGRAM NEVER and the other
..._MODE_* values ONLY.
Native Linux build (README only)
README.md:76-78 documents a native build:
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build
Outputs listed at README.md:80-84 are
build/SerumAlt_artefacts/Release/VST3/SerumAlt.vst3 and the standalone app.
Tests
Tests exist. Source/Tests/TestMain.cpp is a console harness that instantiates the
processor, plays notes through every factory preset, checks for NaN/inf and silence, and
toggles RAVE (TestMain.cpp:37-135). It returns a non-zero exit code on failure
(TestMain.cpp:134).
Tests are run through the native build, documented at README.md:96-98:
cmake --build build --target SerumAltTest
./build/SerumAltTest_artefacts/Release/SerumAltTest
The Windows cross-build does not build or run tests: build_windows.sh:50 passes
-DSERUMALT_BUILD_TESTS=OFF.
Clean build and test command
There is no clean step in build_windows.sh. The script configures into build_windows/
and builds without removing or cleaning it first. A clean rebuild is not scripted; it would
require manually removing build_windows/ (or invoking Ninja's clean target, which the
script does not do). A combined "clean build + run tests" command does not exist: the
Windows script disables tests, and the README test instructions assume an already-built
native build/ tree.
2. Proposed module boundaries
The codebase already has three real subdirectory boundaries (EffectUnits/, GUI/,
Presets/) plus Resources/ (assets only) and Tests/. The flat files at Source/ root
mix four distinct concerns, and three root files clearly belong inside existing
subdirectories. The proposal below is based on the include graph actually present, not on
generic synth architecture.
Dependency facts that drive the split
Params.h/Params.cppdepend only on<JuceHeader.h>(Params.h:3). It declares theids,mapsnamespaces, all enums,ModSource/ModTarget, and the wavetable name table. It is the shared foundation: nearly every other file includes it.Wavetable,Oscillator,SubOscillator,NoiseOscillator,Envelope,LFO,Filter,FilterBank,SynthVoicedepend only onParams,JuceHeader, and each other (Oscillator.h:4-5,FilterBank.h:4-5,SynthVoice.h:4-12).ModulationMatrixandMacroControlsdepend only onParams(ModulationMatrix.h:4,MacroControls.h:4).RAVEButton.h/RaveControllerdepends only onParams(RAVEButton.h:4) but manipulates the APVTS, so it sits between modulation and plugin plumbing.FXProcessor.hdepends only onParams(FXProcessor.h:4); every file inEffectUnits/includes../FXProcessor.h(Hyper.h:3,Chorus.h:3, etc.), soFXProcessoris the base the effect units build on.Engineaggregates the DSP:Engine.h:4-10includesWavetable,SynthVoice,LFO,FXProcessor,ModulationMatrix,MacroControls.PluginProcessor.hincludesParams,Engine,RAVEButton(PluginProcessor.h:4-6); it is the JUCEAudioProcessorboundary plus state/preset/RAVE glue.PluginEditor.hincludesPluginProcessorand everyGUI/header (PluginEditor.h:4-14); it is pure GUI.Resources.hdepends onJuceHeaderplusParamsfor the format helpers (Resources.cpp:2); it holds the theme and SVG strings. The SVG asset files live inSource/Resources/, but theResources.{h,cpp}code lives at root, splitting one concept across two locations.Presets/FactoryPresetsdepends onParamsandModulationMatrix(FactoryPresets.h:4-5).Tests/TestMain.cppdepends only onPluginProcessor(TestMain.cpp:4).
Proposed modules
| Module | Files (current location) | Depends on |
|---|---|---|
| params (foundation) | Params.{h,cpp} |
JuceHeader only |
| synth (DSP building blocks + voice) | Wavetable.{h,cpp}, Oscillator.{h,cpp}, SubOscillator.{h,cpp}, NoiseOscillator.{h,cpp}, Envelope.{h,cpp}, LFO.{h,cpp}, Filter.{h,cpp}, FilterBank.{h,cpp}, SynthVoice.{h,cpp} |
params |
| modulation | ModulationMatrix.{h,cpp}, MacroControls.{h,cpp}, RAVEButton.{h,cpp} |
params |
| effects | FXProcessor.{h,cpp} plus all of EffectUnits/ (including Biquad.h) |
params |
| engine | Engine.{h,cpp} |
synth, effects, modulation |
| gui | PluginEditor.{h,cpp} plus all of GUI/ (including SerumLookAndFeel.h) |
params, synth (for wavetable display), plugin |
| resources | Resources.{h,cpp} plus Resources/*.svg |
params |
| presets | Presets/FactoryPresets.{h,cpp} |
params, modulation |
| plugin (JUCE plumbing) | PluginProcessor.{h,cpp} |
engine, modulation, presets, params |
| tests | Tests/TestMain.cpp |
plugin |
Where the current layout violates the boundaries
FXProcessor.{h,cpp}sits atSource/root but is the base class and owner of theEffectUnits/classes;EffectUnits/*.hinclude it as../FXProcessor.h. It belongs inEffectUnits/(or in a renamedFX/directory with the units).PluginEditor.{h,cpp}sits at root but is pure GUI and includes everyGUI/header. It belongs inGUI/.Resources.{h,cpp}holds the theme and SVG strings, while the.svgassets live inSource/Resources/. The code and assets should live together under oneresourcesmodule.RAVEButton.{h,cpp}is orphaned at root under a filename that does not match its class (RaveController). It is a macro-like boost over the APVTS, so it fits themodulationmodule, though its APVTS manipulation also makesplugina defensible home.- The remaining root files (
Params,Wavetable,Oscillator,SubOscillator,NoiseOscillator,Envelope,LFO,Filter,FilterBank,SynthVoice,ModulationMatrix,MacroControls,Engine,PluginProcessor) are the synthesis core and JUCE boundary. Grouping them intosynth/andmodulation/subdirectories would match the dependencies above, but the flat layout is at least internally consistent: they are the only files that legitimately belong at root today.
3. Memory management standards
Dominant idiom: value semantics plus std::unique_ptr for ownership
- Owned sub-objects are held by value, not by pointer:
juce::AudioProcessorValueTreeState parameters; Engine engine; RaveController rave;atPluginProcessor.h:56-58; theEngineholdsstd::array<SynthVoice, kNumVoices> voices,std::array<LFO, kNumLfos> lfos, andWavetableLibrary,FXProcessor,ModulationMatrix,MacroControlsby value atEngine.h:45-50. - Owned polymorphic objects use
std::unique_ptr:std::array<std::unique_ptr<FXUnit>, 9> unitsatFXProcessor.h:50, populated withstd::make_uniqueatFXProcessor.cpp:15-26. - GUI attachments are owned by
std::unique_ptr:std::vector<std::unique_ptr<... SliderAttachment>>and...ComboBoxAttachmentatPluginEditor.h:103-104, filled atPluginEditor.cpp:169-170and183-184. - Transient GUI objects are
std::unique_ptr:std::unique_ptr<juce::Label> valuePopupatKnob.h:38, created atKnob.cpp:61and released withvaluePopup.reset()atKnob.cpp:83. - JUCE-managed objects are returned as raw
newpointers because JUCE takes ownership throughaddAndMakeVisibleor the plugin API:createEditor()returnsnew PluginEditor (*this)atPluginProcessor.cpp:195; the plugin entry point returnsnew serum::SerumAltAudioProcessor()atPluginProcessor.cpp:380; GUI children are created withnewatPluginEditor.cpp:166,178,193,602,606. These raw pointers are never deleted by the code, which is correct only because JUCE's component tree and plugin host own them. - Non-owning cross-object references are raw pointers or references:
juce::RangedAudioParameter* paramatToggleButton.h:38,const WavetableLibrary* wtLibatWaveformDisplay.h:24, and theRenderContextrawconstpointers to the library/matrix/macros atSynthVoice.h:24,32-33.
Containers and buffers
std::vector<float>is the standard delay line / table buffer:Wavetable.h:24(frames),Filter.h:39(combLine),Chorus.h:21,Delay.h:21,Reverb.h:22-29.std::arrayis used for fixed-size DSP state:Oscillator.h:62(std::array<SubVoice, kMaxUnison>),Filter.h:36,44,Reverb.h:22-27.juce::AudioBuffer<float>is used for block buffers:Engine.h:59(mixBuffer),SynthVoice.h:95(scratch),FXProcessor.h:51(dry,wet). These are sized once inprepare()(for exampleEngine.cpp:68,SynthVoice.cpp:23,FXProcessor.cpp:34-35) and cleared per block.
Realtime vs non-realtime allocation
The code intends to allocate only at prepare time, but it does not fully achieve this:
- Prepare-time allocation is the norm: delay lines and buffers are sized in
prepare()(Filter.cpp:24,Chorus.cpp:9-11,Reverb.cpp:17-33,Engine.cpp:68). - Violation 1:
Engine::processBlockcallsmixBuffer.setSize (2, n, false, false, true)on every block atEngine.cpp:211.AudioBuffer::setSizeis a no-op when the size is unchanged, but it is still a per-callback reconfiguration call on the audio thread. - Violation 2: wavetables are built lazily.
WavetableLibrary::getTablebuilds the table on first access (Wavetable.cpp:126-132) andgetTableis reached from the audio path atSynthVoice.cpp:177-178. Theprebuild()method exists atWavetable.cpp:120-124but is never called (it is not referenced anywhere else, andEngine::prepareatEngine.cpp:58-70does not call it). The first rendered block for a wavetable therefore allocates256 x 2048floats plus harmonic work buffers on the audio thread.
4. Error handling
There is no exception handling and no user-facing alerting. Failures are handled with a mix of asserts, null guards, and return codes.
jassertis used exactly once in the entire codebase:jassert (freqHz > 0.0)atOscillator.cpp:20. There are nothrow,tryorcatchstatements (verified by grep).- Null guards with silent fallback are the dominant pattern. The APVTS helper
v()returns0.0fwhen a parameter is missing:Engine.cpp:8-13. State restore silently returns on malformed input:PluginProcessor.cpp:313-315returns when the XML is null andPluginProcessor.cpp:318-319returns when theValueTreeis invalid. Preset loads skip missing parameters withif (auto* param = ...)atPluginProcessor.cpp:248-250. - Return codes carry failure for the two bounded collections:
ModulationMatrix::addConnectionreturnsfalsewhen the target isNoneor the connection limit is reached (ModulationMatrix.cpp:6-12), andMacroControls::addAssignmentreturnsfalsesimilarly (MacroControls.cpp:6-13). These return values are ignored by their callers (PluginEditor.cpp:536,PluginEditor.cpp:687,PluginProcessor.cpp:254,PluginProcessor.cpp:258). - Out-of-range lookups degrade to empty/default values:
getProgramNamereturns an empty string for a bad index (PluginProcessor.cpp:218-224); the enum-string converters fall through toreturn {}(Params.cpp:27,44,91). - There are no
juce::AlertWindow, noMessageManagercalls, and no logging in the plugin itself. The only reporting is the test harness printing tostd::coutand returning an exit code (TestMain.cpp:73,103,129,132-134).
Net style: programmer errors use jassert (very sparingly), runtime/state errors use null
guards or default values, and bounded-insertion failures use bool return codes that
callers currently ignore.
5. Concurrency
The codebase is single-threaded and has no explicit synchronisation. A grep for threads,
mutexes, locks, atomics, CriticalSection, SpinLock and ScopedLock returns nothing
outside JUCE's own headers.
- The only non-audio execution context is the JUCE message thread, driven by a
juce::Timer:PluginEditorinheritsjuce::Timer(PluginEditor.h:24), starts it at 30 Hz (PluginEditor.cpp:135), and refreshes visuals intimerCallback(PluginEditor.cpp:875-880). - The audio thread is
SerumAltAudioProcessor::processBlock(PluginProcessor.cpp:187-191) delegating toEngine::processBlock(Engine.cpp:176-318). It reads the playhead tempo atEngine.cpp:184-187.
The significant issue is unsynchronised sharing between the message thread and the audio thread:
- The GUI mutates
ModulationMatrix::connectionsviaaddConnection/removeConnection/clear(PluginEditor.cpp:536,549,559) while the audio thread iterates the same vector inSynthVoice::renderatSynthVoice.cpp:114.connectionsis a publicstd::vector(ModulationMatrix.h:30). - The GUI mutates
MacroControls::assignments(PluginEditor.cpp:687,699) while the audio thread iterates it atSynthVoice.cpp:125.assignmentsis a publicstd::arrayof vectors (MacroControls.h:28). - The GUI writes LFO shape data through
engine.setLfoShapeData(PluginEditor.cpp:483-486) which callsLFO::setShapeData(Engine.cpp:84-88,LFO.cpp:51-59), while the audio thread reads the sameshapeBufferinLFO::shapeValue(LFO.cpp:82-91).
None of these shared collections are locked or double-buffered. This is a real data race
today: resizing connections while the audio thread iterates it is undefined behaviour.
The processor header acknowledges GUI-thread access but does not address the race: the
comment "Public DSP state (read/write from the GUI thread)" at PluginProcessor.h:55 only
describes intent.
6. Naming & style conventions
- Namespace: every file is inside
namespace serum(PluginProcessor.h:8,Engine.h:12, etc.). File-local helpers use an anonymous namespace (Engine.cpp:6,SynthVoice.cpp:6,PluginEditor.cpp:6,FactoryPresets.cpp:6). - Include guard:
#pragma oncein every header (for examplePluginProcessor.h:1,Params.h:1,Biquad.h:1). No#ifndefguards exist. - Include order:
<JuceHeader.h>first, then local headers (PluginProcessor.h:3-6). Subdirectory files include root files with a../relative path (Hyper.h:3,Knob.h:5,FactoryPresets.h:4). - File/header pairs: one primary class per
.h/.cpppair, with the pair named after the class (Oscillator.h/Oscillator.cpp,FilterBank.h/FilterBank.cpp). Header-only helpers exist where they have no state logic worth a TU (Biquad.h,SerumLookAndFeel.h). - Class and struct names: PascalCase, no prefix (
SynthVoice,RenderContext,FilterBankParams,ModConnection). - Members: no
m_prefix and no trailing underscore. Plain camelCase fields such asnote,velocity,baseFreq,active,released,noteIdatSynthVoice.h:83-88, andsr,blockSize,bpm,pitchBendatEngine.h:52-57. - Constants:
kprefix for compile-time constants (kNumVoices,kNumLfosatParams.h:221-227;kFrames,kTableSizeatWavetable.h:18-19;kMaxConnectionsatModulationMatrix.h:28;kMaxAssignmentsatMacroControls.h:26). Local constant arrays in anonymous namespaces also usek(kEnvAttackatEngine.cpp:30,kFxTypeatPluginEditor.cpp:97). - Enums:
enum class(scoped) with PascalCase enumerators (WarpMode::BendPlus,ModSource::Lfo1,ModTarget::Filter1Cutoff) atParams.h:213-219andParams.h:232-256. - Namespaces for related constants:
ids::for parameter ID strings (Params.h:18),theme::for colours (Resources.h:12),maps::for unit mappings (Params.h:309). - Method naming: camelCase with a leading verb (
noteOn,processAdd,getValue). JUCE overrides keep JUCE's spelling (processBlock,prepareToPlay,resized,timerCallback). noexceptis applied consistently to the per-sample DSP hot paths (Oscillator.cpp:93,Filter.cpp:181,236,Wavetable.cpp:81,Envelope.cpp:54,SynthVoice.cpp:72,LFO.cpp:98) and to cheap getters (SynthVoice.h:71-74).- Formatting: two-space indentation; a space before the opening parenthesis in function
calls and definitions (
f (x)atPluginProcessor.cpp:12,juce::jlimit (...);if (...),for (...)). Aligned member initialiser lists (PluginEditor.cpp:113-115). - Section dividers:
// ===...banners at class/file tops and// ---...dividers within files (PluginProcessor.cpp:17,198,261,PluginEditor.cpp:161). - Casting idiom:
(size_t)casts on int loop indices when indexingstd::vector/std::array(Engine.cpp:220,SynthVoice.cpp:79,FactoryPresets.cppthroughout), and(int)casts onsize()results.
7. Anti-pattern catalog
Each entry lists the location, the problem, and the standard that a later AGENT.md phase should encode.
-
Parameter ID tables duplicated three times.
Params.h:18-208(canonicalids::),Engine.cpp:30-55(kEnv*,kLfo*,kFx*),PluginEditor.cpp:97-108,433-437,458-464,820-825,833(the same arrays again), plus the display names and defaults inPluginProcessor.cpp:31-171.- Standard: one source of truth in
Params.h(or a single generated table); engine and editor must reference it rather than re-declaring ID arrays.
-
Wavetable library allocates on the audio thread.
WavetableLibrary::getTablebuilds lazily (Wavetable.cpp:126-132), reached fromSynthVoice::render(SynthVoice.cpp:177-178);prebuild()is defined (Wavetable.cpp:120-124) but never called.- Standard: call
prebuild()(or build all tables) inprepareToPlay, never in the render path.
-
Per-block buffer resize in the audio callback.
Engine::processBlockcallsmixBuffer.setSize(...)every block atEngine.cpp:211.- Standard: size the buffer once in
prepare()and onlyclear()per block.
-
Unsynchronised GUI/audio sharing of container state.
ModulationMatrix::connectionsis public (ModulationMatrix.h:30), written by the GUI (PluginEditor.cpp:536,549,559) and read on the audio thread (SynthVoice.cpp:114).MacroControls::assignmentsis public (MacroControls.h:28), written (PluginEditor.cpp:687,699) and read (SynthVoice.cpp:125). LFOshapeBufferis written viasetShapeData(LFO.cpp:51-59) and read (LFO.cpp:82-91).- Standard: either protect these collections with a lock, or use atomic/double-buffered exchange between the message and audio threads.
-
Class name does not match its file.
Source/RAVEButton.hdeclaresRaveController(RAVEButton.h:14), andRAVEButton.cppimplements it.- Standard: file pair name matches the class (
RaveController.h/.cpp), or the class is renamed to match the file.
-
Dead code in
SubOscillator::noteOn.SubOscillator.cpp:9-12computesmultand then discards it with(void) freqHz; (void) mult;because the octave shift is already baked into the frequency by the voice.- Standard: remove the unused parameters or make the signature reflect what is actually
used (the octave shift is applied at
SynthVoice.cpp:179).
-
No-op override.
changeProgramNameis an empty override atPluginProcessor.cpp:226-228.- Standard: implement it or do not override it.
-
Ignored prepare-time parameter.
Filter::prepareacceptsmaxBlockSizeand discards it with(void) maxBlockSize;(Filter.cpp:21-29); effect units name the parameterintbut never use it (Hyper.cpp:6,Chorus.cpp:6,Flanger.cpp:6, etc.).- Standard: keep the
FXUnitinterface uniform but do not add unused parameters to concreteprepare()implementations.
-
Duplicated envelope curve constants.
- The attack/decay shape mapping
0.3 + curve * 2.7/3.0 - curve * 2.7appears inEnvelope.cpp:44-45and again in the displayEnvelopeDisplay.cpp:19-20. - Standard: expose one shared helper so the preview cannot drift from the DSP.
- The attack/decay shape mapping
-
Filter response reimplemented for display.
FilterDisplay::magnitude(FilterDisplay.cpp:6-36) hardcodes a parallel set of transfer-function approximations that duplicate the real DSP inFilter.cpp(Filter.cpp:87-234).- Standard: keep the display response derived from one documented source, or mark it as an approximation with a comment that the two are not the same code.
-
Inconsistent pi constants.
kTwoPiis defined atOscillator.h:67andSubOscillator.h:26;twoPiatWavetable.cpp:21;kPiatWavetable.cpp:156andFilter.cpp:8; raw literals6.28318530717958647692appear atChorus.cpp:40-41,Flanger.cpp:38-39,Phaser.cpp:30-31, and6.2831853fatLFO.cpp:67andLFODisplay.cpp:19.- Standard: use
juce::MathConstants<T>::pi/twoPieverywhere.
-
Public mutable state exposes internals.
PluginProcessor.h:56-58exposesparameters,engineandraveas public fields;ModulationMatrix.h:30andMacroControls.h:28expose their collections;Engine.h:36-38returns non-const references fromgetMatrix(),getMacros()andgetWavetables().- Standard: return
const&from accessors and route mutation through member functions, except where a public field is an explicit, documented design choice.
-
Unchecked raw parameter dereference.
PluginEditor.cpp:794dereferencesgetRawParameterValue(id)->load()without a null check, while the equivalent helper inEngine.cpp:8-13guards the pointer.- Standard: always guard
getRawParameterValue/getParameterresults before use.
-
Mixed ownership for GUI children.
makeKnob/makeCombo/makeTogglereturn raw pointers tonew-allocated children that JUCE owns viaaddAndMakeVisible(PluginEditor.cpp:162-199), andfxUp/fxDownuse rawnewatPluginEditor.cpp:602,606. This is correct only because of JUCE's component-tree ownership, but it is easy to misread as a leak.- Standard: document that JUCE-owned children are raw pointers and never deleted, and
keep
std::unique_ptrfor everything the plugin owns directly.
8. Hard constraints worth codifying
Only rules that the current code and build actually support are listed.
- Do not allocate or resize containers in the audio callback. Evidence that this is the
intent: all DSP state is sized in
prepare()(Engine.cpp:68,SynthVoice.cpp:23,FXProcessor.cpp:34-35) and the code uses pre-sizedjuce::AudioBufferandstd::array. The current violations (Engine.cpp:211and the lazy wavetable build) are exceptions to fix, not the rule. - All audio parameters are normalised to 0..1 in the APVTS, and physical units are mapped
only through
maps::(Params.h:309-342). DSP must consume the normalised values, not physical units. - Parameter IDs live in the
idsnamespace inParams.h(Params.h:18-208). Engine, editor and preset code must referenceids::, not re-declare string literals. (This is the stated intent atParams.h:5-9; the current duplication inEngine.cppandPluginEditor.cppis a violation to eliminate.) - The CMake source list is explicit (
CMakeLists.txt:50-87), not a glob. Every file move or rename must updateSERUMALT_SOURCES. - All code is inside
namespace serum, and every header uses#pragma once(verified across all headers). - No exceptions and no user-facing alert dialogs. Use
jassertfor programmer errors, null guards for runtime lookups, andboolreturns for bounded inserts (seeOscillator.cpp:20,Engine.cpp:8-13,ModulationMatrix.cpp:6-12). - When cross-compiling for Windows, keep the VST3 manifest step disabled and let
build_windows.shinjectmoduleinfo.json, because JUCE's manifest helper must not be built as a Windows executable on a Linux host (CMakeLists.txt:23-31,build_windows.sh:55-126). Keep the injected metadata in sync withPLUGIN_CODE/PLUGIN_MANUFACTURER_CODE/VERSION(build_windows.sh:60-62). - The project is single-threaded by design. Any future thread, mutex or atomic must come with an explicit plan for the GUI-to-audio boundary, because the current matrix/macro/LFO state is shared without synchronisation (section 5).