Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions BUILD-OPTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,16 @@ alone. MSVC flags and `Release` builds are unaffected.

| Option | Default | Purpose and prerequisites |
|---|---|---|
| `ENABLE_DEEPFIST_EXPERIMENT` | OFF | Build the experimental DeepFist CW receive decoder. Requires ONNX Runtime at configure time (configuration fails without it) and a separate verified model bundle at runtime; see below. |
| `ENABLE_RTL` | ON | Build the experimental receive-only RTL-SDR USB backend when both `librtlsdr` and single-precision FFTW (`fftw3f`) are found. Missing either disables the backend. |
| `AETHER_HL2_TX_TXA` | ON | Select WDSP's TXA chain for the Hermes-Lite 2 SSB transmit modulator. OFF builds the in-tree phasing modulator. This choice has no runtime toggle. |

**DeepFist:** enabling `-DENABLE_DEEPFIST_EXPERIMENT=ON` alone does not supply its
model. The configured model-download URL defaults to empty. For developer
qualification, the runtime environment variable `AETHER_DEEPFIST_MODEL_DIR`
can point to the exact verified bundle; it is not a CMake switch. The required
assets and distribution prerequisite are described in
**Neural CW decoders:** DeepFist and DeepCW build whenever ONNX Runtime is
found at configure time (Apple Silicon macOS, Linux and Windows; the pinned
runtime has no Intel-macOS build), with no separate option. Neither model is
bundled: each downloads on first use from its upstream source at a pinned size
and SHA-256. For developer qualification, the runtime environment variables
`AETHER_DEEPFIST_MODEL_DIR` and `AETHER_DEEPCW_MODEL_DIR` point at a local
copy instead; they are not CMake switches. See
[the DeepFist guide](docs/deepfist-cw-backend.md). ggmorse remains the default
CW receive decoder.

