Commit Graph

6577 Commits (f4d5efccfe884c6bcc379586467650ad659eeaac)

Author SHA1 Message Date
Mooneer Salem f4d5efccfe latest user manual PDF 2026-09-14 17:26:58 +00:00
Mooneer Salem f0cb2a3996
Upgrade Windows CI to llvm-mingw 20260908 (#1489)
* Upgrade Windows CI to llvm-mingw 20260908

Bumps the llvm-mingw release used by the Windows CI/CD build, sanitizer,
and PGO jobs from 20251216 to 20260908.

Also fixes two latent bugs surfaced while verifying the upgrade by
cross-compiling locally with the macOS build of llvm-mingw:

- plot_waterfall.cpp used std::atomic without including <atomic>,
  relying on it being pulled in transitively. Newer libc++ no longer
  does so.
- CMakeLists.txt chose the top-level project() LANGUAGES via if(APPLE)
  before any project()/enable_language() call had run, so APPLE
  reflected the host platform rather than CMAKE_SYSTEM_NAME from the
  cross-compile toolchain file. On a macOS host this silently took the
  Apple branch even when cross-compiling for Windows, adding
  -Wl,-ld_classic to the link flags -- a flag ld.lld doesn't understand.
  Windows CI (Linux-hosted) never hit this, but it broke any Windows
  cross-build attempted locally from macOS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk

* Add PR #1489 to changelog.

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 10:24:37 -07:00
Mooneer Salem 2c7aa5ff82
Windows CI: RADE loss testing + ASan/UBSan sanitizer coverage (#1487)
* CI (Windows): add RADE loss testing (prerequisite for sanitizer coverage)

Ports the RADE loss test infrastructure from the ms-rade-v2 branch
(as of commit f013450b there) so the Windows sanitizer jobs added in
the following commits have a RADE loss test to actually run:

- test/TestFreeDVRadeLoss.ps1: transmits a known corpus through FreeDV,
  records the result, plays it back through RX, and compares TX/RX RADE
  features via loss.py against a threshold.
- test/RadeVerificationReport.ps1: assembles the RADE integration
  verification report from a test run.
- cmake-windows.yml: adds the rade-loss-baseline job (computes the loss
  threshold from a software-only baseline on Linux, since rade_tx_wav/
  rade_rx_wav are excluded from the Windows build), wires RADE loss
  testing and verification-report generation into the `test` job, adds
  crash dump collection (WER LocalDumps) for post-mortem diagnosis, and
  switches audio-endpoint readiness waiting to Wait-AudioDevices.ps1 to
  avoid starting a test before a virtual cable is actually enumerable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk

* Windows CI: add UBSan/ASan sanitizer build+test jobs

llvm-mingw supports UBSan on every architecture it targets and ASan on
x86 only (confirmed against the toolchain's own release assets: only
x86_64/i386 ship a libclang_rt.asan_dynamic-*.dll, no aarch64 variant
exists). Adds build-sanitizer (mirrors build-pgo-inst minus PGO, one
variant per {UBSAN x86_64, UBSAN aarch64, ASAN x86_64}) and
test-sanitizer (mirrors test, pointed at the new artifacts).

No manual DLL copy step needed for ASan: cmake/GetDependencies.cmake.in
already walks freedv.exe's PE import table via objdump and derives the
toolchain's <arch>-w64-mingw32/bin/ directory as a search path when
FREEDV_USING_LLVM_MINGW is set -- the same mechanism that bundles every
other DLL dependency already picks up libclang_rt.asan_dynamic-*.dll
automatically once ENABLE_ASAN is on. This path was already prepared
for in a prior commit (73cd6f49, "Fix cross-compile definitions to
allow asan to be used in the first place") but never wired into CI.

Sanitizer test steps are continue-on-error, matching the existing
SANITIZERS_ENABLED leniency on macOS/Linux (ctest doesn't enforce the
PASS_REGULAR_EXPRESSION under sanitizers there either) -- a sanitizer
build is slower and less reliable for real-time audio, so what matters
here is whether a sanitizer catches a genuine memory-safety/UB bug
(an abort with a diagnostic in the log), not the loss threshold.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk

* Fix Windows sanitizer options: drive-letter colon breaks the parser

Every test-sanitizer run was failing at freedv.exe startup with
"AddressSanitizer: ERROR: expected '=' in ASAN_OPTIONS" (and the UBSan
equivalent), masked as job "success" by continue-on-error -- meaning
zero actual testing happened in the previous push.

ASAN_OPTIONS/UBSAN_OPTIONS are colon-separated key=value pairs. The
absolute path used for suppressions= started with a Windows drive
letter (D:\...), and the sanitizer's own option parser split on that
colon too: "suppressions=D" parsed as one pair, then "\...\foo.txt"
(no '=') aborted the parse before FreeDV ever started.

Fix: copy the suppression files next to freedv.exe (alongside the
existing test script copies) and reference them by bare relative
filename, which contains no colon to collide with.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk

* Suppress known wxWidgets MSW UBSan finding (null pointer in tooltip.cpp)

wxToolInfo's constructor does a member access through a null pointer of
type TTTOOLINFOW in wx's own MSW backend (tooltip.cpp:100), hit
consistently on both new Windows UBSAN test-sanitizer jobs
(windows-2022 and windows-11-arm). Third-party code, not ours -- same
treatment as the existing macOS vptr suppressions for wx's own latent
UB.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk

* Suppress libressl UBSan finding; make Windows sanitizer jobs actually fail

test/ubsan_suppressions.txt: add function:crypto/stack/stack.c for
"call to function local_sk_X509_NAME_ENTRY_pop_free through pointer to
incorrect function type" -- a well-known, benign pattern in OpenSSL/
LibreSSL's type-erased sk_TYPE_pop_free callback casting, not a real bug.

cmake-windows.yml: the three test-sanitizer test steps are
continue-on-error (needed so an ordinary loss-threshold miss under
instrumentation overhead doesn't stop the other tests, matching the
SANITIZERS_ENABLED leniency ctest already gives sanitizer builds on
macOS/Linux) -- but continue-on-error suppresses the job's conclusion,
not just the step's outcome, so a genuine sanitizer abort was *also*
being swallowed into job "success" with no fix. Added a "Check for
sanitizer errors" step (no continue-on-error) that scans every test's
captured output for an actual AddressSanitizer/UndefinedBehaviorSanitizer/
LeakSanitizer report and fails the job for real if one is found, while
a plain "Test failed" from a loss-threshold miss (no sanitizer report
in the output) still doesn't. Also wired Tee-Object logging into the
FullDuplex and Reporting test steps (RadeLoss already had it) so all
three are covered by the scan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk

* CI (Windows): drop RadeVerificationReport, fix missing hamlibserver.py

Remove RadeVerificationReport.ps1 and the workflow steps that generate/
upload the RADE verification report from the `test` job -- out of scope
for this PR (Windows ASan/UBSan CI + the RADE loss test infra it needs).

Separately: PR CI showed both `test` and `test-sanitizer (ASAN, ...)`
failing the RADE Reporting step with "python.exe: can't open file
'...\bin\hamlibserver.py'". TestFreeDVReporting.ps1 unconditionally
launches hamlibserver.py (a mock rigctld) as a subprocess before
starting FreeDV, but neither job's "Copy test scripts to install
folder" step actually copied it there -- only TestFreeDVReporting.ps1
itself and its conf template were copied. Added the missing Copy-Item
to both jobs.

(The consequent "Couldn't connect to Radio with hamlib" fatal error
also triggered an apparent AddressSanitizer heap-use-after-free in
wxMutexInternal::LockTimeout during the ASan job's abnormal shutdown --
very likely collateral damage from the broken test flow rather than a
real bug, but worth re-checking once this fix lands.)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk

* CI (Windows): actually commit the RadeVerificationReport/hamlib workflow changes

Follow-up to 54aa6988 -- that commit only picked up the file deletion;
the cmake-windows.yml edits (removing the report generation/upload
steps and the RadeVerificationReport.ps1 copy, adding the missing
hamlibserver.py copy to both the test and test-sanitizer jobs) didn't
get staged due to a failed `git add` on the already-removed path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk

* master still uses RADEV1, not V2.

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 10:53:43 -07:00
Mooneer Salem 07a51dc6ca latest user manual PDF 2026-09-13 16:11:14 +00:00
Mooneer Salem 5f39abb700
Real-time audio thread timing: histogram logging, wake-up debt compensation, high-res Windows waits (#1488)
* TxRxThread: log a processing-time histogram alongside min/max/mean/stdev

min/max/mean/stdev over a run of several thousand frames can't tell a
lone freak outlier apart from a real cluster of slow frames -- a rare
but non-negligible tail gets averaged away by mean/stdev, and only the
single worst frame shows up in max. Several Windows CI failures this
session showed catastrophic RADE loss with a completely unremarkable
existing stats line (low single-digit ms max), so the shape of the
distribution is the piece we've been missing.

Buckets by upper bound in ms (0.5/1/2/5/10/20/50/100/200/500/1000/2000,
plus an overflow bucket for >=2000ms), incremented in the same
startTimer_/endTimer_ pair already used for the existing stats, so no
new measurement points. Verified locally: a healthy run already shows
real spread across multiple buckets (not just noise near one value),
so this should give much better signal on the next Windows failure.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk

* TxRxThread: also measure time spent waiting between wake cycles

Every processing-time stat we had only wrapped pipeline_->execute() --
none of it covered the helper->stopRealTimeWork() semaphore wait between
wake cycles in Entry()'s outer loop. That's a real blind spot: a
scheduling delay (the thread taking longer than expected to actually be
resumed) would show up as extra time spent *waiting*, not as extra time
spent *processing*, and every failure investigated this session showed
clean processing stats -- but we had no way to tell if the wait side
looked clean too, since it was never measured at all.

Refactored the ad hoc min/max/mean/stdev/histogram fields and functions
into a reusable TimingStats struct (was about to duplicate all of it a
second time for the wait measurement, which is a clear sign it wanted
to be one thing parametrized by a label rather than two copies). Now
tracks two independent instances: processingStats_ (unchanged
behavior, just moved) and waitStats_ (new -- measures from right before
stopRealTimeWork() to the top of the next loop iteration).

Verified locally: the wait histogram already shows a real, explainable
signal -- most wait cycles on this run cluster right at ~10ms, which
is exactly MacAudioDevice::stopRealTimeWork()'s fastMode timeout
((1000*1024/48000)>>1 = 10 via integer truncation), i.e. the semaphore
is timing out rather than being signaled early most of the time. That's
expected/healthy behavior, and now we have a baseline to compare a
failing run's wait distribution against -- specifically, whether wait
times ever blow past that ~10-21ms designed bound, which would point at
genuine OS-level scheduling starvation rather than slow processing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk

* Fix and extend real-time thread wake-up debt compensation across platforms

WASAPIAudioDevice::stopRealTimeWork() computed extraTimeMs_ (how much
last cycle's total processing+wait overran its nominal period) and
subtracted it from this cycle's target wait duration -- but then passed
the *uncompensated* nominal period to WaitForSingleObject() instead of
the value it had just computed. So the compensation only ever worked in
the extreme case (this cycle's debt already exceeds a full period, skip
the wait entirely); the common case of a small overrun did nothing at
all, silently. Fixed to actually wait for the compensated duration.

PulseAudioDevice's equivalent logic was already correct for comparison
(it builds the actual wait deadline from the compensated value).

MacAudioDevice had no such compensation at all -- stopRealTimeWork()
always waited the full nominal period regardless of how long processing
took, meaning any nonzero processing time made the average loop period
longer than intended every single cycle, drifting later relative to
real time under load instead of self-correcting like the other two
platforms already did. Ported the same pattern (record start time in
startRealTimeWork(), track extraTimeMs_ debt, shave it off the next
wait, floor at zero) to bring it to parity.

Verified locally on macOS: rade_loss still passes (loss 0.082), wait
times remain correctly bounded within the designed timeout window.
Can't locally verify the WASAPI fix (no Windows dev environment here);
next CI round will confirm.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk

* WASAPIAudioDevice: use a high-resolution waitable timer for sub-ms wait precision

WaitForSingleObject's timeout is a DWORD of whole milliseconds, which forced
the wake-up debt compensation added previously to round its compensated wait
duration down to the nearest millisecond before ever calling it -- discarding
sub-millisecond precision on every cycle.

Replace it with WaitForMultipleObjects() on the existing semaphore_ (for early
wake on new audio) plus a new high-resolution waitable timer (CreateWaitableTimerEx
with CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, falling back to a regular waitable
timer on pre-1803 Windows) armed via SetWaitableTimer with a 100ns-resolution
due time. extraTimeMs_ becomes extraTimeHns_ (100ns units) so the debt
bookkeeping itself no longer loses precision to millisecond rounding, not just
the final wait call.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk

* WASAPIAudioDevice: anchor wait compensation to startTime_, not last cycle's debt

The previous compensation shortened this cycle's wait by a debt figure copied
from the *previous* cycle's total overrun -- a one-cycle lag. Under steady
processing time that converges to the same average as compensating directly,
but when processing time is bursty (one long cycle, one short) it over/under-
corrects: it fully discounts the next wait for a spike that cycle may not
actually have, producing a long-then-short cadence oscillation instead of
absorbing the spike within its own cycle.

Compute the compensation directly against startTime_ (recorded by
startRealTimeWork() right before processing begins) instead: elapsed time
since startTime_ is this cycle's own already-known processing cost, so it
can be subtracted immediately with zero lag. waitOvershootHns_ replaces
extraTimeHns_ and narrows to tracking only the wait call itself overshooting
its requested duration -- the one component that genuinely can't be known
until after it happens -- so a systematic scheduling overshoot still can't
silently accumulate into long-term drift.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk

* Add PR #1488 to changelog.

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 09:09:03 -07:00
Mooneer Salem ae1b7d4435 latest user manual PDF 2026-09-09 15:43:36 +00:00
Mooneer Salem da2fb5b0af
Remove reliable text lock from audio threads. (#1486) 2026-09-09 08:40:24 -07:00
Mooneer Salem c97b73c754
test: fix intermittent rade_reporting_awgn CI failure (#1484)
* test: back off rade_reporting_awgn channel noise by 3 dB

rade_reporting_awgn adds AWGN with `ch --No -18` and passes only if RADE
decodes the callsign and FreeDV logs "Reporting callsign ZZ0ZZZ @ SNR".
`ch` fixes the noise density, not the SNR, its AWGN realisation is random
per run, and its input (test.wav) is a live virtual-cable capture whose
level varies run to run. On CI this landed the decode at ~5-6 dB SNR --
right on the RADE decode cliff -- so the test fails intermittently on the
non-sanitizer Linux legs (usually, but not always, absorbed by
`ctest --repeat until-pass:2`).

Move the AWGN case to `--No -21`: ~3 dB of headroom above the cliff,
still a genuinely noisy channel. mpp (`--No -25`, `-txattempts 7`) is
left alone -- it has ample decode retries and isn't flaking.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AYJXLpaEyPTsegX93QzE1a

* Increase noise slightly.

* Oops, accidentally deleted the - sign.

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 07:30:36 -07:00
Mooneer Salem 1665b53ef7
CI (Windows): make collect-pgo-profile wait for real audio endpoints (#1483)
"Generate PGO data" fails intermittently when a VB-Cable / VAC endpoint
isn't enumerable at the moment FreeDV starts a UT: FreeDV pops a fatal
"device cannot be found" message box and aborts.

The old "Start Windows Audio Service" step only polled WMI
Win32_SoundDevice (driver nodes, not the MMDevice endpoints FreeDV
uses), had an operator-precedence bug in its until-condition
((A -and B) -or C), ran once minutes before the test, and never checked
the specific endpoints the job needs.

* ci/Wait-AudioDevices.ps1: restarts AudioEndpointBuilder/audiosrv and
  polls "Get-AudioDevice -List" (the active-endpoint surface FreeDV
  actually enumerates) until every required playback/recording endpoint
  is present, nudging the audio stack again halfway through and dumping
  full diagnostics before failing on timeout.
* ci/Invoke-PgoProfileCollection.ps1: runs the endpoint wait +
  GeneratePGOProfiles.ps1 and retries the whole thing up to 3x, since an
  endpoint can also drop mid-run right after SoX releases the capture
  device. Partial .profraw files are cleared between attempts.
* collect-pgo-profile now calls these; the Generate PGO data timeout
  goes 10 -> 25 min to cover the retries.
* GeneratePGOProfiles.ps1: best-effort wait for the endpoints before
  each of the TX and RX passes.

The identical "Start Windows Audio Service" step in the test job is left
as-is here; this change is scoped to collect-pgo-profile.


Claude-Session: https://claude.ai/code/session_01AYJXLpaEyPTsegX93QzE1a

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 23:57:12 -07:00
Mooneer Salem f3159ca346 latest user manual PDF 2026-09-07 06:02:39 +00:00
Mooneer Salem 4ca76ddf00
Match std::atomic memory ordering to what each atomic actually synchronises (#1482)
* Match memory ordering to what each atomic actually synchronises.

Two related tidy-ups in how std::atomic orderings are specified. Neither
changes behaviour; both make the code say what it means.

The four FIFO debug counters -- g_infifo1_full, g_outfifo1_empty,
g_infifo2_full and g_outfifo2_empty -- used acquire/release throughout. They
publish nothing: they are incremented in the audio callbacks and read only to
display a count, in the options dialog and in the PTT drain poll. No reader
dereferences memory whose visibility depends on them. Release on those
fetch_add()s advertises a happens-before relationship that nothing consumes,
which is misleading to anyone working out what guards what in the real-time
path. Relaxed is what these actually need, at all 18 sites.

Three accesses were taking the default seq_cst by writing the atomic without
.load()/.store():

* TxRxThread's playback-record predicate read g_recFileFromModulator and
  g_sfRecFileFromModulator bare, while the structurally identical predicate a
  hundred lines above spells out acquire on g_playFileToMicIn/g_sfPlayFile.
  These do publish data -- the opened SNDFILE has to be visible to the RX/TX
  thread -- so they stay acquire, just explicitly, and now match their
  neighbour. This one runs on every 20 ms frame.
* m_run was read as `while (m_run)` and written as `m_run = false`.
* ongui.cpp sampled g_outfifo1_empty by implicit conversion.

Deliberately left alone: the SNDFILE*/bool publishing pairs, isModemRunning,
ParallelStep's exitingThread, and the endingTx/g_eoo_enqueued handshake, all of
which order access to other memory; and tuneSineWaveSampleNumber and
numRealTimeWorkers_, which are relaxable in principle but are part of the
callback and workgroup protocols and run twice per callback at most.

No performance claim is intended. On arm64 the affected operations do change
instruction (ldaddl -> ldadd, ldar -> ldapr, stlr -> str), but every one of
them runs at most a few times per 20 ms frame, and no atomic in this codebase
sits inside a per-sample loop -- those were hoisted out already. The value here
is that the ordering now documents the actual synchronisation.

Verified with the rade_loss test (PASS, loss 0.099), including a clean
thread start/stop cycle, which is what the m_run change touches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADMCssMazpW1igtvMHXNWZ

* Add PR #1482 to changelog.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 23:00:20 -07:00
Mooneer Salem 071d233539 latest user manual PDF 2026-09-06 23:31:10 +00:00
Mooneer Salem 97fe8214f9
Reduce waterfall and scalar plot paint cost; fix uninitialised heatmap LUT entry (#1481)
* Cut waterfall and scalar plot paint cost; fix uninitialised heatmap entry.

On macOS wxGraphicsContext::DrawBitmap(wxBitmap) is far more expensive than the
wxGraphicsBitmap overload: wxBitmapRefData::GetImage() caches nothing, so every
call allocates a fresh NSImage and draws through -[NSImage drawInRect:], where
the wxGraphicsBitmap path goes straight to CGContextDrawImage. The waterfall made
one such call per pixel block -- m_imgHeight/dy of them, ~200 on a 600px tall
plot -- on each of its 10 frames a second.

* Waterfall blocks are now held as {wxBitmap, wxGraphicsBitmap} pairs. The
  wxBitmap stays as the StretchBlit target; the renderer-native copy is made once,
  when the block is filled, rather than on every frame it spends scrolling down
  the screen. Measured 1.648 -> 0.453 ms per paint at 700x600.

* PlotScalar composites its plot area the same way. The conversion is per frame
  here since plotArea_ is redrawn each time, but it still beats the NSImage round
  trip: 0.381 -> 0.205 ms per paint at 700x200 on the data-only repaint path that
  "Frm Mic" and friends actually take.

* Waterfall graticule labels are laid out once per resize instead of per frame.
  wxWindowMac::DoGetTextExtent builds and destroys a wxGraphicsContext per call
  and drawGraticule() was making ~25 of them a frame for text that only moves when
  the control is resized. It also measured every one second tick before checking
  whether that tick gets a label, so most of the measuring was discarded.

* Dropped BeginLayer(1.0)/EndLayer from the waterfall, scalar and spectrum plots.
  At opacity 1.0 the transparency layer allocates and composites an offscreen
  buffer to produce exactly what drawing directly produces.

Separately, the heatmap LUT was filled over 0..254 while plotPixelData() clamps
intensity to 255 and reaches it whenever a bin sits at the top of the current
range -- so the hottest pixels took their colour from indeterminate memory on
essentially every frame carrying signal. Rendering a synthetic spectrum offscreen,
1665 pixels in the peak column came out as RGB(15,0,0), RGB(62,0,0) and
RGB(220,0,0) instead of the intended full-scale red.

Verified by driving draw() offscreen and diffing the output against master over
400 frames, enough to fill the waterfall and exercise block recycling roughly 200
times: the performance changes alone are bit-for-bit identical, and the only
pixels the LUT fix moves are the max-intensity ones described above.

test_rade_loss.sh passes with these changes, though it cannot really speak to
them: it exercises no plotting, and on this machine it fails intermittently on
master too (5 pass / 1 fail on master, 4 pass / 3 fail here across interleaved
runs of the two builds).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADMCssMazpW1igtvMHXNWZ

* Add PR #1481 to changelog.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 16:29:03 -07:00
Mooneer Salem a32c54d1f5 latest user manual PDF 2026-09-06 08:59:43 +00:00
Mooneer Salem 9e803577bc
Equalizer: make sure number of output samples is correct. (#1480)
* Equalizer: make sure number of output samples is correct.

There may be a scenario where `EqualizerStep` doesn't properly set the number of output samples. This ensures that we're always setting the number of output samples to a valid value.

* Add PR #1480 to changelog.
2026-09-06 01:57:26 -07:00
Mooneer Salem 48c007d552 latest user manual PDF 2026-09-03 04:50:01 +00:00
Mooneer Salem fe0986d497
Fix compiler error with Hamlib 5.0. (#1477)
* Fix compiler error with Hamlib 5.0.

* Fix typo.

* Bring back old code for Ubuntu 22.04.

* Update CMake to explicitly look for rig_state.h.

* Add PR #1477 to changelog.

* Fix typo.
2026-09-02 21:46:35 -07:00
Mooneer Salem 1f0b6c19c5 latest user manual PDF 2026-09-03 01:16:13 +00:00
Mooneer Salem 1bfb8f2cb7
CI: Use macos-26 for running tests (#1478)
* Remove deprecated macos-14, test only on macos-26 due to GitHub issues.

* Work around dyld error when enabling sanitizers.

* Readd macOS 14 and 15 to tests.

* Add PR #1478 to changelog.
2026-09-02 18:13:56 -07:00
Mooneer Salem 397e55b069
CI: Add Ubuntu 26.04 LTS to tests. (#1479)
* CI: Add Ubuntu 26.04 LTS to tests.

* Fix misspellings due to switch to 26.04.
2026-09-02 16:29:08 -07:00
Barry Jackson 6a1bf2b999
Fix RX/TX level-meter gauge contamination (#1475)
* Fix RX/TX level-meter gauge contamination -- OnTimer's demod-in branch read a stale txState local instead of g_tx

* Update fix to update txState instead.

* Add PR #1475 to changelog.

---------

Co-authored-by: merge-test <test@test.local>
Co-authored-by: Mooneer Salem <mooneer@gmail.com>
2026-08-29 17:24:00 -07:00
Mooneer Salem 4984c7d047 latest user manual PDF 2026-08-27 19:24:23 +00:00
Mooneer Salem 168a6cf3fb
Add logic to prevent FIFO sizes that are too small to allow TX thread to function. (#1474)
* Add logic to prevent FIFO sizes that are too small to allow TX thread to function.

* Add PR #1474 to changelog.
2026-08-27 12:21:02 -07:00
Mooneer Salem 8973e01786 latest user manual PDF 2026-08-26 06:47:14 +00:00
Mooneer Salem 8df2ffd578
Increment version to 2.4.1 to begin development. (#1473)
* Increment version to 2.4.1 to begin development.

* Add changelog section.
2026-08-25 23:43:56 -07:00
Mooneer Salem d4acddf1c5 latest user manual PDF 2026-08-25 04:37:00 +00:00
Mooneer Salem 6eaaebcc05
Release version 2.4.0. (#1471)
* Release version 2.4.0.

* Move old changelog out of user manual.
2026-08-24 21:34:09 -07:00
Barry Jackson fa65961580
Fix waterfall/spectrum filling solid colour during half-duplex TX (#1462)
* Fix waterfall/spectrum filling solid colour during half-duplex TX

The half-duplex-TX "blank the display" path memset() the spectrum
arrays to zero bytes, but this display's dB scale runs 0 (loudest) to
MIN_MAG_DB (quietest) -- zero meant "loudest possible", not silence,
so it painted the waterfall solid instead of blanking it (yellow,
occasionally clamping to red at the top of the colour scale).

Filling with MIN_MAG_DB alone wasn't sufficient either: the
waterfall's colour scale is relative to each row's own peak (peak
minus a fixed 20dB window), so a perfectly flat row -- regardless of
its absolute level -- always reads as "loud relative to itself". This
showed up live as a green-to-red fade as the auto-ranging baseline
(m_max_mag) caught up to the new flat level.

Fixed properly by detecting a row where every bin sits exactly at the
floor (real spectra essentially never do) and rendering it as plain
black directly, bypassing the relative intensity scale and leaving
the auto-ranging baseline untouched, rather than running synthetic
placeholder data through logic designed for real spectra.

Confirmed identical on master and v3.0-dev; tested live against a
real half-duplex TX/RX cycle.

* Add PR #1462 to changelog.

---------

Co-authored-by: Barry Jackson <barjac@mageia.org>
Co-authored-by: Mooneer Salem <mooneer@gmail.com>
2026-08-15 13:21:23 -07:00
Mooneer Salem 6f782682ef latest user manual PDF 2026-08-15 18:26:25 +00:00
Mooneer Salem b0acc0b063
Fix issue preventing TX thread from sleeping. (#1465)
* Fix issue preventing TX thread from sleeping.

* Forgot additional change.

* Try 40ms frames.

* 10ms

* Probably still need to feed zeroes as needed.

* Revert frame duration back to 20ms.

* Add PR #1465 to changelog.
2026-08-15 11:23:44 -07:00
Mooneer Salem 86199f747e latest user manual PDF 2026-08-14 08:54:01 +00:00
Mooneer Salem 3888055eeb
Remove CLIP indicator from main window. (#1461)
* Remove CLIP indicator from main window.

* Add PR #1461 to changelog.

* Remove no longer used TOO_HIGH_LABEL constant.
2026-08-14 01:51:24 -07:00
Mooneer Salem 431191c42c
Windows/macOS: Zero audio on startup. (#1463)
* Windows/macOS: Zero audio on startup.

* Add PR #1463 to changelog.
2026-08-14 01:00:36 -07:00
Mooneer Salem 7cbbd85124 latest user manual PDF 2026-08-13 00:46:29 +00:00
Mooneer Salem 52f1b5daf9
Enable CCache to make CI builds run more quickly. (#1328)
* Enable CCache to make CI builds run more quickly.

* Ensure Hamlib is also part of ccache.

* Try creating symlink.

* Increase ccache max size limit.

* Add ccache for Windows builds.

* Add cache for lint as well.

* Make sure PGO is covered by ccache.

* Dummy change to make sure build is cached.

* macOS: Ensure node 24 is used for ccache

* Windows: use node 24.

* Increase max cache size on macOS to 5G.

* Add Windows debugging for ccache.

* Use CMake compiler launcher to launch ccache.

* Use symlinks for PGO builds.

* Don't cache PGO use steps, we can't afford to with only 10GB available.

* Fix issue causing cache misses.

* Forgot that we need to use single quotes.

* Revert previous whitespace change.

* Enable ccache for Linux builds.

* Fix shell command error.

* Add PR #1328 to changelog.
2026-08-12 17:43:22 -07:00
Barry Jackson c3102f4ac5
Fix FreeDV Reporter column-order corruption and Last TX column width (#1458)
* Fix FreeDV Reporter column-order repair to detect gaps, not just trailing entries

If a persisted freedvReporterColumnOrder value was missing an index
from the middle of its range (not just missing new indices appended
after a NUM_COLS increase), the old repair logic never noticed --
it only ever appended indices above the current max element. A
column permanently missing from the list means the wxDataViewCtrl
never creates it, so any later getColumnForModelColId_() lookup for
that column asserts/crashes (e.g. while sorting on it as new spots
arrive).

Reproduced and fixed against a real corrupted config
(ColumnOrder missing index 12/SNR_COL from the middle, confirmed to
crash on assert `item != nullptr` in getColumnForModelColId_ during
live FreeDV Reporter use) -- also explains a previously-unexplained
symptom of an extra empty column appearing when dragging the
rightmost column divider.

* Fix column-order gap detection, sentinel-column drag corruption, and Last TX width

Brings this branch's earlier column-order fix up to the same final
state as testing surfaced on v3.0-dev, since all three are general
bugs unrelated to the LAST_RX_MODE_COL difference between the two
branches:

- Validate/repair the persisted column order unconditionally rather
  than only when its length looks wrong -- a config value can have
  the right length while still containing an out-of-range/duplicate
  entry and missing a genuine one, which the old size()-only check
  never caught.
- OnColumnReordered's save path assumed the trailing sentinel spacer
  column always stays in the last visual position; nothing enforces
  that, so a drag could leave it captured as if it were one of the
  real NUM_COLS columns, silently dropping a real one and persisting
  the sentinel's invalid model ID instead -- the actual cause of an
  "empty column" appearing after reordering.
- Keep the Last TX column at least as wide as Last Update's actual
  current width (both use the same date/time format), checked
  periodically via the dialog's existing timer rather than once at
  construction time, since neither column's real autosized width is
  known reliably that early.

Confirmed working live on the desktop against a real corrupted
config with a real radio.

* Minor style tweak.

* Add PR #1458 to changelog.

---------

Co-authored-by: Barry Jackson <barjac@mageia.org>
Co-authored-by: Mooneer Salem <mooneer@gmail.com>
2026-08-11 12:53:52 -07:00
Mooneer Salem 758d03c71b latest user manual PDF 2026-08-10 17:56:29 +00:00
Mooneer Salem dbc770c6fe
Enable PGO for Windows builds. (#1457)
* Enable PGO for Windows builds.

* instrumented prefix wasn't actually being added.

* Port generate_pgo_profiles.sh to PowerShell (thanks Claude).

* Try disabling RX so we can at least debug rest of pipeline.

* Fix profraw path.

* Try enabling RX code again.

* See if we can get some extra debugging.

* Bound process wait.

* Use Reporting sox invocation.

* Redo config file prior to RX run.

* Fix LLVM profile merge error.

* Add PR #1457 to changelog.
2026-08-10 10:53:59 -07:00
Mooneer Salem fc8dec0ffd latest user manual PDF 2026-08-07 23:35:18 +00:00
Mooneer Salem c4881274cf
Enable LTO/PGO for macOS .app build. (#1456)
* Experiment: Enable LTO/PGO for macOS .app build.

* PGO: Fix tab layout to match new encoding.

* Try disabling LTO during instrumented build.

* Revert "Try disabling LTO during instrumented build."

This reverts commit e6b4e1a191.

* Try disabling temporal profiles.

* Need to run profiling on both x86_64 and arm64.

* Disable universal builds for the PGO instrumented build.

* Revert "Try disabling temporal profiles."

This reverts commit 89036a3ed6.

* Add PR #1456 to changelog.

* Fake change to force rebuild.

* Revert "Fake change to force rebuild."

This reverts commit a2a8886377.

* Add Intel checks for .app.

* No torch on Intel anymore.
2026-08-07 16:32:00 -07:00
Mooneer Salem 718c015d66
Prevent PulseAudio/pipewire from quitting during CI tests. (#1455)
* Prevent PulseAudio/pipewire from quitting during CI tests.

* Output tmp.log during test runs.

* Fix typos.
2026-08-05 14:48:45 -07:00
Mooneer Salem e0164750f2 latest user manual PDF 2026-08-05 15:37:16 +00:00
Mooneer Salem 33cea45310
Fix Radio Frequency coloring on dark/light mode transition. (#1453)
* Fix Radio Frequency coloring on dark/light mode transition.

* Fix UBSan warning.

* Add PR #1453 to changelog.
2026-08-05 08:34:41 -07:00
Barry Jackson 9398f589d4
Fix crash and long hang on main window close with an unresponsive rig (#1452)
* Fix crash on repeat main-window close while shutdown is in progress

topFrame_OnClose() unconditionally dereferenced m_reporterDialog, but a
second wxEVT_CLOSE_WINDOW arriving while the async RX/PTT shutdown from
a first close is still running (e.g. during a slow Hamlib rig disconnect
against an unresponsive radio) re-enters the handler after
m_reporterDialog has already been set to null on the first pass --
SIGSEGV in wxWindowBase::GetPosition(). Guard re-entry via terminating_
and null-check m_reporterDialog, matching the pattern already used in
the destructor and setConfiguration_().

* Don't block on rig disconnect against an unresponsive radio when turning modem off

Dropping the last shared_ptr reference to a rig controller runs its
destructor, which blocks until the rig actually finishes disconnecting.
Against an unresponsive radio (e.g. powered off, connected via rigctld)
this can take far longer than Hamlib's own client-side timeout/retry
settings suggest, since those don't bound however long rigctld itself
waits on the physical radio -- observed over a minute with no feedback
to the user. Move the last reference onto a detached thread for both
rig controllers so that wait can't hold up turning the modem off or
app shutdown.

* Bound the wait for rig-disconnect threads during app-close shutdown

The detached rig PTT/frequency controller disconnect threads added in
the previous commit have no upper bound on app-close: MainFrame::Destroy()
fires right after performFreeDVOff_() returns, with nothing joining those
threads, so an unresponsive rig still mid-disconnect when the process
actually exits gets its thread killed outright -- e.g. a queued ptt(false)
might never reach the radio. Have each detached thread signal completion
via a future, and on the terminating path wait up to 3s (shared deadline
across both) before proceeding to Destroy() -- long enough for a merely
slow rig to finish cleanly, still bounded so a truly unresponsive one
can't hang shutdown.

Also expand the terminating_ guard comment in topFrame_OnClose() to note
it prevents more than the m_reporterDialog crash: m_RxRunning stays true
until deep inside the same async shutdown, so a repeat close request
could otherwise re-enter OnTogBtnOnOff() a second time concurrently with
the shutdown already in progress.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

* Add PR #1452 to changelog.

---------

Co-authored-by: Barry Jackson <barjac@mageia.org>
Co-authored-by: Mooneer Salem <mooneer@gmail.com>
2026-08-04 19:23:04 -07:00
Barry Jackson f1cdc8b262
Fix CMAKE_REQUIRED_DEFINITIONS missing -D prefix in GTK3 wx probe (regression from #1449) (#1450)
* Fix CMAKE_REQUIRED_DEFINITIONS missing -D prefix in GTK3 wx probe (#1449)

wxWidgets_DEFINITIONS is a list of bare macro tokens (correct for the
COMPILE_DEFINITIONS property UsewxWidgets.cmake sets it via), but
CMAKE_REQUIRED_DEFINITIONS requires full "-DFOO" strings. Without the
prefix, check_cxx_symbol_exists()'s probe compile passes each bare
token as a raw command-line argument, which the compiler treats as a
(nonexistent) input filename and errors out on -- silently failing the
whole check closed (WX_BUILT_FOR_GTK3 always false) even against a
genuinely GTK3-built wxWidgets.

With HAS_GTK3 then never defined, topFrame.cpp's GTK3 guard takes the
non-GTK3 fallback branch and creates a stray 1x1 reference wxWindow
directly on the main frame -- disabling wx's "auto-resize sole child
to fill the frame" behaviour and freezing the entire main window's
layout on resize.

Verified on a real GTK3 wxWidgets 3.3.1 build (Mageia 10): reproduced
the false negative on a completely fresh configure (no stale cache
involved), then confirmed the fix resolves it on the first try.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Add PR #1450 to changelog.

* Revert accidental commit.

---------

Co-authored-by: Barry Jackson <barjac@mageia.org>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Mooneer Salem <mooneer@gmail.com>
2026-08-02 15:25:48 -07:00
Barry Jackson 68043af8a6
Fix heard-stations callsign combo stuck-highlight, right-click behaviour, and a stale-index crash (#1448)
* Fix stale selection index crash in wxListViewComboPopup

OnMouseMove() deselected the previously hot-tracked row (m_value)
without checking it was still a valid index, so any external
DeleteAllItems() on the underlying list left a stale index that
crashed the next mouse-move over the popup.

* Fix heard-stations callsign combo stuck-highlight and right-click behaviour

OnCloseCallsignList/OnRightClickCallsignList moved keyboard focus away
from the read-only combo synchronously, but the popup window overlaps
the combo's own value area while open and GTK doesn't always repaint
that region on dismiss, leaving it showing a stale "focused/selected"
highlight. Defer the focus change via CallAfter and force a repaint
once the popup's dismissal has fully finished.

Also stop OnRightClickCallsignList from clearing the displayed
callsign text on deselect -- it should only remove the selection,
consistent with how right-click deselect works in the FreeDV Reporter
list.

* Add PR #1448 to changelog.

---------

Co-authored-by: Barry Jackson <barjac@mageia.org>
Co-authored-by: Mooneer Salem <mooneer@gmail.com>
2026-08-01 22:25:40 -07:00
Mooneer Salem 78910b53b8 latest user manual PDF 2026-08-02 02:03:19 +00:00
Mooneer Salem af3fffa131
Ensure wxWidgets actually uses GTK3 before enabling workarounds. (#1449)
* Ensure wxWidgets actually uses GTK3 before enabling workarounds.

* Add PR #1449 to changelog.
2026-08-01 19:00:09 -07:00
Mooneer Salem 91b1c85805 latest user manual PDF 2026-07-30 21:48:21 +00:00
Mooneer Salem 25f4419aad
Unconditionally add new station to Heard Station list if first heard. (#1444)
* Unconditionally add new station to Heard Station list if first heard.

* Add check for non-zero number of items.

* Add PR #1444 to changelog.
2026-07-30 14:40:57 -07:00