chore: move build output under build/ and scripts into scripts/

Layout:
- scripts/ holds the four build scripts plus the new clean.sh
- cmake/ holds mingw64-toolchain.cmake
- the scripts default to build/linux, build/macos, build/windows, build/harness
- docs/ holds the audit documents, with the round 1 task list archived

Cleanup:
- scripts/clean.sh removes generated output, with --dry-run, --staging,
  --legacy and --all, and refuses tracked, protected or outside paths
- the Windows VST3 manifest now comes from cmake/moduleinfo.json.in, so its
  version, vendor and class IDs follow cmake/Plugin.cmake instead of a
  hand-written heredoc that could drift
- deleted the unused Source/GUI/Slider, which no code constructed
- fixed the hidden double processBlock overload: three warning lines to zero
- unslopped the build scripts, the two .cjs helpers and the CMake comments
- .gitignore covers the upstream JUCE Projucer and DemoRunner binaries

Verified: five scripts pass bash -n; the harness passes CTest with zero
warning lines; the Linux VST3 and Standalone link; the Windows cross-build
passes its x86-64, runtime-import and entry-point checks; each configure
exposes only its own targets; and a full build leaves Source/ and
third_party/JUCE unchanged. The generated manifest is identical to the
previous hand-written one.
This commit is contained in:
biggy committed 2026-10-06 09:06:53 +02:00
1 parent e4cea300db
commit facd5c5b5e
25 files changed
+438 -313

No files matched your search

