Skip to content

RTL WFM: native stereo, dedicated applet and bounded receive exchange - #5974

Merged
jensenpat merged 1 commit into
codex/rtl-wfm-dsp-foundationfrom
codex/rtl-wfm-filters-stereo
Oct 7, 2026
Merged

jensenpat merged 1 commit into
codex/rtl-wfm-dsp-foundationfrom
codex/rtl-wfm-filters-stereo

Conversation

@Ozy311

@Ozy311 Ozy311 commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

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

@Ozy311 Ozy311 self-assigned this Sep 27, 2026
@github-actions

Copy link
Copy Markdown

Accessibility check findings

This PR touches src/gui/ files with 5 accessibility issue(s).
Full details appear as inline annotations on the diff above.

Files scanned : 6
Files with findings: 1
Total findings : 5

See docs/a11y.md for the canonical
Qt patterns (setAccessibleName, QAccessibleValueChangeEvent,
QAccessibleInterface subclasses, etc.). Structural remediation
(keyboard navigation, custom QAccessibleInterface subclasses)
is tracked in #3288.

Findings are warnings only — the build is not blocked.

3 similar comments
@github-actions

Copy link
Copy Markdown

Accessibility check findings

This PR touches src/gui/ files with 5 accessibility issue(s).
Full details appear as inline annotations on the diff above.

Files scanned : 6
Files with findings: 1
Total findings : 5

See docs/a11y.md for the canonical
Qt patterns (setAccessibleName, QAccessibleValueChangeEvent,
QAccessibleInterface subclasses, etc.). Structural remediation
(keyboard navigation, custom QAccessibleInterface subclasses)
is tracked in #3288.

Findings are warnings only — the build is not blocked.

@github-actions

Copy link
Copy Markdown

Accessibility check findings

This PR touches src/gui/ files with 5 accessibility issue(s).
Full details appear as inline annotations on the diff above.

Files scanned : 6
Files with findings: 1
Total findings : 5

See docs/a11y.md for the canonical
Qt patterns (setAccessibleName, QAccessibleValueChangeEvent,
QAccessibleInterface subclasses, etc.). Structural remediation
(keyboard navigation, custom QAccessibleInterface subclasses)
is tracked in #3288.

Findings are warnings only — the build is not blocked.

@github-actions

Copy link
Copy Markdown

Accessibility check findings

This PR touches src/gui/ files with 5 accessibility issue(s).
Full details appear as inline annotations on the diff above.

Files scanned : 6
Files with findings: 1
Total findings : 5

See docs/a11y.md for the canonical
Qt patterns (setAccessibleName, QAccessibleValueChangeEvent,
QAccessibleInterface subclasses, etc.). Structural remediation
(keyboard navigation, custom QAccessibleInterface subclasses)
is tracked in #3288.

Findings are warnings only — the build is not blocked.

@github-actions

Copy link
Copy Markdown

Accessibility check findings

This PR touches src/gui/ files with 5 accessibility issue(s).
Full details appear as inline annotations on the diff above.

Files scanned : 6
Files with findings: 1
Total findings : 5

See docs/a11y.md for the canonical
Qt patterns (setAccessibleName, QAccessibleValueChangeEvent,
QAccessibleInterface subclasses, etc.). Structural remediation
(keyboard navigation, custom QAccessibleInterface subclasses)
is tracked in #3288.

Findings are warnings only — the build is not blocked.

@Ozy311 Ozy311 changed the title RTL WFM: native 48 kHz stereo and confirmed broadcast controls RTL WFM: native stereo, broadcast controls and bounded receive exchange Sep 27, 2026
@Ozy311 Ozy311 changed the title RTL WFM: native stereo, broadcast controls and bounded receive exchange RTL WFM: native stereo, dedicated applet and bounded receive exchange Sep 28, 2026
@Ozy311
Ozy311 marked this pull request as ready for review September 28, 2026 05:55
@Ozy311
Ozy311 requested review from a team as code owners September 28, 2026 05:55
@Ozy311
Ozy311 requested a review from a team as a code owner October 2, 2026 00:58
@aethersdr-agent

Copy link
Copy Markdown
Contributor

Static checks: the touchpoint manifest is one include behind

The failure is a single stale generated line — not a compile error, and not CI infrastructure. The check did its job; everything else is green.

What failed

Run 36948659783, job Static checks, step aetherd touchpoint manifest (static-checks.yml:178):

python tools/gen_touchpoint_manifest.py --check

The other 20 steps in that job passed, and Sanitizer option configures passed. The two cancelled runs on this SHA are just the superseded earlier push — nothing to chase there.

Reproduced locally at 4ddc58fe (checked out that exact commit):

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

Why

tools/gen_touchpoint_manifest.py scans src/gui/ (.h and .cpp) plus src/main.cpp for quoted engine includes and counts the includers per engine header. --check regenerates in memory and compares against the committed docs/architecture/aetherd-touchpoints.md.

This branch adds two new src/gui/ includers of core/backends/RadioCapabilities.h:

