Skip to content

RTL WFM: opt-in DSP, native stereo applet, and bounded receive exchange - #5973

Merged
jensenpat merged 3 commits into
mainfrom
codex/rtl-wfm-dsp-foundation
Oct 7, 2026
Merged

jensenpat merged 3 commits into
mainfrom
codex/rtl-wfm-dsp-foundation

Conversation

@Ozy311

@Ozy311 Ozy311 commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

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

@aethersdr-agent

Copy link
Copy Markdown
Contributor

CI is red on 824b784a, but not because of the WFM work — and not because of anything you did wrong. One step failed:

Static checks → aetherd touchpoint manifest (run 36948659295). Sanitizer option configures passed.

::error::aetherd-touchpoints.md is stale — run `python tools/gen_touchpoint_manifest.py`

The fix

python tools/gen_touchpoint_manifest.py
git add docs/architecture/aetherd-touchpoints.md && git commit

That 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 RadioCapabilities.h row:

-| `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 here

I bisected the branch: it was green through 1f71e08c and went stale at 6607ce9e — which is the current head of your base draft #5924, inherited here rather than introduced by the WFM commit. So #5924 is red for the same single cell. Regenerating there is the tidier fix and #5973 picks it up on the next reconciliation; doing it in both is also fine, since an identical one-cell change merges cleanly.

Why it went stale — a merge artifact git could not see

tools/gen_touchpoint_manifest.py counts, per engine header, how many files under src/gui/ + src/main.cpp #include it, and --check fails when the committed table disagrees. 6607ce9e is the reconciliation merge with main 68cdf661, and each side had added one new #include "core/backends/RadioCapabilities.h" under src/gui/, in a different file:

new includer row said
branch, before the merge src/gui/ModeFilterPresets.cpp (the isFmMode/squelch helpers) 9
main @ 68cdf661 src/gui/TxAudioPathPolicy.h (from 97cb6be, "Gate AetherTX on computer microphone routing", #5958) 9
after the merge both 9 ❌ — should be 10

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 src/gui/ → src/core/|src/models/ include needs the generator re-run afterwards, even when the merge is conflict-free. That is inherent to a generated counting table, not a mistake on your part.

Notes

  • The manifest is generated and carries a do-not-hand-edit note; please let the script write it rather than editing the cell by hand, so the rest of the table stays in sync. The two hand-authored sidecars (aetherd-touchpoint-tags.json, aetherd-touchpoint-status.json) survive regeneration, so you do not need to touch them.
  • main itself is clean — --check passes at 68cdf661 — so this really is specific to the merged tree.
  • No Copilot or reviewer comments on this PR right now, so there is nothing flagged that bears on the failure.
  • Generator: tools/gen_touchpoint_manifest.py · wired up at .github/workflows/static-checks.yml:178 · background in docs/aetherd-headless-engine-design.md.

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

@Ozy311
Ozy311 marked this pull request as ready for review October 2, 2026 01:10
@Ozy311
Ozy311 requested review from a team as code owners October 2, 2026 01:10
Base automatically changed from codex/rtl-m1a-capture-transactions to main October 4, 2026 06:03
@ten9876 ten9876 self-assigned this Oct 4, 2026

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. 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: false does not force freshness. This review does not establish merge readiness against that newer main.
  2. 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, production WdspChannel.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 ten9876 removed their assignment Oct 4, 2026
ten9876
ten9876 previously requested changes Oct 4, 2026

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. 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, including RtlSdrBackend.{h,cpp}, WdspChannel.{h,cpp}, RadioModel.{h,cpp}, SliceModel.{h,cpp}, RxApplet.{h,cpp}, VfoWidget.{h,cpp}, ModeFilterPresets.{h,cpp}, tests/tests.cmake and third_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, RxApplet registers SQL as available whenever !caps.receiveSquelchModel (src/gui/RxApplet.cpp:2032 at 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 / fmFilterAdjustable don'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 17d56197 ancestry entirely) rather than merging main into it. The fm_filter_controls_test cases added by fe91a982f are the pin that will tell you the resolution is right.

  2. The governance evidence describes baselines that main has already lowered. At this head, check_capability_records.py reports "71 boolean(s), at the frozen 71" and check_command_plane.py reports 401 commands across 32 files — I reproduced both, and they match the earlier review. But main has since lowered FROZEN_BOOL_COUNT 71 → 70 and the command-plane rows (SliceModel.cpp 56 → 25 via #5919, MainWindow.cpp 17 → 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 main at 732b99a9". 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. processIq copies into the prefilter's fixed 2*inputBlockSize buffer 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/setWbfmDeemphasis run only under beginControlOperation(), which refuses while m_callbacksInFlight != 0, so setFreqs_bps can't free mults under a running xbps. close() resets m_wbfm inside the same fence. Gain is applied only when wdspError == 0, and the stereo indicator is cleared on stop.
  • Profile validation. validateConfig pins the recipe to 384k/192k/48k, 2048/1024 blocks, a ±0.45·Fs filter, finite gain in [0,1] and a known de-emphasis. setMode refuses leaving WBFM on a recipe channel. setFilter writes the accepted edges back to m_config (WdspChannel.cpp:634-635), so a later reconfigure() keeps them.
  • Built and ran wdsp_wbfm_test at this head (Arch, GCC, Qt 6.x; isolated AETHER_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 matching setFreqs_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 matches HEAD), 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

@Ozy311

Ozy311 commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

I am carrying the requested remediation through #5973 → #5974 → #6082 → #6083. The starting main snapshot is 035a8583. I will replay only the unmerged increments onto the final merged #5924/#5919 behavior, preserve the upstream squelch/filter and receive-dispatch fixes, and re-run the current gates. Existing heads are preserved for recovery; any branch replacement will use exact expected-head leases.

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.

@Ozy311
Ozy311 force-pushed the codex/rtl-wfm-dsp-foundation branch from 9194745 to 45028ce Compare October 6, 2026 01:53
@Ozy311
Ozy311 force-pushed the codex/rtl-wfm-dsp-foundation branch from 45028ce to 1398f3b Compare October 6, 2026 05:38
jensenpat
jensenpat previously approved these changes Oct 7, 2026

@jensenpat jensenpat left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: processIq rejects a block that is not inputBlockSize before it writes the prefilter; setFilter / setWbfmDeemphasis / setRunning take beginControlOperation() so setFreqs_bps (which rebuilds the FFTW plans) cannot run under xbps. setFilter writes the accepted edges back to m_config on the recipe path.
  • Apple Silicon, Qt 6.12, RelWithDebInfo, ENABLE_RTL=ON, QT_QPA_PLATFORM=offscreen, at this SHA: wdsp_wbfm_test passed (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_test passed (1/1, 67.4 s).
  • Exact-head CI is green: build, check-macos, check-windows, Static checks, Sanitizer option configures on 1398f3b9. Those jobs do not execute wdsp_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.

Comment thread third_party/wdsp/README.md
@jensenpat
jensenpat dismissed ten9876’s stale review October 7, 2026 14:34

Dismissing the changes request on 9194745. That review asked for the nine-file WFM increment to be replayed onto main after #5924/#5919, without the pre-squash ancestry. Current head 1398f3b is that replay. Approving review on 1398f3b: #5973 (review)

…#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 jensenpat left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 jensenpat changed the title RTL WFM: qualify opt-in RF filtering, headroom and stereo RTL WFM: opt-in DSP, native stereo applet, and bounded receive exchange Oct 7, 2026

@jensenpat jensenpat left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@jensenpat
jensenpat enabled auto-merge (squash) October 7, 2026 14:53
@jensenpat
jensenpat merged commit ca4e7cb into main Oct 7, 2026
7 checks passed
@jensenpat
jensenpat deleted the codex/rtl-wfm-dsp-foundation branch October 7, 2026 15:21
jensenpat pushed a commit that referenced this pull request Oct 7, 2026
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>
ten9876 added a commit that referenced this pull request Oct 7, 2026
… 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>
ten9876 added a commit that referenced this pull request Oct 9, 2026
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>
ten9876 added a commit that referenced this pull request Oct 9, 2026
… 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>
jensenpat pushed a commit that referenced this pull request Oct 10, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants