fix(spectrum): use the radio's auto-black level for an evenly-levelled waterfall floor#3586
Conversation
…d waterfall floor
AetherSDR computed its own client-side noise-floor estimate and disabled the
radio's auto-black (auto_black=0), giving a textured, black-speckled waterfall
floor instead of the even floor the radio's auto-levelling produces.
The radio computes a noise-floor black level and embeds it in every waterfall
tile (FlexLib's WaterfallTile.AutoBlackLevel). AetherSDR already parsed it and
emitted waterfallAutoBlackLevel(), but discarded the value (clamping the raw
uint16 to 0-125).
Wire it up:
• RadioModel sends auto_black=1 so the radio populates the tile field.
• waterfallAutoBlackLevel feeds setRadioAutoBlackLevel() into the renderer.
• intensityToRgb maps low = radio auto-black, high = wfHighThresholdRaw()
(a cubic colour-gain curve), t = (intensity - low) / (high - low).
• The client-side estimate stays as a fallback when no radio value is
available (FFT fallback, or auto-black off).
Verified on a live FLEX-8600 (fw 4.2.20.41343).
1b19c89 to
fdf9c0b
Compare
There was a problem hiding this comment.
Thanks @sblanchard — this is a well-reasoned change with a clear constitution writeup. Wiring up the radio's per-tile AutoBlackLevel instead of clamping it to 0–125 and throwing it away is the right call (Principle I), and the implementation is clean: the new m_radioAutoBlackRaw member is kept distinct from m_wfBlackLevel, the renderer path is null-guarded (if (auto* sw = m_panStack->spectrum(...))), the per-tile update() is gated behind a change check, and the client estimate is correctly preserved as a fallback. CI is green across all six checks (build/mac/win/CodeQL/cpp/a11y).
A couple of substantive points, plus confirmation of two open questions you already flagged:
1. lowRaw can go negative; black point and range then derive from different lows. In the radio-authoritative branch:
const float lowRaw = m_radioAutoBlackRaw + (50 - m_wfAutoBlackOffset) * 0.5f * 128.0f; // ±3200 raw at offset 0/100
const float highRaw = wfHighThresholdRaw(lowRaw, m_wfColorGain); // internally qBound(0, lowRaw, 65535)
blackThresh = lowRaw / 128.0f; // uses the *unbounded* lowRaw
rangeWidth = std::max(1.0f, (highRaw - lowRaw) / 128.0f); // also unbounded lowRawWith a low radio level and offset pushed toward 100, lowRaw goes negative. wfHighThresholdRaw clamps it to 0 for the white-point math, but blackThresh and rangeWidth use the raw negative value — so the black point and the range width are computed against two different low points. No crash, but the mapping is internally inconsistent at the extremes. Suggest clamping once up front (lowRaw = qBound(0.0f, lowRaw, 65535.0f);) and feeding the clamped value to all three.
2. Confirming your open question on unconditional auto_black=1. configureWaterfall() now enables radio auto-black at session start regardless of the persisted client auto-black state. If the user has client auto-black off, the radio is in auto_black=1 mode embedding a level the renderer ignores (it takes the !m_wfAutoBlack fallback). It's harmless since the native path colours client-side, but it's a state mismatch — I'd reflect the persisted client state here as you suggested, so the radio and renderer agree.
3. Maintainer sign-off on the visual default. You've correctly flagged this (Principle XIII) — it reverses a deliberate client-side decision and changes the floor rendering for all colour schemes, not just Purple. That's a feel call I can't make from code review; leaving it for the maintainer to validate on hardware.
Conventions all check out — no QSettings/flat-key AppSettings added, RAII intact, no meter UI touched. Nice work.
🤖 aethersdr-agent · cost: $2.3484 · model: claude-opus-4-8
…d range Addresses review on aethersdr#3586: with a low radio auto-black level and the offset slider toward 100, lowRaw could go negative. wfHighThresholdRaw() clamped it internally for the white-point math, but blackThresh and rangeWidth used the unclamped value, so the black point and the range derived from two different low points. Clamp lowRaw once to [0, 65535] and feed the clamped value to all three.
|
Thanks for the thorough review! 1. 2. Unconditional 3. Visual default / maintainer sign-off. Agreed — flagged under Principle XIII; it's a hardware feel call. Suggested starting point is in the description: Color Gain ≈ 30, Auto Black ≈ 30, especially on the Purple scheme. |
jensenpat
left a comment
There was a problem hiding this comment.
Thanks for this — the diagnosis is genuinely good work (tracing WaterfallTile.AutoBlackLevel through PanadapterStream, the live-FLEX verification, and the clean second commit fixing the lowRaw clamp ordering), and Principle I does favor the radio's authoritative black level over our client-side estimate. I want to be straight with you about why I'm not comfortable merging it as-is, because it's about rollout, not the idea.
Blast radius. Auto-black is on by default, and this rewrites the shared intensity→t mapping that every color scheme renders through. So for most users it silently changes the look of all six schemes (Default, Grayscale, Blue-Green, Fire, Plasma, Purple) at once — and by your own reviewer note the defaults (Color Gain / offset 50) aren't the sweet spot; the good look is at ~30/30. Practically, merging this obligates a re-validation of all six schemes on live hardware to confirm none regressed, for what the PR itself flags as a subjective design reversal (Principle XIII). That's a lot of untestable surface.
Coupling with #3583. This PR notes it pairs with the new Purple scheme, but Purple merged separately in #3583 with no declared dependency on this one — so right now Purple is in main rendering through the old pipeline, i.e. not the look it was tuned for. Coupled visual work like this should land together (or the dependent piece should wait), so reviewers can judge the intended result in one place.
Concrete path forward, if you're up for it: make the radio-authoritative mapping opt-in rather than the new default — a distinct mode/toggle (or fold it in alongside a re-tuned Purple) so existing users keep their current look and only people who choose it take on the retune. Bundle the per-scheme tuning so a reviewer can validate the look scheme-by-scheme. That shrinks the review from "re-test everything" to "evaluate one new path," and it gives the Principle I improvement a home without a forced global change.
Happy to talk through the design if you want to take it that direction — and thanks again for digging into the protocol layer rather than papering over it client-side.
|
On opt-in — I want to push back on making this a separate mode, but I think you can still get the safety you're after. The snag: "auto-black" already means "let the system pick the black level automatically." The radio's per-tile So I'd rather keep it working with auto-black on, and kill the regression risk a different way:
Net: the disagreement narrows to a single thing — what the default floor source is — which is exactly the sign-off that's yours to make on the captures. If after seeing all six tuned you still want it opt-out, I'll flip the default. Fair? |
Folds the client/radio auto-black source into a single 3-way cycle button instead of a binary AUTO plus a separate source toggle: Off = manual black level SW = client-side (software) noise-floor estimate — the prior default look HW = radio's per-tile (hardware) auto-black level (FlexLib AutoBlackLevel) Keeps both methods available so the radio-side level is opt-in (HW), not a forced default — addresses the blast-radius review concern. Default stays SW so existing users keep today's floor until they choose HW. The render path takes the HW branch only when both flags are set and a tile level has arrived; the radio receives auto_black=1 only in HW mode (no unconditional auto_black=1 state mismatch). SW/HW labels chosen to fit the compact button. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@sblanchard — heads up, I pushed a commit to your branch ( What changedThe auto-black button is now a 3-way cycle instead of a binary on/off, folding the client-vs-radio source choice into the single button:
Click cycles WhyThis addresses the core review blocker — the blast radius of making radio-authoritative auto-black the global default for every color scheme:
Both methods stay fully available, which is the "make it a distinct mode rather than the new default" outcome jensenpat suggested. Implementation notes
StatusBuilds clean (Linux, GPU=ON). Verified the cycle drives the three render paths on a FLEX-8600. The visual sign-off you flagged for the maintainer (Principle XIII) is now lower-stakes since the default look is unchanged — only the opt-in HW path needs a look-check. |
Works well with commit revisions from @ten9876. Approving.
ten9876
left a comment
There was a problem hiding this comment.
Approving. (Transparency: I pushed the opt-in 3-way-toggle commit 4f8ae02f to this branch via maintainer-can-modify, so this approval is maintainer-side sign-off on the resolved design, not an independent third review of my own change.)
The PR now lands the right way:
- Principle I is honored — the radio's authoritative per-tile
AutoBlackLevelis wired in instead of being clamped away. - The blast-radius concern is resolved — radio-side auto-black is opt-in (HW) with the default unchanged (SW = today's client-side look), so no silent change to any of the six color schemes and no forced re-validation.
- The radio receives
auto_black=1only in HW mode (no state mismatch), the HW render path degrades gracefully to SW until a tile arrives, and thelowRawsingle-clamp fix is retained. - CI is green across all six checks (build / macOS / Windows / CodeQL / analyze / a11y).
Nice work @sblanchard — the protocol-layer diagnosis and the radio-authoritative mapping are the substance here; the toggle just makes it opt-in.
Summary
Makes the waterfall noise floor render as a flat, evenly-levelled band by using the radio's own per-tile auto-black level instead of a client-side estimate.
AetherSDR was computing its own noise-floor estimate and sending the radio
auto_black=0, which produced a textured, black-speckled floor. The radio already computes a noise-floor black level and embeds it in every waterfall tile (FlexLib'sWaterfallTile.AutoBlackLevel) — AetherSDR even parsed it (PanadapterStream.cpp) and emittedwaterfallAutoBlackLevel(), but the value was discarded (the handler clamped the raw uint16 to 0–125).This wires it up:
RadioModelsendsauto_black=1so the radio populates the tile field.MainWindow_Sessionfeeds the per-tile value into the renderer via the newSpectrumWidget::setRadioAutoBlackLevel().intensityToRgbmapslow= radio auto-black,high=wfHighThresholdRaw()— a cubic colour-gain curve:t = (rawValue − low) / (high − low)→ gradient. The client-side estimate stays as a fallback (FFT fallback / auto-black off).Both display paths are covered — the native waterfall is coloured on the CPU via
intensityToRgb, then uploaded as a texture (no GPU-side mapping).Reviewer note
Try it with Waterfall (Color) Gain ≈ 30 and Auto Black Level ≈ 30 — it works very well, particularly with the new Purple colour scheme (#3583). At default Color Gain and Black Level offset = 50 (no bias), the floor follows the radio's computed level directly.
Constitution principle honored
Principle I — FlexLib Is The Protocol Authority. Replaces a client-side approximation with the radio's own auto-black value (FlexLib's per-tile
AutoBlackLevel), the authoritative noise-floor figure.It also reverses a deliberate client-side decision and changes a rendering default for all waterfall schemes, so it is flagged for maintainer sign-off on the look (Principle XIII — the operator/maintainer is the authority on visual design).
Test plan
cmake --build build) — links cleanOpen questions for review
(50 − offset)·0.5·128raw); scaling is open to tuning.auto_black=1unconditionally; could instead reflect the persisted client auto-black state.Checklist
AppSettingscalls (none added)MeterSmoother— N/A (no meters touched)