Feature/wfm demod: add WideFM software demodulator via DAX IQ#3407
Feature/wfm demod: add WideFM software demodulator via DAX IQ#3407ea5wa wants to merge 4 commits into
Conversation
|
Hi @ea5wa — thanks for putting this together! CI is red on all three platforms ( The big one — Windows-only sources added to cross-platform targetsIn
All of them Fix: wrap those entries in if(WIN32)
list(APPEND CORE_SOURCES
src/core/WfmDemodulator.cpp
src/core/WaveOutWriter.cpp
src/core/WaveInReader.cpp
)
list(APPEND GUI_SOURCES
src/gui/WfmDeviceDialog.cpp
)
endif()Good news — A few smaller cleanups while you're in there
Really appreciate the contribution — WFM demod via DAX IQ is a great addition. Once the sources are Win32-gated this should turn green on all three platforms. 🤖 aethersdr-agent · cost: $5.2795 · model: claude-opus-4-7 |
|
Hi @ea5wa — thank you for the work, the on-air verification against 9k6 satellite telemetry through hs-soundmodem is exactly the kind of end-to-end test that earns review time. I want to redirect on architecture before you invest more cycles. @aethersdr-agent already nailed the immediate CI diagnosis (Win32-only sources added to cross-platform CMake lists, three hardcoded `C:/Users/reigc/wfm_debug.txt` paths, stray `.gitignore` entry), but adding `if(WIN32)` gates isn't the right fix here. The project ruleEvery feature in AetherSDR should be cross-platform unless it specifically addresses a platform-only problem. AetherSDR exists to give Linux/macOS operators feature-parity with the Windows-only SmartSDR client — see AGENTS.md "Project Goal". A Windows-only feature inside AetherSDR re-creates the gap the project is trying to close. Examples already in tree that ARE genuinely platform-specific because they address a platform-only problem: `MacNRFilter` (macOS-only neural NR using Apple's framework), the Windows WASAPI 24k-artifact policy (#2120), the macOS Bluetooth-HFP guard (#2615). They're each solving a platform-shaped bug, not just convenient to ship that way. Why WFM doesn't qualifyLooking at the diff, the Win32 dependency is only in the audio output stage — `WaveOutWriter` wraps `waveOut` to deliver Int16 PCM to a Virtual Audio Cable. The actual demodulation chain in `WfmDemodulator` is pure math (atan2 discriminator + 75 µs de-emphasis + volume scaling) and would run identically on every platform. The use case ("route demodulated audio into another application") also exists on every platform:
All three loopback drivers register as ordinary audio output devices, so they'd show up in `QMediaDevices::audioOutputs()` with no special handling. The architectural changeReplace `WaveOutWriter` with `QAudioSink`. It's part of Qt 6 Multimedia (already a dependency), works on Win32/CoreAudio/PipeWire/PulseAudio/ALSA, and the existing AetherSDR audio stack already routes through it — see `AudioEngine::startRxStream()` for the canonical pattern (opens a `QAudioSink` on a selected `QAudioDevice` at a negotiated format, writes float PCM to its `QIODevice*` from a timer). You'll also want to replace `WfmDeviceDialog` with a standard Qt device picker — enumerate `QMediaDevices::audioOutputs()`, store the selected ID in `AppSettings` under a `"WFM"` nested-JSON root (Constitution Principle V — flat `"WfmAudioDevice"` keys are non-compliant; see `docs/audio-pipeline.md` or `CwDecodeSettings.h` for the pattern). Once that's done:
Bonus: there's a foundation coming that fits this exactlyWe just landed #3397 (and #3398 is in flight) — the consolidated audio sink/rate negotiation factory (`AudioFormatNegotiator` + `AudioDeviceNegotiator`, tracked in #3306). Once #3398 lands, WFM's output stage can go through that helper for free: it'll handle the 48 kHz preferred rate, the Int16-only-device fallback for VAC clones, and the per-OS preferred-format ladder without you having to encode any of it in WFM-specific code. You don't have to wait for #3398 to refactor — `QAudioSink` direct works today. But if the wait is easier, that's also fine. What I'd like next
I'm happy to point at more of the existing patterns or sketch the `QAudioSink` wrapper if it'd help. The demod chain itself is great work — I just want it usable for everyone. 73, KK7GWY |
M7HNF-Ian
left a comment
There was a problem hiding this comment.
Nice work on this, Juan Carlos — and welcome! 👋 The DSP core here is genuinely solid: the atan2 phase-difference discriminator, the incremental-rotation frequency shifter with per-block phasor renormalization, and the limiter are all done properly. That's the hard part and you nailed it.
I had a read through and there are a few things that'll need sorting before this can go green. Grouping them so it's easy to work through:
Build-blocking (CI will fail on these):
-
macOS / Linux builds —
WaveOutWriter.handWaveInReader.hinclude<windows.h>/<mmsystem.h>and use the Win32waveOut/waveInAPIs, but the three new files are added toCORE_SOURCESinCMakeLists.txtunconditionally. The MainWindow call sites are nicely guarded with#ifdef Q_OS_WIN, but the core files themselves aren't, so the macOS-DMG and AppImage jobs won't compile. These need to be gated to Windows-only (conditional source inclusion in CMake + the file bodies guarded). -
Hardcoded debug log path — both
WfmDemodulator.cppandWaveOutWriter.cpphave awfmLog()that writes toC:/Users/reigc/wfm_debug.txt. Looks like leftover debugging — it opens/closes the file on every call (including in the audio path). Worth removing, or switching to the project'sqCDebug()logging category if you want to keep the diagnostics. -
.gitignore— theResumen para empezar de nuevo.txtentry looks like a personal scratch file that snuck in; probably wants dropping from this PR. -
Leftover
qDebug()counters inDaxIqModel.cpp(s_feedCount/s_emitCount) — same deal, debug remnants.
Smaller stuff:
- The summary mentions a 75 µs de-emphasis filter, but I don't see one in
processSamples(). For 9k6 G3RUH FSK you actually want flat discriminator audio, so I think the code is right and the description just needs a tweak. m_agcGainis declared but never used.
One bigger thing I'll leave for @ten9876 since he's reviewing: the primary IQ path opens the SmartSDR DAX waveIn device directly, with the native VITA-49 DaxIqModel as fallback. Given AetherSDR is a native client, it might be worth discussing whether the VITA-49 path should be the primary one — but that's a design call above my pay grade. 😄
Really promising feature though — fed-FM-into-soundmodem is a great use case. Happy to help if any of the above is unclear. 73!
|
Agree with @ten9876's direction here — the |
Adds a software FM demodulator that receives IQ samples from DAX IQ RX, demodulates them and routes audio to a selected output device (e.g. Hi-Fi Cable) at 48 kHz. New files: - WfmDemodulator: FM discriminator, de-emphasis, volume control - WaveInReader / WaveOutWriter: Win32 audio I/O wrappers - WfmDeviceDialog: output device picker with remember-choice option Wiring: - DaxIqModel: expose iqSamplesReady signal to feed the demodulator - RxApplet / VfoWidget: WFM toggle button (visible in FM/NFM mode only) - MainWindow: activateWFM/deactivateWFM, widens IF filter to ±20 kHz, centers panadapter on slice frequency, restores filter on deactivation - CMakeLists: register new source files
- CMakeLists: move WfmDemodulator, WaveOutWriter, WaveInReader, and WfmDeviceDialog into if(WIN32) block — these files use waveIn/waveOut Win32 APIs unavailable on macOS and Linux - WaveOutWriter, WaveInReader, WfmDemodulator: remove wfmLog() helper that wrote to a hardcoded local path (C:/Users/reigc/wfm_debug.txt); replace all calls with qDebug().noquote() and remove unused QFile include
…platform) Replace the Windows-only WaveOutWriter (waveOut API) and WaveInReader (waveIn API) with cross-platform Qt6 equivalents so WFM demod works on Windows, macOS, and Linux without platform guards in CMake. Changes: - WaveOutWriter: rewritten using QAudioSink; device selected by QAudioDevice::id() (persistent opaque string) instead of a human-readable name fragment - WaveInReader: removed — the VITA-49 DaxIqModel::iqSamplesReady path is cross-platform and already works; the Win32 DAX waveIn path is no longer needed - WfmDemodulator: simplified to use only the VITA-49 IQ path; updated all debug output to qCDebug(lcAudio) - WfmDeviceDialog: rewritten using QMediaDevices::audioOutputs() and QAudioDevice::id() — works on all platforms - WfmSettings: new header-only settings class following constitution Principle V (nested JSON under 'WFM' key); migrates the legacy flat 'WfmAudioDevice' AppSettings key on first access - CMakeLists: WFM sources moved back to unconditional CORE_SOURCES / GUI_SOURCES — no WIN32 guard needed - MainWindow: resolveAudioDevice() updated to use WfmSettings API
|
Thanks for the detailed architectural guidance Refactor complete in ef5b0e9: WaveOutWriter — rewritten using WaveInReader — removed entirely. The VITA-49 WfmDeviceDialog — rewritten using WfmSettings — new header-only settings class following Principle V: all WFM settings live under a single WfmDemodulator — simplified to the single cross-platform VITA-49 path; all debug output now goes through CMakeLists — WFM sources are back in the unconditional I don't have a macOS or Linux test box available right now — happy to re-verify on Windows once CI confirms the other platforms are green. If you're able to run the Linux side I'd appreciate it. Compiled without errors. Tomorrow I'll test it on windows 11 73 de EA5WA Juan Carlos |
Summary
Adds a software FM demodulator that receives raw IQ samples from the
radio's DAX IQ RX stream, demodulates them in software, and routes the
resulting audio to a user-selected output device (e.g. a Virtual Audio
Cable such as Hi-Fi Cable Input) at 48 kHz. This makes it possible to
use the Flex radio as a wideband FM / satellite receiver feeding any
Windows audio application — in particular 9k6 packet/telemetry modems
such as hs-soundmodem.
Constitution principle honored: Principle I — the demodulator is
fully self-contained; it activates only when the operator presses the
WFM button and deactivates cleanly on toggle-off or mode change,
restoring the original IF filter without side-effects on any other
subsystem.
Audio path:
Flex DAX IQ RX (channel 1, 24 kHz IQ @ 48 kHz sample rate)
└─► DaxIqWorker::processIqPacket()
└─► samplesReady(channel, QVector iqInterleaved, sampleRate)
└─► WfmDemodulator::onIqSamples()
├─► FM discriminator (atan2 phase difference)
├─► 75 µs de-emphasis filter
├─► volume scaling
└─► WaveOutWriter → selected output device @ 48 kHz Int16
On activation the IF filter is widened to ±20 kHz (G3RUH standard) and
the panadapter is centered on the slice frequency so DAX IQ RX 1 is
aligned with the signal. Both are restored on deactivation.
The audio output device is resolved via WfmDeviceDialog on first use;
the choice can be saved to AppSettings ("WfmAudioDevice") to skip the
dialog on subsequent activations.
Changes
New files:
src/core/WfmDemodulator.{h,cpp} — FM discriminator, de-emphasis,
volume control, start/stop lifecycle.
src/core/WaveInReader.{h,cpp} — Win32 waveIn capture wrapper
(reserved for loopback / future use).
src/core/WaveOutWriter.{h,cpp} — Win32 waveOut playback wrapper
used by WfmDemodulator to write PCM to the selected VAC.
src/gui/WfmDeviceDialog.{h,cpp} — output device picker dialog with
"remember my choice" checkbox.
Modified files:
src/models/DaxIqModel.{h,cpp}: expose iqSamplesReady / samplesReady
signals so the demodulator can subscribe to raw IQ without polling.
src/gui/RxApplet.{h,cpp}: WFM toggle button (36×20 px, visible
alongside the mode combo); intercepts "WFM" mode-combo selection.
src/gui/VfoWidget.{h,cpp}: WFM toggle button on the DSP/OPT tab,
visible only when the slice is in FM or NFM mode; auto-unchecks on
mode change.
src/gui/MainWindow.{h,cpp}: activateWFM / deactivateWFM methods;
wires both RxApplet and VfoWidget signals; manages filter save/restore
and panadapter centering.
CMakeLists.txt: registers the four new source files.
Test plan
Platform: Windows 11, Flex-6600, Hi-Fi Cable (VB-Audio).
WideFM demodulation — satellite 9k6 telemetry:
Tuned Slice A to a satellite downlink frequency in FM mode.
Pressed the WFM button in the RxApplet — WfmDeviceDialog appeared;
selected Hi-Fi Cable Input and checked "remember choice".
Confirmed that:
IF filter widened to ±20 kHz automatically.
Panadapter recentered on the slice frequency.
Audio appeared on Hi-Fi Cable Input at 48 kHz.
Launched hs-soundmodem pointed at Hi-Fi Cable Output; decoded 9k6
FSK satellite telemetry frames correctly with no audio glitches.
Pressed WFM again to deactivate:
Audio stopped.
IF filter restored to its previous width.
No crashes; repeated activate/deactivate cycle 5+ times without issues.
Changed slice mode away from FM — WFM button hid automatically and
demodulator stopped cleanly.
Cleared saved device preference, reactivated — dialog appeared again
as expected.
Device error handling:
Unplugged Hi-Fi Cable, attempted activation — demodulator detected
failed open, cleared the saved preference, and returned gracefully
without crashing.