docs: add agent guidance and historical audit report

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.
This commit is contained in:
2026-09-09 13:25:02 +02:00
parent a50666355c
commit 05fd6440bd
12 changed files with 1010 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
# effects module
## Owned files
- Related rack implementation: `../FXProcessor.h`, `../FXProcessor.cpp`
- Biquad.h
- Hyper.h, Hyper.cpp
- Chorus.h, Chorus.cpp
- Flanger.h, Flanger.cpp
- Phaser.h, Phaser.cpp
- Distortion.h, Distortion.cpp
- EQ.h, EQ.cpp
- Compressor.h, Compressor.cpp
- Delay.h, Delay.cpp
- Reverb.h, Reverb.cpp
## Rules
- Implement the `FXUnit` interface with `prepare`, `reset` and `process (float* l, float* r, int numSamples, const float p[4])`.
- Process full-wet in place; FXProcessor handles the dry/wet mix.
- Document what p[0] through p[3] mean for each unit in the header comment.
- Use `std::vector<float>` for delay lines and size them in `prepare()`.
- Use `juce::MathConstants<T>::pi` and `twoPi` in delay LFOs, not raw literals.
- Never allocate or resize a delay line inside `process`.
- Guard empty delay lines with an early `if (len < 4) return;`.
## IF-THEN
- IF a unit needs a delay line THEN allocate it in `prepare()` at the full required length and only read and write it in `process`.
- Include the existing rack interface via `../FXProcessor.h`; no source relocation is implied by this guidance.
## Examples
```cpp
// BAD: reallocates a delay line on the audio thread
void process (float* l, float* r, int n, const float p[4]) override
{
std::vector<float> delay ((size_t) (sr * 0.05), 0.0f);
...
}
```
```cpp
// GOOD: sized once at prepare time, reused per block
void prepare (double sampleRate, int) override
{
sr = sampleRate;
delayL.assign ((size_t) (sr * 0.05), 0.0f);
delayR.assign ((size_t) (sr * 0.05), 0.0f);
}
```
This file overrides /AGENT.md where they conflict.
+47
View File
@@ -0,0 +1,47 @@
# gui module
## Owned files
- Related editor implementation: `../PluginEditor.h`, `../PluginEditor.cpp`
- SerumLookAndFeel.h
- Knob.h, Knob.cpp
- Slider.h, Slider.cpp
- ToggleButton.h, ToggleButton.cpp
- Display.h, Display.cpp
- Panel.h, Panel.cpp
- WaveformDisplay.h, WaveformDisplay.cpp
- FilterDisplay.h, FilterDisplay.cpp
- EnvelopeDisplay.h, EnvelopeDisplay.cpp
- LFODisplay.h, LFODisplay.cpp
## Rules
- Draw all controls through `SerumLookAndFeel`.
- Use `theme::` colours from Resources.h instead of hardcoded colour values.
- Own GUI attachments with `std::unique_ptr`.
- Own component children by value or with `std::unique_ptr`; `addAndMakeVisible` only attaches and shows them, and does not manage their lifetime.
- Refresh visuals from a `juce::Timer` callback on the message thread, not from the audio thread.
- Keep layout constants in the `layout` namespace and derive every position from them.
- Use `std::function` formatters for value readouts.
- Reference `ids::` for APVTS parameter IDs.
## IF-THEN
- IF a control reads a DSP value THEN refresh it on the message thread from a thread-safe snapshot or atomic value. A `timerCallback` alone does not make a shared DSP read safe.
- IF a raw parameter pointer is dereferenced THEN null-guard it first.
## Examples
```cpp
// BAD: unchecked raw parameter dereference
const float v = processor.parameters.getRawParameterValue (ids::f1Cutoff)->load();
```
```cpp
// GOOD: null guard with fallback
const float v = processor.parameters.getRawParameterValue (ids::f1Cutoff)
? processor.parameters.getRawParameterValue (ids::f1Cutoff)->load()
: 0.0f;
```
This file overrides /AGENT.md where they conflict.
+33
View File
@@ -0,0 +1,33 @@
# presets module
## Owned files
- FactoryPresets.h
- FactoryPresets.cpp
## Rules
- Build presets as sparse maps; unset parameters keep their APVTS defaults.
- Use `ids::` for every parameter ID, never raw strings.
- Use named helpers (coarse, unison, slope, fx, lfoShape, mod) to build normalized values.
- Return the preset list as `const std::vector<FactoryPreset>&` from a static local.
- Reference `ModConnection` and macro assignment structs instead of ad hoc tuples.
## IF-THEN
- IF a preset omits a parameter THEN leave it at its default; do not write an explicit zero.
- IF a preset sets a physical value THEN encode it through the same `maps::` helpers the DSP uses.
## Examples
```cpp
// BAD: raw string ID and an opaque normalized number
pr.params = { { "oscAUnison", 0.4f } };
```
```cpp
// GOOD: ids:: and a named helper
pr.params = { p (ids::oscAUnison, unison (7)) };
```
This file overrides /AGENT.md where they conflict.
+34
View File
@@ -0,0 +1,34 @@
# resources module
## Owned files
- Related embedded assets and helpers: `../Resources.h`, `../Resources.cpp`
- logo.svg
- background.svg
## Rules
- Keep every theme colour in `namespace theme` in Resources.h.
- Embed SVG assets as raw string literals.
- Keep formatting helpers (formatPercent, formatHz, formatSeconds, formatSemis, formatCents, formatPan, formatInt) here.
- Use `maps::` from Params for unit conversions in formatters.
- Never hardcode a theme colour in a widget; reference `theme::`.
## IF-THEN
- IF a widget needs a colour THEN reference a `theme::` entry rather than a literal Colour.
- IF an SVG asset ships under Source/Resources THEN keep its embedded string in Resources.cpp consistent with the file.
## Examples
```cpp
// BAD: hardcoded colour in a widget
g.setColour (juce::Colour (0xff4fc3f7));
```
```cpp
// GOOD: theme is the single source
g.setColour (theme::accent);
```
This file overrides /AGENT.md where they conflict.
+40
View File
@@ -0,0 +1,40 @@
# tests module
## Owned files
- TestMain.cpp
## Rules
- Keep this a headless console harness that drives the real SerumAltAudioProcessor.
- Check rendered blocks for NaN and inf. Check non-silence over the full preset render, not each block, because note release can legitimately produce silent blocks.
- Print PASS or FAIL per preset to `std::cout`.
- Return a non-zero exit code when any check fails.
- Never show a JUCE alert window.
- Do not add GUI interaction beyond `juce::ScopedJuceInitialiser_GUI`.
## IF-THEN
- IF a check fails THEN increment the failure count and print the preset name.
- Build and run natively from the repository root with `./build_harness.sh --run`. Building the `SerumAltTest` target alone does not run checks.
- The harness-only configuration sets `SERUMALT_BUILD_PLUGIN=OFF` and `SERUMALT_BUILD_TESTS=ON`. Production scripts disable the harness.
- The harness links the real processor and editor sources but creates no plugin bundles. Keep that build separation when adding checks.
## Examples
```cpp
// BAD: a failing check does not fail the run
bool ok = ! hasNaN (buffer) && peak (buffer) > 0.001f; // result ignored
return 0;
```
```cpp
// GOOD: accumulate failures and exit non-zero
int failures = 0;
const bool ok = ! hasNaN (buffer) && peak (buffer) > 0.001f;
if (! ok) ++failures;
...
return failures == 0 ? 0 : 1;
```
This file overrides /AGENT.md where they conflict.
+48
View File
@@ -0,0 +1,48 @@
# engine module
## Related source files
This directory contains guidance; the implementation remains in `../Engine.h`
and `../Engine.cpp`. The preparation rules below are requirements for changes;
current buffer resizing and lazy wavetable issues are recorded in the root AGENT.md.
## Rules
- Own the voice pool, LFOs, wavetable library, FX rack, matrix and macros by value.
- Read every APVTS value through the null-guarding `v()` helper.
- Build the `RenderContext` once per block and pass it to voices as `const&`.
- Size `mixBuffer` once in `prepare()` and only `clear()` it per block.
- Prebuild wavetables in `prepare()` before the first render.
- Clamp the final output with the soft limiter.
- Never call `setSize` on any buffer inside `processBlock`.
## IF-THEN
- IF a per-block buffer is needed THEN allocate it in `prepare()` and clear it per block.
- IF a module needs the matrix, macros or wavetables THEN go through Engine accessors rather than reaching into another module.
## Examples
```cpp
// BAD: per-callback buffer reconfiguration on the audio thread
void processBlock (...)
{
mixBuffer.setSize (2, n, false, false, true);
...
}
```
```cpp
// GOOD: size once, clear per block
void prepare (double sr, int blockSize)
{
mixBuffer.setSize (2, blockSize, false, false, true);
}
void processBlock (...)
{
mixBuffer.clear();
...
}
```
This file overrides /AGENT.md where they conflict.
+34
View File
@@ -0,0 +1,34 @@
# modulation module
## Related source files
This directory contains guidance; implementations remain in the parent `Source/`
directory:
- `../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.
- Use `ids::` for every APVTS parameter reference in RaveController.
- Keep the existing RAVEButton filenames unless a rename is explicitly in scope.
## GUI/audio state
The current public connection and assignment vectors are shared mutable state.
GUI writes can race with audio-thread iteration. A mutex taken only by the writer
does not protect an unlocked reader, and taking a blocking mutex in the audio
callback conflicts with the real-time requirements.
When fixing this path, publish a stable snapshot or use a bounded thread-safe
handoff. Keep allocation and snapshot reclamation off the audio thread. Validate
reader and writer lifetimes together, including preset and state restore.
If `addConnection` or `addAssignment` returns false, handle the failed insert.
This file refines the repository-root AGENT.md for the related sources.
+39
View File
@@ -0,0 +1,39 @@
# params module
## Related source files
This directory contains guidance; the implementation remains in `../Params.h`
and `../Params.cpp`.
## Rules
- Use the `ids::` namespace for every APVTS parameter ID string.
- Require one `ids::` entry per audio parameter, and one entry only.
- Use `enum class` for every enum. Give each a trailing `Count` enumerator.
- Use `inline constexpr` for parameter ID strings and `k`-prefixed integer constants.
- Keep all 0..1 to physical unit conversions in the `maps::` namespace.
- Keep the wavetable name table (`kWavetableNames`) in sync with `kNumWavetables`.
- Return an empty `juce::String` from enum-to-string switches for unknown values.
- Never re-declare a parameter ID string literal outside Params.h.
- Never add a physical unit mapping anywhere but `maps::`.
## IF-THEN
- IF you add an audio parameter THEN add its ID to `ids::` and, when it has physical units, add its mapping to `maps::` before any engine or GUI code reads it.
- IF a switch converts an enum to a string THEN include a default case that returns an empty string.
## Examples
```cpp
// BAD: re-declares the ID and drifts from Params.h
if (auto* p = apvts.getParameter ("f1Cutoff"))
return p->getValue();
```
```cpp
// GOOD: one source of truth
if (auto* p = apvts.getParameter (ids::f1Cutoff))
return p->getValue();
```
This file overrides /AGENT.md where they conflict.
+41
View File
@@ -0,0 +1,41 @@
# plugin module
## Related source files
This directory contains guidance; the implementation remains in
`../PluginProcessor.h` and `../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`).
- Prefer const accessors over the public `parameters`, `engine` and `rave` fields for new code.
## 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 /AGENT.md where they conflict.
+58
View File
@@ -0,0 +1,58 @@
# synth module
## Related source files
This directory contains guidance. All files listed below remain in the parent
`Source/` directory. The preparation rules below are requirements for changes,
not a claim that lazy wavetable allocation has already been fixed.
- Wavetable.h, Wavetable.cpp
- Oscillator.h, Oscillator.cpp
- SubOscillator.h, SubOscillator.cpp
- NoiseOscillator.h, NoiseOscillator.cpp
- Envelope.h, Envelope.cpp
- LFO.h, LFO.cpp
- Filter.h, Filter.cpp
- FilterBank.h, FilterBank.cpp
- SynthVoice.h, SynthVoice.cpp
## Rules
- Use `noexcept` on every per-sample DSP path and cheap getter.
- Use `std::array` for fixed-size state and `std::vector<float>` for delay lines and table buffers.
- Size every buffer and delay line in `prepare()`, never in `process` or `render`.
- Clamp filter and envelope state so extreme settings cannot produce NaN or inf.
- Use `juce::MathConstants<T>::pi` and `twoPi` instead of raw pi literals.
- Use `(size_t)` casts when indexing containers with int loop variables.
- Pass global per-block state into voices as a `const RenderContext&`.
- Never allocate memory in the audio path.
## IF-THEN
- IF a wavetable is read in the render path THEN build all tables in `prepare()` by calling `WavetableLibrary::prebuild()` before the first block.
- IF a parameter is read every block THEN snapshot it into a struct (`OscParams`, `RenderContext`) instead of reading the APVTS per sample.
## Examples
```cpp
// BAD: lazy table build reached from the render path allocates on the audio thread
const Wavetable& wt = library.getTable (wave);
```
```cpp
// GOOD: build once at prepare time, then only read const references
library.prebuild(); // in prepareToPlay
const Wavetable& wt = library.getTable (wave); // lookup, no allocation
```
```cpp
// BAD: raw pi literal duplicated across files
const double step = 6.28318530717958647692 * freq / sr;
```
```cpp
// GOOD: single source
const double step = juce::MathConstants<double>::twoPi * freq / sr;
```
This file overrides /AGENT.md where they conflict.