Files
serumalt/UI_UX_AUDIT.md
T
biggy e4cea300db chore: land janitor round 1 and graft integration
Round 1 findings and fixes, all previously uncommitted:
- FXProcessor per-slot DSP history repair (HEAD does not compile without it)
- SynthVoice::getModulationValue, editor control-lock guard
- ExactCompare.h naming the deliberate exact float comparisons
- KnobLookAndFeel replacing the never-installed SerumLookAndFeel
- 55 first-party compiler warnings to 0
- AGENT.md renamed to AGENTS.md across Source/
- graft repository context graph and its hooks
- JANITOR_TASKS.md and UI_UX_AUDIT.md records
2026-10-06 08:50:45 +02:00

31 KiB

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.