Repository navigation
RTL WFM: native stereo, dedicated applet and bounded receive exchange - #5974
Conversation
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
3 similar comments
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
Static checks: the touchpoint manifest is one include behindThe 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 ( The other 20 steps in that job passed, and Sanitizer option configures passed. The two Reproduced locally at Why
This branch adds two new
So The fix 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 I verified at On reviewer comments: no Copilot review on this PR. The only bot comments are the five accessibility findings in one 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 🤖 aethersdr-agent · cost: $5.0053 · model: claude-opus-5 |
There was a problem hiding this comment.
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)
-
applyPendingRxSinkRatediscards the reopen result — unrecovered silent-speaker path. (inline onAudioEngine.cpp:4405.)closeRxSink(); openRxSink(pending->producerRate);with the return ignored. If the reopen fails,m_audioSinkis null, and the 10 ms RX pacing timer lambda (AudioEngine.cpp:2029-2033) early-returns on!m_audioSink, sodrainRxAudio()never runs and none of the #1361/#1411/#1569 watchdogs can fire — they all live inside it. TheQAudioSink::stateChangedrestart 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 previousm_rxSinkProducerRate, or at minimum aqCWarningplus something that re-arms, would close it. -
No test exercises a failing opener. All four
NegotiatingFixtureinstantiations inaudio_engine_rates_test.cpp(lines 386, 442, 459, 509) pass a rate list containing at least one working rung, so the!selected.ok → return falsebranch at line 367 is never taken throughapplyPendingRxSinkRate. The fixture already injects exactly the right boundary — aNegotiatingFixture({})case would cover nit 1 cheaply. Socket-free CTest, so this is within the policy-compliant seam. -
broadcastFmReceiveis absent fromdocs/architecture/radio-capabilities-map.md. (inline onRadioCapabilities.h:306.) Every sibling optional receive record is documented there (receiveModeControl/receiveFilterControl/receiveAudioControlat line 60,receiveSquelchModelat line 73); the new one is not. The gating test does exist (wfm_controls_test.cppasserts 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. -
CI on this head is thin.
get_check_runsfor9164385freturns only Static checks and Sanitizer option configures (success), plus two cancelled duplicates — no build orctestjob. Nothing here is "green means tested"; the body's Apple-Silicon result is for the downstream aggregate, not this head. -
Two
*ForTestsymbols are nowPORT-exported from vendored WDSP (iobuffs.c:GetChannelOutputSamplesForTest,SynchronizeChannelWorkerForTest). Documented inAETHERSDR-PATCHES.mdand 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
iobuffsdepth generalization regressing every non-WFM channel.create_iobuffsnow multiplies bych[channel].exchangeDepthinstead ofDSP_MULT. If legacyOpenChannelleft that field at zero, every Flex/HL2/Icom WDSP ring would allocate size 0. It doesn't:channel.c:120passesDSP_MULTexplicitly, andOpenChannelWithExchangeDepthclamps to[2,8]at line 127. The rewritten init arithmeticr2_inidx = r2_active_buffsize - r2_sizeis algebraically identical to the old(DSP_MULT-1)*r2_sizeat depth two. Held. - WFM reaching a squelch path. Manual squelch is refused twice independently —
RtlCaptureTransaction::submitrejects an out-of-rangewfmDeemphasisUs, andprepareDetailedrejectsmode == Wfm && squelchEnabled. Themode < Am || mode > Cwrrange guard also covers the whole enum (Wfmsits mid-enum at index 4, so it is not accidentally excluded). Held. - Sink churn on every PCM packet.
requestRxSinkRate()is called per legacyfeedAudioData, so I checked for a cross-thread event storm: theaudioFrameReady → feedPcmFrameconnect inMainWindow.cpp:7134is auto/queued, so the slot already runs on the engine's own thread and the thread check is a no-op; theproducerRate == m_rxSinkProducerRateearly return makes the steady state free. Theladder.first().rate == m_rxOutputRatebranch correctly preserves an already-48 kHz Windows/macOS sink, andm_rxSinkProducerRate(separate fromm_rxOutputRate) stops a negotiated 48→44.1 fallback from reopening forever. Held. - Lattice divergence between extractor and mixer.
mixerOrigin()inRtlReceivePipeline.cpprecomputes the origin independently ofRtlRfExtractor::alignedCaptureFirst; I checked the two for drift. Both bound the rate to[225001, 3000000], both deriveperiod = 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 fractionalachievedSampleRateHzcould tripmixerOrigin's integrality check and fault all eight receivers (m_faults.fetch_or(0xff)): it can't — the only assignment isactual.achievedSampleRateHz = hardware.sampleRateHz(RtlCaptureTransaction.cpp:190) from astd::uint32_t. Held. - Allocation/logging on the acquisition callback.
missingFrames()and the new first-failure trace both write into a fixedstd::array<TraceEvent, 64>ring with a separate drop counter; theMissingMaskis astd::array<uint64_t,2>with astatic_assert(kQuantum == 128)guarding thei/64indexing.wdspPortOutputCopyHoldForTest()on the production copy path is a relaxed atomic load when disarmed. Held. - a11y on the new applet. Every interactive control in
WfmApplet.cppcarriessetAccessibleName, the status/diagnosticsQLabels are non-interactive readouts with throttledQAccessible::updateAccessibility, and the unavailable state sets an explanatoryaccessibleDescriptionrather 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 flaggedmemcpys inRtlRfExtractor.cpp:153-154are single-float copies with the destination bounded bym_stagedOutput < blockSizeand the source by an exactcountI * sizeof(float)resize. Worth noting for the maintainer:AutomationServer.cppandClientDisplaySettings.hare not in this PR's 83 files (they come from the stacked base), and theRadioModel.cppsites the harness flagged (:5278NetCW 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
| // Only the speaker changes format. Dedicated CW/Quindar sinks and their | ||
| // timers remain live; setRxDeviceRate expires old device-domain queues. | ||
| closeRxSink(); | ||
| openRxSink(pending->producerRate); |
There was a problem hiding this comment.
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:
m_audioSinkis null andm_rxSinkProducerRateis 0;- the 10 ms RX pacing timer lambda (
AudioEngine.cpp:2029-2033) early-returns on!m_audioSink, sodrainRxAudio()never runs — and the Sound through AetherSDR stops working, have to close and reopen ADR to get it to work again. #1361 zombie, Perdida de audio #1411 liveness and Loss of sound in ASDR #1569 stale watchdogs all live insidedrainRxAudio, so none of them can fire; - the
QAudioSink::stateChangedrestart handler belonged to the sink that was just deleted.
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:
| 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.)
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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
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.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: falsedoes 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_testandrtl_wfm_pipeline_testpass. Production-path measurement:samples=191744 peak=0.916075 ... clipped=0; the deliberate held-consumer case reportssuccessful_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_testpass 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
left a comment
There was a problem hiding this comment.
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
-
broadcastFmReceiveisn'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 sixcapabilities()implementations set it, five tostd::nulloptand RTL to{{50, 75}, true, true}atRtlSdrBackend.cpp:209. The second half isn't.docs/architecture/radio-capabilities-map.mdhas nobroadcastFmReceiverow at this head, at #6083's head, or on main. For contrast, #6083 does add a map row for its ownreceiveAudioExport. Please add a row giving each family's value and what consumes it (WfmAppletavailability, and the twoRadioModel::wireSliceAudioIntentsToBackendgates). -
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 aReceiveDispatch(IRadioBackend.h:214-240on main:requestSliceDsp,requestSliceAudio,requestSliceSquelch, …), so that a refused operator edit is reported aloud under the 2026-10-04 ruling on #5919. It also cutSliceModel.cpp's raw-command baseline from 56 to 25. This PR adds two new per-feature signal bindings in the old shape:SliceModel::wfmForceMonoRequestedandwfmDeemphasisRequested, each with its owncanDispatch/mode/capability lambda inRadioModel::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.cppsetRxDeviceRate: this no longer callsresetMainPcmState(...), 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 instopRxStream, 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::kDefaultOrdergainsWFM, andsetWfmAvailable(true)appends the tile to a savedm_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.
primaryRateOrderchanges only wheninternalRate == 48000 && PreservePan.IRadioBackend's speaker producer starts with a default (24 kHz) format, andAnanRxDsphardcodes 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
broadcastFmReceiveexplicitly, and all consumers checkhas_value()before dereferencing (WfmApplet.cpp:272-302,RadioModel.cpp:9893/9903). - The
ContainerWidget::setVisibleoverride. Defaults arem_presentationAvailable{true}andm_presentationManaged{false}(ContainerWidget.h:178-180), so every container except WFM keepsQWidget::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::nulloptand offers only USB/LSB, so the WFM tile can't appear in a demo session. I relied on the PR's offscreenwfm_controls_testand 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
e3464c9 to
b8aba26
Compare
9194745 to
45028ce
Compare
b8aba26 to
ebb35cf
Compare
45028ce to
1398f3b
Compare
jensenpat
left a comment
There was a problem hiding this comment.
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
wfmStereoDetectedfrom 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_testcovers 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; } |
There was a problem hiding this comment.
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.
…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>
Rebased on current main — 2026-10-06
Current head:
ebb35cfd20892431758fd8a165b7c66357b74090. Base stack starts at main5f09371e25ffc032666d36f0ea265dc506621066; 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 upstreamunified_title_bar_testfails its Alt+F/menu-rounding assertions under Mac offscreen rendering; an independent rebuild and run on clean main5f09371ereproduces 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
broadcastFmReceivecapability 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
7601593fis 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