Expand Down Expand Up @@ -148,7 +149,7 @@ the ASR library (`aetherasr`) sees.
| `HAVE_NVIDIA_AFX` | `ENABLE_NVIDIA_AFX` on x86-64 Linux or Windows. |
| `HAVE_MQTT` | `ENABLE_MQTT`. |
| `HAVE_MQTT_TLS` | `MQTT_TLS`, and for the bundled library OpenSSL is found. |
| `HAVE_DEEPFIST` | `ENABLE_DEEPFIST_EXPERIMENT`. |
| `HAVE_DEEPFIST`, `HAVE_DEEPCW`, `HAVE_CW_RX_BACKENDS` | ONNX Runtime is found. |
| `HAVE_MIDI` | Always; RtMidi is bundled. |
| `HAVE_SERIALPORT`, `HAVE_WEBSOCKETS`, `HAVE_KEYCHAIN` | The Qt SerialPort, Qt WebSockets or QtKeychain package is found. |
| `HAVE_DBUS` | Qt DBus is found on Linux or another non-Apple Unix; it is never looked for on macOS or Windows. |
Expand Down Expand Up @@ -176,7 +177,7 @@ Additional cache values accept a value rather than ON/OFF:
| Setting | Default | Values and purpose |
|---|---|---|
| `AETHERSDR_SANITIZER` | `none` | `none`, `address`, `undefined`, `address,undefined`, or `thread`. Instruments the main CMake tree with a GNU-driver GCC/Clang build; MSVC and clang-cl are rejected, and so is combining `address` with `thread`. Adds `-g3 -fno-omit-frame-pointer` to every configuration, Release included. ExternalProject children need separate sanitizer flags. |
| `DEEPFIST_MODEL_BASE_URL` | Empty | Published, versioned HTTPS directory for the exact DeepFist assets. See the distribution prerequisite in [the DeepFist guide](docs/deepfist-cw-backend.md). |
| `DEEPFIST_MODEL_BASE_URL` | Empty (N9BC's `exp27_bt-champion` release) | Replaces the HTTPS directory holding the exact DeepFist model assets; empty uses the published release. The LICENSE asset carries its own pinned source. See [the DeepFist guide](docs/deepfist-cw-backend.md). |
| `RADE_TAP_DIR` | `<build-directory>/rade_taps` | Directory for RADE WAV diagnostics; available when RADE and its taps are enabled. |
| `AETHER_TEST_FFTW_TIMELIMIT` | `0.001` | Seconds FFTW may spend measuring each plan under test; an empty value allows unbounded measurement. |
| `AETHER_SANITIZER_TIMEOUT_SCALE` | `4` | Positive integer. Multiplier applied to every test `TIMEOUT` when the build is sanitizer-instrumented (`AETHERSDR_SANITIZER` set, or `-fsanitize=` in the global C/C++ flags or in any configuration's `CMAKE_<LANG>_FLAGS_<CONFIG>`). An uninstrumented build keeps every limit exactly as written. |
Expand Down
48 changes: 34 additions & 14 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3014,27 +3014,36 @@ endif()

target_sources(aethercore PRIVATE src/models/CwRxModel.cpp src/models/CwRxModel.h)

option(ENABLE_DEEPFIST_EXPERIMENT "Build the local DeepFist CW prototype" OFF)
set(DEEPFIST_MODEL_BASE_URL "" CACHE STRING "Published, versioned HTTPS DeepFist asset directory (empty before publication)")
if(ENABLE_DEEPFIST_EXPERIMENT)
if(NOT ORT_FOUND)
message(FATAL_ERROR "The DeepFist prototype requires the packaged ONNX Runtime")
endif()
target_compile_definitions(aethercore PUBLIC HAVE_DEEPFIST)
target_compile_definitions(aethercore PRIVATE DEEPFIST_MODEL_BASE_URL="${DEEPFIST_MODEL_BASE_URL}")
# Neural CW receive decoders (RFC #4817): built wherever ONNX Runtime is found
# (Apple Silicon, Linux, Windows; the pinned runtime has no Intel-macOS build).
# Their models are never shipped: each downloads on first use from its upstream
# release at a pinned size and SHA-256.
# Empty selects the published release; a value replaces it. Not a cache default,
# so build directories configured before the release keep downloading.
set(DEEPFIST_MODEL_BASE_URL "" CACHE STRING "DeepFist asset directory override (empty = the published release)")
set(_deepfist_model_url "https://github.com/n9bc/DeepFist/releases/download/exp27_bt-champion/")
if(DEEPFIST_MODEL_BASE_URL)
set(_deepfist_model_url "${DEEPFIST_MODEL_BASE_URL}")
endif()
target_compile_definitions(aethercore PRIVATE DEEPFIST_MODEL_BASE_URL="${_deepfist_model_url}")

if(ORT_FOUND)
target_compile_definitions(aethercore PUBLIC HAVE_ONNX)
# Every CW receive backend beyond ggmorse needs ONNX Runtime, so this one
# gate decides whether the CW panel offers a decoder selector (RFC #4817).
target_compile_definitions(aethercore PUBLIC HAVE_DEEPFIST HAVE_DEEPCW HAVE_CW_RX_BACKENDS)
target_sources(aethercore PRIVATE
src/models/DeepFistCwModel.cpp src/models/DeepFistCwModel.h
src/core/deepfist/DeepFistStream.cpp
src/core/deepfist/DeepFistModelAssets.cpp src/core/deepfist/DeepFistModelAssets.h
third_party/deepfist/DeepFistConditioner.cpp
third_party/deepfist/DeepFistSpectrogram.cpp
third_party/deepfist/DeepFistCtc.cpp
third_party/deepfist/DeepFistModel.cpp)
third_party/deepfist/DeepFistModel.cpp
src/core/DeepCwEngine.cpp src/core/DeepCwEngine.h
src/core/DeepCwCommitter.cpp src/core/DeepCwCommitter.h
src/models/DeepCwRxBackend.cpp src/models/DeepCwRxBackend.h
src/core/deepfist/DeepFistModelAssets.cpp src/core/deepfist/DeepFistModelAssets.h)
target_include_directories(aethercore PRIVATE "${CMAKE_SOURCE_DIR}/third_party/deepfist")
endif()

if(ORT_FOUND)
target_compile_definitions(aethercore PUBLIC HAVE_ONNX)
# PUBLIC include dir: core/SignalClassifier.h includes onnxruntime under
# HAVE_ONNX (PUBLIC) and is pulled in by gui/MainWindow.h. Same transitive
# leak as FFTW3 above. Library link stays PRIVATE (transitive via the lib).
Expand Down Expand Up @@ -3376,6 +3385,17 @@ endforeach()
# it keep resolving against the repository root. The header of that file explains
# why that matters and why it should not be "tidied up" into a subdirectory.
enable_testing()
# DeepCW offline replay harness (RFC #4817 regression take): not built by default.
# Declared above the tests include: executables declared after it fail configure.
if(ORT_FOUND)
add_executable(deepcw_replay EXCLUDE_FROM_ALL
tools/deepcw_replay.cpp src/core/DeepCwEngine.cpp src/core/DeepCwCommitter.cpp
src/core/Resampler.cpp)
target_compile_definitions(deepcw_replay PRIVATE HAVE_ONNX)
target_include_directories(deepcw_replay PRIVATE src
${CMAKE_SOURCE_DIR}/third_party/r8brain ${ORT_INCLUDE_DIRS})
target_link_libraries(deepcw_replay PRIVATE Qt6::Widgets ${ORT_LIBRARIES})
endif()
include(tests/tests.cmake)


Expand Down
28 changes: 26 additions & 2 deletions THIRD_PARTY_LICENSES
Original file line number Diff line number Diff line change
Expand Up @@ -621,8 +621,8 @@ report PortAudio/portaudio#1176.
Source: https://github.com/PortAudio/portaudio (tag v19.7.0, sha256
5af29ba58bbdbb7bbcefaaecc77ec8fc413f0db6f4c4e286c40c3e1b83174fa0)

