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:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user