+500
View File
@@ -0,0 +1,500 @@
# 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)` at `CMakeLists.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 with `juce_add_plugin(...)` at
`CMakeLists.txt:33-46`. Formats are `VST3 AU Standalone` at `CMakeLists.txt:44`. JUCE
generates the concrete per-format targets (`SerumAlt_VST3`, `SerumAlt_Standalone`, and
`SerumAlt_AU` on macOS). There is no separate `PluginProcessor`/`PluginEditor` target;
those are ordinary classes compiled into `SerumAlt`.
- A console test target `SerumAltTest` is added at `CMakeLists.txt:126-140` via
`juce_add_console_app`. It compiles the entire `${SERUMALT_SOURCES}` list plus
`Source/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_utils` and `juce::juce_dsp`
(private), `juce::juce_recommended_config_flags` and `juce::juce_recommended_warning_flags`
(public).
- Warning flags at `CMakeLists.txt:142-146`: `/W4` under MSVC, `-Wall -Wextra` otherwise.
- MinGW cross-build static runtime at `CMakeLists.txt:113-118`: when
`CMAKE_CROSSCOMPILING`, `SerumAlt_VST3` gets `-static-libgcc -static-libstdc++ -static`
so the DLL is self-contained.
- VST3 manifest handling at `CMakeLists.txt:23-31`: `SERUMALT_VST3_AUTO_MANIFEST` is `FALSE`
when cross-compiling (because JUCE 7.0.12 builds the manifest helper with the same
toolchain, producing a Windows `.exe` that cannot run on the Linux host). The manifest is
instead injected by `build_windows.sh`.
### Windows cross-build (build_windows.sh)
`build_windows.sh` is the canonical Windows build path. Exact steps, in order:
1. Verify `x86_64-w64-mingw32-g++` exists, else exit with an error (`build_windows.sh:16-21`).
2. Regenerate `mingw64-toolchain.cmake` from a heredoc inside the script
(`build_windows.sh:26-42`). This is identical in content to the checked-in
`mingw64-toolchain.cmake` (see below), so the toolchain file is defined in two places.
3. 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.
4. Build: `cmake --build build_windows --target SerumAlt_VST3` (`build_windows.sh:53`).
5. Inject `Contents/Resources/moduleinfo.json` into the `.vst3` bundle by writing the file
directly, because the automatic manifest step was disabled (`build_windows.sh:63-126`).
6. 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.cpp` depend only on `<JuceHeader.h>` (`Params.h:3`). It declares the
`ids`, `maps` namespaces, 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`, `SynthVoice` depend only on `Params`, `JuceHeader`, and each
other (`Oscillator.h:4-5`, `FilterBank.h:4-5`, `SynthVoice.h:4-12`).
- `ModulationMatrix` and `MacroControls` depend only on `Params` (`ModulationMatrix.h:4`,
`MacroControls.h:4`).
- `RAVEButton.h`/`RaveController` depends only on `Params` (`RAVEButton.h:4`) but manipulates
the APVTS, so it sits between modulation and plugin plumbing.
- `FXProcessor.h` depends only on `Params` (`FXProcessor.h:4`); every file in
`EffectUnits/` includes `../FXProcessor.h` (`Hyper.h:3`, `Chorus.h:3`, etc.), so
`FXProcessor` is the base the effect units build on.
- `Engine` aggregates the DSP: `Engine.h:4-10` includes `Wavetable`, `SynthVoice`, `LFO`,
`FXProcessor`, `ModulationMatrix`, `MacroControls`.
- `PluginProcessor.h` includes `Params`, `Engine`, `RAVEButton` (`PluginProcessor.h:4-6`);
it is the JUCE `AudioProcessor` boundary plus state/preset/RAVE glue.
- `PluginEditor.h` includes `PluginProcessor` and every `GUI/` header
(`PluginEditor.h:4-14`); it is pure GUI.
- `Resources.h` depends on `JuceHeader` plus `Params` for the format helpers
(`Resources.cpp:2`); it holds the theme and SVG strings. The SVG asset files live in
`Source/Resources/`, but the `Resources.{h,cpp}` code lives at root, splitting one
concept across two locations.
- `Presets/FactoryPresets` depends on `Params` and `ModulationMatrix`
(`FactoryPresets.h:4-5`).
- `Tests/TestMain.cpp` depends only on `PluginProcessor` (`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 at `Source/` root but is the base class and owner of the
`EffectUnits/` classes; `EffectUnits/*.h` include it as `../FXProcessor.h`. It belongs in
`EffectUnits/` (or in a renamed `FX/` directory with the units).
- `PluginEditor.{h,cpp}` sits at root but is pure GUI and includes every `GUI/` header. It
belongs in `GUI/`.
- `Resources.{h,cpp}` holds the theme and SVG strings, while the `.svg` assets live in
`Source/Resources/`. The code and assets should live together under one `resources`
module.
- `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 the `modulation`
module, though its APVTS manipulation also makes `plugin` a 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 into `synth/` and `modulation/` 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;` at `PluginProcessor.h:56-58`; the
`Engine` holds `std::array<SynthVoice, kNumVoices> voices`, `std::array<LFO, kNumLfos>
lfos`, and `WavetableLibrary`, `FXProcessor`, `ModulationMatrix`, `MacroControls` by value
at `Engine.h:45-50`.
- Owned polymorphic objects use `std::unique_ptr`: `std::array<std::unique_ptr<FXUnit>, 9>
units` at `FXProcessor.h:50`, populated with `std::make_unique` at `FXProcessor.cpp:15-26`.
- GUI attachments are owned by `std::unique_ptr`: `std::vector<std::unique_ptr<...
SliderAttachment>>` and `...ComboBoxAttachment` at `PluginEditor.h:103-104`, filled at
`PluginEditor.cpp:169-170` and `183-184`.
- Transient GUI objects are `std::unique_ptr`: `std::unique_ptr<juce::Label> valuePopup`
at `Knob.h:38`, created at `Knob.cpp:61` and released with `valuePopup.reset()` at
`Knob.cpp:83`.
- JUCE-managed objects are returned as raw `new` pointers because JUCE takes ownership
through `addAndMakeVisible` or the plugin API: `createEditor()` returns
`new PluginEditor (*this)` at `PluginProcessor.cpp:195`; the plugin entry point returns
`new serum::SerumAltAudioProcessor()` at `PluginProcessor.cpp:380`; GUI children are
created with `new` at `PluginEditor.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*
param` at `ToggleButton.h:38`, `const WavetableLibrary* wtLib` at `WaveformDisplay.h:24`,
and the `RenderContext` raw `const` pointers to the library/matrix/macros at
`SynthVoice.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::array` is 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 in
`prepare()` (for example `Engine.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::processBlock` calls `mixBuffer.setSize (2, n, false, false, true)`
on every block at `Engine.cpp:211`. `AudioBuffer::setSize` is 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::getTable` builds the table
on first access (`Wavetable.cpp:126-132`) and `getTable` is reached from the audio path at
`SynthVoice.cpp:177-178`. The `prebuild()` method exists at `Wavetable.cpp:120-124` but is
never called (it is not referenced anywhere else, and `Engine::prepare` at
`Engine.cpp:58-70` does not call it). The first rendered block for a wavetable therefore
allocates `256 x 2048` floats 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.
- `jassert` is used exactly once in the entire codebase: `jassert (freqHz > 0.0)` at
`Oscillator.cpp:20`. There are no `throw`, `try` or `catch` statements (verified by grep).
- Null guards with silent fallback are the dominant pattern. The APVTS helper `v()` returns
`0.0f` when a parameter is missing: `Engine.cpp:8-13`. State restore silently returns on
malformed input: `PluginProcessor.cpp:313-315` returns when the XML is null and
`PluginProcessor.cpp:318-319` returns when the `ValueTree` is invalid. Preset loads skip
missing parameters with `if (auto* param = ...)` at `PluginProcessor.cpp:248-250`.
- Return codes carry failure for the two bounded collections:
`ModulationMatrix::addConnection` returns `false` when the target is `None` or the
connection limit is reached (`ModulationMatrix.cpp:6-12`), and
`MacroControls::addAssignment` returns `false` similarly (`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: `getProgramName` returns an empty
string for a bad index (`PluginProcessor.cpp:218-224`); the enum-string converters fall
through to `return {}` (`Params.cpp:27,44,91`).
- There are no `juce::AlertWindow`, no `MessageManager` calls, and no logging in the plugin
itself. The only reporting is the test harness printing to `std::cout` and returning an
exit code (`TestMain.cpp:73,103,129,132-134`).
Net style: programmer errors use `jassert` (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`:
`PluginEditor` inherits `juce::Timer` (`PluginEditor.h:24`), starts it at 30 Hz
(`PluginEditor.cpp:135`), and refreshes visuals in `timerCallback` (`PluginEditor.cpp:875-880`).
- The audio thread is `SerumAltAudioProcessor::processBlock` (`PluginProcessor.cpp:187-191`)
delegating to `Engine::processBlock` (`Engine.cpp:176-318`). It reads the playhead tempo at
`Engine.cpp:184-187`.
The significant issue is unsynchronised sharing between the message thread and the audio
thread:
- The GUI mutates `ModulationMatrix::connections` via `addConnection`/`removeConnection`/
`clear` (`PluginEditor.cpp:536,549,559`) while the audio thread iterates the same vector in
`SynthVoice::render` at `SynthVoice.cpp:114`. `connections` is a public `std::vector`
(`ModulationMatrix.h:30`).
- The GUI mutates `MacroControls::assignments` (`PluginEditor.cpp:687,699`) while the audio
thread iterates it at `SynthVoice.cpp:125`. `assignments` is a public `std::array` of
vectors (`MacroControls.h:28`).
- The GUI writes LFO shape data through `engine.setLfoShapeData` (`PluginEditor.cpp:483-486`)
which calls `LFO::setShapeData` (`Engine.cpp:84-88`, `LFO.cpp:51-59`), while the audio
thread reads the same `shapeBuffer` in `LFO::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 once` in every header (for example `PluginProcessor.h:1`,
`Params.h:1`, `Biquad.h:1`). No `#ifndef` guards 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`/`.cpp` pair, 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 as
`note`, `velocity`, `baseFreq`, `active`, `released`, `noteId` at `SynthVoice.h:83-88`,
and `sr`, `blockSize`, `bpm`, `pitchBend` at `Engine.h:52-57`.
- Constants: `k` prefix for compile-time constants (`kNumVoices`, `kNumLfos` at
`Params.h:221-227`; `kFrames`, `kTableSize` at `Wavetable.h:18-19`; `kMaxConnections` at
`ModulationMatrix.h:28`; `kMaxAssignments` at `MacroControls.h:26`). Local constant arrays
in anonymous namespaces also use `k` (`kEnvAttack` at `Engine.cpp:30`, `kFxType` at
`PluginEditor.cpp:97`).
- Enums: `enum class` (scoped) with PascalCase enumerators (`WarpMode::BendPlus`,
`ModSource::Lfo1`, `ModTarget::Filter1Cutoff`) at `Params.h:213-219` and `Params.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`).
- `noexcept` is 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)` at `PluginProcessor.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 indexing `std::vector`/`std::array`
(`Engine.cpp:220`, `SynthVoice.cpp:79`, `FactoryPresets.cpp` throughout), and `(int)`
casts on `size()` results.
---
## 7. Anti-pattern catalog
Each entry lists the location, the problem, and the standard that a later AGENTS.md phase
should encode.
1. Parameter ID tables duplicated three times.
- `Params.h:18-208` (canonical `ids::`), `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 in `PluginProcessor.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.
2. Wavetable library allocates on the audio thread.
- `WavetableLibrary::getTable` builds lazily (`Wavetable.cpp:126-132`), reached from
`SynthVoice::render` (`SynthVoice.cpp:177-178`); `prebuild()` is defined
(`Wavetable.cpp:120-124`) but never called.
- Standard: call `prebuild()` (or build all tables) in `prepareToPlay`, never in the
render path.
3. Per-block buffer resize in the audio callback.
- `Engine::processBlock` calls `mixBuffer.setSize(...)` every block at `Engine.cpp:211`.
- Standard: size the buffer once in `prepare()` and only `clear()` per block.
4. Unsynchronised GUI/audio sharing of container state.
- `ModulationMatrix::connections` is public (`ModulationMatrix.h:30`), written by the GUI
(`PluginEditor.cpp:536,549,559`) and read on the audio thread (`SynthVoice.cpp:114`).
`MacroControls::assignments` is public (`MacroControls.h:28`), written
(`PluginEditor.cpp:687,699`) and read (`SynthVoice.cpp:125`). LFO `shapeBuffer` is
written via `setShapeData` (`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.
5. Class name does not match its file.
- `Source/RAVEButton.h` declares `RaveController` (`RAVEButton.h:14`), and
`RAVEButton.cpp` implements it.
- Standard: file pair name matches the class (`RaveController.h/.cpp`), or the class is
renamed to match the file.
6. Dead code in `SubOscillator::noteOn`.
- `SubOscillator.cpp:9-12` computes `mult` and 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`).
7. No-op override.
- `changeProgramName` is an empty override at `PluginProcessor.cpp:226-228`.
- Standard: implement it or do not override it.
8. Ignored prepare-time parameter.
- `Filter::prepare` accepts `maxBlockSize` and discards it with `(void) maxBlockSize;`
(`Filter.cpp:21-29`); effect units name the parameter `int` but never use it
(`Hyper.cpp:6`, `Chorus.cpp:6`, `Flanger.cpp:6`, etc.).
- Standard: keep the `FXUnit` interface uniform but do not add unused parameters to
concrete `prepare()` implementations.
9. Duplicated envelope curve constants.
- The attack/decay shape mapping `0.3 + curve * 2.7` / `3.0 - curve * 2.7` appears in
`Envelope.cpp:44-45` and again in the display `EnvelopeDisplay.cpp:19-20`.
- Standard: expose one shared helper so the preview cannot drift from the DSP.
10. Filter response reimplemented for display.
- `FilterDisplay::magnitude` (`FilterDisplay.cpp:6-36`) hardcodes a parallel set of
transfer-function approximations that duplicate the real DSP in `Filter.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.
11. Inconsistent pi constants.
- `kTwoPi` is defined at `Oscillator.h:67` and `SubOscillator.h:26`; `twoPi` at
`Wavetable.cpp:21`; `kPi` at `Wavetable.cpp:156` and `Filter.cpp:8`; raw literals
`6.28318530717958647692` appear at `Chorus.cpp:40-41`, `Flanger.cpp:38-39`,
`Phaser.cpp:30-31`, and `6.2831853f` at `LFO.cpp:67` and `LFODisplay.cpp:19`.
- Standard: use `juce::MathConstants<T>::pi` / `twoPi` everywhere.
12. Public mutable state exposes internals.
- `PluginProcessor.h:56-58` exposes `parameters`, `engine` and `rave` as public fields;
`ModulationMatrix.h:30` and `MacroControls.h:28` expose their collections;
`Engine.h:36-38` returns non-const references from `getMatrix()`, `getMacros()` and
`getWavetables()`.
- Standard: return `const&` from accessors and route mutation through member functions,
except where a public field is an explicit, documented design choice.
13. Unchecked raw parameter dereference.
- `PluginEditor.cpp:794` dereferences `getRawParameterValue(id)->load()` without a null
check, while the equivalent helper in `Engine.cpp:8-13` guards the pointer.
- Standard: always guard `getRawParameterValue`/`getParameter` results before use.
14. Mixed ownership for GUI children.
- `makeKnob`/`makeCombo`/`makeToggle` return raw pointers to `new`-allocated children
that JUCE owns via `addAndMakeVisible` (`PluginEditor.cpp:162-199`), and `fxUp`/
`fxDown` use raw `new` at `PluginEditor.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_ptr` for everything the plugin owns directly.
---
## 8. Hard constraints worth codifying
Only rules that the current code and build actually support are listed.
1. 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-sized `juce::AudioBuffer` and
`std::array`. The current violations (`Engine.cpp:211` and the lazy wavetable build) are
exceptions to fix, not the rule.
2. 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.
3. Parameter IDs live in the `ids` namespace in `Params.h` (`Params.h:18-208`). Engine,
editor and preset code must reference `ids::`, not re-declare string literals. (This is
the stated intent at `Params.h:5-9`; the current duplication in `Engine.cpp` and
`PluginEditor.cpp` is a violation to eliminate.)
4. The CMake source list is explicit (`CMakeLists.txt:50-87`), not a glob. Every file move
or rename must update `SERUMALT_SOURCES`.
5. All code is inside `namespace serum`, and every header uses `#pragma once` (verified
across all headers).
6. No exceptions and no user-facing alert dialogs. Use `jassert` for programmer errors,
null guards for runtime lookups, and `bool` returns for bounded inserts (see
`Oscillator.cpp:20`, `Engine.cpp:8-13`, `ModulationMatrix.cpp:6-12`).
7. When cross-compiling for Windows, keep the VST3 manifest step disabled and let
`build_windows.sh` inject `moduleinfo.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 with
`PLUGIN_CODE`/`PLUGIN_MANUFACTURER_CODE`/`VERSION` (`build_windows.sh:60-62`).
8. 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).
+412
View File
@@ -0,0 +1,412 @@
# SerumAlt UI and UX audit
Written from the working tree on branch `main`. The user report was "much of
features arent correctly described and UI is bearly functional". That bundles
two problems, and this audit keeps them apart:
1. Description. Controls lack names, units and plain descriptions.
2. Function. Some controls do not reach the DSP they claim to drive.
The single largest finding explains the "barely functional" complaint on its
own. No knob ever painted its name. `Knob` is a `juce::Slider` with
`NoTextBox`, and JUCE does not draw a slider's component name, so every one of
the roughly fifty knobs was an unlabelled arc whose only identification was a
tooltip hover. The label strings passed to `makeKnob` were correct but
invisible, and the runtime FX rename table in `updateVisuals` never reached a
user's eyes.
---
## How this was produced
Reconnaissance read `Source/PluginEditor.{h,cpp}`, `Source/Params.{h,cpp}`,
`Source/Resources.{h,cpp}`, `Source/PluginProcessor.cpp`, `Source/Engine.cpp`,
the nine effects in `Source/EffectUnits/`, and every file in `Source/GUI/`.
Four specialist passes then covered naming and descriptions, dead controls,
interaction state, and layout plus accessibility. Findings were checked against
`AUDIT_REPORT.md` (a codebase structure audit, no UI text coverage) and
`JANITOR_TASKS.md` (fourteen tasks, all reported done; JT-01 removed the unused
look-and-feel, JT-06 built the FX type list from `FXProcessor::fxTypeName`).
Nothing in either file duplicates the findings below.
The two prior documents also correct the task brief in one respect.
`Source/GUI/SerumLookAndFeel.h` no longer exists; JT-01 deleted it because it was
never installed. The GUI has no custom look-and-feel, so "draw all controls
through `SerumLookAndFeel`" resolves to "use `theme::` colours", which this work
does.
Verification used `BUILD_DIR=/tmp/... ./build_harness.sh --run`.
`UI_UX_AUDIT.md` is the deliverable that survives; the fixes listed as applied
are already in the tree.
---
## Status vocabulary
- Functioning. The parameter is read by the DSP and changes what you hear.
- Dead. Nothing reads it, or the thing that reads it throws the value away.
- Conditional. Read and audible only in some modes or for some effect types.
- Design decision. Fixing it needs a choice about behaviour or layout, so it was
left alone and recorded for the user.
---
## Control inventory
Every user-visible control, by tab. "Readout" is what the knob shows while you
drag it.
### Top bar
| Control | Parameter | Current label | Current tooltip | What it does and its unit | Recommended label | Recommended tooltip | Status |
|---|---|---|---|---|---|---|---|
| Preset combo | n/a | (preset name) | Select a factory preset | Loads one of the factory presets and replaces the whole patch | Preset | Loads a factory preset and replaces the current patch | Functioning |
| RAVE toggle | `rave` | RAVE | RAVE one-click boost (unison, width, drive, OTT, reverb) | Render-time override: unison to 8, spread and detune to full, both filter drives to 60 %, Hyper intensity to 100 %, reverb mix +0.4 | Rave | Adds unison, width, drive, OTT and reverb to the current patch | Functioning, description was accurate, label case fixed |
| Scale combo | `uiScale` | 75%..200% | Interface scale (75% - 200%) | Window scale, 75 % to 200 %. Index maps to a five-entry scale table | Scale | Interface scale, 75 % to 200 % | Functioning |
| Master knob | `master` | (invisible) | Master output volume (0-100%) | Master output gain, 0 to 100 % of full scale | Master | Master output level, 0 % to 100 % | Functioning. Percent is not the engineering unit for a gain, see deferred D-8 |
| Master readout | `master` | MASTER | none | Painted numeric readout of the same parameter | Master | none needed | Functioning |
| Tabs | n/a | OSC, FILTER, MOD, FX, MACRO | Oscillators (Tab cycles sections) etc. | Switches the visible section. Plain Tab also cycles sections | Osc, Filter, Mod, FX, Macro | Oscillators. Tab cycles sections. | Functioning, label case fixed |
### Oscillator tab
Oscillator A and Oscillator B have identical control sets. Rows 1 and 2 are
listed once.
| Control | Parameter | Current label | Current tooltip | What it does and its unit | Recommended label | Recommended tooltip | Status |
|---|---|---|---|---|---|---|---|
| On | `oscAOn` / `oscBOn` | On | Enable oscillator A | Gates the oscillator in `Oscillator::render` | On | Turns oscillator A on or off | Functioning |
| Wavetable | `oscAWave` / `oscBWave` | (combo, no visible label) | Oscillator A wavetable | Selects one of 10 factory wavetables | Table | Selects the wavetable, 10 factory tables | Functioning, combo shows its own item text |
| Warp mode | `oscAWarp` / `oscBWarp` | (combo) | Oscillator A warp mode | Selects one of 8 warp modes | Warp | Selects the wavetable warp mode | Functioning |
| Wavetable position | `oscAWtPos` / `oscBWtPos` | Wavetable Position | Wavetable position (0-100%) | Frame position in the table, 0 to 100 % | Wt position | Wavetable frame position, 0 % to 100 % | Functioning |
| Warp amount | `oscAWarpAmt` / `oscBWarpAmt` | Warp Amount | Warp amount (0-100%) | Warp depth, 0 to 100 % | Warp amount | Warp depth, 0 % to 100 % | Functioning |
| Coarse | `oscACoarse` / `oscBCoarse` | Coarse | Pitch coarse (-24 to +24 semitones) | Coarse pitch, -24 to +24 semitones | Coarse | Coarse pitch, -24 to +24 semitones | Functioning |
| Fine | `oscAFine` / `oscBFine` | Fine | Pitch fine (-100 to +100 cents) | Fine pitch, -100 to +100 cents | Fine | Fine pitch, -100 to +100 cents | Functioning |
| Level | `oscALevel` / `oscBLevel` | Level | Oscillator A level (0-100%) | Linear amplitude gain, 0 to 1 | Level | Oscillator A level, 0 % to 100 % | Functioning |
| Pan | `oscAPan` / `oscBPan` | Pan | Oscillator A pan (L100-R100) | Stereo position, hard left to hard right | Pan | Oscillator A pan, hard left to hard right | Functioning |
| Unison | `oscAUnison` / `oscBUnison` | Unison | Unison voices (1-16) | Voice count, 1 to 16. Now detented so the dial rests on whole voices | Unison | Unison voices, 1 to 16 | Functioning |
| Detune | `oscADetune` / `oscBDetune` | Detune | Unison detune (0-100%) | Per-voice detune spread, -50 to +50 cents | Detune | Unison detune spread, -50 to +50 cents | Functioning. Readout used to say "50 %" for 25 cents |
| Spread | `oscASpread` / `oscBSpread` | Spread | Unison spread (0-100%) | Stereo spread of the unison voices, 0 to 100 % | Spread | Unison stereo spread, 0 % to 100 % | Functioning |
| Phase | `oscAPhase` / `oscBPhase` | Phase | Phase (0-100%) | Start phase at note on, 0 to 360 degrees | Phase | Start phase at note on, 0 to 360 degrees | Functioning. Readout used to say a percentage |
| Random phase | `oscARandPh` / `oscBRandPh` | Random Phase | Random phase (0-100%) | Random start phase per voice, up to 360 degrees | Rand phase | Random start phase per voice, 0 to 360 degrees | Functioning. Readout used to say a percentage |
| Sub On | `subOn` | On | Enable sub oscillator | Gates the sub oscillator | On | Turns the sub oscillator on or off | Functioning |
| Sub shape | `subShape` | (combo) | Sub oscillator waveform | Sine or triangle | Shape | Sub oscillator waveform, sine or triangle | Functioning |
| Sub octave | `subOct` | (combo) | Sub oscillator octave | -2, -1 or 0 octaves, engine reads `value - 2` | Octave | Sub oscillator octave, 2 below to unison | Functioning |
| Sub level | `subLevel` | Level | Sub oscillator level (0-100%) | Linear amplitude gain | Level | Sub oscillator level, 0 % to 100 % | Functioning |
| Noise On | `noiseOn` | On | Enable noise | Gates the noise source | On | Turns noise on or off | Functioning |
| Noise type | `noiseType` | (combo) | Noise type | White or pink | Type | Noise colour, white or pink | Functioning |
| Noise level | `noiseLevel` | Level | Noise level (0-100%) | Linear amplitude gain | Level | Noise level, 0 % to 100 % | Functioning |
### Filter tab
| Control | Parameter | Current label | Current tooltip | What it does and its unit | Recommended label | Recommended tooltip | Status |
|---|---|---|---|---|---|---|---|
| On | `f1On` / `f2On` | On | Enable filter 1 | Gates the filter in `FilterBank` | On | Turns filter 1 on or off | Functioning |
| Type | `f1Type` / `f2Type` | (combo) | Filter 1 type | One of 7 models: Ladder LP, Ladder HP, Ladder BP, Diode, Comb, Formant, Screamer | Type | Selects the filter 1 model | Functioning |
| Slope | `f1Slope` / `f2Slope` | (combo) 6 dB, 12 dB, 24 dB | Filter 1 slope | One, two or four one-pole stages on the ladder and diode models. Comb, Formant and Screamer ignore it | Slope | Slope, 6, 12 or 24 dB per octave on the ladder and diode models | Conditional, see D-2 |
| Cutoff | `f1Cutoff` / `f2Cutoff` | Cutoff | Filter 1 cutoff (20 Hz - 20 kHz) | Cutoff, 20 Hz to 20 kHz before key tracking, clamped to 18 kHz after it | Cutoff | Filter 1 cutoff, 20 Hz to 18 kHz | Functioning. Tooltip overstated the reachable top |
| Resonance | `f1Res` / `f2Res` | Resonance | Filter 1 resonance (0-100%) | Resonance feedback, 0 to 98 % | Resonance | Filter 1 resonance, 0 % to 100 % | Functioning |
| Drive | `f1Drive` / `f2Drive` | Drive | Filter 1 drive (0-100%) | Per-sample drive. Damps the comb model. Formant ignores it | Drive | Filter 1 drive, 0 % to 100 %. Damps the comb model. | Conditional, see D-3 |
| Keytrack | `f1Key` / `f2Key` | Keytrack | Filter 1 keytrack (0-100%) | Cutoff follows note pitch, 100 % is full tracking | Keytrack | Filter 1 key tracking, 0 % to 100 % | Functioning |
| Routing | `fRoute` | (combo) | Filter routing (serial / parallel / split) | Serial, parallel or split | Routing | Filter routing, serial, parallel or split | Functioning |
| Mix | `fMix` | Mix | Filter mix (0-100%) | Crossfades filter 1 into filter 2. Read only in parallel routing | Mix | Blends filter 2 over filter 1 in parallel routing, 0 % to 100 % | Conditional, see D-1 |
| Output | `fOut` | Output | Filter output (0-100%) | Output gain, engine multiplies by 1.5, so the 0.667 default is unity | Output | Filter output gain, 0 % to 150 % | Functioning. The old readout understated the range: 67 % is unity |
### Modulation tab
| Control | Parameter | Current label | Current tooltip | What it does and its unit | Recommended label | Recommended tooltip | Status |
|---|---|---|---|---|---|---|---|
| Attack | `env1A`..`env4A` | Attack | Attack time (0.5 ms - 12 s) | Attack time, 0.5 ms to 12 s, cubic taper | Attack | Attack time, 0.5 ms to 12 s | Functioning |
| Decay | `env1D`..`env4D` | Decay | Decay time (0.5 ms - 12 s) | Decay time, 0.5 ms to 12 s | Decay | Decay time, 0.5 ms to 12 s | Functioning |
| Sustain | `env1S`..`env4S` | Sustain | Sustain level (0-100%) | Sustain level, 0 to 100 % | Sustain | Sustain level, 0 % to 100 % | Functioning |
| Release | `env1R`..`env4R` | Release | Release time (0.5 ms - 12 s) | Release time, 0.5 ms to 12 s | Release | Release time, 0.5 ms to 12 s | Functioning |
| Curve | `env1Curve`..`env4Curve` | Curve | Envelope curve (0-100%) | Shape exponent, mapped to 0.3 to 3.0 | Curve | Curve shape. Low is a fast attack and slow decay, high is the reverse. | Functioning. The readout is still a percentage of an exponent, see D-9 |
| Envelope graph | n/a | n/a | none | ADSR curve preview, drawn from the same shape functions as the DSP | n/a | Add a tooltip naming the envelope | Functioning |
| Shape | `lfo1Shape`..`lfo4Shape` | (combo) | LFO shape | Sine, Triangle, Saw, Square, S&H, Step, Freehand | Shape | Selects the LFO waveform | Functioning |
| Sync | `lfo1Sync`..`lfo4Sync` | Sync | Tempo-sync LFO | Locks the rate to host tempo and uses Beat instead of Rate | Sync | Locks the LFO to host tempo and uses the Beat knob | Functioning |
| Rate | `lfo1Rate`..`lfo4Rate` | Rate | LFO rate (0.02 Hz - 30 Hz) | Rate, 0.02 Hz to 30 Hz, log taper | Rate | LFO rate, 0.02 Hz to 30 Hz | Functioning. Readout used to say "50 %" for about 0.77 Hz |
| Beat | `lfo1Beat`..`lfo4Beat` | Beat | Beat division (1/32 - 4 bars) | Period of one cycle, one of 16 divisions from 1/32 beat to 16 beats | Beat | Cycle length, 1/32 beat to 16 beats | Functioning. Readout used to say a percentage, and the old tooltip was wrong at the fast end |
| Phase | `lfo1Phase`..`lfo4Phase` | Phase | Phase (0-100%) | Start phase offset, 0 to 360 degrees | Phase | Start phase, 0 to 360 degrees | Functioning. Readout used to say a percentage |
| Fade | `lfo1Fade`..`lfo4Fade` | Fade | Fade in (0-100%) | Fade in time, 0 to 8 seconds | Fade | Fade in time, 0 s to 8 s | Functioning. Readout and tooltip both said percent for seconds |
| Delay | `lfo1Delay`..`lfo4Delay` | Delay | Start delay (0-100%) | Start delay, 0 to 4 seconds | Delay | Start delay, 0 s to 4 s | Functioning. Readout and tooltip both said percent for seconds |
| LFO graph | n/a | n/a | none | Shape preview. Step and Freehand are drag-editable | n/a | Describe the drag gesture | Functioning, discoverability is D-11 |
| Source | n/a | (combo) | Modulation source | One of 17 sources | Source | Selects the modulation source | Functioning |
| Target | n/a | (combo) | Modulation destination | One of 35 destinations | Target | Selects the modulation destination | Functioning |
| Depth | n/a | (invisible) | Modulation depth (-1 to +1) | Amount applied to the next Add, -100 % to +100 % of the target range. Negative inverts | Depth | Modulation amount for the next Add, -100 % to +100 % of the target range | Functioning. Readout and tooltip rewritten |
| Bipolar | n/a | Bipolar | Bipolar modulation depth | Recentres a one-sided source for the next Add | Bipolar | Recentres a one-sided source so the modulation swings both ways, for the next Add | Functioning |
| Add | n/a | Add | Add modulation connection | Appends a connection, capped at 32 | Add | Adds the selected source and destination to the matrix | Functioning |
| Remove last | n/a | Remove Last | Remove last modulation connection | Removes the newest row only | Remove last | Removes the newest matrix row, older rows stay | Functioning |
| Clear | n/a | Clear | Clear all modulation connections | Removes every connection | Clear all | Removes every modulation connection | Functioning |
| Connection list | n/a | (read-only text) | none | Source, target, depth and polarity per row | n/a | Every modulation connection, newest last | Functioning |
### Effects tab
Each of the eight slots has the same shape: a type combo, up and down arrows,
and five knobs. The knob names, tooltips and readout units follow the selected
effect, and that table was rebuilt from the DSP each effect actually runs.
| Control | Parameter | Current label | What it does and its unit | Readout unit | Status |
|---|---|---|---|---|---|
| Type | `fx1Type`..`fx8Type` | (combo) | Off, Hyper, Chorus, Flanger, Phaser, Distortion, EQ, Compressor, Delay, Reverb | n/a | Functioning |
| Up / Down | n/a | arrow glyphs | Swaps this slot's six parameters with the neighbour | n/a | Functioning. The end arrows are now disabled instead of doing nothing |
| Mix | `fx1Mix`..`fx8Mix` | Mix | Dry and wet mix, 0 to 100 % | percent | Functioning |
| Off P1..P4 | `fx*P1`..`fx*P4` | Unused | The rack skips an Off slot, so all four do nothing | n/a | Dead by design, now labelled and greyed |
| Hyper | | Intensity, Low Amount, High Amount, Output | 0-100 %, 40-100 %, 40-100 %, 0.5x-1.5x | percent, percent, percent, x | Functioning |
| Chorus | | Rate, Depth, Width, Unused | 0.1-5 Hz, 0.5-6 ms, 0-100 %, nothing | Hz, ms, percent | Rate and Depth used to read as percent; P4 is genuinely dead |
| Flanger | | Rate, Depth, Feedback, Unused | 0.05-2.05 Hz, 0.3-5 ms, 0-90 %, nothing | Hz, ms, percent | Rate and Depth used to read as percent; P4 is genuinely dead |
| Phaser | | Rate, Depth, Feedback, Stages | 0.05-2.05 Hz, 30-90 %, 0-70 %, 2-6 | Hz, percent, percent, count | Rate used to read as percent; Stages used to read "50 %" |
| Distortion | | Drive, Shape, Tone, Output | 1x-25x, 0-100 %, 800 Hz-16 kHz, 0.4x-1.6x | x, percent, Hz, x | All four used to read as percent |
| EQ | | Low Gain, Mid Gain, High Gain, Mid Freq | +/-15 dB, +/-12 dB, +/-15 dB, 200 Hz-5 kHz | dB, dB, dB, Hz | All four used to read as percent |
| Compressor | | Threshold, Ratio, Attack, Release | -40-0 dB, 2:1-20:1, 2-100 ms, 30-500 ms | dB, ratio, ms, ms | All four used to read as percent, and two of them share a name with a different unit on the Modulation tab |
| Delay | | Time, Feedback, Damping, Ping-Pong | 1-2000 ms, 0-92 %, 0-95 %, on or off | ms, percent, percent, on/off | Time used to read as percent; Ping-Pong used to read "50 %" |
| Reverb | | Size, Damping, Width, Predelay | 20-99 %, 0-100 %, 0-100 %, 0-150 ms | percent, percent, percent, ms | Predelay used to read as percent |
The old tooltips were worse than the readouts. They were only rewritten when a
knob's name changed, so two effects sharing a name (Rate, Damping, Feedback,
Output) left the previous effect's tooltip on screen, and every one of them
claimed "normalised 0-100%".
### Macro tab
| Control | Parameter | Current label | Current tooltip | What it does and its unit | Recommended label | Recommended tooltip | Status |
|---|---|---|---|---|---|---|---|
| Macro knobs | `macro1`..`macro4` | Energy, Width, Drive, Atmosphere | (name) macro (0-100%) | Macro amount, 0 to 100 %, drives its assigned destinations | Energy / Width / Drive / Atmosphere | Energy macro, 0 % to 100 %, drives its assigned destinations | Functioning. Names come from `MacroControls::macroName`; the macro panels have no title of their own, so the knob label is the only identifier |
| Macro | n/a | (combo) | Macro to assign | Selects which of the four macros to edit | Macro | Selects the macro to edit | Functioning |
| Target | n/a | (combo) | Destination parameter | One of 35 destinations | Target | Selects the destination parameter | Functioning |
| Depth | n/a | (invisible) | Macro assignment depth (0 to 1) | Amount applied to the next Assign, 0 to 100 % of the target range | Depth | Macro amount for the next Assign, 0 % to 100 % of the target range | Functioning. Started at zero, so a default Assign was silent; now starts at half |
| Assign | n/a | Assign | Assign destination to the selected macro | Appends an assignment, capped at 8 per macro | Assign | Adds this destination to the selected macro | Functioning |
| Clear all | n/a | Clear All | Clear all macro assignments | Empties all four macros | Clear all | Removes every macro assignment from all four macros | Functioning |
| Assignment list | n/a | (read-only text) | none | Target and depth per macro | n/a | Every macro assignment, grouped by macro | Functioning |
---
## Applied mechanical fixes
Every item below was applied, compiles, and passes the harness.
### Description fixes
- A1. Knobs now paint their name. New `Source/GUI/KnobLookAndFeel.{h,cpp}` draws
the dial from `theme::` colours and reserves a 16 px strip at the foot of the
knob for the label, which `Knob::paint` fills. This is the change that makes
every other label in the editor visible for the first time.
- A2. All eight FX parameter knobs are named, tooltipped and unit-formatted per
effect type from one table (`kFxParamDisplay` in `PluginEditor.cpp`), replacing
the two tables that disagreed with each other.
- A3. Unit formatters added: LFO rate in Hz, LFO beat as a cycle length, LFO fade
and delay in seconds, oscillator detune and both phases in cents and degrees,
filter output as a percentage of its real 0-150 % range, modulation depth as
-100 % to +100 %, macro depth as 0-100 %, compressor threshold and ratio,
filter and EQ gains in dB, chorus and flanger rate in Hz and depth in ms,
distortion drive and output as multiples, phaser stage count, ping-pong as on
or off, reverb predelay in ms.
- A4. Tooltips rewritten for every control that had a vague one: all eight
effect types, both filters (cutoff now says 18 kHz, which is the real ceiling),
all four envelopes, all four LFOs, the modulation matrix buttons, the macro
buttons, the filters' drive and slope, both lists, the preset and scale combos,
and the RAVE toggle.
- A5. Labels corrected where the name was wrong or abbreviated: tab labels are
now Osc/Filter/Mod/FX/Macro, "Random Phase" is "Rand phase" to fit its 76 px
knob, and the RAVE toggle reads "Rave".
### Function fixes that were one line each
- B1. Double click restored a hardcoded 0.5 for every knob. Forty-nine knob
parameters have a different default, so the gesture silently wrote the wrong
value. `makeKnob` now reads the parameter's own default through
`AudioProcessorParameter::convertFrom0to1`. The modulation depth knob double
clicks to its neutral zero, and the macro depth knob to its new half.
- B2. The macro depth knob started at zero, so an Assign made without touching
it created an inaudible assignment. It now starts at half.
- B3. Unison had no detent, so the dial rested between voices. Both unison knobs
now step in whole voices.
- B4. The first FX slot's up arrow and the last slot's down arrow were enabled
but did nothing. They are now disabled.
- B5. An FX parameter the selected effect ignores stayed live and editable. Those
knobs are now disabled and labelled Unused.
- B6. The knob value popup was never clamped to the window, so it was cut off for
knobs near an edge. It is now clamped on both axes.
- B7. The popup is parented to the editor while the interface scale is applied to
a child, so it ignored the 75-200 % scale. It now measures the scale from its
own transform and sizes itself to match.
- B8. Switching section mid-drag could strand the popup on screen, because it is
not a child of the hidden view. `Knob::visibilityChanged` now takes it down.
### Interaction and accessibility fixes
- C1. Toggle buttons repainted on mouse activity but never read the hover state,
so every On, Sync and Bipolar LED looked identical under the cursor. `paint`
now brightens the LED and the name on hover and dims both when disabled.
- C2. `ToggleButton::paint` ignored `isEnabled()`. It no longer does.
- C3. Knobs now take keyboard focus. `juce::Slider` disables focus in its own
constructor, which left every knob unreachable without a mouse and cut off the
accessible value interface.
### Layout constant fixes
`Source/GUI/AGENTS.md` requires every position to be derived from the `layout`
namespace. Each tab declared its own private `colW`, `colGap` and panel heights,
and the tab-bar height was the bare literal `60` in six places. Those metrics now
live in `layout`, and the per-tab locals alias them, so the value has one home:
- L1. Named the tab-bar height as `layout::tabBarHeight` and used it in all five
tab views plus the matrix and macro panel height calculations.
- L2. Hoisted the panel grids: `twoColW`, `threeColW`, `fourColW`, `fourColGap`,
and the panel heights `oscPanelH`, `smallPanelH`, `filterPanelH`, `envPanelH`,
`lfoPanelH`, `macroPanelH`.
- L3. Named `depthKnobHeight` for the matrix and macro depth knob, which was the
literal `60` in two `setBounds` calls and again inside both list tops.
- L4. Named `toggleWide` for the Bipolar toggle width, which was the literal `60`
in its `setBounds` and again in the `addX` offset that had to stay in step
with it.
- L5. Named `headerCentreY` for the header row centre line, which was
`titleHeight + 30` in two places with the `30` unexplained.
The first attempt at L5 derived the centre from `headerHeight`, which is the
36 px toggle, and produced 40 instead of 52. That would have shifted the whole
matrix and macro header row by 12 px. It is derived from `depthKnobHeight` now,
and a separate check compiles the namespace and asserts all fourteen new
constants equal the literals they replaced.
Two layout reports were left in place rather than acted on. The top bar still
positions its controls with literals (`presetCombo` at 200, 16, 220, 24;
`masterKnob` at 906, 0, 64, 60; the tab pitch of 92, and so on), because naming
those means deciding what the top bar's own grid is, which is a design choice
rather than a rename. The `layout::labelHeight` constant was deleted: it was
declared and never read.
---
## Deferred items needing a design decision
None of these were applied. Each changes behaviour, layout or sound, and each
needs the user's call.
- D-1. Filter Mix does nothing in serial and split routing. `FilterBank` reads
`p.mix` only in the parallel branch. Either define a crossfade for the other
two routes or disable the knob in them. Real DSP work.
- D-2. Filter Slope does nothing for Comb, Formant and Screamer, and on
Ladder BP the 6 dB and 12 dB settings produce the same two-stage filter.
Making the label honest needs DSP work; the tooltip now at least names the
models it affects.
- D-3. Filter Drive does nothing for Formant. `Filter::formant` is called
without the drive argument.
- D-4. The Modulation and Macro depth knobs and the Bipolar toggle only affect
the next Add or Assign. They look like live controls for an existing
connection. Relabel them as defaults, or make them edit the selected row.
- D-5. The first FX slot's down arrow moves an effect earlier in the chain while
pointing down. In the two-column grid the reading order wraps right to left
between rows, so slot 2 sits to the right of slot 3. Number the slots, or
relabel the arrows for a two-column grid.
- D-6. The Filter tab leaves about 370 px of empty space below three 300 px
panels. Nothing breaks, but the tab looks unfinished.
- D-7. The OSC tab puts Sub and Noise in a second row beneath both oscillators.
The reading order is fine; the geometry suggests Sub belongs to Osc A and
Noise to Osc B without saying so.
- D-8. Master and the oscillator, sub and noise Level knobs show a percentage
for a linear gain. dB is the engineering unit. Changing it changes what every
readout says, so it was left as percent.
- D-9. Envelope Curve shows a percentage of a shape exponent that runs 0.3 to
3.0. "Amount" or a named curve would be better than a percentage.
- D-10. No control marks itself as changed from its default. Nothing reads
`getDefaultValue()`.
- D-11. Step and Freehand LFO shapes are drag-editable, but nothing says so: no
tooltip on an editable graph, no cursor change, no hover state, no undo, and a
preset load silently discards the drawn shape. `LFODisplay` is not a
`SettableTooltipClient` today.
- D-12. Plain Tab switches section, which means JUCE's focus traversal never
runs and a keyboard-only user cannot reach any control except a combo box or a
text editor. This is a convention choice: keep Tab for sections and move it to
Ctrl+Tab, or keep Tab for controls and use function keys for sections.
- D-13. `ToggleButton` derives from `juce::Component`, so it has no accessible
role, name or state, and cannot be activated from the keyboard. Deriving from
`juce::Button` would fix all of that at once but changes the class contract.
- D-14. The waveform, filter, envelope and LFO graphs have no text or numeric
equivalent, so a screen reader announces nothing for any of them. Each needs a
description string.
- D-15. Knobs have no focus outline. `hasKeyboardFocus` is read nowhere.
- D-16. No group is marked, so a group that the selected shape ignores keeps its
old values.
---
## Verified as functioning, with evidence
The following were suspected dead and are not. Recorded so the check is not
repeated.
- Modulation depth and polarity reach `SynthVoice` and `Engine`, and the bipolar
flag changes the modulation math.
- Macro depth reaches both the global and per-voice modulation sums.
- Remove Last removes the newest matrix row, matching its label, with no hidden
ordering rule.
- The master readout and the master knob drive the same parameter the output
stage uses.
- The filter type and LFO shape combo indices agree between the combo, the
stored value, the DSP and the display, for all 7 and all 7 values.
- The UI scale combo's five items map exactly onto the five-entry scale table.
- All six On toggles gate their DSP. All eight warps, all four sub and noise
controls, all four envelopes, all four LFOs, and the matrix and macro editors
are live.
- RAVE's tooltip matched its implementation exactly.
- No knob can write outside its parameter range, and the Unison readout and the
DSP round identically.
---
## Janitor cleanups made beyond UI text
Kept separate from the fixes above.
- J-1. Deleted the now-dead formatter `fmtDepth`, which the modulation and macro
depth knobs were the only users of.
- J-2. Deleted `layout::labelHeight`, which was declared and never read. Toggle
and readout label sizes are hardcoded, so the constant described nothing.
- J-3. Replaced the duplicated per-effect label table with the single
`kFxParamDisplay` table, which removes the latent stale-tooltip bug rather than
only fixing its symptoms.
- J-4. `Source/GUI/Slider.h` and `Slider.cpp` define a horizontal slider that no
code constructs. Reported only: deleting it means editing `CMakeLists.txt` and
the GUI module's owned-file list, which is wider than this change. Recommend
it as a follow-up.
- J-5. `MacroControls::removeAssignment` is defined and never called. The Macro
tab has no control that would call it, so a single macro assignment cannot be
deleted. Reported only: adding the control is a new feature.
---
## What could not be verified
There is no headless way to check a visual change in this repository. The
harness drives `SerumAltAudioProcessor` and never constructs the editor, so it
proves the code compiles and the audio path still renders every preset, and it
proves nothing about the pixels. The knob label, the toggle hover state, the
disabled FX arrows and the popup clamping were reasoned from the JUCE 7.0.12
sources in `third_party/JUCE` and checked against them line by line, but they
have not been seen on screen. Do not treat a passing harness run as visual
proof.
One consequence worth stating plainly: the new knob look-and-feel replaces
`LookAndFeel_V4::drawRotarySlider` for knobs only. Every other control still uses
the shared default look-and-feel, exactly as before.
---
## Verification
- `BUILD_DIR=/tmp/sa-verify ./build_harness.sh --run` reaches `100% tests passed`
with zero first-party warnings.
- The parameter surface is unchanged, checked at runtime rather than by reading
the source. A temporary build printed every parameter of the live processor as
`id | name | default`. It reported 155 parameters (the 96 declared in
`createParameterLayout` plus the 59 generated entries), and the sorted list had
fingerprint `62b4556078a261a5b1aaa8ba34081f44` both before and after the UI
edits. No ID, name, range or default moved. The only parameter interaction the
fix added is a read of `getDefaultValue()` and `convertFrom0to1()`.
- Nothing under `Source/Params.h`, `Source/Params.cpp` or
`createParameterLayout` in `Source/PluginProcessor.cpp` was edited.
- `third_party/JUCE` is untouched. The untracked `third_party/JUCE/DemoRunner`
and `third_party/JUCE/Projucer` files are upstream binaries dated 2024-04-15,
not build products of this work.
- No checked-in `build_*` directory was written to. Every build used a directory
under `/tmp`.
- The pre-existing uncommitted work in the tree was preserved.
+699
View File
@@ -0,0 +1,699 @@
# Prepared janitor tasks
Prepared from the current working tree on branch `main`, using the `graft` context
graph and a read-only verification pass. All 14 tasks were then dispatched to
`janitors-coder` agents on the user's confirmation and completed. See the
Execution status section at the end of this file for what each agent changed and
the one remaining limitation.
## How to read this file
Each task is a handoff for one `janitors-coder` agent. The shape follows the
janitor skill: observed problem, desired behavior, file locations, acceptance
check, and conflicts.
Task types are `Defect fix` when current behavior is wrong for the user, and
`Cleanup` when the change should preserve behavior. Fixes that change what the
user sees or hears are marked `Risk`.
Baseline at preparation time. A clean harness build and CTest pass:
`BUILD_DIR=/tmp/serumalt-check ./build_harness.sh --run` reaches `100% tests
passed`. The build also reports first-party compiler warnings (task JT-05).
## Ownership before dispatch
The worktree is dirty. These files have uncommitted edits and should be
committed or frozen before any task touches them:
```text
Source/FXProcessor.cpp Source/PluginEditor.cpp
Source/SynthVoice.cpp Source/SynthVoice.h
README.md AGENTS.md
AUDIT_REPORT.md opencode.json
Source/AGENT.md -> AGENTS.md (staged rename)
Source/*/AGENT.md -> AGENTS.md (staged renames)
```
The `Source/FXProcessor.cpp` edit repairs code that does not compile at `HEAD`
(`HEAD` declares a two-dimensional `units` array in `FXProcessor.h` but indexes
it as one-dimensional in `FXProcessor.cpp`). Commit it first. Do not let two
agents edit the same file at once.
## Task index
| Id | Title | Type | Priority | Primary files |
| --- | --- | --- | --- | --- |
| JT-01 | Remove the unused look-and-feel | Cleanup | P1 | `Source/PluginEditor.h`, `Source/GUI/` |
| JT-02 | Make the LFO preview use the real LFO shape | Defect fix | P1 | `Source/LFO.{h,cpp}`, `Source/GUI/LFODisplay.{h,cpp}` |
| JT-03 | Give macros one set of names | Defect fix | P1 | `Source/MacroControls.*`, `Source/PluginProcessor.cpp`, `Source/PluginEditor.cpp` |
| JT-04 | Report rejected mod and macro inserts | Defect fix | P1 | `Source/PluginEditor.cpp` |
| JT-05 | Drive first-party compiler warnings to zero | Cleanup | P2 | several `Source/` files |
| JT-06 | Use `FXProcessor::fxTypeName` for the FX type list | Cleanup | P2 | `Source/PluginEditor.cpp` |
| JT-07 | Remove unused members and dead tracking | Cleanup | P2 | `Source/PluginEditor.h`, `Source/Filter.*`, `Source/Engine.*` |
| JT-08 | Remove dead parameters and discarded computations | Cleanup | P2 | `Source/SubOscillator.*`, `Source/Oscillator.*`, `Source/Wavetable.cpp`, `Source/Filter.*`, `Source/FilterBank.*` |
| JT-09 | De-duplicate the test render loop | Cleanup | P2 | `Source/Tests/TestMain.cpp` |
| JT-10 | Unify pi and two-pi constants | Cleanup | P2 | `Source/EffectUnits/*`, `Source/Wavetable.cpp`, `Source/SubOscillator.h`, `Source/GUI/LFODisplay.cpp`, `Source/EffectUnits/Biquad.h` |
| JT-11 | Share the envelope curve and mark the filter preview | Cleanup | P2 | `Source/Envelope.*`, `Source/GUI/EnvelopeDisplay.cpp`, `Source/GUI/FilterDisplay.cpp` |
| JT-12 | Reuse `paramIds::` in `PluginProcessor` | Cleanup | P3 | `Source/PluginProcessor.cpp` |
| JT-13 | Guard the mono-buffer and capacity assumptions | Hardening | P3 | `Source/FXProcessor.cpp`, `Source/SynthVoice.cpp` |
| JT-14 | Fix stale guidance and remaining prose slop | Cleanup | P3 | `AGENTS.md` files, `.claude/`, `build_windows.sh`, `AUDIT_REPORT.md` |
---
## JT-01 Remove the unused look-and-feel
Type. Cleanup.
Priority. P1.
Risk. None visible. The class was never installed, so deleting it changes nothing
on screen. This outcome was chosen by the user on 2026-05-10: keep JUCE defaults.
Observed problem. `Source/PluginEditor.h:39` declares `SerumLookAndFeel laf`, and
no source file installs it. A repo-wide search finds no `setLookAndFeel` or
`setDefaultLookAndFeel` call, and `git log -S setLookAndFeel` shows none ever
existed. Every control draws with JUCE's default look-and-feel, and the custom
`drawRotarySlider` and `drawLinearSlider` in `Source/GUI/SerumLookAndFeel.h` have
never run. The class and its member are dead, and `Source/GUI/AGENTS.md:19` still
requires controls to be drawn through it. `Source/GUI/Knob.h:11` repeats the claim.
Desired behavior. JUCE's default look-and-feel stays in use, the dead class and
member are gone, and the guidance matches the code.
Work. Delete `Source/GUI/SerumLookAndFeel.h`. Remove the include at
`Source/PluginEditor.h:5` and the `laf` member at `Source/PluginEditor.h:39`.
Rewrite the comment at `Source/GUI/Knob.h:11` so it no longer says the knob is
styled by `SerumLookAndFeel`. In `Source/GUI/AGENTS.md`, drop `SerumLookAndFeel.h`
from Owned files and remove the "Draw all controls through `SerumLookAndFeel`"
rule. The header is header-only and absent from `SERUMALT_SOURCES`, so CMake needs
no change.
Files in scope. `Source/PluginEditor.h`, `Source/GUI/SerumLookAndFeel.h`
(delete), `Source/GUI/Knob.h`, `Source/GUI/AGENTS.md`.
Acceptance. `BUILD_DIR=/tmp/serumalt-check ./build_harness.sh --run` passes.
`grep -rn "SerumLookAndFeel" Source/` returns nothing. The GUI is unchanged,
because the class was never installed.
---
## JT-02 Make the LFO preview use the real LFO shape
Type. Defect fix.
Priority. P1.
Risk. Changes the LFO display. The audio path should not change.
Observed problem. `Source/GUI/LFODisplay.cpp:23-52` re-implements every LFO
shape. The audio path implements the same shapes in `Source/LFO.cpp:65-96`. Two
problems follow.
1. Sample and hold disagrees. The display computes a fixed multiplicative hash
at `Source/GUI/LFODisplay.cpp:31-32`
(`((step * 2654435761u) & 0xffffu) / 32768.0f - 1.0f`), while the audio
returns `holdValue`, which comes from `juce::Random`
(`Source/LFO.cpp:13,79,131-133`). The preview shows a sequence the synth
never plays.
2. The remaining shapes can drift. Triangle, saw, square, step and freehand are
copied at `LFODisplay.cpp:26-28,34-52` and `LFO.cpp:73,75,77,80-92`.
The display also hardcodes `64` at `Source/GUI/LFODisplay.cpp:8,9,10,17`, while
`LFO.h:16` already exposes `static constexpr int kShapePoints = 64;`.
Desired behavior. One function decides the value of a shape, and both the audio
render and the display call it. The preview then cannot disagree with the sound.
Work. Extract the shape switch from `LFO::shapeValue` (`Source/LFO.cpp:65-96`)
into a shared, pure function that takes phase, shape, step count and the shape
buffer, for example `float lfoShapeValue (float phase, LfoShape shape, int steps,
const float* data, int dataSize)`. Call it from both `LFO::shapeValue` and
`LFODisplay::valueAt`. Replace the hardcoded `64` in `LFODisplay` with
`LFO::kShapePoints`. For sample and hold, the preview should not invent values;
either have the display read the live hold value through `Engine::getLfos()`
(`Source/Engine.h:42`), or draw the S&H shape as a clearly labelled schematic and
stop pretending it is the played sequence.
Files in scope. `Source/LFO.h`, `Source/LFO.cpp`, `Source/GUI/LFODisplay.h`,
`Source/GUI/LFODisplay.cpp`.
Acceptance. `BUILD_DIR=/tmp/serumalt-check ./build_harness.sh --run` passes. A
focused check confirms the display and `LFO::shapeValue` return the same value for
the same phase and shape, including sample and hold. State which S&H approach you
took.
---
## JT-03 Give macros one set of names
Type. Defect fix.
Priority. P1.
Risk. The macro parameter display names change in the host. Parameter IDs do not
change, so saved projects still load.
Observed problem. The same four macros have two names. `Source/MacroControls.cpp:30-40`
returns "Energy", "Width", "Drive" and "Atmosphere". The APVTS parameters are
named "Macro 1" through "Macro 4" at `Source/PluginProcessor.cpp:168-171`, and
the assignment combo repeats "Macro 1" through "Macro 4" at
`Source/PluginEditor.cpp:686`. The macro panels and the macro list use
`MacroControls::macroName` (`Source/PluginEditor.cpp:668-669,948`). A user sees
"Energy" on the panel and "Macro 1" in the host automation lane and in the assign
combo.
Desired behavior. One source of truth for the four macro names.
Work. Make `MacroControls::macroName` the source of truth. Use it to build the
APVTS display names in `PluginProcessor.cpp` and the `macroAssignIndex` items in
`PluginEditor.cpp`. Leave the parameter IDs (`ids::macro1` and friends) and the
normalized values unchanged.
Files in scope. `Source/MacroControls.h`, `Source/MacroControls.cpp`,
`Source/PluginProcessor.cpp`, `Source/PluginEditor.cpp`.
Acceptance. `BUILD_DIR=/tmp/serumalt-check ./build_harness.sh --run` passes. The
host automation names, the assign combo and the panel labels show the same four
strings. `grep -rn '"Macro 1"' Source/` returns only
`Source/Params.cpp:22`, which is the stable `ModSource` serialisation token used by
saved state and must not change.
---
## JT-04 Report rejected mod and macro inserts
Type. Defect fix.
Priority. P1.
Risk. Small UI change, no audio change.
Observed problem. `ModulationMatrix::addConnection` and
`MacroControls::addAssignment` return `bool` and reject inserts past
`kMaxConnections` or `kMaxAssignments` (`Source/ModulationMatrix.cpp:6-12`,
`Source/MacroControls.cpp:6-13`). The editor ignores both results at
`Source/PluginEditor.cpp:550` and `Source/PluginEditor.cpp:716`, so a click that
fails looks identical to a click that worked. The list does not change and no
message appears.
Desired behavior. A rejected insert is visible to the user.
Work. Check the returned value at both call sites. The smallest useful fix is to
disable or grey the Add buttons when the relevant count reaches its cap, and to
keep the list text and selection unchanged on rejection. A status message is also
acceptable. Do not change the caps.
Files in scope. `Source/PluginEditor.cpp`. Read the caps from
`ModulationMatrix::kMaxConnections` and `MacroControls::kMaxAssignments`; do not
hardcode the numbers.
Acceptance. `BUILD_DIR=/tmp/serumalt-check ./build_harness.sh --run` passes. With
the connection or assignment count at its cap, the Add control is visibly
unavailable and clicking it changes nothing.
---
## JT-05 Drive first-party compiler warnings to zero
Type. Cleanup.
Priority. P2.
Observed problem. The harness build compiles with `-Wall -Wextra` plus JUCE's
recommended warning flags and reports more than fifty warning lines from files
under `Source/`. Representative sites, all current:
- `Source/SynthVoice.cpp:131,138` sign conversion from `int` to `size_t`.
- `Source/GUI/LFODisplay.cpp:11,12,16,32` sign conversion and a float equality.
- `Source/PluginEditor.cpp:88` `fmtSlope` is defined and never used.
- `Source/Oscillator.cpp:15` unused parameter `freqHz`.
- `Source/GUI/Slider.cpp:6` a local named `formatter` shadows a member.
- `Source/GUI/Knob.cpp:77` a local named `cursor` shadows a member.
- `Source/Params.cpp:8,33,58,97`, `Source/Oscillator.cpp:85,108`,
`Source/LFO.cpp:68`, `Source/Filter.cpp:57`, `Source/FXProcessor.cpp:53`,
`Source/GUI/FilterDisplay.cpp:13`, `Source/GUI/LFODisplay.cpp:23`
`-Wswitch-enum` cases.
- `-Wfloat-equal` comparisons in `Source/GUI/EnvelopeDisplay.cpp:8`,
`Source/GUI/FilterDisplay.cpp:40`, `Source/GUI/WaveformDisplay.h:18`,
`Source/Filter.cpp:99`, `Source/LFO.cpp:43`, `Source/Oscillator.cpp:28,29`,
`Source/MacroControls.cpp:82`, `Source/PluginProcessor.cpp:409`.
Desired behavior. The harness build reports no warnings from `Source/`. Warnings
from `third_party/JUCE` are out of scope.
Work. List the warnings with:
```bash
BUILD_DIR=/tmp/serumalt-check ./build_harness.sh 2>&1 | grep 'warning:' | grep -v third_party
```
Fix the unused function, the unused parameter and the two shadowed locals by
deletion or rename. Fix the sign-conversion sites with explicit `(size_t)` or
`(juce::uint32)` casts, matching the surrounding style. For `-Wswitch-enum`, name
every enumerator explicitly. For `-Wfloat-equal`, check the meaning of each
comparison first. Some are deliberate cache keys that compare a stored parameter
against the current one. If the exact comparison is intended, keep the behavior
and make the intent explicit with a small named helper or a narrow, commented
suppression. Do not silently weaken a change-detection check.
Files in scope. The `Source/` files named above and any other first-party file
that the command reports.
Acceptance. The command above prints nothing from `Source/`.
`BUILD_DIR=/tmp/serumalt-check ./build_harness.sh --run` passes.
---
## JT-06 Use `FXProcessor::fxTypeName` for the FX type list
Type. Cleanup.
Priority. P2.
Observed problem. `FXProcessor::fxTypeName` is declared at
`Source/FXProcessor.h:47` and defined at `Source/FXProcessor.cpp:51-67` with no
callers. The same ten labels are written again as a literal `juce::StringArray`
at `Source/PluginEditor.cpp:604-605`. The two lists must stay in the same order as
the `FxType` enum, and nothing enforces that.
Desired behavior. The FX type combo is built from the one label function.
Work. In `PluginEditor::buildFxTab`, replace the literal array with a loop over
`(int) FxType::Count` that adds `FXProcessor::fxTypeName (t)`. Confirm the combo
indices still line up with `FxType` values, since the combo index is used
directly as the parameter value (`Source/PluginEditor.cpp:246` area and the FX
type attachment).
Files in scope. `Source/PluginEditor.cpp`, `Source/FXProcessor.h`.
Acceptance. `BUILD_DIR=/tmp/serumalt-check ./build_harness.sh --run` passes. The
combo shows Off through Reverb in enum order and selecting a type still sets the
matching slot type. `FXProcessor::fxTypeName` now has a caller.
Conflict. `Source/FXProcessor.cpp` and `Source/PluginEditor.cpp` both have
uncommitted edits. Commit or freeze them first.
---
## JT-07 Remove unused members and dead tracking
Type. Cleanup.
Priority. P2.
Observed problem. Four unused declarations remain:
1. `Source/PluginEditor.h:66` declares
`std::vector<juce::Component*> oscAComponents, oscBComponents;`. Neither name
appears anywhere else in `Source/`.
2. `Filter::svfLow` and `Filter::svfHigh` are declared at
`Source/Filter.h:53,55` and defined at `Source/Filter.cpp:108,130`. Each name
occurs twice, at the declaration and the definition. The sibling `svfBand` is
used at `Source/Filter.cpp:222`.
3. `Engine::getActiveVoiceCount` (`Source/Engine.h:46`) has no callers, so
`activeVoiceCount` (`Source/Engine.h:70`) is written at
`Source/Engine.cpp:59` and `Source/Engine.cpp:363` and never read. The per-block
tally at `Source/Engine.cpp:360-362` exists only to feed it.
4. `PluginEditor.cpp:88` `fmtSlope` is unused, which the compiler also reports
(task JT-05 overlaps here; one agent should own both).
Desired behavior. No unused storage and no work that feeds nothing.
Work. Delete the two `PluginEditor` vectors. Delete `svfLow` and `svfHigh` from
the header and the implementation. Delete the getter, the atomic field, the
`reset()` store and the per-block tally, unless you can point to an intended
caller and wire it up instead. Delete `fmtSlope` unless a knob needs it; the
slope readout currently uses a different formatter.
Files in scope. `Source/PluginEditor.h`, `Source/Filter.h`, `Source/Filter.cpp`,
`Source/Engine.h`, `Source/Engine.cpp`. Optionally `Source/PluginEditor.cpp` for
`fmtSlope`.
Acceptance. `BUILD_DIR=/tmp/serumalt-check ./build_harness.sh --run` passes and
`grep -rn "oscAComponents\|oscBComponents\|svfLow\|svfHigh\|getActiveVoiceCount\|activeVoiceCount" Source/`
returns nothing.
---
## JT-08 Remove dead parameters and discarded computations
Type. Cleanup.
Priority. P2.
Observed problem. Four sites compute or accept values that nothing uses:
1. `Source/SubOscillator.cpp:9-11` computes `mult` from `octaveShift`, then
discards it and `freqHz` with `(void)` casts. Only `phase = 0.0;` has an
effect. The octave shift is already baked into `freqHz` by the voice
(`Source/SynthVoice.cpp:200,212`). The only caller passes `-1` for the shift
(`Source/SynthVoice.cpp:58`).
2. `Source/Oscillator.cpp:15` accepts `freqHz` only to feed
`jassert (freqHz > 0.0)`. In a release build the assert compiles out and the
parameter is unused.
3. `Source/Wavetable.cpp:238` and `:244` compute `const float u = ...` and
discard it with `(void) u` at `:240` and `:246`, while `:248` immediately
recomputes the same expression as `tt`, which is the value actually used.
4. `Source/Filter::prepare` accepts `maxBlockSize` and discards it at
`Source/Filter.cpp:28`. `FilterBank::prepare` forwards it to four `Filter`
objects (`Source/FilterBank.cpp:8-11`), so the parameter is dead end to end.
Desired behavior. Signatures and locals describe what the code uses.
Work. Reduce `SubOscillator::noteOn` to the work it does, which is resetting
phase, and update the one caller. Drop `freqHz` from `Oscillator::noteOn` and
update the two callers at `Source/SynthVoice.cpp:170-171`. Delete the two `u`
locals and both `(void) u` casts in `Wavetable.cpp`, keeping `tt`. Drop
`maxBlockSize` from `Filter::prepare`, `FilterBank::prepare` and their headers,
and update the `FilterBank` call at `Source/SynthVoice.cpp:20`. The `FXUnit`
interface keeps its `prepare (double, int)` shape because all units share it, and
the units that ignore the block size already leave the parameter unnamed, which is
the existing convention. See `Source/EffectUnits/AGENTS.md` and
`Source/synth/AGENTS.md`.
Files in scope. `Source/SubOscillator.h`, `Source/SubOscillator.cpp`,
`Source/Oscillator.h`, `Source/Oscillator.cpp`, `Source/Wavetable.cpp`,
`Source/Filter.h`, `Source/Filter.cpp`, `Source/FilterBank.h`,
`Source/FilterBank.cpp`, `Source/SynthVoice.cpp`.
Acceptance. `BUILD_DIR=/tmp/serumalt-check ./build_harness.sh --run` passes.
`grep -rn "(void)" Source/SubOscillator.cpp Source/Wavetable.cpp Source/Filter.cpp`
returns nothing. The sub oscillator still tracks the voice frequency, because
that frequency is applied in `SynthVoice::render`.
Conflict. `Source/SynthVoice.cpp` has uncommitted edits.
---
## JT-09 De-duplicate the test render loop
Type. Cleanup.
Priority. P2.
Observed problem. `Source/Tests/TestMain.cpp` repeats the same render loop twice.
The init-preset block at lines 49-74 and the per-preset block at lines 76-105 do
the same work: build three note-ons, render 240 blocks of 512 samples, release at
block 180, accumulate a NaN flag and a peak, compare against `0.001f`, print, and
count failures. The RAVE block at lines 107-130 is a smaller variant.
Desired behavior. One helper runs a preset and returns whether it passed.
Work. Extract a function, for example
`bool runPreset (serum::SerumAltAudioProcessor&, const juce::String& label, double maxPeakFloor)`,
and call it for the init case and for each factory preset. Keep the output format
and the exit code behavior. Keep the rule from `Source/Tests/AGENTS.md`: check
non-silence across the whole render, not per block, because a release can produce
a legitimately silent block.
Files in scope. `Source/Tests/TestMain.cpp`.
Acceptance. `BUILD_DIR=/tmp/serumalt-check ./build_harness.sh --run` passes and
still prints one PASS line per preset.
---
## JT-10 Unify pi and two-pi constants
Type. Cleanup.
Priority. P2.
Observed problem. `Source/synth/AGENTS.md` and
`Source/EffectUnits/AGENTS.md` require `juce::MathConstants`. Raw literals and
local constants remain:
- `Source/SubOscillator.h:26` `kTwoPi = 6.28318530717958647692`.
- `Source/Wavetable.cpp:21` `twoPi = 6.28318530717958647692`.
- `Source/Wavetable.cpp:156` `kPi = 3.14159265358979323846f`.
- `Source/EffectUnits/Chorus.cpp:40,41` two-pi literals, `:46` a pi literal.
- `Source/EffectUnits/Flanger.cpp:38,39` two-pi literals.
- `Source/EffectUnits/Phaser.cpp:30,31` two-pi literals.
- `Source/GUI/LFODisplay.cpp:25` `6.2831853f`.
- `Source/EffectUnits/Biquad.h:31,45,60,75,90` write
`2.0 * juce::MathConstants<double>::pi`, which should be `twoPi`.
Already converted and correct: `Source/Oscillator.h:72`, `Source/Filter.cpp:8`,
`Source/LFO.cpp:71`, `Source/Oscillator.cpp:63` (half pi, leave it).
Desired behavior. One spelling for each constant, taken from
`juce::MathConstants`.
Work. Replace each literal with `juce::MathConstants<double>::twoPi` or
`juce::MathConstants<float>::twoPi` as the surrounding precision requires, and
`pi` where only pi is meant. Remove the now-unused local constants. Keep the
per-sample DSP value types unchanged, so an `f`-suffixed literal becomes
`juce::MathConstants<float>::twoPi` and a `double` literal becomes the `double`
form.
Files in scope. `Source/SubOscillator.h`, `Source/SubOscillator.cpp`,
`Source/Wavetable.cpp`, `Source/EffectUnits/Chorus.cpp`,
`Source/EffectUnits/Flanger.cpp`, `Source/EffectUnits/Phaser.cpp`,
`Source/EffectUnits/Biquad.h`, `Source/GUI/LFODisplay.cpp`.
Acceptance. `BUILD_DIR=/tmp/serumalt-check ./build_harness.sh --run` passes.
`grep -rn "6.28318530717958647692\|3.14159265358979323846\|6.2831853f" Source/`
returns nothing. The harness reports no NaN and preset peaks match the values
from the unmodified build.
---
## JT-11 Share the envelope curve and mark the filter preview
Type. Cleanup.
Priority. P2.
Observed problem. Two smaller duplications remain between DSP and display.
1. Envelope curve. `Source/Envelope.cpp:44-45` computes
`attackShape = 0.3 + curve * 2.7` and `decayShape = 3.0 - curve * 2.7`.
`Source/GUI/EnvelopeDisplay.cpp:22-23` repeats both formulas with `f` suffixes.
2. Filter preview. `Source/GUI/FilterDisplay.cpp:6-36` re-implements a magnitude
response that does not match the DSP in `Source/Filter.cpp:87-88`. The display
uses `q = 0.6f + res * 14.0f`, while the DSP uses
`k = clamp (2.0 * (1.0 - res), 0.05, 2.0)`. The header calls the view
approximate (`Source/GUI/FilterDisplay.h:11,17`), but the re-implementation
site has no comment at all.
Desired behavior. The envelope preview cannot drift from the envelope DSP, and
the filter preview states at the code that it is schematic.
Work. Add one shared helper for the curve mapping, for example
`attackShapeFor (float curve)` and `decayShapeFor (float curve)` in
`Source/Envelope.h`, and call it from both `Envelope::setParams` and
`EnvelopeDisplay::paint`. Add a short comment at the top of
`FilterDisplay::magnitude` that says the response is a schematic preview and does
not model the DSP, the drive, the slope or modulation, and point to
`Source/Filter.cpp`. Do not try to make the preview numerically match the
nonlinear DSP.
Files in scope. `Source/Envelope.h`, `Source/Envelope.cpp`,
`Source/GUI/EnvelopeDisplay.cpp`, `Source/GUI/FilterDisplay.cpp`.
Acceptance. `BUILD_DIR=/tmp/serumalt-check ./build_harness.sh --run` passes.
`grep -n "2.7" Source/Envelope.h Source/Envelope.cpp Source/GUI/EnvelopeDisplay.cpp`
returns the two uses in the shared helper only, and nothing in the two callers.
`FilterDisplay.cpp` carries the approximate comment.
---
## JT-12 Reuse `paramIds::` in `PluginProcessor`
Type. Cleanup.
Priority. P3.
Observed problem. `Source/Params.h:229-269` defines the `paramIds::` indexed
groups. `Source/Engine.cpp` and `Source/PluginEditor.cpp` now iterate those
groups. `Source/PluginProcessor.cpp` still builds its own arrays that mirror the
same groups:
- `:102-106` five envelope arrays.
- `:121-127` seven LFO arrays.
- `:142-155` FX type, mix and parameter arrays.
The arrays reference `ids::`, so no string literal is duplicated. The structure
is.
Desired behavior. The parameter layout walks the shared groups instead of
re-declaring parallel arrays.
Work. Replace the local arrays with `paramIds::envAttack` and the other groups, or
iterate them directly. Keep the parameter names and defaults exactly as they are,
because they define the host-visible layout and the saved-state order. Do not
change parameter IDs.
Files in scope. `Source/PluginProcessor.cpp`.
Acceptance. `BUILD_DIR=/tmp/serumalt-check ./build_harness.sh --run` passes.
The parameter count and order are unchanged. A saved state from before the change
still loads.
---
## JT-13 Guard the mono-buffer and capacity assumptions
Type. Hardening.
Priority. P3.
Risk. No behavior change for the current 2-channel, 64-sample path.
Observed problem. `FXProcessor::process` reads and writes channel 1
unconditionally (`Source/FXProcessor.cpp:92,98`). It is safe today only because
`Engine` always hands it a 2-channel view (`Source/Engine.cpp:307`). A future
one-channel caller reads a null channel. Similarly, `FXProcessor::process` and
`SynthVoice::render` trust the sample count to fit the buffers sized in
`prepare` (`Source/FXProcessor.cpp:38`, `Source/SynthVoice.cpp:23`) with no
assert. The engine's 64-sample segmentation (`Source/Engine.cpp:270`) is the only
thing keeping that true.
Desired behavior. The boundaries are explicit, and a debug build catches a
violation instead of corrupting memory.
Work. In `FXProcessor::process`, handle `buffer.getNumChannels() < 2` by
processing only the channels present, or by returning early with a `jassert`.
Add `jassert (n <= wet.getNumSamples())` and
`jassert (numSamples <= scratch.getNumSamples())` at the top of the two render
functions. Do not add allocation and do not change the audio-thread behavior for
valid input.
Files in scope. `Source/FXProcessor.cpp`, `Source/SynthVoice.cpp`.
Acceptance. `BUILD_DIR=/tmp/serumalt-check ./build_harness.sh --run` passes. A
debug build asserts when `SynthVoice::render` is called with
`numSamples > scratch.getNumSamples()`.
Conflict. Both files have uncommitted edits.
---
## JT-14 Fix stale guidance and remaining prose slop
Type. Cleanup.
Priority. P3.
Observed problem. Two kinds of documentation drift.
Guidance that contradicts the code:
- `Source/params/AGENTS.md` says to use the `ids::` namespace for every
parameter ID and never to re-declare one outside `Params.h`. It does not mention
the `paramIds::` indexed groups at `Source/Params.h:229`, which the engine and
editor now use.
- `Source/synth/AGENTS.md` says "Wavetables are prebuilt during Engine
construction" but its example still ends with `// in prepareToPlay`. The real
call is `Source/Engine.cpp:22` in the constructor.
- `Source/engine/AGENTS.md` shows a "GOOD" example that calls
`mixBuffer.clear()` in `processBlock`. The code instead builds a non-owning view
per segment (`Source/Engine.cpp:307`). Update the example to match.
Prose that still violates the unslop rules:
- `.claude/skills/graft/SKILL.md` carries em dashes, an emoji at line 129,
boldface across whole sentences, and colon connectors. This is the largest
remaining prose problem.
- `build_windows.sh:3` has an em dash, and `:58-59` uses a colon as a connector.
- `AUDIT_REPORT.md:123,141,247,320,322` use colons as mid-sentence connectors, and
`:286` reads "very sparingly".
- `.claude/helpers/graft-hooks.cjs:67` and `.claude/helpers/graft-statusline.cjs:67`
have an em dash in a trailing comment.
- Root `AGENTS.md` has nine em dashes inside the generated
`<!-- graft:start -->` block. That block is produced by `graft init` from a
Graft template, so editing it in place will be overwritten on the next
`graft init`. Fix the template source, or leave the block and note it as
generated.
Desired behavior. Guidance matches the code, and prose reads plainly.
Work. Apply the fixes above. For each unslop edit, replace em dashes with a
comma or a full stop, split colon connectors into two sentences, remove the
emoji, and unbold body text. Keep literal technical terms such as "harness",
"vector" and "surface" when they name a real thing. Do not edit
`third_party/JUCE`.
Files in scope. `Source/params/AGENTS.md`, `Source/synth/AGENTS.md`,
`Source/engine/AGENTS.md`, `.claude/skills/graft/SKILL.md`,
`.claude/helpers/graft-hooks.cjs`, `.claude/helpers/graft-statusline.cjs`,
`build_windows.sh`, `AUDIT_REPORT.md`, and root `AGENTS.md`.
Acceptance. `bash -n` passes for `build_*.sh`.
`grep -rn "—" AGENTS.md Source/*/AGENTS.md build_windows.sh .claude/skills/graft/SKILL.md`
returns nothing outside the generated graft block. A build and harness run still
pass, since only comments and prose changed.
Conflict. `AGENTS.md`, `AUDIT_REPORT.md` and the `Source/**/AGENTS.md` files have
uncommitted edits. Commit or freeze them first.
---
## Considered and rejected
These came up during verification and are deliberately not tasks.
- Rename `Source/RAVEButton.{h,cpp}` to match `class RaveController`. Both
`Source/modulation/AGENTS.md:19` and root `AGENTS.md` say to keep the filenames
unless a rename is in scope.
- Rewrite `AUDIT_REPORT.md`. Its own header and root `AGENTS.md` mark it as a
historical snapshot, so it is not a pending instruction. Only its prose is
touched, in JT-14.
- Move `PluginEditor`, `FXProcessor`, `Resources` and `RAVEButton` into
subdirectories. The proposed moves in `AUDIT_REPORT.md` are suggestions, and
root `AGENTS.md` says the current layout is intentional until a move is part of
a requested task.
- Make `ModulationMatrix::connections` and `MacroControls::assignments` private
and force all mutation through locked members. Every current caller already
holds `Engine::getControlLock()`, and the change is a wide interface refactor,
not cleanup. Revisit only if a caller appears outside the lock.
- Make the lazy `WavetableLibrary::getTable` build branch unreachable. The
constructor prebuilds every index (`Source/Engine.cpp:22`), so the branch is
dead today. JT-13 covers the defensive asserts; deleting the lazy path is a
separate design decision.
- Add tests. `AGENTS.md` says to add test infrastructure only when requested.
## Proposed dispatch order
Dispatch one task at a time per file cluster. The conflicts column above names
the files with uncommitted edits.
- Batch 1, independent files: JT-02, JT-09, JT-11, JT-13.
- Batch 2, editor cluster, serial: JT-01, then JT-03, then JT-04, then JT-06.
- Batch 3, dead-code cluster, serial across `SynthVoice` and `Filter`: JT-08,
then JT-07.
- Batch 4, wide edits, serial: JT-05, then JT-10, then JT-12, then JT-14.
Each agent must run the harness before handing back:
```bash
BUILD_DIR=/tmp/serumalt-check ./build_harness.sh --run
```
## Execution status
All 14 tasks were dispatched and completed. Final consolidated verification on the
frozen tree:
```bash
BUILD_DIR=/tmp/jt-final CMAKE_BUILD_PARALLEL_LEVEL=8 ./build_harness.sh --run
```
Result: exit 0, `1/1 Test #1: SerumAltTest ... Passed`, `100% tests passed`, and
zero first-party compiler warnings in `Source/` (down from 55 warning lines).
| Id | Result | Note |
| --- | --- | --- |
| JT-01 | Done | `Source/GUI/SerumLookAndFeel.h` deleted. The class was never installed, so the GUI is unchanged. |
| JT-02 | Done | One `lfoShapeValue` function now serves `LFO` and `LFODisplay`. The sample-and-hold preview is a labelled schematic and no longer invents a sequence. |
| JT-03 | Done | Macro names come from `MacroControls::macroName` in the APVTS, the assign combo, the panels and the list. Only the `Params.cpp` serialisation tokens keep the old strings. |
| JT-04 | Done | Add and Assign grey out at `kMaxConnections` and `kMaxAssignments`, and rejected inserts no longer look like success. |
| JT-05 | Done | 55 first-party warning lines to 0. New `Source/ExactCompare.h` names the deliberate exact float comparisons instead of weakening them to an epsilon. |
| JT-06 | Done | The FX type combo is built from `FXProcessor::fxTypeName`; the duplicate literal list is gone. |
| JT-07 | Done | Removed the unused `oscAComponents`/`oscBComponents`, `Filter::svfLow`/`svfHigh`, `Engine::getActiveVoiceCount` and the per-block tally, and the unused `fmtSlope`. |
| JT-08 | Done | Removed the dead `SubOscillator` octave math, the assert-only `Oscillator::noteOn` frequency, the discarded `Wavetable` locals, and the unused `maxBlockSize` chain through `Filter` and `FilterBank`. |
| JT-09 | Done | The duplicated test loops are one `runPreset` helper. Output format and peak values are unchanged. |
| JT-10 | Done | Raw pi and two-pi literals replaced with `juce::MathConstants`. The only remaining match is the intentional "BAD" example in `Source/synth/AGENTS.md`. |
| JT-11 | Done | The envelope curve mapping lives in `Envelope.h` and both callers use it. `FilterDisplay::magnitude` now states that it is a schematic preview. |
| JT-12 | Done | `PluginProcessor::createParameterLayout` uses the `paramIds::` groups. A layout comparison confirmed all 96 `(id, name, default)` entries are identical and in the same order. |
| JT-13 | Done | `FXProcessor::process` returns early on a mono buffer, and capacity asserts guard `FXProcessor::process` and `SynthVoice::render`. |
| JT-14 | Done, one limitation | Guidance and prose fixed across the `AGENTS.md` files, `.claude/skills/graft/SKILL.md`, `build_windows.sh`, `AUDIT_REPORT.md` and the two helper scripts. Nine em dashes remain inside the generated `<!-- graft:start -->` block in root `AGENTS.md`. |
The JT-14 limitation. The graft block in root `AGENTS.md` is generated by the
installed `@nanonets/graft` package, whose template carries the em dashes.
`graft init` would reproduce them, so the block was left as generated. Removing
them needs a change to the package template at
`/home/biggy/Documents/Coding/_skills/Graft/src/hosts/instructions.ts` and a
reinstall, which is outside this repository.
Other notes.
- The pre-existing uncommitted work in the tree was preserved throughout. It
includes the `FXProcessor` per-slot repair, the `SynthVoice::getModulationValue`
addition, the editor control-lock line, and the `AGENT.md` to `AGENTS.md`
renames. `HEAD` does not compile without the `FXProcessor` repair, so commit the
current tree before treating any single task as an isolated diff.
- Every task ran its own harness build in `/tmp`, so no `build_*` directory in the
repository was modified and `third_party/JUCE` was not touched.
- No task changed APVTS parameter IDs, so saved projects stay compatible.