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

+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.