commit file manifest after
41fc9314 (from the #5973 stack) src/gui/ModeFilterPresets.cpp not regenerated — row stayed at 9
208bc220 src/gui/WfmApplet.h (line 3) regenerated to 10

So 208bc220 did run the generator — it caught up with the older stacked include (9 → 10) — but that run didn't see the applet's own new include. main is at 9, the committed row says 10, and the live tree now resolves to 11. One file behind.

The fix

python tools/gen_touchpoint_manifest.py
git add docs/architecture/aetherd-touchpoints.md
git commit -S -m "docs: regenerate the aetherd touchpoint manifest"

The whole regenerated diff is that one count:

-| `core/backends/RadioCapabilities.h` | 10 | universal — ... | unconverted |
+| `core/backends/RadioCapabilities.h` | 11 | universal — ... | unconverted |

Nothing else to do: the row already carries a valid universal semantic tag, so there's no sidecar or classification work for the new applet. The manifest's own description of that header — "Backend-neutral capability descriptor consumed above the radio seam. Universal by definition" — is exactly what WfmApplet.h is doing when it asks the capability whether the backend declares broadcast-FM support. The applet reaching that header is the seam working as designed.

I verified at 4ddc58fe that after regenerating, --check passes (manifest up to date), and gen_seam_probe_table.py --check is also clean (51 signals). One thing to watch: the count is derived from the tree, so it's worth re-running the generator after any rebase of the #5973 → #5924 stack — if either of those regenerates independently, the number can drift again.

On reviewer comments: no Copilot review on this PR. The only bot comments are the five accessibility findings in one src/gui/ file — those are warnings only, explicitly non-blocking, and unrelated to this failure. They're worth a look on their own merits given the new applet's accessible-status indicator, but they didn't cause this red check.

This is a generated-artifact bookkeeping miss, the cheapest kind of CI red to clear. Thanks for the careful scoping in the PR body — the explicit limits on what the 6e87be85 validation does and doesn't establish make this stack much easier to review.


🤖 aethersdr-agent · cost: $5.0053 · model: claude-opus-5

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1. Issue fit

There is no fixes #N, so I reviewed against the PR's own stated increment plus RFC #5468 §4 and the recorded #5924 rulings the body cites. GOVERNANCE is satisfied by the governing RFC — this is not an un-RFC'd architectural change.

Partially verified. The diff does what the body says: Transaction::Mode::Wfm now routes through WdspChannel::WbfmReceive with the 384/192/48 kHz recipe (RtlReceivePipeline.cpp, prepareDetailed), the capability arrives as the correct per-feature record rather than a bool, the applet is registered and built, and the speaker renegotiates for accepted 48 kHz PCM. The DSP qualification numbers in docs/rtl-wfm.md (91 dB selectivity, 1.483 de-emphasis ratio, 2.10× benchmark) are generated-signal claims I have no way to reproduce headless — I read the test sources and they do assert those shapes, but I cannot confirm the measured values. The body is candid that live soak on this head is missing.

One staleness note: the body says Current head: a28b4f8e…. The real head is 9164385f ("Integrate current DSP and main fixes into analog WFM"), two main merges later. Worth refreshing before a maintainer reads it.

2. Scope

The PR's own GitHub diff is 83 files. (A git diff against the stack base shows ~900 because of the interleaved main merges — I used list_pr_files for the authoritative set.)

File / group Changes Claimed in body? Verdict
docs/rtl-wfm.md, rtl-m1-runtime.md, audio-engine-rate-domains.md New + updated WFM/rate contracts Yes In scope
RtlReceivePipeline, RtlReceiverRegistry, RtlRfExtractor, RtlCaptureTransaction, RtlAudioMixer, RtlSdrBackend/Worker WFM recipe, lattice alignment, trace ring, missing-frame mask Yes In scope
WdspChannel, third_party/wdsp/{wbfm,channel,iobuffs,fir,firmin} + AETHERSDR-PATCHES.md Opt-in WBFM recipe, patches 15/17, exchangeDepth Yes (runtime tradeoffs) In scope; vendored-patch call below
RadioCapabilities.h, IRadioBackend.h, SliceDelta.h, WfmReceptionDiagnostics.h New capability record, 2 seam verbs, 4 delta fields Yes Needs maintainer decision — new public seam surface
Anan/Flex/Hl2/IcomCiv backends One c.broadcastFmReceive = std::nullopt; each Implied In scope (explicit non-declaration)
AudioEngine.{h,cpp}, AudioFormatNegotiator.cpp Sink generation guards, openRxSink/closeRxSink split, 48 kHz ladder Yes In scope; see Finding 1
RtlSliceSettings.{h,cpp} Two optional schema-1 per-slice fields Yes In scope — additive, old docs keep 75 µs
WfmApplet, WfmLockScope, AppletPanel, ControlAvailabilityRegistry, RxApplet, ModeFilterPresets, CMakeLists.txt Dedicated applet; broadcast row removed from RX Yes In scope
containers/ContainerWidget, FloatingContainerWindow + container_widget_test isPresentationManaged() / presentation-suppression plumbing Only as "temporary unavailability suppresses presentation" In scope, but it changes a generic container contract for one applet's benefit — please call it out explicitly in the body
tests/qso_recorder_rates_test.cpp Touched Not mentioned Unexplained — probably a knock-on of the producer-rate change, but say so
src/models/RadioModel.cpp, SliceModel.{h,cpp} WFM delta/status plumbing Yes In scope
tests/ (12 files incl. 3 new WFM targets) Coverage Yes In scope

No CHANGELOG.md entry — correct. No credentials, no flat settings keys, no new protocol wire text above the seam. The capability lands as std::optional<BroadcastFmReceive> (RadioCapabilities.h:306), which is exactly what docs/agents/backends.md:153-158 asks for, so the FROZEN_BOOL_COUNT ratchet is untouched.

Socket-test disclosure: I grepped all four new/modified WFM test sources (wfm_controls_test.cpp, rtl_wfm_pipeline_test.cpp, wdsp_wbfm_test.cpp, audio_engine_rates_test.cpp) for QTcpServer/QTcpSocket/QUdpSocket/QLocalServer/QWebSocketServer/bind(/listen(/connectToHost/Fake*. Zero hits. All three new targets are registered in tests/tests.cmake (lines 912-915, 1566-1570, 7157-7184) with explicit timeouts. The body's claim "No new test binds a socket or represents third-party firmware" holds.

3. Blockers

None. Nothing here breaks users on current main or violates a canon sentence I can cite.

4. Nits (non-blocking)

  1. applyPendingRxSinkRate discards the reopen result — unrecovered silent-speaker path. (inline on AudioEngine.cpp:4405.) closeRxSink(); openRxSink(pending->producerRate); with the return ignored. If the reopen fails, m_audioSink is null, and the 10 ms RX pacing timer lambda (AudioEngine.cpp:2029-2033) early-returns on !m_audioSink, so drainRxAudio() never runs and none of the #1361/#1411/#1569 watchdogs can fire — they all live inside it. The QAudioSink::stateChanged restart handler was attached to the sink that is now deleted. Net: RX audio is permanently silent until the operator reconnects or changes device. Previously only a watchdog could reach the sink-less state; now an ordinary WFM→SSB mode change can. A fallback retry at the previous m_rxSinkProducerRate, or at minimum a qCWarning plus something that re-arms, would close it.

  2. No test exercises a failing opener. All four NegotiatingFixture instantiations in audio_engine_rates_test.cpp (lines 386, 442, 459, 509) pass a rate list containing at least one working rung, so the !selected.ok → return false branch at line 367 is never taken through applyPendingRxSinkRate. The fixture already injects exactly the right boundary — a NegotiatingFixture({}) case would cover nit 1 cheaply. Socket-free CTest, so this is within the policy-compliant seam.

  3. broadcastFmReceive is absent from docs/architecture/radio-capabilities-map.md. (inline on RadioCapabilities.h:306.) Every sibling optional receive record is documented there (receiveModeControl/receiveFilterControl/receiveAudioControl at line 60, receiveSquelchModel at line 73); the new one is not. The gating test does exist (wfm_controls_test.cpp asserts capability gating), so this is a documentation gap rather than a missing-gate problem. I could not find a sentence in canon making the caps-map entry merge-gating, so: nit, not blocker.

  4. CI on this head is thin. get_check_runs for 9164385f returns only Static checks and Sanitizer option configures (success), plus two cancelled duplicates — no build or ctest job. Nothing here is "green means tested"; the body's Apple-Silicon result is for the downstream aggregate, not this head.

  5. Two *ForTest symbols are now PORT-exported from vendored WDSP (iobuffs.c: GetChannelOutputSamplesForTest, SynchronizeChannelWorkerForTest). Documented in AETHERSDR-PATCHES.md and guarded by comments, and the production path genuinely does not call them — but they are permanent public surface on the vendored tree, worth a maintainer's eye alongside patches 15/17.

5. What I tried to break (and failed)

  • The iobuffs depth generalization regressing every non-WFM channel. create_iobuffs now multiplies by ch[channel].exchangeDepth instead of DSP_MULT. If legacy OpenChannel left that field at zero, every Flex/HL2/Icom WDSP ring would allocate size 0. It doesn't: channel.c:120 passes DSP_MULT explicitly, and OpenChannelWithExchangeDepth clamps to [2,8] at line 127. The rewritten init arithmetic r2_inidx = r2_active_buffsize - r2_size is algebraically identical to the old (DSP_MULT-1)*r2_size at depth two. Held.
  • WFM reaching a squelch path. Manual squelch is refused twice independently — RtlCaptureTransaction::submit rejects an out-of-range wfmDeemphasisUs, and prepareDetailed rejects mode == Wfm && squelchEnabled. The mode < Am || mode > Cwr range guard also covers the whole enum (Wfm sits mid-enum at index 4, so it is not accidentally excluded). Held.
  • Sink churn on every PCM packet. requestRxSinkRate() is called per legacy feedAudioData, so I checked for a cross-thread event storm: the audioFrameReady → feedPcmFrame connect in MainWindow.cpp:7134 is auto/queued, so the slot already runs on the engine's own thread and the thread check is a no-op; the producerRate == m_rxSinkProducerRate early return makes the steady state free. The ladder.first().rate == m_rxOutputRate branch correctly preserves an already-48 kHz Windows/macOS sink, and m_rxSinkProducerRate (separate from m_rxOutputRate) stops a negotiated 48→44.1 fallback from reopening forever. Held.
  • Lattice divergence between extractor and mixer. mixerOrigin() in RtlReceivePipeline.cpp recomputes the origin independently of RtlRfExtractor::alignedCaptureFirst; I checked the two for drift. Both bound the rate to [225001, 3000000], both derive period = rate/gcd, and the frame counts differ only by the intended 384000-vs-48000 numerator (8:1). Both carry the same overflow guard. I also chased whether a fractional achievedSampleRateHz could trip mixerOrigin's integrality check and fault all eight receivers (m_faults.fetch_or(0xff)): it can't — the only assignment is actual.achievedSampleRateHz = hardware.sampleRateHz (RtlCaptureTransaction.cpp:190) from a std::uint32_t. Held.
  • Allocation/logging on the acquisition callback. missingFrames() and the new first-failure trace both write into a fixed std::array<TraceEvent, 64> ring with a separate drop counter; the MissingMask is a std::array<uint64_t,2> with a static_assert(kQuantum == 128) guarding the i/64 indexing. wdspPortOutputCopyHoldForTest() on the production copy path is a relaxed atomic load when disarmed. Held.
  • a11y on the new applet. Every interactive control in WfmApplet.cpp carries setAccessibleName, the status/diagnostics QLabels are non-interactive readouts with throttled QAccessible::updateAccessibility, and the unavailable state sets an explanatory accessibleDescription rather than a bare disable. Held.
  • Security audit (CodeGuard): ran as required over AutomationServer.cpp, ClientDisplaySettings.h, RtlRfExtractor.cpp, RadioModel.cpp — no confirmed findings. The two flagged memcpys in RtlRfExtractor.cpp:153-154 are single-float copies with the destination bounded by m_stagedOutput < blockSize and the source by an exact countI * sizeof(float) resize. Worth noting for the maintainer: AutomationServer.cpp and ClientDisplaySettings.h are not in this PR's 83 files (they come from the stacked base), and the RadioModel.cpp sites the harness flagged (:5278 NetCW VITA-49, :6716, staleSessionEvictionEnabled) are long-standing code unrelated to WFM — the triage ranges look stale.

Honest limits: no build, no test run, no GUI. Every finding above is reasoned from the source at 9164385f, not reproduced at runtime. I could not verify any measured DSP number in docs/rtl-wfm.md, the 42.667 ms latency figure, the 2.10× benchmark, or the claimed mutation checks — those need the maintainer's own run or the operator's soak. The ContainerWidget presentation-suppression behavior and the applet's docked/floating interaction are likewise unexercised by me.

6. Recommendation

Needs maintainer decision. The engineering is careful and unusually well-documented about its own limits — the lattice alignment, the generation fencing on the sink, the fixed-size trace ring and the refusal to invent a quality score are all the right instincts, and the vendored-patch provenance is recorded properly. Nothing in it looks like smuggled preference: the broadcast row moving out of RX, the 200 kHz default and the de-emphasis choices all trace to RFC #5468 §4 and the #5924 rulings.

What genuinely needs a human is the surface, not the code: two new IRadioBackend verbs, a new capability record, four new SliceDelta fields, patches 15/17 on the vendored WDSP tree, a generic container contract bent for one applet, and +32 ms of RX transport delay on the opt-in path. The body asks for exactly that review, which is the right posture.

Concrete next steps I'd want before merge: close nit 1 (fallback or documented re-arm for a failed speaker reopen) and add the NegotiatingFixture({}) case from nit 2, since both are cheap and the injection seam already exists; add the caps-map entry; refresh the stale head SHA in the body and name the ContainerWidget and qso_recorder_rates_test touches there. Thank you for the unusually honest qualification section — "generated-signal measurements, not antenna selectivity or hardware reception proof" is the sentence that made the rest of the document trustworthy.


🤖 aethersdr-agent · cost: $15.0285 · model: claude-opus-5

Comment thread src/core/AudioEngine.cpp Outdated
// Only the speaker changes format. Dedicated CW/Quindar sinks and their
// timers remain live; setRxDeviceRate expires old device-domain queues.
closeRxSink();
openRxSink(pending->producerRate);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

openRxSink()'s return value is dropped here. If the reopen fails after closeRxSink() has already torn down a working sink, the engine lands in a state nothing recovers from:

Result: RX audio is silent until the operator reconnects or changes output device. openRxSinkDevice() does log "failed to open RX audio sink on any negotiated format" before returning false, but no caller acts on it here — and startRxStream(), the other openRxSink() caller, does propagate the failure.

Before this PR a sink-less engine was only reachable via a watchdog path; now an ordinary WFM→SSB producer-rate change can reach it. Reasoned from code — I have no build here, so I have not observed it.

A fallback to the rate that was working would keep the previous behaviour intact:

Suggested change
openRxSink(pending->producerRate);
const int previousRate = m_rxSinkProducerRate;
closeRxSink();
if (!openRxSink(pending->producerRate) && previousRate != 0
&& previousRate != pending->producerRate) {
// The speaker was live before this renegotiation. Losing the preferred
// rate must not cost the operator the sink itself.
qCWarning(lcAudio) << "AudioEngine: speaker reopen at" << pending->producerRate
<< "Hz failed, restoring" << previousRate << "Hz";
openRxSink(previousRate);
}

(If the intended design is that a failed reopen should instead be repaired by a higher layer, a comment naming what re-arms it would settle this just as well.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in b8aba26. Speaker renegotiation now checks the open result and makes one bounded attempt with the prior rate policy. A successful fallback keeps the current producer domain and records it so subsequent same-rate epochs do not repeatedly reopen; failure of both attempts is explicitly reported for reconnect/output-device recovery.

Exact-head audio_engine_rates_test passes, including injected recoverable and unrecoverable open failures, actual-rate stereo rendering and preservation of producer buffers/DSP/lifecycle state. Removing this recovery produced six intended failures; restoring it passed. The three accompanying WFM/filter/model tests also pass.

73, Ozy K6OZY · Codex

std::optional<ReceiveFilterControl> receiveFilterControl;
std::optional<ReceiveAudioControl> receiveAudioControl;
std::optional<ReceiveSquelchModel> receiveSquelchModel;
std::optional<BroadcastFmReceive> broadcastFmReceive;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

broadcastFmReceive has no entry in docs/architecture/radio-capabilities-map.md. Its siblings are all documented there — receiveModeControl/receiveFilterControl/receiveAudioControl at line 60 and receiveSquelchModel at line 73 — so a reader of the caps map will not learn this record exists or what deemphasisUs / forceMonoControl / receptionDiagnostics mean.

The record shape itself is right: std::optional<BroadcastFmReceive> is exactly the per-feature pattern docs/agents/backends.md:153-158 requires ("a new capability lands as a per-feature record … because a bool cannot carry shape"), so the FROZEN_BOOL_COUNT ratchet is correctly untouched, and the gating test exists in tests/wfm_controls_test.cpp. Documentation gap only — non-blocking.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in b8aba26. The capability map now documents broadcastFmReceive, its deemphasis/mono/diagnostic fields, consumers and all six backend values. WFM control requests also now follow the shared SliceWfmRequest/requestSliceWfm typed dispatcher and refusal reporting introduced by #5919. Exact-head wfm_controls_test passes; capability and command-plane ratchets remain at main’s 70/369 baselines.

73, Ozy K6OZY · Codex

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

At e3464c915dcbed562678aa5c134e974eaf562963, this addresses #5468's analog WFM integration: the opt-in recipe is actually adopted, stereo PCM reaches receiver taps/mixer, controls report accepted state, and sink negotiation follows the accepted producer rate. I found no implementation blocker in the reviewed increment. The increased exchange latency and new applet/default presentation are disclosed tradeoffs for maintainer acceptance; Digital FM remains separate.

Scope

Files/group Claimed? Verdict
RTL pipeline/registry/extractor/backend and scoped WFM settings Yes: native WFM stereo, accepted controls, taps and epochs In scope
WDSP wrapper/vendor exchange and observation changes Yes: bounded nonblocking exchange, Mono and diagnostics In scope; recipe-only depth change
AudioEngine, format negotiation and rate docs Yes: 48 kHz sink preference and lifetime fencing In scope; cross-family regression coverage is necessary and present
WfmApplet, lock scope, AppletPanel and container geometry Yes: dedicated applet, optional diagnostics/scope, selected slice In scope; new interaction/layout is explicit
Models/capabilities/other-backend declarations and manifests Yes: neutral accepted-control and observation surface In scope
Tests, registration and runtime/vendor documentation Yes In scope; socket-free changed regression sources

The 83-path parent-branch diff contains no unclaimed Digital decoder dependency or unrelated feature. Inherited #5924 architecture rulings are not reopened.

Blockers

None found.

Non-blocking notes

  1. src/core/dsp/WdspChannel.cpp:1380: the eight-slot recipe trades 32 ms additional exchange transport for bounded scheduling tolerance. The test establishes bounded exhaustion and correct withdrawal, not zero dropouts under every host load. Preserve the stated 42.667 ms total exchange transport and the distinction from end-to-end speaker latency. The later report of an intermittent real-time receiver withdrawal is not explained merely by a passing offline test.
  2. tests/tests.cmake:7161: no exact-head Linux/macOS/Windows application-build checks appear in the hosted rollup beyond Static Checks. Earliest head run 2026-10-03 04:56:21 UTC predates current main 2026-10-04 18:26:40 UTC; strict: false does not force a fresh merge run. Current integration qualification still needs to precede landing.

Verification and attacks survived

  • Exact-head standalone Release harness compiles the production WFM decoder, pipeline, receiver registry, RF extractor, mixer and DDC (GCC 16.2.1 / Qt 6.11.2; isolated FFTW wisdom). wdsp_wbfm_test and rtl_wfm_pipeline_test pass. Production-path measurement: samples=191744 peak=0.916075 ... clipped=0; the deliberate held-consumer case reports successful_blocks=8 repair=1, and the pipeline finishes with 0 failures.
  • Forced-Mono negative control: made the wrapper always configure stereo. The regression failed Force Mono produces identical nonzero L/R while truthfully observing the actual pilot (exit 1). Restored exact source and reran the passing test. The control changes decoded samples, not just its label.
  • Native Qt 6.12 focused integration: audio_engine_rates_test, audio_format_negotiation_test, wfm_controls_test pass 3/3 in 25.03 s. To reuse the compiled engine, these ran in the #6082 child worktree; Git comparison confirms its engine, WFM UI and these test sources are unchanged from this PR. This is evidence for those identical paths, not a claim of a full #5974 application build.
  • Widget tests exercise accepted-state presentation, Mono/de-emphasis/bandwidth, stale/foreign/removed slice refusal, disconnect, capability replacement, scope visibility and observed diagnostics. Rate tests cover existing 24 kHz and new 48 kHz sources, fallback negotiation, stale requests, producer retirement and speaker-only rate changes without pretending hardware negotiation occurred.
  • Source audit checked diagnostics' 250 ms publication throttle, invalid/stale observation clearing, input-rate/time-lattice bounds, paired audio prior to monitor gain, and sink-generation/producer-lease guards. Four governance gates pass against parent and head: identical 102 existing engine findings, 71 capability booleans and 401 frozen wire commands, with current manifests.

No live radio was used. Did not drive an application bridge session because the demo does not expose this RTL WFM graph or pilot/USB acceptance surface; the real applet was instead driven by its offscreen injected-model test. No claim of live-RF stereo, native Windows/macOS audio-device negotiation, sanitizer or ARM workload qualification. Separate automated code-review skill unavailable.

Recommendation: Approve with nits, subject to maintainer acceptance of the disclosed applet/latency tradeoffs and current integration checks. Keep hardware/performance and production receiver-limit gates separate from these offline proofs.

— Jeremy KK7GWY + Codex

@ten9876 ten9876 removed their assignment 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 increment delivers what its body claims for #5468's analog WFM (RTL adopts the opt-in recipe, accepted-state WFM controls, stereo PCM through taps and mixer, a 48 kHz speaker preference). It has one canon gap of its own, and it inherits #5973's stale base. Reviewed 9194745..e3464c91 (this PR's own base..head) at head e3464c915dcbed562678aa5c134e974eaf562963. This is an independent second pass after the earlier ten9876 + Codex review on the same head.

Stack: main ← #5973 ← #5974 ← #6082 ← #6083. This PR's base is #5973's head branch.

Scope

Files / group Claimed? Verdict
RTL pipeline/registry/extractor/backend, RtlSliceSettings Yes In scope
WdspChannel, vendored WDSP exchange (iobuffs.c, channel.c, wdsp_port.c, wbfm.c) Yes — eight-slot nonblocking exchange, Mono, observation In scope; recipe-only depth
AudioEngine, AudioFormatNegotiator, rate docs Yes — 48 kHz speaker preference, sink generation fencing In scope, cross-family (the speaker sink is shared by every radio); disclosed
WfmApplet, WfmLockScope, AppletPanel, ContainerWidget, FloatingContainerWindow Yes — dedicated applet In scope; new default tile order (kDefaultOrder gains WFM after RX) is hardware-conditional
RadioCapabilities.h (BroadcastFmReceive), IRadioBackend.h (two new virtuals), SliceDelta.h, one-line declarations in Flex/HL2/ANAN/Icom/Sim Yes — neutral surface New public seam surface; see Blocker 1 for the missing map row
Tests, registration, docs Yes In scope; socket-free (no test opens, binds, listens or connects)

No CHANGELOG entry.

Blockers

  1. broadcastFmReceive isn't recorded in the capabilities map. src/core/backends/RadioCapabilities.h:227-230 (ADDING A FIELD): "Set every field explicitly in every backend and record it in docs/architecture/radio-capabilities-map.md." The first half is done: all six capabilities() implementations set it, five to std::nullopt and RTL to {{50, 75}, true, true} at RtlSdrBackend.cpp:209. The second half isn't. docs/architecture/radio-capabilities-map.md has no broadcastFmReceive row at this head, at #6083's head, or on main. For contrast, #6083 does add a map row for its own receiveAudioExport. Please add a row giving each family's value and what consumes it (WfmApplet availability, and the two RadioModel::wireSliceAudioIntentsToBackend gates).

  2. Inherited: this PR has to be rebuilt on the rebased #5973, and its seam additions have to adopt #5919's routing. #5973 is built on a pre-squash #5924 snapshot (see that review). Separately, #5919 (consolidate slice receive-control routing) landed on main on 2026-10-04. It moved per-slice receive controls onto typed requestSlice* verbs that return a ReceiveDispatch (IRadioBackend.h:214-240 on main: requestSliceDsp, requestSliceAudio, requestSliceSquelch, …), so that a refused operator edit is reported aloud under the 2026-10-04 ruling on #5919. It also cut SliceModel.cpp's raw-command baseline from 56 to 25. This PR adds two new per-feature signal bindings in the old shape: SliceModel::wfmForceMonoRequested and wfmDeemphasisRequested, each with its own canDispatch/mode/capability lambda in RadioModel::wireSliceAudioIntentsToBackend. Jeremy's 09-23 merge-order ruling asked for "one implementation of each guard" across exactly this seam. On the rebase, please route the two WFM controls through #5919's request path, or say in the body why they can't, rather than adding a parallel binding style.

Non-blocking

  • src/core/AudioEngine.cpp setRxDeviceRate: this no longer calls resetMainPcmState(...), and it no longer clears the Kiwi raw/packet queues or the NR/DSP state on a sink (re)open. That is deliberate, so that a speaker-only renegotiation keeps producer-domain state. The full-restart paths (stopRxStream → startRxStream, used by the #1361/#1411/#1569 watchdogs) still clear producer state in stopRxStream, so I couldn't build a case where stale producer audio survives a restart. It is still a behaviour change on the shared speaker for Flex and Kiwi users, so it deserves a sentence in the body.
  • AppletPanel::kDefaultOrder gains WFM, and setWfmAvailable(true) appends the tile to a saved m_buttonOrder. On a non-RTL radio it stays hardware-hidden. That is a default-layout change for RTL users only; the earlier review already routed it to maintainer acceptance.
  • The eight-slot exchange adds 32 ms (42.667 ms total exchange transport), and the body reports an unresolved intermittent real-time withdrawal. Both are disclosed; carry them forward unchanged.

What I verified, and what I tried to break

  • Re-checked the prior review.
    • The scope and table claims hold.
    • "No unclaimed Digital decoder dependency" holds: no HD/Digital sources in the 83-path diff.
    • "Other producers retain their rate contracts" holds by source. primaryRateOrder changes only when internalRate == 48000 && PreservePan. IRadioBackend's speaker producer starts with a default (24 kHz) format, and AnanRxDsp hardcodes 24000. The only 48 kHz main producer in the tree is RTL WFM.
    • The earlier review did not catch the missing map row (Blocker 1) or the #5919 routing collision (Blocker 2).
  • Cross-family capability declarations. Every backend sets broadcastFmReceive explicitly, and all consumers check has_value() before dereferencing (WfmApplet.cpp:272-302, RadioModel.cpp:9893/9903).
  • The ContainerWidget::setVisible override. Defaults are m_presentationAvailable{true} and m_presentationManaged{false} (ContainerWidget.h:178-180), so every container except WFM keeps QWidget::setVisible(visible) semantics.
  • a11y. Every interactive WFM control has an accessible name (WfmApplet.cpp:95/101/123/134/137/322). The mode button's name carries the current state, and its description explains Auto Stereo's fallback.
  • Not driven in the app. The demo radio declares broadcastFmReceive = std::nullopt and offers only USB/LSB, so the WFM tile can't appear in a demo session. I relied on the PR's offscreen wfm_controls_test and on source reading. The demo-side "tile stays hidden on a non-RTL radio" check is reported on #6083, whose head contains this code.

Recommendation: Request changes. One small canon fix (the map row), plus rebuilding on the rebased #5973 with the WFM controls routed through #5919's dispatch. The applet and latency trade-offs remain the maintainer decisions the earlier review named.

— Jeremy KK7GWY + Claude

@Ozy311
Ozy311 force-pushed the codex/rtl-wfm-filters-stereo branch from e3464c9 to b8aba26 Compare October 6, 2026 01:53
@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-filters-stereo branch from b8aba26 to ebb35cf Compare October 6, 2026 05:38
@Ozy311
Ozy311 force-pushed the codex/rtl-wfm-dsp-foundation branch from 45028ce to 1398f3b Compare October 6, 2026 05:38
@jensenpat jensenpat self-assigned this 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 ebb35cfd20892431758fd8a165b7c66357b74090 (one commit on codex/rtl-wfm-dsp-foundation, parent #5973). Issue fit: this increment delivers the analog WFM claim for RFC #5468: RTL adopts the opt-in 48 kHz stereo recipe, accepted-state Mono/de-emphasis controls go through requestSliceWfm, and the dedicated applet follows a connected WFM slice. No blockers.

Prior request-changes on e3464c91 are addressed on this head. broadcastFmReceive is set on all six families and recorded in docs/architecture/radio-capabilities-map.md. WFM force-mono and de-emphasis emit intents that RadioModel::dispatchSliceWfm validates and sends through requestSliceWfm; receiveCommandSource() still drops an off-thread caller before the backend is touched. The speaker reopen now tries the previous rate once after a failed preferred open.

Scope

Files / group Claimed? Verdict
RTL pipeline, registry, extractor, backend, RtlSliceSettings Yes In scope. Accepted WFM state is what is published and patched into the existing RtlSlices document. Pilot status is not persisted.
WdspChannel and vendored WDSP exchange (channel.c, iobuffs.c, wbfm.c, port exports) Yes In scope. Depth 8 is only the nonblocking WbfmReceive open. OpenChannel still resets every other owner, including a reused slot, to DSP_MULT.
AudioEngine, AudioFormatNegotiator, rate docs Yes, disclosed cross-family The 48/44.1/24 ladder runs only for a 48 kHz PreservePan output. Other producer rates keep the existing per-OS order. Speaker-only renegotiation keeps producer queues; stopRxStream still owns a full restart.
WfmApplet, WfmLockScope, applet order, container presentation Yes In scope. The tile is hardware-conditional. Unmanaged containers keep m_presentationManaged == false, so setVisible stays ordinary QWidget behavior.
BroadcastFmReceive, SliceWfmRequest, SliceDelta, one-line null capability on Flex/HL2/ANAN/Icom/Sim Yes In scope. RTL is the only family that fills the record, and only while kQualifiedWfmEnabled.
Tests, tests.cmake, docs, touchpoint counts Yes In scope. No new socket owner (bind/listen/connectToHost/Qt socket server). No CHANGELOG entry. Touchpoint count edits match the new includes; Static checks passed on this SHA.

Flex, HL2, ANAN, Icom, and Sim do not gain a WFM receiver. They share the speaker sink and the WDSP open path; the boundaries above are what keep their rate and exchange contracts intact.

Findings

No blockers. One non-blocking parentheses nit is inline on RtlSdrBackend::observeWfm.

The disclosed applet placement, eight-slot exchange (+32 ms, 42.667 ms transport), and the unresolved intermittent receiver withdrawal stay as the body states them. They are not treated as merge blockers on this parent-branch increment.

Verification

GitHub on this head: Static checks and Sanitizer option configures succeeded (an earlier duplicate run was cancelled). build / check-macos / check-windows do not run because the base is not main. Commit signature is valid.

Local scratch tree /tmp/aether-review-pr-5974 at ebb35cfd, Qt 6.12.0, RelWithDebInfo, AppleClang. Six headless tests passed: wfm_controls_test, audio_engine_rates_test, audio_format_negotiation_test, wdsp_wbfm_test, rtl_wfm_pipeline_test, container_widget_test. Disabling the previous-rate speaker retry failed audio_engine_rates_test on that guard (6 failures / 15502 checks); restoring the source and rebuilding passed again. The app bundle was not built. No live radio, physical audio device, Windows, or sanitizer run.

Non-blocking follow-ups

  • Parenthesize the stereo/pilot consistency check called out inline. The producer sets wfmStereoDetected from the same predicate, so a current packet cannot trip the fail-open parse; the guard should still match that predicate when the pilot is unlocked.
  • A saved unsupported WFM filter is warned when it is the only restore candidate (rtl_runtime_settings_test covers that). A mixed document that also restores another receiver leaves the bad slice omitted and preserved, without that warning.
  • Live-RF stereo, native device negotiation, and multi-receiver capacity remain outside this increment, as the body says.

|| reception.lockDurationMs > reception.observationDurationMs
|| reception.stableDurationMs > reception.observationDurationMs
|| (!reception.pilotLocked && reception.lockDurationMs != 0)
|| (reception.pilotLocked && !receiver->wfmForceMono) != *packet.wfmStereoDetected) { return; }

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.

Non-blocking. != binds tighter than &&, so this is pilotLocked && (!forceMono != stereoDetected). When the pilot is unlocked the comparison is skipped, and a packet with pilotLocked == false and wfmStereoDetected == true would be published as Stereo.

The producer in RtlReceivePipeline::audioBlockWithStatus sets wfmStereoDetected to pilotLocked && !forceMono, so a packet from that path does not hit the hole. The intended reject is the fully parenthesized comparison:

|| ((reception.pilotLocked && !receiver->wfmForceMono) != *packet.wfmStereoDetected)

Not a merge blocker.

@jensenpat
jensenpat merged commit d8c4440 into codex/rtl-wfm-dsp-foundation Oct 7, 2026
2 of 4 checks passed
@jensenpat
jensenpat deleted the codex/rtl-wfm-filters-stereo branch October 7, 2026 14:35
jensenpat added a commit that referenced this pull request Oct 7, 2026
…ge (#5973)

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>

---------

Co-authored-by: jensenpat <patjensen@gmail.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