DeepFist native helpers (experimental)
--------------------------------------
DeepFist native helpers
-----------------------
MIT License

Copyright (c) 2026 Brent Crier
Expand Down Expand Up @@ -726,3 +726,27 @@ tools/docs/pdf/fonts/OFL.txt.
Copyright: Copyright 2022 The Noto Project Authors
License: SIL Open Font License, Version 1.1
Source: https://github.com/notofonts/symbols


29. DeepCW — Neural CW Decoder (ported code in-binary + model download-on-demand)
---------------------------------------------------------------------------------
The optional neural CW decode backend (DeepCwEngine, RFC #4817). Unlike the
download-only ASR entries above, this has TWO distinct AGPL-3.0 obligations:

a) Ported code (compiled into the binary). src/core/DeepCwEngine.{h,cpp} is a
C++ port of decode_morse.py from e04/deepcw-engine — the spectrogram build
(256-pt FFT, hop 48, periodic Hann, 400-1200 Hz -> 65 bins, log1p) and the
greedy-CTC decode. A faithful port of AGPL-3.0 source is a derivative of it,
so this code ships AGPL-3.0 in every binary regardless of whether a user
ever downloads the weights. AetherSDR is GPL-3.0-or-later; GPLv3 §13
permits the combination (its only added obligation, AGPL's network clause,
affects hosting the combined work as a network service — desktop use is
unaffected, and AetherSDR source is already public).

b) Model weights (NOT bundled). Downloaded on first enable of the DeepCW
backend (source: the e04/deepcw-engine repository), SHA-256-verified,
cached under the user's data dir. Run by ONNX Runtime (entry 11).

Attribution: "deepcw-engine" by e04.
License: AGPL-3.0-or-later (both the ported code and the model)
Source: https://github.com/e04/deepcw-engine
35 changes: 13 additions & 22 deletions docs/deepfist-cw-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
This implementation supplies a shared `CwRxModel` receive-backend interface and
an optional DeepFist backend. ggmorse remains the default receive decoder and
continues to decode transmit sidetone. Only the selected receive backend runs.
The RFC is #4817; DeepCW remains outside this change. The operator authorized
preparing this PR before DeepCW, overriding the earlier implementation order.
The RFC is #4817. DeepCW is the third backend, behind the same interface.

DeepFist consumes selected-slice pre-monitor PCM through `PcmFrame`, resamples
on its worker, and uses the pinned native Lyra frontend. Source changes, discontinuities,
Expand All @@ -25,26 +24,18 @@ DeepFist output does not feed automatic callsign spotting. Other monitored
slices and speaker gain/mute do not alter the selected decoder input. This is
an audio slice tap, not an RF separation claim.

## Distribution prerequisite
## Distribution

**Not ready for general release:** no upstream standalone download directory
has been established for this exact model bundle. As checked during PR
preparation, n9bc/DeepFist publishes no release assets or committed weights;
Lyra's releases expose Windows installers, not the three standalone files.
The application must not download or execute an installer to obtain a model.

`ENABLE_DEEPFIST_EXPERIMENT` is therefore default OFF and requires ONNX Runtime
when enabled. `DEEPFIST_MODEL_BASE_URL` remains empty. A missing model produces
an unavailable status, not a request to an invented endpoint. A developer can
point `AETHER_DEEPFIST_MODEL_DIR` at the exact verified bundle for qualification.
This override is not the intended end-user installation workflow.

Before enabling the feature in released builds, the upstream author must
publish the exact assets at a versioned HTTPS directory and confirm model
redistribution provenance. Then configure that directory, exercise the real
public download, and test fresh-cache, cancellation, retry and offline reuse
on each supported platform. The RFC's upstream-only hosting decision remains
in effect; this change does not publish an AetherSDR mirror.
DeepFist builds whenever ONNX Runtime is found (Apple Silicon macOS, Linux and
Windows); there is no separate option. The model is never bundled with the
application. It downloads from N9BC's versioned `exp27_bt-champion` release
(`https://github.com/n9bc/DeepFist/releases/download/exp27_bt-champion/`), whose
`deepfist.onnx` and `deepfist.onnx.json` match the pinned lengths and hashes
below; a non-empty `DEEPFIST_MODEL_BASE_URL` replaces that directory. That release publishes no LICENSE; the manifest's LICENSE asset carries
its own source, the same pinned bytes from the DeepFist repository's first
commit (`061fc1d7`). The RFC's upstream-only hosting decision remains in effect;
there is no AetherSDR mirror. A developer can point `AETHER_DEEPFIST_MODEL_DIR`
at a local verified bundle instead.

The downloader verifies exact lengths and SHA-256 hashes, takes a cache lock,
uses atomic file replacement, and checks the complete bundle before loading.
Expand Down Expand Up @@ -72,7 +63,7 @@ is disabled. The committer and injected model-assets tests also run in the
default build, without ONNX Runtime or weights. Optional worker tests use
injected PCM and download replies. Real inference tests require the pinned local
bundle and return skip code 77 when absent; a skipped test is not model proof.
Tests behind the default-OFF option do not run in the default CI graph.
Tests that need ONNX Runtime do not run in the default CI graph, which installs none.

