Repository navigation
RTL WFM: opt-in DSP, native stereo applet, and bounded receive exchange - #5973
Conversation
|
CI is red on
The fixThat is the whole fix. I reproduced it on your head, and the regeneration is a one-line, one-cell change — the include count in the -| `core/backends/RadioCapabilities.h` | 9 | universal — Backend-neutral capability descriptor … |
+| `core/backends/RadioCapabilities.h` | 10 | universal — Backend-neutral capability descriptor … |No source change is needed, and nothing else in the manifest moves. It belongs in #5924, not hereI bisected the branch: it was green through Why it went stale — a merge artifact git could not see
Both sides had independently regenerated to the same number for different reasons, so that table row was byte-identical on both sides. Git had no conflict to report and took the line verbatim — while the union of the two include sets is 10. The generator is the only thing that can notice, which is what it did. The practical rule: any merge that brings in a new Notes
Thanks for the careful evidence write-up on this one — the scope limits you state explicitly (positive-offset-only sweep, the ≥40 dB claim confined to the reference vectors) are exactly what makes a DSP branch reviewable. This is a one-command unblock; everything else in the lane is green. 🤖 aethersdr-agent · cost: $5.9217 · model: claude-opus-5 |
ten9876
left a comment
There was a problem hiding this comment.
The opt-in broadcast-FM recipe addresses the #5468 DSP increment: filtering runs before demodulation, paired normalization preserves headroom, and the 50/75 µs and stereo observations have behavioral coverage. I reviewed 9194745314037df3cb676e903af007db5103235a. The broader capture/control changes in this main-targeted diff are the disclosed #5924 prerequisite; the foundation-only difference from 17d56197f is nine files. Existing maintainer rulings on that prerequisite remain in force.
Scope
| Files/group | Claimed? | Verdict |
|---|---|---|
WdspChannel.{h,cpp}, WDSP WBFM implementation/API |
Yes: opt-in RF filter, normalization, de-emphasis, observation | In scope; existing owners do not opt in automatically |
tests/wdsp_wbfm_test.cpp, registration |
Yes: generated-IQ qualification | In scope; socket-free |
| WDSP patch manifest/README | Yes: provenance and response tradeoffs | In scope |
| Inherited #5924 capture transaction/registry/DDC, shared capabilities/model/UI/settings and tests | Yes: expressly stacked prerequisite | Dependency scope, not a new request to ratify those already-decided seams |
No unrelated foundation commit or hidden Digital-FM dependency found. The full file list was checked from Git (119 paths), rather than the truncated 100-path API summary.
Blockers
None found in the DSP increment.
Non-blocking notes
tests/tests.cmake:912: exact-head hosted evidence currently contains Static Checks, not the Linux/macOS/Windows application builds. Keep the current-head build qualification distinct from the downstream/native and historical measurements. The earliest head run was created 2026-10-03 04:56:21 UTC, before current main's 2026-10-04 18:26:40 UTC tip;strict: falsedoes not force freshness. This review does not establish merge readiness against that newer main.third_party/wdsp/AETHERSDR-PATCHES.md: the generated vectors establish component response, not antenna selectivity or universal stereo separation. The body correctly preserves this distinction; retain it in release claims. I did not establish live-RF, sanitizer or ARM workload qualification.
Verification and attempted failures
- Built the exact foundation's
tests/wdsp_wbfm_test.cpp, productionWdspChannel.cpp/FftwPlannerLock.cpp, and its vendored WDSP in a minimal Release harness (GCC 16.2.1, Qt 6.11.2). No network/device transport; FFTW wisdom and planning budget isolated. Test passed, then passed again after restoring a deliberate mutation. - Removed paired output normalization in the scratch build: the test failed its 90%-modulation and stereo headroom assertions (exit 1). Restoring the original production source restored the passing control. The regression genuinely detects loss of the load-bearing gain behavior.
- Generated no-pilot input stayed mono; ±100 ppm clock and ±2 Hz coherent pilot offsets retained stereo, with roughly 57–60 dB separation in those cases. Noisy/marginal cases produced the lower separation documented by the test instead of a universal guarantee. Filter selectivity, de-emphasis and profile-refusal assertions also passed.
- Audited recipe validation, reject-on-mode-change, filter frequency convention, paired gain application, stop/flush observation reset and teardown exclusion. Four governance gates pass on merge base and head: identical 102 tracked engine findings, 71 capability booleans and 401 frozen above-seam wire commands; manifests current.
- Did not drive the GUI: this increment is a headless opt-in DSP recipe, not yet enabled by the RTL runtime at this checkpoint. No live radio was accessed. Separate automated code-review skill unavailable; this was a manual adversarial audit plus executable probes.
Recommendation: Approve with nits for the DSP increment, subject to normal current-integration build checks and the stack's merge order. This is a comment review, not a merge authorization.
— Jeremy KK7GWY + Codex
ten9876
left a comment
There was a problem hiding this comment.
Issue fit: the DSP increment does what #5468 asks, but the branch cannot land as built — it carries a pre-merge snapshot of #5924 underneath the 9-file increment, and main has moved on top of the final #5924 and #5919. Reviewed at 9194745314037df3cb676e903af007db5103235a. This is an independent second pass after the earlier ten9876 + Codex review on the same head; where I re-checked that review's claims, the result is noted below.
Stack map (whole lane, for context)
| PR | Base | Head branch | Base owned by |
|---|---|---|---|
| #5973 | main |
codex/rtl-wfm-dsp-foundation |
— |
| #5974 | codex/rtl-wfm-dsp-foundation |
codex/rtl-wfm-filters-stereo |
#5973's head |
| #6082 | codex/rtl-wfm-filters-stereo |
codex/rtl-live-ppm |
#5974's head |
| #6083 | codex/rtl-live-ppm |
codex/rtl-multirx-delivery |
#6082's head |
A clean linear stack; no orphan bases. Every head shares main merge base 732b99a9 (2026-10-03).
Scope
| Files / group | Claimed? | Verdict |
|---|---|---|
WdspChannel.{h,cpp}, aether_wdsp.h, wbfm.{c,h}, WDSP patch manifest/README (own increment over 17d56197) |
Yes — opt-in RF prefilter, paired gain, 50/75 µs, pilot observation | In scope |
tests/wdsp_wbfm_test.cpp + registration |
Yes | In scope, socket-free |
The other ~110 paths: #5924's capture/registry/DDC/model/UI changes as of 17d56197 |
Body: "stacked on #5924" | Stale — #5924 merged as squash a325fb99 on 2026-10-04; see Blocker 1 |
No CHANGELOG entry. Step-0 socket preflight: no new or modified test opens, binds, listens or connects.
Blockers
-
Rebase onto current main; the branch still carries the pre-squash #5924 commits and conflicts in 26 files. #5924 merged on 2026-10-04 as
a325fb99, so the prerequisite the body says this "stacks on" is now on main in a different shape. A trial merge (git merge-tree --write-tree origin/main 91947453) conflicts in 26 paths, includingRtlSdrBackend.{h,cpp},WdspChannel.{h,cpp},RadioModel.{h,cpp},SliceModel.{h,cpp},RxApplet.{h,cpp},VfoWidget.{h,cpp},ModeFilterPresets.{h,cpp},tests/tests.cmakeandthird_party/wdsp/include/aether_wdsp.h.This is more than mechanical churn. The branch forked from #5924 at
17d56197, before three later #5924 commits that are now on main and are absent from every head in this stack (git merge-base --is-ancestor fe91a982f <head>→ false for #5973, #5974, #6082 and #6083):fe91a982f— keep shared SQL and FM-filter rules for non-RTL radios. As built here,RxAppletregisters SQL as available whenever!caps.receiveSquelchModel(src/gui/RxApplet.cpp:2032at this head). That re-enables SQL in DIGU/DIGL/RTTY/CW on every radio without a declared squelch model — Flex included — undoing #2504. The HL2/ANAN FM passband gate regresses with it.ModeFilters::squelchAvailableInMode/fmFilterAdjustabledon't exist on any head in the stack.5dd50ff74— hide the empty RX filter row for an adjustable FM passband.f82656def— CI toolchain trimming.
Anything resolving the conflicts in this branch's favour brings that regression back. Please rebase the 9-file increment onto main (drop the
17d56197ancestry entirely) rather than merging main into it. Thefm_filter_controls_testcases added byfe91a982fare the pin that will tell you the resolution is right. -
The governance evidence describes baselines that main has already lowered. At this head,
check_capability_records.pyreports "71 boolean(s), at the frozen 71" andcheck_command_plane.pyreports 401 commands across 32 files — I reproduced both, and they match the earlier review. But main has since loweredFROZEN_BOOL_COUNT71 → 70 and the command-plane rows (SliceModel.cpp56 → 25 via #5919,MainWindow.cpp17 → 16 via #6124/#6111). These are shrink-only ratchets, so "identical counts on parent and head" is no longer the bar. The rebased head has to pass against main's numbers. Re-run the four gates after the rebase and report those numbers.
Non-blocking
- Stale body. It says "stacked on #5924" and "Reconciled through the updated parent with
mainat732b99a9". Main is now 38+ commits ahead and includes #5924 and #5919. Please refresh it after the rebase. - CI freshness. The only checks on this head are Static checks and Sanitizer-configure. The earliest run was created 2026-10-03 04:56Z, before main's current tip. There is no exact-head application build in the rollup. The earlier review said the same; it still holds.
What I verified, and what I tried to break
- Re-checked the prior review. The DSP increment is genuinely 9 files over
17d56197(git diff --stat), as the earlier review said. The capability and command-plane gates reproduce 71/401 at this head (see Blocker 2 for why that's no longer enough). The earlier review missed that #5924 had already merged and that main had moved its baselines. - Real-time path.
processIqcopies into the prefilter's fixed2*inputBlockSizebuffer only after the size precondition at the top of the function (inputI.size() != m_config.inputBlockSize→InvalidBuffer), so a short or long block can't overrun it.setFilter/setWbfmDeemphasisrun only underbeginControlOperation(), which refuses whilem_callbacksInFlight != 0, sosetFreqs_bpscan't freemultsunder a runningxbps.close()resetsm_wbfminside the same fence. Gain is applied only whenwdspError == 0, and the stereo indicator is cleared on stop. - Profile validation.
validateConfigpins the recipe to 384k/192k/48k, 2048/1024 blocks, a ±0.45·Fs filter, finite gain in [0,1] and a known de-emphasis.setModerefuses leaving WBFM on a recipe channel.setFilterwrites the accepted edges back tom_config(WdspChannel.cpp:634-635), so a laterreconfigure()keeps them. - Built and ran
wdsp_wbfm_testat this head (Arch, GCC, Qt 6.x; isolatedAETHER_SETTINGS_DIR): exit 0 in 25.6 s, no FAIL lines; marginal-noise and no-pilot cases report the documented lower separation / mono. - Mutation the earlier review didn't try: un-negating the prefilter edges (
create_bps(…, config.filterLowHz, config.filterHighHz, …)and the matchingsetFreqs_bps), i.e. testing the "WDSP fir_bandpass uses the negative complex-frequency convention" claim. The test fails exactly where it should —FAIL: asymmetric RF filter follows mathematical IQ frequency sign(exit 1). Restored (sha256 matchesHEAD), rebuilt, passes again. The sign convention is genuinely pinned, not just commented. - Not driven in the app: the recipe is headless and opt-in at this checkpoint, and the demo backend offers only USB/LSB, so neither the WBFM path nor the DIGU SQL regression above is reachable from a demo session. The regression is established from source and ancestry, not reproduced in the GUI. No hardware was touched.
Recommendation: Request changes. The DSP increment itself looks sound and survived the attacks above. The blocker is the base: rebase the 9-file increment onto current main, keep fe91a982f's squelch and passband rules, and re-qualify the gates against main's lowered baselines. #5974 → #6082 → #6083 then rebase onto the result.
— Jeremy KK7GWY + Claude
|
I am carrying the requested remediation through #5973 → #5974 → #6082 → #6083. The starting main snapshot is The packaging fix will bundle the pinned libusb SONAME as ruled on #6083. Current main already contains the full-suite disk-space mitigation. I will also update stale evidence and cross-family disclosures. Digital #6002 stays separate and draft. This is remediation for review, not an approval or merge. |
9194745 to
45028ce
Compare
45028ce to
1398f3b
Compare
jensenpat
left a comment
There was a problem hiding this comment.
Reviewed at 1398f3b9f2cdcc351924545554bb0b0f0fae25b7. Issue fit: yes. RFC #5468 assigns this PR the opt-in WDSP broadcast-FM recipe (RF response, headroom, stereo/de-emphasis, tests) and keeps Digital FM, the WFM applet, and live RF on later PRs. The earlier request to replay the nine-file increment onto current main is met: one commit, parent 5f09371e, and the squelch/filter files from the merged #5924/#5919 fixes are not in this diff. Main has moved since that base, and those later commits do not touch WdspChannel or third_party/wdsp.
Scope
| Files | Claim | Verdict |
|---|---|---|
src/core/dsp/WdspChannel.{h,cpp} |
Opt-in WbfmReceive: 384k/192k/48k, RF FIR before the decimator, paired gain, live deemphasis, lock-free stereo sample |
In scope |
third_party/wdsp/upstream/wbfm.{c,h}, include/aether_wdsp.h |
Patch 19: live deemphasis filters, atomic stereo publication, flush reset, opt-in discriminator/DC correction, BPS facade | In scope |
third_party/wdsp/AETHERSDR-PATCHES.md |
Patch 19 write-up | In scope |
third_party/wdsp/README.md |
Vendor-boundary summary | In scope; patch number is wrong (nit below) |
tests/wdsp_wbfm_test.cpp, tests/tests.cmake |
Socket-free registration of the recipe | In scope |
No CHANGELOG entry. No new socket-owning test (QTcp/QUdp/bind/listen/connectToHost absent). disc_compensate stays zero for every channel that does not open the recipe (malloc0 plus the setter only from open() when wbfmReceive is set). SetRXAWBFMdmph had no callers on the parent, so the live-filter update does not retune existing FM/SSB/legacy WBFM owners.
Nit
third_party/wdsp/README.md says the WFM work is patch 14. Patch 14 is the minimum-phase workspace free; this work is Patch 19 in AETHERSDR-PATCHES.md. The long patch note is the one a WDSP refresh should follow. Not a merge blocker.
Verification
- Control fence:
processIqrejects a block that is notinputBlockSizebefore it writes the prefilter;setFilter/setWbfmDeemphasis/setRunningtakebeginControlOperation()sosetFreqs_bps(which rebuilds the FFTW plans) cannot run underxbps.setFilterwrites the accepted edges back tom_configon the recipe path. - Apple Silicon, Qt 6.12,
RelWithDebInfo,ENABLE_RTL=ON,QT_QPA_PLATFORM=offscreen, at this SHA:wdsp_wbfm_testpassed (1/1, 24.7 s). Measured: adjacent selection 92.2 dB, 50/75 µs ratio 1.483, asymmetric RF +30 kHz in band and −30 kHz at −86 dB, reference stereo 58.6/59.0 dB, 300/500 Hz 58.8/60.2 dB, 14/15 kHz 51.4/48.1 dB. Near-full modulation is reported at 38.4/34.9 dB and is not pinned to 40 dB, matching the patch note.wdsp_channel_testpassed (1/1, 67.4 s). - Exact-head CI is green:
build,check-macos,check-windows,Static checks,Sanitizer option configureson1398f3b9. Those jobs do not executewdsp_wbfm_test. - Not claimed: on-air audio, receiver capacity, or the #5974 applet. No hardware was connected.
Recommendation: Approve. One documentation nit; no code blocker.
Non-blocking follow-up: this recipe is not adopted by a backend yet, so a demo session still cannot reach it. #5974 is the seam for mono/stereo presentation, observation latency, and a receiver that actually constructs WbfmReceive.
…#5974) ## Rebased on current main — 2026-10-06 Current head: `ebb35cfd20892431758fd8a165b7c66357b74090`. Base stack starts at main `5f09371e25ffc032666d36f0ea265dc506621066`; parent PR/branch is unchanged. This is a signed replay of the existing increment. It preserves main's unified window chrome and release debug-symbol archives, alongside the RTL dependency packaging and explicit Digital-OFF release flags. Generated touchpoint counts were regenerated from the combined sources. Fresh rebase validation on analog `ca3a36bd`: native Apple Silicon / Qt 6.12 / RTL required / Digital OFF application build succeeds. **51/52 selected CTests pass** in 226.55 seconds: all 47 RTL regressions and four additional UI checks pass. The new upstream `unified_title_bar_test` fails its Alt+F/menu-rounding assertions under Mac offscreen rendering; an independent rebuild and run on clean main `5f09371e` reproduces the identical two failures. This is an inherited test failure, not a green full-suite claim. Static engine/routing/capability, generated manifests, registration/frozen-gate, theme and build-options checks pass; package-helper tests pass 9/9 and required-RTL configuration checks pass 16 combinations. The Digital increment and vendor trees are unchanged by replay; decoder-ON tests and release packaging were not rerun for these heads. Hosted checks are being refreshed by the push. All validation recorded below predates this rebase and remains evidence for the specific revisions named there. No new live-RF, capacity, or release-package qualification is claimed. Readiness is unchanged; Digital #6002 remains draft. --- ## Previous checkpoint record — 2026-10-05 ## Scope Analog WFM backend and applet integration under RFC #5468, based on the nine-file DSP foundation #5973 and main `035a858356949a0c902e286ee5250c04f5ef86c0`. Head: `b8aba26e677f49f07d155685e5f7bef8d2bfe425`. Parent: #5973. Digital #6002 / RFC #6079 is not a dependency. The selected RTL WFM receiver exposes Mono/Auto stereo, 50/75 µs deemphasis, bandwidth and measured pilot/stereo diagnostics. Accepted backend state drives readback and per-device persistence. The `broadcastFmReceive` capability is documented for all six backend families; RTL advertises the implemented controls and other families do not. WFM requests use the existing typed receive dispatcher through `SliceWfmRequest` / `requestSliceWfm`, including thread, current receiver identity, ownership, mode, value and backend-support guards. Refusals use the shared reporting path. This carries the #5919 request-routing contract into WFM. Speaker renegotiation prefers 48 kHz only for an accepted 48 kHz producer. If opening the new sink fails, it makes one bounded attempt using the previous rate policy and reports failure if neither opens. Producer-domain buffers, DSP and rate state are preserved. This shared AudioEngine behavior affects every backend family. The associated recorder-rate test follows producer rate rather than inferring it from the speaker sink. The opt-in nonblocking WFM exchange uses eight slots: 42.667 ms total exchange buffering, 32 ms above the legacy depth. Existing blocking/legacy owners retain their prior depth. Vendored WDSP exchange changes and test exports are included. Generic ContainerWidget managed presentation is used by the WFM applet; the default WFM tile placement applies to RTL. ## Validation and remaining decisions Exact checkpoint, native Apple Silicon / Qt 6.12 / optional decoder OFF: 4/4 rebuilt tests pass in 17.10 seconds (`wfm_controls_test`, `audio_engine_rates_test`, `fm_filter_controls_test`, `rtl_model_acceptance_test`). Removing speaker recovery produces six failures in the rate test; restored code passes. The regression injects failed opens, checks bounded fallback and actual-rate stereo rendering, and preserves producer/DSP/lifecycle state. Static ratchets and generated manifests pass; independent source review found no remaining typed-WFM routing defect. Historical operator stability on `7601593f` is not a new-head soak. An earlier Mac filter/stereo transition intermittently withdrew a WFM receiver; passing diagnostic reruns did not establish a root cause, and this remains unresolved. Maintainer review still covers WFM applet/default presentation, the typed public seam and vendor exchange/test surface, and the buffering tradeoff. The approved RTL-only free-pan and confirmed-control seam from #5924 are not reopened. Normal admission remains one receiver; 2/4/8 evaluation and capacity qualification belong to the delivery increment. No Digital approval is requested. Principles II, V/XIV and XI: accepted state, scoped persistence and demonstrated behavior. --- 73, Ozy **K6OZY** · Codex
jensenpat
left a comment
There was a problem hiding this comment.
Merge withheld. This comment is pinned to d8c4440127bb7e49f374cc1f07162d926f676d3b and does not extend the approval on 1398f3b9.
While that nine-file review was in progress, #5974 was squash-merged into codex/rtl-wfm-dsp-foundation (parent 1398f3b9, commit d8c444012, 2026-10-07 14:35Z). This PR's head is now two commits and 86 files: the opt-in DSP recipe plus the native-stereo applet and receive-exchange work the #5973 body leaves to #5974. I have not reviewed that second commit.
GitHub reports the head conflicting. A merge-tree against current origin/main (d5f34f0c9) auto-merges WdspChannel and conflicts only in docs/architecture/aetherd-touchpoints.md.
The approval at #5973 (review) still describes 1398f3b9 only. I did not push the local replay of that commit onto d5f34f0c9, and I am not merging this head.
The README nit from that review still applies to the first commit: third_party/wdsp/README.md calls the WFM work patch 14; AETHERSDR-PATCHES.md records it as Patch 19.
The only conflict was the generated touchpoint manifest. Regenerated it from the merged tree instead of keeping either side's includer counts.
jensenpat
left a comment
There was a problem hiding this comment.
Approving a5f001e. This brings current main into the branch that already holds the 5973 DSP recipe and the squash of 5974.
The only conflict was docs/architecture/aetherd-touchpoints.md. Regenerated with tools/gen_touchpoint_manifest.py. Recount versus main: AppSettings 106→107, ThemeManager 165→167, RadioCapabilities 10→11, PanadapterModel 4→5, RadioModel 51→52, SliceModel 34→36.
Local proof on this tree, Qt 6.12, ENABLE_RTL=ON, QT_QPA_PLATFORM=offscreen: wdsp_wbfm_test, wfm_controls_test, rtl_wfm_pipeline_test, and audio_engine_rates_test passed (4/4). check_engine_boundary.py --strict, check_capability_records.py --strict (70 booleans), and check_command_plane.py --strict (369) passed. The blanker still runs before the WFM prefilter in WdspChannel::processIq.
The earlier approval on 1398f3b covered only the nine-file recipe. This approval covers the combined head, including the #5974 applet and receive path that was squash-merged onto the branch.
Live per-device PPM edits apply after a 200 ms debounce, or immediately on Apply. The requested value stays separate from hardware readback and from accepted persistence. Hiding, minimizing, or leaving the page cancels an edit that has not been sent. Enter commits the field without closing the settings dialog. This is the five-file increment on top of the WFM stack already on main (#5973). Squashed-from: #6082 Co-authored-by: Ozy <6413560+Ozy311@users.noreply.github.com>
… the RTL backend. Principle XI. (#6263) ## Summary Fixes #6248, and the System Libraries Canary failure on `main`. Both came from #5973 (`ca4e7cb`). - **Full Suite: `control_receive_test`.** #5973 turned on `RtlReceivePipeline::kQualifiedWfmEnabled`, so RTL now declares WFM as a third adjustable filter mode along with `broadcastFmReceive`. That is intended; #5973's description says "RTL advertises `broadcastFmReceive`". The assertion was written by #5924 while the gate was off and still demanded exactly FM and FMN. It now follows the gate the way the other RTL tests do: - FM and FMN with their ±21.6 kHz edges - when qualified: WFM with its own ±100 kHz range, and the broadcast-FM controls - when not: no broadcast-FM controls - legacy, unqualified WFM is never offered - **System Libraries Canary: link error.** `rtl_wfm_pipeline_test` is registered on every build, but its opt-in DDC benchmark calls `RtlSdrDdc`. That class is compiled only with the RTL backend (librtlsdr plus fftw3f), which the canary image doesn't install, so the link failed with 9 undefined references. The benchmark and its header now sit behind `AETHER_BACKEND_RTL`, as `control_receive_test`'s RTL block already does. The pipeline checks still run on every build, and asking for the benchmark on a build without RTL prints that it was skipped. Test-only change: no production code moves. ## Constitution principle honored **Principle XI.** `main`'s CI should re-run green on its own merge commits. This restores the two red workflows. ## Test plan - [x] **RTL build:** the full `ctest` suite passes, 672/672 including `control_receive_test`, which failed on `main` before this change. - [x] **`-DENABLE_RTL=OFF`** (same effect as the canary, which has no librtlsdr): - the entire tree builds with `ninja -k 0` and no link errors - `rtl_wfm_pipeline_test` reports 0 failures, and with `AETHER_WFM_BENCHMARK=1` prints "benchmark skipped, this build has no RTL backend" - restoring `main`'s `rtl_wfm_pipeline_test.cpp` in that tree reproduces the canary's 9 undefined `RtlSdrDdc` references - [x] `check_test_registration` and `check_ci_test_gate` pass. ## Checklist - [x] Commits are signed - [x] Test-only change, with no settings, colour or meter changes - [x] No CHANGELOG entry 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The weekly sanitizer lanes build with AETHER_SHARED_CORE=ON. There the core does not re-export WDSP's symbols: aether_wdsp is a PRIVATE dependency built with hidden visibility. wdsp_wbfm_test (added in #5973) calls OpenChannelWithExchangeDepth directly but linked only aethercore, so both the ASan + UBSan and TSan builds stopped at link with an undefined reference. No tests ran. Link aether_wdsp as well, as rtl_wfm_pipeline_test (also from #5973) and rtl_receive_pipeline_test already do. In the default static-core build the archive is already on the link line, so nothing changes there. Fixes #6283 Fixes #6284 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… Static checks enforce it. Principle VIII. (#6309) ## Summary Both weekly sanitizer lanes stopped at **Build**, at link, so no tests ran (#6283 ASan+UBSan, #6284 TSan, fingerprint `b83f0666e782`). #5973 added `wdsp_wbfm_test`, which calls `OpenChannelWithExchangeDepth` directly while linking only `aethercore`. The lanes build with `AETHER_SHARED_CORE=ON`, where `libaethercore.so` keeps WDSP's symbols hidden (a PRIVATE dependency with hidden visibility), so the link failed: ``` tests/wdsp_wbfm_test.cpp:610: undefined reference to `OpenChannelWithExchangeDepth' ``` Every per-PR build uses the static core, where the test links fine, so nothing caught it before merge. Linking `aether_wdsp` into the test would build, but the test would then drive a second copy of WDSP that the core never touches. Its checks against that copy would prove nothing. Two other tests already did this. So every test now reaches WDSP through `core/dsp/WdspChannel.h`, and Static checks enforce it. ## Changes - **`WdspChannel` test hooks:** - `openWithExchangeDepthForTest()` runs `OpenChannelWithExchangeDepth` with the prepared-WBFM geometry, on the WDSP the core drives. It takes `g_setupMutex` and loads wisdom in the same order as `open()`. - It is a refusal probe only. A call WDSP would accept (id 0..31 with depth 2..8) returns `kOpenProbeWouldOpen` and never reaches WDSP, so it cannot open an orphan channel or rebuild a live one. - `threadAllocationSequenceForTest()` reads the core WDSP's per-thread allocation counter. - **One geometry:** `open()`, the hook and `validateConfig()`'s WBFM clause share one set of constants, and one helper turns a `Config` into the `OpenChannel` call. - **Tests:** - `wdsp_wbfm_test` goes through the hook, and now also checks that a probe WDSP would accept is refused. - `rtl_receive_pipeline_test` warms the core WDSP's TLS through the new hook. - `rtl_receive_pipeline_test` and `rtl_wfm_pipeline_test` no longer link `aether_wdsp`. All three link `aethercore` only. - **`tools/check_wdsp_test_links.py`**, run by the "WDSP test links" step in the required **Static checks** job: - fails a test source that includes a WDSP header, directly or through quoted repo headers; - fails a test target that links `aether_wdsp`; - scans every file under `tests/`, whether or not a target lists it literally; - allows only a commented allowlist: `wdsp_allocation_scope_test`, a pure WDSP unit test with no `aethercore`. An entry that links `aethercore`, stops including WDSP, or names a missing target is itself a finding; - is stdlib-only and takes about 0.4 s. It was chosen instead of a shared-core link build on every PR, on per-PR cost. Its docstring names the known limits: a hand-written `extern "C"` declaration, and the other PRIVATE statics (`zlibstatic`, `mspack_static`). - **`tools/test_check_wdsp_test_links.py`** tests the checker on synthetic trees: include cycles, transitive headers, the link rule and the allowlist. It runs as the `wdsp_test_links_checker` ctest and as a Static checks step. - The `AETHER_SHARED_CORE` note in `CMakeLists.txt` and the `/pr-review` build recipe now state the rule. ## Testing All runs at this head; sanitizer builds used the shared-core configuration with the lane's flags. - **ASan+UBSan** (lane `ASAN/UBSAN/LSAN_OPTIONS`, serial): all six pass. They are `wdsp_wbfm_test`, `wdsp_channel_test`, `wdsp_allocation_scope_test`, `rtl_wfm_pipeline_test`, `rtl_receive_pipeline_test` and `wdsp_test_links_checker`. No WDSP symbols are linked into the two RTL tests. - **TSan** (lane options plus `tests/sanitizers/tsan.supp`): `wdsp_wbfm_test` passes with no ThreadSanitizer warnings. - **Static core, lean RelWithDebInfo:** the same six pass. - **Mutations:** - `channel.c` rebuilds the live channel on a bad depth and still returns 0: fails "refused preparation leaves existing ring and allocations intact". - The hook's refusal guard removed: fails "test probe refuses a preparation WDSP would accept" 4×, and UBSan reports undefined behaviour in `iobuffs.c:732`. - In the checker, each of these fails the self-test: memoising cut results, dropping the link rule, or dropping the `tests/` scan. - **The checker** fails on `origin/main` (4 findings) and on the previous head of this PR (3). It passes on this head (672 targets). - **Static checks:** every script passes. The engine-boundary, touchpoint-manifest, capability-record and command-plane outputs are identical between merge base and head. Fixes #6283 Fixes #6284 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…II. (#6254) Refs #5942 **Hermes-Lite 2 receive audio becomes 12.04 dB quieter, at every slider position.** The panel does not depend on the mode. The level is measured in USB and LSB. AM is checked only for the setting surviving a rebuild, and CW and FM are not measured. That covers the speaker (and the HL2's codec, which the speaker mix feeds), the per-slice audio sent to TCI, and the in-app decoders. Today the chain output sits above full scale: WDSP's panel multiplies by 4.0 after the AGC has limited at about -0.16 dBFS. **No maintainer has ruled on this level change.** It needs a release note: anyone who pulled the slice AF slider down to about 25 to stop strong stations distorting can put it back at 100. ## Limits first - **HL2 only.** ANAN and RTL behave exactly as before. ANAN in every mode, and RTL outside narrow FM, still run WDSP's 4.0. Those backends have their own owners, and this PR does not decide for them. RTL narrow FM already opens the panel at 1.0 (#5924). The new field is not set on any RTL path, wide FM (#5973) included. - **Offline only.** The figures below come from a real `Hl2RxDsp` fed synthetic IQ. No radio, simulator or socket was involved, and nothing was measured on the air. Step 5 of #5942 (does the slider-25 crossover hold across LNA gain) is still unrun. This PR fixes the gain structure, which is provably 12 dB hot. It does not claim to cure any one listener's distortion. - **Unity does not promise `|out| <= 1.0`.** Speak, mpeak and the second bandpass pass all run between the AGC and the panel, as the triage noted. The test therefore bounds a steady tone, not every signal. - This is #5942's step 1, plus step 4's test for the HL2 chain only. Step 2 (the slider moving into `SetRXAPanelGain1`) is not here, because the triage asked for it as a separate change. Step 3 (`Meter::AgcGain`) is not here either. - Measured on one Mac (macOS, Qt 6.12.0). ## What changes - `WdspChannel::Config` gains `std::optional<double> rxPanelGain`. When it is unset, `open()` leaves WDSP's `create_rxa()` value of 4.0 untouched, so every existing owner is unaffected. When it is set, `open()` calls `SetRXAPanelGain1` after the FM block, so a set value wins over the FM recipe's 1.0 (no caller sets both). `validateConfig()` refuses a set value that is non-finite, negative, or on a transmit channel. - `Hl2RxDsp::buildChannel()` sets it to `Hl2RxDsp::kRxPanelGain = 1.0`, so every HL2 build, rebuild and rate change opens at unity. - Two comments in `hl2_rxdsp_unmute_staleness_test` that described the x4 are updated (its marker is still above its own floor). `docs/HERMES.md` §AGC notes that its recorded peaks (3.186, 2.664) include the x4. It also notes that the stages between the AGC and the panel mean the AGC's ceiling does not bound the output. ## The test: `hl2_rx_panel_gain_test` (socket-free, about 1 s) | leg | this commit | HL2 without the panel line (= main) | |---|---|---| | AGC off, 1500 Hz tone at 0.01: chain gain | **+10.01 dB** (the 10.00 dB fixed gain alone) | +22.05 dB | | control: the built config re-opened directly | -0.01 dB vs the HL2 chain | -0.01 dB | | negative control: the same config, panel unset | **+12.03 dB** vs the HL2 chain | -0.01 dB (fails, as it should) | | AGC med (39 dB ceiling), tone at 0.1, USB peak | **0.830 (-1.62 dBFS)**, rms 0.587 | 3.318 (+10.42 dBFS) | | the same, LSB | 0.828 (-1.64 dBFS) | 3.311 (+10.40 dBFS) | | rebuild to AM at 96 kHz keeps unity | yes | no | The ratio between the two AGC-med columns is 4.00 (12.04 dB), and the AGC-off gain moves by the same amount. The negative control takes `Hl2RxDsp::channelConfig()` and clears only `rxPanelGain`. That isolates the panel from everything else the HL2 sets. **Red on mutation** (scratch commits, never pushed; the full suite each time): | mutation | result | |---|---| | `Hl2RxDsp` does not set `rxPanelGain` (main's HL2 behaviour) | RED: the gain, negative-control, both AGC-med and both config checks | | `WdspChannel::open()` ignores `rxPanelGain` (the field is set but never applied) | RED: the gain, negative-control and both AGC-med checks, while the config checks pass. So the level legs check the effect, not the field. | Full ctest suite on macOS (Qt 6.12.0, offscreen), on `121340c0` (head `594d96cf`): 654 of 658 pass at -j8 and 655 after the serial rerun. The three remaining failures are known on clean main on this machine, each with only its known check lines: `control_receive_test` (#6248), `peripheral_auth_dialog_test` (#6008) and `unified_title_bar_test` (#6224). `vkamp_connection_test` (#5254) failed at -j8 and passed serially. This PR touches none of their files. The figures in the table above were re-read from `hl2_rx_panel_gain_test` on this head and are unchanged; the main-column figures and the two mutations are from before the rebase. The rebase brought in #5973 (RTL wide FM, inside `WdspChannel`) and #5975 (HL2 CW carrier level, transmit side); neither touches the panel gain or `Hl2RxDsp`. On the second mutation `vkamp_connection_test` also failed at -j8 and passed serially (#5254). The level figures and the mutations below were taken at `e9a0c396`; `594d96cf` changes comments and one doc line, no executable line. 73, ON8ST 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Lands the analog WFM work through #5974 on current main.
The DSP recipe accepts 384 kHz channel IQ, runs a Blackman-Harris RF prefilter, decodes broadcast FM at 192 kHz, and produces paired 48 kHz audio. Output gain holds headroom, deemphasis switches between 75 µs and 50 µs on the live filters, and a lock-free flag reports the stereo pilot from the last decoded block. Channels that do not opt in keep their previous recipe.
The RTL receiver wires that recipe through the typed receive path: Mono/Auto stereo, deemphasis, bandwidth, and pilot readback, persisted per device. RTL advertises
broadcastFmReceive; the other families declare the record and do not implement the controls. A WFM applet is the RTL tile. The speaker path prefers 48 kHz for an accepted 48 kHz producer and makes one bounded attempt at the previous rate if that open fails. This path uses a non-blocking eight-slot WDSP exchange. Other modes keep the previous exchange depth.The second impulse blanker from main stays ahead of the WFM prefilter. The touchpoint manifest was regenerated after the merge; its includer counts were the only conflict.
Digital FM remains draft #6002.
Squashed-from: #5973
Co-authored-by: Ozy311 6413560+Ozy311@users.noreply.github.com