fix(dax-iq): make DAX-IQ fully usable — meter, rate switching, state#3522
fix(dax-iq): make DAX-IQ fully usable — meter, rate switching, state#3522Ozy311 wants to merge 9 commits into
Conversation
The IQ pipe was only (re)built when a daxiq_rate CHANGE arrived. A fresh
enable at the default rate (daxiq_rate=48000 == IqStream::sampleRate{48000})
produced no rate delta, so the pipe was never created. Track isNew on the
new-stream branch and merge the guard to fire createPipe on isNew OR a rate
change, hoisted outside the daxiq_rate-present check.
Co-authored-by: Don @ cloaked.agency <don@cloaked.agency> & K6OZY
Rewrite createPipe/destroyPipe: mkfifo the owner-only FIFO ourselves first (0600 under $XDG_RUNTIME_DIR/aethersdr, not world-writable /tmp), then load module-pipe-source SYNCHRONOUSLY and capture the module index so we know it succeeded and can unload it cleanly by index. Open the write end O_RDWR| O_NONBLOCK (never ENXIOs before the read side attaches). Single-quote source_properties so the spaced device.description "AetherSDR DAX IQ N" survives the module-arg parser and drives the label shown in qpwgraph/JACK. Adds anon-namespace helpers (iqFifoDir/iqPipePath/makeOwnedFifo/runPactlSync) and an m_pipeModuleIdx[] member to track the loaded module per channel. Co-authored-by: Don @ cloaked.agency <don@cloaked.agency> & K6OZY
dax_iq float32 samples arrive little-endian (payload_endian=little), unlike the big-endian pan/wf/meter/audio streams. Reading them big-endian reversed each float into a denormal ≈ 0, flat-lining the RMS meter and feeding garbage to the format=float32le pipe. Decode little-endian (a no-op on LE hosts). Co-authored-by: Don @ cloaked.agency <don@cloaked.agency> & K6OZY
DAX-IQ samples are the radio's raw baseband amplitude (int16 full-scale ~32768), not normalized [-1,1] like DAX audio, so the old rms * 200 pegged the bar on real signals. Map RMS to dBFS against int16 full-scale over a [-70,-10] dBFS window with attack-fast/decay-slow ballistics, turning the bar into a level/overload meter that only fills near digital overload. Co-authored-by: Don @ cloaked.agency <don@cloaked.agency> & K6OZY
Store the user-selected rate in m_desiredRate as soon as it is chosen, even when no stream exists yet. When a stream first appears at the radio's 48k default, re-apply the desired rate via "stream set ... daxiq_rate=" and suppress the transient streamChanged so the combo never visibly flips to 48k. On the applet side, push the combo's rate to the model just before enabling, and sync the combo from radio state only while the stream exists so a disable (which resets sampleRate to 48k) cannot clobber the user's selection. Co-authored-by: Don @ cloaked.agency <don@cloaked.agency> & K6OZY
A levelReady queued just before a disable or pan-unbind would freeze the bar at its last (random, timing-dependent) value. Force the bar to 0 and reset the dBFS ballistics whenever the channel is not live — button != "On", no model, the stream does not exist, or it is unbound from a pan (pan==0x0). Mirror the same bound check in the streamChanged sync so an unbound-but- existing stream also drops to 0 instead of freezing. Co-authored-by: Don @ cloaked.agency <don@cloaked.agency> & K6OZY
Extract the connect-time restore into a public restoreEnabledChannels() that re-creates persisted-enabled IQ streams ~1.5s after connect, idempotently (skips channels whose stream already exists) and restores each saved rate via iqRateChanged before iqEnableRequested. Trigger it from the connect handler, from the already-connected path in setRadioModel (the initial connectionStateChanged can fire before setRadioModel runs and be missed), and from MainWindow::startDax once the lazy IQ enable->createStream wiring exists on PipeWire/macOS. The On button is left to the streamChanged sync so a restore that runs before the wiring is in place cannot show a dead "On". Co-authored-by: Don @ cloaked.agency <don@cloaked.agency> & K6OZY
…nabling DAX audio On PipeWire/macOS the DAX-IQ enable->createStream wiring is established lazily inside startDax() (Windows wires it at construction). So enabling a DAX-IQ channel before DAX audio was started silently dropped the request — no stream create reached the radio and the meter never moved. Auto-start DAX on the first DAX-IQ enable and create the triggering channel directly; once the bridge is up the guard is inert and startDax()s own connection handles later enables. Co-authored-by: Don @ cloaked.agency <don@cloaked.agency> & K6OZY
There was a problem hiding this comment.
Thanks @Ozy311 — this is a well-documented series, and the meter rework is a clear improvement (the dBFS window with attack/decay ballistics is the right model for a wideband IQ level meter, and the int16-scale convention is well-evidenced). The FIFO/state work follows repo conventions (AppSettings, owner-only XDG runtime dir mirroring the DAX-audio hardening). I reviewed the 5 commits unique to this PR; the DaxIqModel plumbing commits are #3521's and should get their review there.
A few real issues to address, all in the new auto-start / restore paths:
1. Double stream create after a DAX stop → IQ enable (the !m_daxBridge guard is not equivalent to "wiring absent"). stopDax() (MainWindow.cpp:15568) tears down the bridge but does not disconnect the IQ connections startDax() wires (statusReceived→applyStreamStatus, iqEnableRequested→createStream, iqRateChanged→setSampleRate — they're connected to this/the model, not to m_daxBridge). So after a start→stop cycle, an IQ enable click fires the surviving iqEnableRequested→createStream connection and your new lambda (which sees m_daxBridge == nullptr, calls startDax() again — wiring a duplicate set of connections — then calls createStream(ch) directly): two stream create type=dax_iq commands for one click, and every subsequent enable double-creates via the duplicated connections. The connection accumulation across start/stop cycles is pre-existing, but this PR makes it user-visible. Options, best first:
- Take the alternative you offered in the description: un-lazy the DAX-IQ wiring so it's established once at construction on all platforms (the IQ path doesn't actually need the audio bridge). That deletes the auto-start guard entirely and fixes the pre-existing duplication too. I'd prefer this route.
- Or: disconnect the IQ connections in
stopDax()and track "IQ wiring up" with a member flag instead of inferring it fromm_daxBridge.
2. The first-enable-before-DAX-start path loses a non-default rate. In buildUI() the enable handler emits iqRateChanged then iqEnableRequested — but on PipeWire/macOS before startDax(), iqRateChanged isn't connected to anything yet, so the rate emit is dropped, m_desiredRate stays at default, and the stream your lambda creates comes up at 48 k (and the exists combo-sync then flips the combo back to 48 k, clobbering the visible selection). This is the exact flow commit 5 fixes for the meter, so the rate should survive it too: in the auto-start lambda, call daxIqModel().setSampleRate(ch, rate) (rate read from the applet) before createStream(ch) — or moot the whole thing via option 1 above.
3. Please confirm m_desiredRate is initialized to {48000, …} in DaxIqModel.h (the header hunk was cut off in my diff view). If it zero-initializes, the path in issue 2 — createStream with no prior setSampleRate call — makes applyStreamStatus see isNew && m_desiredRate(0) != 48000 and send stream set … daxiq_rate=0 to the radio.
4. Suppressed streamChanged can leave a restored channel stuck "Off". When reapplyPending suppresses the isNew emit, the On-button sync (which the startup restore deliberately relies on) only happens when the follow-up rate status arrives. If the radio rejects the stream set … daxiq_rate= (or never echoes it), the button stays "Off" while the stream exists — and since setDaxIqLevel now gates on the button text, the meter stays pinned at 0 even with IQ flowing; clicking the button then issues a second createStream. Consider emitting streamChanged unconditionally and suppressing only the combo sync for the transient 48 k state (a flag or skipping the combo update when reapplyPending), rather than suppressing the whole signal.
5. Minor: overlapping restore timers can double-create. restoreEnabledChannels() can be scheduled from both the connect handler and startDax() within the same 1.5 s window (e.g. user clicks DAX On right after connect). The exists idempotence guard lags the radio round-trip, so two timers firing close together both pass it. A simple "restore pending" flag (or a single reusable QTimer member) would close this.
None of these block the core value here — the meter, rate persistence, and unbind-zeroing logic are all sound, and the live verification across three platforms is appreciated. Issue 1 is the one I'd consider required before merge; if you take the un-lazy-wiring route it resolves 1 and 2 together. Agreed that #3521 should land first.
🤖 aethersdr-agent · cost: $12.7408 · model: claude-fable-5
…eview aethersdr#3522) Address aethersdr-agent's review on aethersdr#3522: - Un-lazy the DAX-IQ wiring (review aethersdr#1, aethersdr#2). The IQ-side connections (stream status, VITA-49 feed, level meter, enable/disable/rate) are now established once at construction on every platform instead of lazily in startDax() on Mac/PipeWire. They never needed the DAX audio bridge, so they survive a DAX-audio stop and stopDax() can no longer strand an iqEnableRequested->createStream connection. This removes the auto-start lambda and the per-start duplicate wiring: one DAX-IQ enable now issues exactly one `stream create`, regardless of DAX on/off cycles (was one duplicate per stranded cycle). Verified live on a FLEX-6700: 3 creates pre-patch across two DAX toggles -> exactly 1 post-patch. - Always emit streamChanged; gate only the rate combo (review aethersdr#4). A restored channel no longer sticks at "Off" with the meter pinned at 0 when the radio rejects or never echoes the daxiq_rate re-apply. A new IqStream::rateSettling flag suppresses only the rate-combo sync during the transient; it tracks the actual rate gap (m_desiredRate != sampleRate) so an interleaved non-rate status (e.g. a pan bind) can't clear it early and flicker the combo. - Guard overlapping restoreEnabledChannels() timers with m_restorePending (review aethersdr#5). - aethersdr#3: confirmed already safe -- m_desiredRate and IqStream::sampleRate both default to 48000, so daxiq_rate=0 is never sent. Co-authored-by: Don @ cloaked.agency <don@cloaked.agency> & K6OZY
|
Thanks for the thorough review — addressed all five points (numbering below matches your review). Your preferred route (un-lazy the wiring) did collapse points 1 and 2 as you predicted. What changedItems 1 & 2 — un-lazy the DAX-IQ wiring. The IQ-side connections (stream-status routing, VITA-49 feed, level meter, and enable/disable/rate) are now wired once at construction on all platforms — the
This does mean enabling DAX-IQ no longer spins up the DAX audio bridge on Mac/PipeWire (it goes straight to Item 3 — confirmed safe, no change. Item 4 — emit Item 5 — Verification
73, Ozy K6OZY |
Summary
Stacked on #3521 (the core fix that makes DAX-IQ actually deliver samples). Because both PRs target
main, this one currently shows all 8 commits — the first 3 belong to #3521; the 5 commits unique to this PR are the UX/state fixes listed below. Once #3521 merges, this diff auto-reduces to just those 5. Please review/merge #3521 first.With IQ finally flowing, the DAX-IQ applet's UX/state handling — never exercised because the feature never worked — needed fixing: the level meter, sample-rate switching, and enable/disable/startup state. Four focused commits.
Fixes
1.
fix(dax-iq): scale the level meter in dBFS for raw int16 IQ— the meter didrms * 200assuming normalized[0, 0.5]IQ, so it pegged at 100% on the noise floor. It now maps RMS to dBFS over a[-70, -10]window (a level/overload meter: noise sits low, the bar fills toward digital overload) with attack/decay ballistics matching the DAX-audio meter.Before (pegged
×200) vs after (responsive dBFS):rms × 200pegs on the noise floor2.
fix(dax-iq): apply and persist the selected sample rate across enable— picking 24/96/192 k reverted to 48 k:createStreamsent no rate (radio defaults 48 k) andsetSampleRatedropped the selection when no stream existed yet. The chosen rate is now remembered and re-applied (stream set … daxiq_rate=) once the stream exists; the combo retains the user's choice across disable; and the transient 48k UI flip on enable is suppressed.3.
fix(dax-iq): zero the level meter when a channel is disabled or unbound— a level update queued just before a channel was switched off (or unbound from its panadapter,pan=0x0) could re-freeze the bar at its last value. The meter now drops to 0 whenever a channel isn't actively bound and receiving.4.
fix(dax-iq): restore persisted-enabled IQ channels and rates on startup— enabled channels showed "On" at launch but had no stream (dead). On Linux/macOS the IQ↔model wiring is established lazily insideMainWindow::startDax(), so the applet's connect-time restore fired before the wiring existed and was dropped. The restore is now triggered fromstartDax()(idempotent), and saved rates restore too.5.
fix(dax-iq): auto-start DAX so a DAX-IQ channel works without first enabling DAX audio— on PipeWire/macOS the DAX-IQenable → stream createwiring is established lazily insidestartDax()(Windows wires it at construction). So enabling a DAX-IQ channel before DAX audio was started silently dropped the request — nothing reached the radio and the meter never moved. Enabling a DAX-IQ channel now auto-starts DAX and creates the triggering channel directly; once the bridge is up the guard is inert andstartDax()'s own connection handles later enables (no double-create). (Open design note for the maintainer: this auto-starts DAX audio as a side effect — the small, additive fix. The alternative is to un-lazy the DAX-IQ connections so they need no DAX-audio bridge at all; happy to take that route if preferred.)Verified
Live on a FLEX-6700:
Scope
src/gui/DaxIqApplet.{cpp,h}+ one call added tosrc/gui/MainWindow.cpp::startDax(). All portable Qt/STL — no platform guards needed.Test plan
73, Ozy K6OZY
AI compute partnership: cloaked.agency — drafted with Don, our VP of Engineering agent, on Linux dev stack (
nobara-dell), live-verified against a FLEX-6700.