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