Synthetic replay has shown useful gains from normalization and emission
protection. Those measurements are not an accuracy estimate for arbitrary
Expand Down
35 changes: 31 additions & 4 deletions docs/user/docs/cw-decoder.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "CW Decoder"
slug: "/cw-decoder"
description: "AetherSDR includes a built-in CW (Morse code) decoder powered by ggmorse (MIT license)."
description: "AetherSDR includes a built-in CW (Morse code) decoder: ggmorse by default, with two optional neural decoders, DeepFist and DeepCW."
status: "Supported"
applies_to: ["FlexRadio", "Hermes-Lite 2 (experimental)", "Networked Icom (early; IC-7300MK2 supported)"]
---
Expand All @@ -14,6 +14,8 @@ applies_to: ["FlexRadio", "Hermes-Lite 2 (experimental)", "Networked Icom (early

AetherSDR includes a built-in CW (Morse code) decoder powered by [ggmorse](https://github.com/ggerganov/ggmorse) (MIT license). It automatically detects the CW tone pitch and keying speed, and displays decoded text in real time in a panel below the waterfall. It can decode what you receive, what you send, or both.

On Apple Silicon Macs, Linux and Windows you can also choose one of two neural decoders, **DeepFist** or **DeepCW**, which can copy weak or irregular signals that ggmorse misses. See [Choosing a decoder](#choosing-a-decoder).

The decoder works on every radio family that has a CW mode: FlexRadio, the [Hermes-Lite 2](./hermes-lite-2.md), and [Networked Icom](./networked-icom.md) radios (it opens when an Icom slice is in CW).

## Requirements
Expand Down Expand Up @@ -46,6 +48,29 @@ Drag the grip on the panel's edge to resize it; the height is remembered. Right-

*The CW decoder pane under the waterfall, with its sensitivity, pitch and speed controls.*

### Choosing a decoder

The decoder selector in the panel's toolbar picks which decoder reads the received CW. Only the selected decoder runs. Your own sending (the **TX** toggle) is always decoded by ggmorse.

| Decoder | Text appears | Typical use | Model |
|---|---|---|---|
| **ggmorse** (default) | About half a second after each character | Clean, steady signals | None; built in |
| **DeepFist** | About 2 seconds behind the signal | Weak and hand-sent CW | Downloaded on first use, about 13 MB |
| **DeepCW** | About 6–8 seconds behind the signal, a few characters at a time; it holds text back so later audio can correct it | Weak signals | Downloaded on first use, about 15 MB |

No decoder is best on every signal. Try each on the signals you work.

The neural decoders' models are not part of the AetherSDR download. The first time you select one, AetherSDR downloads its model from the model's own published source and checks it before use; the status beside the selector shows the progress. **Cancel** stops a download, and **Retry** appears if it fails. Once downloaded, the model is kept and works offline. If a model cannot be downloaded, the decoder shows **Model unavailable** and you can switch back to ggmorse.

With a neural decoder selected:

- **Sens**, the lock buttons and the Pitch and WPM ranges are unavailable; they apply to ggmorse only.
- Characters are coloured by the model's own confidence, on the same green-to-red scale, and are never hidden.
- Callsign contact cards and the MQTT text stream come from ggmorse only.
- AetherSDR's **Zero Beat** button is unavailable; it needs ggmorse's pitch estimate. A FlexRadio's own **Autotune** (on radios with SmartSDR+) works with any decoder.

The neural decoders are not available on Intel Macs; there the selector does not appear and ggmorse decodes as before.

### How it works

The decoder processes the received audio on a separate worker thread. It detects the dominant tone frequency, then uses a Goertzel filter and timing analysis to decode Morse characters.
Expand Down Expand Up @@ -73,7 +98,7 @@ When the decoder copies a station identifying itself (`DE <call>`), and QRZ.com

### MQTT

Decoded text is published to the MQTT topic `aethersdr/cw/decode`, one JSON message per character, with `"rx": false` marking text from the TX decoder. See [MQTT Station Automation](./mqtt-station-automation.md).
Decoded text is published to the MQTT topic `aethersdr/cw/decode`, one JSON message per character, with `"rx": false` marking text from the TX decoder. Received text is published only while ggmorse is the selected decoder. See [MQTT Station Automation](./mqtt-station-automation.md).

### Tips for best results

Expand All @@ -91,7 +116,8 @@ Decoded text is published to the MQTT topic `aethersdr/cw/decode`, one JSON mess

| Control | What it does |
|---|---|
| **Stats** | Detected pitch (Hz) and speed (WPM) |
| **Decoder** | ggmorse, DeepFist or DeepCW (Apple Silicon, Linux and Windows); see [Choosing a decoder](#choosing-a-decoder) |
| **Stats** | Detected pitch (Hz) and speed (WPM); with a neural decoder, its status |
| **Sens** | 0–100 (default 30). Hides low-confidence characters: 0 shows everything, higher values show only confident decodes. |
| **🔒P** | Lock the decoder's pitch at the current value |
| **🔒S** | Lock the decoder's speed at the current WPM |
Expand All @@ -116,7 +142,8 @@ Colours are based on ggmorse's cost function:
### Technical details

- Library: [ggmorse](https://github.com/ggerganov/ggmorse) by Georgi Gerganov (MIT license)
- Bundled directly — no external dependency
- Neural decoders: DeepFist ([n9bc/DeepFist](https://github.com/n9bc/DeepFist)) and DeepCW ([e04/deepcw-engine](https://github.com/e04/deepcw-engine), AGPL-3.0); both run with ONNX Runtime, and their models are downloaded, not bundled
- ggmorse is built in — no external dependency
- Runs on a dedicated worker thread — does not block audio playback
- CPU usage: negligible

Expand Down
Loading
Loading