commit 228cd5393451aa211b305f5e2f99b887c9bb3cb2 Author: Mooneer Salem Date: Wed Sep 30 22:47:02 2026 -0700 CI diag: A/B freedv-backend TapStep threaded vs. inline. cmake/freedv-backend-inline-tap.patch makes TapStep run the tapped step inline on the pipeline thread, still in exact 10 ms chunks, instead of on its own Utility-QoS thread fed through a FIFO: every tapped step (RecordStep, which has its own file I/O thread, LinkStep input, the plot resamplers, ComputeRfSpectrumStep) is non-blocking and costs tens of microseconds per 10 ms. It's applied when FREEDV_BACKEND_PATCH is set (by BuildFreeDVBackend.cmake), for the matrix's inline jobs. FreeDV now runs under /usr/bin/time -l, and each round records the TX and RX runs' CPU time and voluntary/involuntary context switches. BlackHole latency is back to 0. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit 591a6e5f2e6713fed55d66d50a5179e208e6a2c1 Author: Mooneer Salem Date: Wed Sep 30 20:42:29 2026 -0700 CI diag: rerun BlackHole latency 1024 vs 0 without thread-state recording. Add a RECORD_THREAD_STATE switch (off): the traces already showed the stalls' cause, and xctrace was what hung in earlier runs. Checks whether latency 1024's higher failure rate on macOS 26 holds without it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit b7cbb312a7a8ba3c96b5613e0e3228c8dc7d47dd Author: Mooneer Salem Date: Wed Sep 30 18:14:19 2026 -0700 CI diag: bound every wait in a round, not just the test. The watchdog only covered test_rade_loss.sh, but a round also waits, unbounded, for audioprobe to exit after SIGTERM (it calls AudioDeviceStop) and for xctrace to save its trace; a job hung again with the watchdog in place. Bound those (30 s and 600 s) with the same capture-and-fail path, and the per-round log show (180 s, skipped if slow). Check liveness with ps, since kill -0 fails on the root-owned sudo xctrace. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit 270539a5717d2917eb587877af6dd5d42e142996 Author: Mooneer Salem Date: Wed Sep 30 17:02:25 2026 -0700 CI diag: macOS 26 only, fail fast and capture state when a round hangs. Both latency-1024 macOS 26 jobs in the last run hung partway through a round until the job timeout, leaving no logs. Run the test under a 720 s watchdog; on timeout, record the process tree, sample the test's processes, coreaudiod and xctrace, take a spindump and the audio logs, then fail the job. Upload artifacts even when the job fails or is cancelled. Three instances each of latency 0 and 1024. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit e8f586ed2b1cb31ab2ab2f75ef498da44a2c2b14 Author: Mooneer Salem Date: Wed Sep 30 13:11:55 2026 -0700 CI diag: compare BlackHole kLatency_Frame_Size 1024 against 0. The thread-state traces show each failure is a 14-40 ms vCPU stall that leaves a BlackHole1 IO cycle more than one buffer late, beyond the margin of BlackHole's write-side overload check (one buffer + kLatency_Frame_Size, which is 0). Run both in the same workflow run to rule out run-to-run variance. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit 885b458b1c27bb9c1d4a11d300b424759dc57dce Author: Mooneer Salem Date: Wed Sep 30 10:01:14 2026 -0700 CI diag: keep thread-state traces only for failed rounds, record down to 5 GB free. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit dd200a1e534221115c9c07d4c2222c326dfc8096 Author: Mooneer Salem Date: Wed Sep 30 07:53:48 2026 -0700 CI experiment: pick the thread state instrument by name on each runner. The runners have xctrace 15.3-16.0, which don't have "Thread Activity" (the name in newer Xcode), so nothing was recorded. Use "Thread State Trace" there, add "System Load" only if it exists, fall back to the System Trace template if neither exists, and log the instrument list. Skip recording if the Instruments setup step failed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit 94ecdeed60e1de3563cc86e185dcd71ae8d1b73d Author: Mooneer Salem Date: Tue Sep 29 23:43:29 2026 -0700 CI experiment: record thread states instead of Time Profiler; drop ps sampler. Every audio stall so far lined up with a window in which the Time Profiler recorded no samples on any of the runner's 3 cores. To tell whether threads were runnable but not running then (the VM not being scheduled) or simply idle, record Instruments' Thread Activity and System Load instruments (the thread state part of System Trace) instead. These traces are much larger, so record only the first 100 s of each round (the TX transmission, where the capture gaps occur), skip recording with less than 20 GB free, and keep traces only for failed rounds plus the first passing round of instance-1 jobs. Also turn off the per-process ps sampler: its ps calls averaged a third of a core on these 3-core runners, more than FreeDV itself. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit b94dcb22d43b511df2e72ebf5e4013abce0d039c Author: Mooneer Salem Date: Tue Sep 29 21:44:52 2026 -0700 CI experiment: record each rade_loss round with Instruments. Record every round with Instruments' Time Profiler across all processes, to see what was running on the machine when the audio stalled. Traces are kept (compressed) for failed rounds, plus the first passing round of each job for comparison. clock_ref.txt maps the trace's wall clock start to host time, for lining samples up with the TIMINGDIAG stall lines. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit 8854d81f78cc9749f8de88de821c98c14284f518 Author: Mooneer Salem Date: Tue Sep 29 21:41:51 2026 -0700 Revert "macOS: rasterize PlotScalar's plot area on the CPU." This reverts commit 26131bd13c52d9b77cb8c02c7e35af69493f9cb0. It only helps when Core Animation composites on the CPU, as it does with the screen locked. With the screen unlocked, Core Animation renders on the GPU, where the original vectors are cheap and uploading a new 3.5 MB image every frame is not: the GUI's total CPU while transmitting (main thread plus Core Animation's render threads) was ~12% higher than master in a rade_loss TX pass on an Apple M1 (4,968 -> 5,587 ms, median of 2-3 runs). It also didn't change the macOS CI rade_loss failure rate. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit 679c43ad6314905af54451f4bb213de2528ca169 Merge: 6760a834 821855f4 Author: Mooneer Salem Date: Tue Sep 29 17:20:09 2026 -0700 Merge ms-waterfall-canvas (PR #1527) into ms-ci-rade-loss-diag. To see whether the GUI thread load reductions in #1527 (and #1524, which it builds on) change the macOS CI rade_loss failure rate. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit 821855f4a1f6d32a3e4394fd9689902a2353a9d6 Author: Mooneer Salem Date: Tue Sep 29 17:14:31 2026 -0700 FreeDV Reporter: fix Linux build (macOS-only refit code). The per-column refit path was in the non-Windows branch, which also compiles on Linux, where refitAll and refitChangedColumns_() don't exist. Limit it to macOS and keep Linux's original setColumnAutosize_(true). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit bc487c8bd0090d6f02235707e64aade95a74cb3f Author: Mooneer Salem Date: Tue Sep 29 15:24:13 2026 -0700 FreeDV Reporter: reduce GUI thread work per server update. - Refit only the columns whose widest text changed, rather than every column (macOS). A column's fitted width only depends on its widest text and its header; across 49 partial refits in a test run, every width matched a full refit. Each refit went from ~19 ms to ~3 ms. - Re-measure only the cells whose text changed when a row updates (macOS and Windows), by caching each cell's text alongside its width. - Compile makeValidTime_()'s regexes once, format SNRs with a cached decimal separator, check the sort column by model column instead of looking columns up, and look up the system colors once per highlight pass instead of per row. fullduplex_RADEV1, 45 s Time Profiler, Apple M1: FreeDV Reporter on the main thread 1034 ms -> 289 ms (traffic from qso.freedv.org varies between runs; ~1800 updates per run in both cases). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit 26131bd10c5f2d39ffa69f48b77a657f875d3c13 Author: Mooneer Salem Date: Tue Sep 29 15:08:25 2026 -0700 macOS: rasterize PlotScalar's plot area on the CPU. While transmitting, the Frm Mic plot repaints ten times a second, and Core Animation replaying its vector drawing (filling the background and the waveform polygon at 2x, and stroking every dash of the gridlines) was about a third of the GUI thread's time. On macOS the background, waveform and gridlines are now rasterized into a pixel buffer at the display's pixel density and drawn as one image in copy mode, in CoreGraphics' own pixel format (see PlotPixelBuffer in plot_osx.mm). The waveform is a vertical span per pixel column; the gridlines' pixels are worked out once per size. Colors are converted once from sRGB to the window's color space. Axis labels are still text. The half-height (SNR) plot, bar graphs and other platforms are unchanged. Compared with CoreGraphics filling and stroking the same paths (unantialiased, 2x), the gridlines are pixel-identical and at most 0.035% of waveform pixels (edge pixels) differ. rade_loss TX pass, Apple M1, main thread: - PlotScalar code + Core Animation replay: 1112 -> 699 ms - main thread excluding FreeDV Reporter: 3826 -> 3140 ms Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit 48934a29178c03a886e5931a9bbe5b0e695aa7e4 Author: Mooneer Salem Date: Tue Sep 29 14:19:01 2026 -0700 macOS: draw the waterfall from a single ring-buffer image. The waterfall kept one image per 0.1 s block (~100 on screen) and drew each of them on every paint. CoreGraphics allocated, cleared and converted a buffer for every block on every frame, which was most of the GUI thread's time while receiving. On macOS the blocks now go into one image, kept in a ring buffer twice its height so the newest rows are always one contiguous run; a new block only writes its own rows and each paint draws a single image that points straight at the buffer. The canvas and graticule bitmaps use opaque host-order premultiplied ARGB, CoreGraphics' own format, and the canvas is drawn in copy mode, so compositing it is a plain memory copy. fullduplex_RADEV1, 45 s Time Profiler, Apple M1, main thread: - total: 5331 ms -> 2912-3495 ms (two runs) - Core Animation commit: 4277 -> 1644 ms - ripc_DrawImage: 2400 -> ~205 ms Other platforms keep drawing the per-block images. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit 82fc25d8a22f731b8cb0f53c1c49a58af0f93918 Author: Mooneer Salem Date: Tue Sep 29 10:19:55 2026 -0700 Suppress clang-analyzer false positive for wxTranslations allocation wxTranslations::Set() takes ownership of the object, but clang-analyzer reports it as a potential leak. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit 14145d01f42fe2c6a950c14dd1cd5400295079a1 Author: Mooneer Salem Date: Tue Sep 29 09:37:40 2026 -0700 Add infrastructure for translating FreeDV into other languages - Add po/ directory with gettext build support: update-pot and update-po targets, compilation of languages listed in po/LINGUAS, and installation into the macOS app bundle or share/locale (Linux/Windows/AppImage). wxWidgets' own catalogs are included when bootstrapping wxWidgets. - Load the freedv catalog at startup based on the system language. - Mark user-visible strings for translation, convert concatenated messages to format strings, and unmark config keys/defaults and other strings that must not be translated. - Compare PTT method by selection index rather than displayed text. - Fix PTT serial port error handlers capturing a local string by reference in CallAfter(). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit 6760a834999dc03a5102b151f9ee70f92278e858 Author: Mooneer Salem Date: Mon Sep 28 21:07:49 2026 -0700 CI experiment: record what the runner was doing during audio stalls. FreeDV and a separate CoreAudio client stall together, and FreeDV's own threads don't explain it. For each round, also record per-process CPU every 200 ms (test/diag/proc_sampler.py, on the same host clock as the stall lines), powermetrics every 250 ms, and coreaudiod's system log. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit e7edab59b32a1f20de9b6e4c3db19f1992ea2e66 Author: Mooneer Salem Date: Mon Sep 28 19:58:20 2026 -0700 CI experiment: log FreeDV Reporter update batches. To check whether bursts of FreeDV Reporter updates line up with the CoreAudio stalls behind the regular build's rade_loss failures, log how many updates each batch handles on the GUI thread and how long it takes, with host times comparable to the stall lines. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit bae2757edeceb31f026dd283e32cb45b30d9d386 Author: Mooneer Salem Date: Mon Sep 28 18:12:14 2026 -0700 CI experiment: log long TX/RX processing iterations. FreeDV's TX and RX threads run under a real-time time-constraint policy (45 ms of every 60 ms), so a long burst from them could delay coreaudiod's and other clients' audio threads on the runners' few cores, which would match every CoreAudio client stalling at once. Log any processing iteration over 20 ms with host times, to line up against the stalls. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit e0ac94a55dc2a1a06feab8dd55fc3274963d212b Author: Mooneer Salem Date: Tue Sep 29 00:11:25 2026 +0000 latest user manual PDF commit 812a232a350f85e9b61075b3b53b8c91430ec6a9 Merge: dea89d33 e4c94adc Author: Mooneer Salem Date: Mon Sep 28 17:10:57 2026 -0700 Merge branch 'v3.0-dev' into ms-rade-v2 commit e4c94adc389e9295aa67be05e4cae26d079d687f Author: Mooneer Salem Date: Mon Sep 28 17:08:47 2026 -0700 Try enabling dark mode support on Windows. (#1518) * Try enabling dark mode support on Windows. * Fix wxWidgets fetch when pinned to a commit hash. GIT_SHALLOW clones with --depth 1, which only fetches branch tips, so checking out an arbitrary commit fails with "fatal: invalid reference". Disable shallow cloning and use the full commit hash. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 * Add PR #1518 to changelog. --------- Co-authored-by: Claude Opus 5.5 commit 6800f633dd5e9d3dfa65cc773ab4394d554612ff Author: Mooneer Salem Date: Mon Sep 28 14:32:53 2026 -0700 Add PR #1518 to changelog. commit 9e2d0ae4437ed18a56236ade2700538c4a5b37e7 Merge: 49c4255d beb27414 Author: Mooneer Salem Date: Mon Sep 28 14:32:25 2026 -0700 Merge branch 'v3.0-dev' into ms-windows-dark-mode commit beb27414960537e15ea1a3536e5805ce83423af7 Merge: 26bd68e5 e3bbecd2 Author: Mooneer Salem Date: Mon Sep 28 14:31:43 2026 -0700 Merge branch 'master' into v3.0-dev commit f73bc8dabb787e3dd67658de8ce817274e9d58ec Author: Mooneer Salem Date: Mon Sep 28 12:25:55 2026 -0700 CI experiment: watch a separate CoreAudio client for the same stalls. FreeDV's output callbacks stall for 70-170 ms while its other threads keep running. Run a tiny separate CoreAudio client (test/diag/audioprobe.c, playing silence to the otherwise unused BlackHole2 2ch 2) during each round and log its own skipped/late cycles on the same host clock. If it stalls at the same moments, the stalls come from coreaudiod or the runner's audio stack rather than from FreeDV's process. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit e3bbecd2e0b26fbb4f41ef2bfa53cd1eddfd66c3 Author: Mooneer Salem Date: Mon Sep 28 11:16:07 2026 -0700 Reduce GUI thread load from plot repaints (macOS layers, cached spectrum graticule) (#1524) * macOS: give each plot its own layer. AppKit was drawing the plots into the notebook's layer. A layer only tracks one dirty rectangle, so each plot's 10 Hz refresh grew it to cover every plot and everything between them, and the notebook and all of its tab strips were fully repainted on every refresh. It also made the scalar plots redraw their axis labels every time, since the whole panel was always marked dirty. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 * PlotSpectrum: draw the graticule from a cached bitmap. The gridlines, ticks and axis labels only change with the control's size, font, colors or magnitude range, but redrawing them on every refresh was most of the spectrum's drawing time (the dashed gridlines especially on GTK/cairo). Render them once into a transparent bitmap and draw that over the spectrum instead. The tuning markers are still drawn live. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 * PlotSpectrum: draw the graticule directly on wxWidgets < 3.2. wxWidgets 3.0 has no GetDPIScaleFactor(), wxBitmap::CreateWithDIPSize() or wxBitmap::UseAlpha(), and without the latter there's no way to get a bitmap with an alpha channel to cache the graticule in. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 * PlotSpectrum: fix the cached graticule's scale on Windows. On a 200% display the graticule was drawn at half size. Window sizes on Windows are already in physical pixels, so the bitmap has to be created with GetContentScaleFactor() (1 there) rather than GetDPIScaleFactor() (2); on macOS and GTK the two are the same. The labels were also half size, because a graphics context on a memory DC renders fonts at 96 DPI on Windows, so scale the font to match the window's text metrics. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 * PlotSpectrum: only scale the graticule font on wxWidgets 3.2+. wxFont::Get/SetFractionalPointSize() don't exist in wxWidgets 3.0, and the scaling is only needed when drawing into the cached bitmap, which older wxWidgets doesn't use. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 * Add PR #1524 to changelog. --------- Co-authored-by: Claude Opus 5.5 commit 1d84340fc0d1994a5ff04a6c354ac4b864f39a66 Author: Mooneer Salem Date: Mon Sep 28 11:15:09 2026 -0700 CI experiment: probe for process-wide pauses alongside audio stalls. Output callback stalls of 85-128 ms line up with the regular build's rade_loss failures, and hit both output devices at once. To tell a process- or machine-wide pause from a stall of only the CoreAudio I/O threads, also log when a plain thread's 10 ms sleep overruns and when the GUI timer goes more than 300 ms between ticks, with host times on the same clock as the audio events. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit b848ff1bb349e38da3e11a3b6336b04e612eee43 Author: Mooneer Salem Date: Mon Sep 28 11:13:19 2026 -0700 [skip ci] Add PR #1524 to changelog. commit f7eb01d75c86d7afe8f2c810b8601e1182a0e0ee Author: Mooneer Salem Date: Mon Sep 28 10:01:09 2026 -0700 PlotSpectrum: only scale the graticule font on wxWidgets 3.2+. wxFont::Get/SetFractionalPointSize() don't exist in wxWidgets 3.0, and the scaling is only needed when drawing into the cached bitmap, which older wxWidgets doesn't use. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit 35a1485f2c553761a368d302580ea7dcca06d90e Author: Mooneer Salem Date: Mon Sep 28 09:54:05 2026 -0700 CI experiment: don't abort a round when its log lacks an expected line. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit c9c6d292e2add2aa751ca875dac7f360f79a1cbd Author: Mooneer Salem Date: Mon Sep 28 09:53:47 2026 -0700 CI experiment: don't abort when a round logs no timing events. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit 8d4444c6eb9f254a23a49da4cf64f91234c1a583 Author: Mooneer Salem Date: Mon Sep 28 09:21:49 2026 -0700 CI experiment: instrument macOS output callback timing. The regular build's rade_loss failures are 2-9 device buffers of silence inserted into the captured transmission while FreeDV's output FIFO never ran dry, i.e. while its output callback apparently wasn't being called. Record cycles where CoreAudio's sample time jumps, the callback comes late or FreeDV's work in it takes long, and processor overloads, and log them from a separate thread. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit b30057f9bedc2f6f1dc81ec6aa035e9d38a5f289 Author: Mooneer Salem Date: Mon Sep 28 09:13:22 2026 -0700 PlotSpectrum: fix the cached graticule's scale on Windows. On a 200% display the graticule was drawn at half size. Window sizes on Windows are already in physical pixels, so the bitmap has to be created with GetContentScaleFactor() (1 there) rather than GetDPIScaleFactor() (2); on macOS and GTK the two are the same. The labels were also half size, because a graphics context on a memory DC renders fonts at 96 DPI on Windows, so scale the font to match the window's text metrics. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit 2e72291b49f9de8750086a248d906b91a3ada785 Author: Mooneer Salem Date: Mon Sep 28 08:08:14 2026 -0700 CI experiment: look for regular-build rade_loss failures instead. TSAN's rade_loss failures were inserted silence (a process-wide stall) and don't matter; the occasional failures of the regular build do. Run only the plain build, 8 rounds, on macOS 14, 15 and 26 (two runners each), and compare each capture against FreeDV's own TX output to tell silence inserted on the way out from samples lost on the way in. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit c8c1692e4fbd226025b0db150091cfcdb696e467 Author: Mooneer Salem Date: Mon Sep 28 02:10:42 2026 -0700 CI experiment: diagnose macOS rade_loss failures. Runs rade_loss 5 times per job on macOS only (plain and TSAN builds, with the BlackHole drivers built for 0 and 128 frames of latency), keeping the captured TX audio and FreeDV's own TX output from each run and measuring how much of the transmission BlackHole zeroed. build_macos_sound_drivers.sh meant to set kLatency_Frame_Size to 128, but the define was outside the quoted preprocessor definitions and BlackHole.c defines it unconditionally, so it stayed 0. It's now set by patching BlackHole.c from BLACKHOLE_LATENCY_FRAMES (default 0, the old behavior). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit 2076d5e0d1e5ac9489dca8a46f458ce64d9e6cbe Author: Mooneer Salem Date: Mon Sep 28 01:56:09 2026 -0700 PlotSpectrum: draw the graticule directly on wxWidgets < 3.2. wxWidgets 3.0 has no GetDPIScaleFactor(), wxBitmap::CreateWithDIPSize() or wxBitmap::UseAlpha(), and without the latter there's no way to get a bitmap with an alpha channel to cache the graticule in. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit 67ae87721ac37801f9086a9a53b3b8abd879ddb9 Author: Mooneer Salem Date: Mon Sep 28 08:00:41 2026 +0000 latest user manual PDF commit f90f6871837fc0e6bc1f17da353a78c7118f7bb2 Merge: da6d361f 20e22a4a Author: Mooneer Salem Date: Mon Sep 28 00:58:25 2026 -0700 Merge remote-tracking branch 'origin/master' into ms-plot-repaint-perf commit da6d361f790fdfc624e2e92c68d69ee0dd7efa6c Author: Mooneer Salem Date: Mon Sep 28 00:58:24 2026 -0700 PlotSpectrum: draw the graticule from a cached bitmap. The gridlines, ticks and axis labels only change with the control's size, font, colors or magnitude range, but redrawing them on every refresh was most of the spectrum's drawing time (the dashed gridlines especially on GTK/cairo). Render them once into a transparent bitmap and draw that over the spectrum instead. The tuning markers are still drawn live. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit 871fab855b04b3f429af75c6c31ef474e99c59f1 Author: Mooneer Salem Date: Mon Sep 28 00:58:24 2026 -0700 macOS: give each plot its own layer. AppKit was drawing the plots into the notebook's layer. A layer only tracks one dirty rectangle, so each plot's 10 Hz refresh grew it to cover every plot and everything between them, and the notebook and all of its tab strips were fully repainted on every refresh. It also made the scalar plots redraw their axis labels every time, since the whole panel was always marked dirty. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit 20e22a4a380e910eee448432856ca72a416b7ee4 Author: Mooneer Salem Date: Mon Sep 28 00:57:41 2026 -0700 PulseAudio: give each device its own connection (#1523) * PulseAudio: give each device its own connection. All of FreeDV's PulseAudio streams shared one main loop and context. On PipeWire (seen with 1.6.2), when a module-loopback connects the sink FreeDV plays radio audio to with the sink whose monitor it records radio audio from -- as the test scripts' loopback does -- pipewire-pulse stopped delivering the recorded audio for seconds to minutes at a time, and RX-from-file decoding ran 2-20x slower than real time. The same happens with a minimal libpulse program and with PortAudio's PulseAudio backend, both of which share one connection, but not when each stream has its own connection. Each PulseAudioDevice now creates its own threaded main loop and context in start() and tears them down in stop(), as OpenAL Soft does. The engine keeps its connection for device enumeration. Connection setup, including raising the main loop thread's priority via rtkit, moves into PulseAudioEngine::CreateConnection()/DestroyConnection(), and a context that fails to connect now returns an error instead of asserting. Audio callbacks now run on one thread per device, as they already do on macOS and Windows; each uses its own FIFO and scratch buffer. Measured on Fedora 44 (RX from file of a 26 s recording): with the loopback, 60-875 s before and 39.4 s after in every run, matching runs without it. generate_pgo_profiles.sh's RX phase went from 100-1038 s to 81 s. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 * PulseAudio: ask rtkit for priority once per connection. The context state callback asked rtkit to raise the main loop thread's priority on every state change (connecting, authorizing, setting name, ready), about three requests per connection. rtkit allows 25 requests per user per 20 s by default, and with a connection per device, starting the modem used 23 of them, so a quick stop/start or another application's request would have been refused and left threads at normal priority. Ask only when the context becomes ready, once per connection. A modem start now makes 9 requests, down from 23 (11 before connections were per-device). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 * PulseAudio: don't request rtkit priority for the engine's connection. The engine's connection is only used for device enumeration, and it's opened up to three times per modem start (startup, validateSoundCardSetup() and startRxStream()). Requesting rtkit priority for it used up rtkit's per-user burst budget for no benefit. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 * PulseAudio: fix unused parameter warning when building without rtkit. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 * [skip ci[ Add PR #1523 to changelog. --------- Co-authored-by: Claude Opus 5.5 commit 464c0738ae9a2316abdcd3bb5615d0dc7148aa3f Author: Mooneer Salem Date: Mon Sep 28 00:57:19 2026 -0700 [skip ci[ Add PR #1523 to changelog. commit 13a4035be39d9c696d8e070625fb25231f4b5214 Author: Mooneer Salem Date: Sun Sep 27 22:59:04 2026 -0700 PulseAudio: fix unused parameter warning when building without rtkit. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit 1b261d7239263bca5809d2e30b76f332bfe674b9 Merge: 2f662f86 a86125dc Author: Mooneer Salem Date: Sun Sep 27 22:42:25 2026 -0700 Merge remote-tracking branch 'origin/master' into ms-pulse-per-device-connection commit 2f662f86311e40d9f194d29f459d32941069fcfb Author: Mooneer Salem Date: Sun Sep 27 22:16:09 2026 -0700 PulseAudio: don't request rtkit priority for the engine's connection. The engine's connection is only used for device enumeration, and it's opened up to three times per modem start (startup, validateSoundCardSetup() and startRxStream()). Requesting rtkit priority for it used up rtkit's per-user burst budget for no benefit. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit a86125dc60eb52d97e752f45456a0d1907c3d498 Author: Mooneer Salem Date: Sun Sep 27 22:08:16 2026 -0700 Only use system libraries that can actually be linked (#1520) * Only use system libraries that can actually be linked. find_library() can return libraries that can't be linked with the current toolchain, e.g. MacPorts' libhamlib.a when cross-compiling for Windows with LLVM-MinGW (resulting in undefined rig_* symbols at link time), or single-architecture libraries when building macOS universal binaries. Add CheckLibraryUsable(), which compiles and links a test program against the found library using the target toolchain, and fall back to building the dependency ourselves when it fails. Applied to hamlib, sndfile and PortAudio. PortAudio now also falls back to a static build instead of erroring out when no usable system copy is found. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 * Add PR #1521 to changelog. --------- Co-authored-by: Claude Opus 5.5 commit 1246fac6eb83afa14739a8569ea5d6a7cefe1ef7 Author: Mooneer Salem Date: Sun Sep 27 22:05:58 2026 -0700 PulseAudio: ask rtkit for priority once per connection. The context state callback asked rtkit to raise the main loop thread's priority on every state change (connecting, authorizing, setting name, ready), about three requests per connection. rtkit allows 25 requests per user per 20 s by default, and with a connection per device, starting the modem used 23 of them, so a quick stop/start or another application's request would have been refused and left threads at normal priority. Ask only when the context becomes ready, once per connection. A modem start now makes 9 requests, down from 23 (11 before connections were per-device). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit c01e712bd85507409200fbeafd48aa06f5c0340a Author: Mooneer Salem Date: Sun Sep 27 21:56:02 2026 -0700 PulseAudio: give each device its own connection. All of FreeDV's PulseAudio streams shared one main loop and context. On PipeWire (seen with 1.6.2), when a module-loopback connects the sink FreeDV plays radio audio to with the sink whose monitor it records radio audio from -- as the test scripts' loopback does -- pipewire-pulse stopped delivering the recorded audio for seconds to minutes at a time, and RX-from-file decoding ran 2-20x slower than real time. The same happens with a minimal libpulse program and with PortAudio's PulseAudio backend, both of which share one connection, but not when each stream has its own connection. Each PulseAudioDevice now creates its own threaded main loop and context in start() and tears them down in stop(), as OpenAL Soft does. The engine keeps its connection for device enumeration. Connection setup, including raising the main loop thread's priority via rtkit, moves into PulseAudioEngine::CreateConnection()/DestroyConnection(), and a context that fails to connect now returns an error instead of asserting. Audio callbacks now run on one thread per device, as they already do on macOS and Windows; each uses its own FIFO and scratch buffer. Measured on Fedora 44 (RX from file of a 26 s recording): with the loopback, 60-875 s before and 39.4 s after in every run, matching runs without it. generate_pgo_profiles.sh's RX phase went from 100-1038 s to 81 s. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit 79d3210a54100ec100fe322bf0323f7fe62dcf6d Author: Mooneer Salem Date: Sun Sep 27 19:25:37 2026 -0700 Fix intermittent rade_reporting_awgn failures on PipeWire Linux (#1522) * Tests: keep parecord's buffer small so the end of a recording isn't lost. By default parecord asks for a very large buffer. On PipeWire the recording then lagged 1-1.7 s behind, and since hamlibserver.py kills parecord 80 ms after the last PTT off, rade_reporting's recording always lost the end of its second over, including the EOO frame carrying the callsign. The test was left with only the first over's callsign to decode, which is why rade_reporting_awgn failed on the PipeWire Linux legs (up to a third of runs on ubuntu-24.04-arm) but not on macOS or PulseAudio (ubuntu-22.04). With --latency-msec=20, both overs are recorded in full. Apply the same to the other tests that record with parecord. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 * rade_reporting_awgn: use --No -18 (~5 dB SNR). With both overs now recorded in full, the noisiest level that still passes well above 95% of the time is --No -18. Measured on Linux (PipeWire) by decoding complete recordings with ch's noise shifted to 30 different alignments per level (callsign decodes per over / test passes): --No -22 (~9 dB): 98% / 100% --No -18 (~5 dB): 93% / 100% (~99.5% expected) --No -17 (~4 dB): 67% / 97% --No -16 (~3 dB): 45% / 73% --No -14 (~1 dB): 0% / 0% Also correct the comment: the recorded level is steady run to run, and ch never seeds rand(), so its noise is the same every run. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 --------- Co-authored-by: Claude Opus 5.5 commit 8fdcf080e467473e9fd970f1db4d364cfb8532ea Author: Mooneer Salem Date: Sun Sep 27 17:48:26 2026 -0700 rade_reporting_awgn: use --No -18 (~5 dB SNR). With both overs now recorded in full, the noisiest level that still passes well above 95% of the time is --No -18. Measured on Linux (PipeWire) by decoding complete recordings with ch's noise shifted to 30 different alignments per level (callsign decodes per over / test passes): --No -22 (~9 dB): 98% / 100% --No -18 (~5 dB): 93% / 100% (~99.5% expected) --No -17 (~4 dB): 67% / 97% --No -16 (~3 dB): 45% / 73% --No -14 (~1 dB): 0% / 0% Also correct the comment: the recorded level is steady run to run, and ch never seeds rand(), so its noise is the same every run. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit 4fe9e57a563b44852ed1240e9902759b98d2bd50 Author: Mooneer Salem Date: Sun Sep 27 17:28:47 2026 -0700 [skip ci] Add PR #1521 to changelog. commit 74cc425db484897981b3f009cbdea26b66fe3516 Author: Mooneer Salem Date: Sun Sep 27 17:27:21 2026 -0700 Reduce GUI thread load from the scalar plots during TX (macOS and Windows) (#1521) * PlotScalar: draw directly into the window on macOS and Windows. The Frm Mic plot repaints ten times a second while transmitting, and scrolling, converting and compositing PlotScalar's two bitmaps (the waveform and the gridline overlay) on every frame was the largest single cost on the GUI thread: - macOS: a wxBitmap is always sRGB and 1x while the window usually isn't, so CoreGraphics color matched and rescaled both bitmaps on every frame. - Windows: drawing the bitmaps through GDI+ took over twice as long as rasterizing the paths directly. On macOS and Windows, fill the waveform and stroke the gridlines straight into the window's context instead. Gridlines and the waveform land on the same pixels as before on both. Linux keeps the bitmap path. Measured over the rade_loss TX phase: - macOS CI runner: GUI thread 3.10 s -> 2.27 s; PlotScalar::draw ~640 ms -> ~58 ms. - Windows (200% scaling): GUI thread 7.6 s -> 6.6 s CPU; PlotScalar::draw -58%. - Linux: unchanged (bitmap path). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 * PlotPanel: paint into a 32bpp buffer on Windows. wxAutoBufferedPaintDC paints into wxWidgets' shared 24bpp buffer. GDI+ can only rasterize straight into 32bpp bitmaps, so with 24bpp each primitive went through GDI in the kernel (cdd.dll) instead. Paint into a 32bpp DIB of our own, with its alpha channel made opaque once up front: wxGDIPlusRenderer::CreateContext() otherwise redoes that for the whole buffer on every paint. Rendering is pixel-identical. Over the rade_loss TX phase on Windows (200% scaling), GUI thread CPU fell about 12% on top of drawing PlotScalar directly. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 * PlotScalar: copy rather than blend when drawing directly on Windows. Everything PlotScalar draws directly is opaque and unantialiased, so copying pixels gives the same result as blending them. Telling GDI+ so made filling the plot's background about 40% cheaper. Rendering is pixel-identical, colors included. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 * Add PR #1521 to changelog. --------- Co-authored-by: Claude Opus 5.5 commit e0f338ef5dc3af7c63411e9f89abda53d2898060 Author: Mooneer Salem Date: Sun Sep 27 17:26:59 2026 -0700 [skip ci] Add PR #1521 to changelog. commit 57fa7e96ef687d627645f6a87e6bc053ce1fee54 Author: Mooneer Salem Date: Sun Sep 27 16:59:59 2026 -0700 Tests: keep parecord's buffer small so the end of a recording isn't lost. By default parecord asks for a very large buffer. On PipeWire the recording then lagged 1-1.7 s behind, and since hamlibserver.py kills parecord 80 ms after the last PTT off, rade_reporting's recording always lost the end of its second over, including the EOO frame carrying the callsign. The test was left with only the first over's callsign to decode, which is why rade_reporting_awgn failed on the PipeWire Linux legs (up to a third of runs on ubuntu-24.04-arm) but not on macOS or PulseAudio (ubuntu-22.04). With --latency-msec=20, both overs are recorded in full. Apply the same to the other tests that record with parecord. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit 9bf93deb8e1c9a8eb15ed777993c139a375cbc9d Author: Mooneer Salem Date: Sun Sep 27 14:36:51 2026 -0700 CI: A/B rade_loss with the current and previous macOS device layouts. Experiment branch: Linux and Windows CI are skipped, and macOS CI is one job per runner (2x macos-26, 2x macos-15) that builds FreeDV once and then alternates rade_loss runs between the device layout #1516 switched to (radio on BlackHole1, speaker on VB-Cable) and the previous one (radio on VB-Cable, speaker on BlackHole1), 6 runs of each. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit 96ee28c9e94b22adbd4cface62ff1c5771c3c567 Author: Mooneer Salem Date: Sun Sep 27 12:49:22 2026 -0700 PlotScalar: copy rather than blend when drawing directly on Windows. Everything PlotScalar draws directly is opaque and unantialiased, so copying pixels gives the same result as blending them. Telling GDI+ so made filling the plot's background about 40% cheaper. Rendering is pixel-identical, colors included. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit 6c3919bd7ab1bbe2a1626cb359c9b84416e4c9cf Author: Mooneer Salem Date: Sun Sep 27 12:48:51 2026 -0700 PlotPanel: paint into a 32bpp buffer on Windows. wxAutoBufferedPaintDC paints into wxWidgets' shared 24bpp buffer. GDI+ can only rasterize straight into 32bpp bitmaps, so with 24bpp each primitive went through GDI in the kernel (cdd.dll) instead. Paint into a 32bpp DIB of our own, with its alpha channel made opaque once up front: wxGDIPlusRenderer::CreateContext() otherwise redoes that for the whole buffer on every paint. Rendering is pixel-identical. Over the rade_loss TX phase on Windows (200% scaling), GUI thread CPU fell about 12% on top of drawing PlotScalar directly. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit f6b69af08164e40f65302bb20df50c71ab6dac6e Author: Mooneer Salem Date: Sun Sep 27 11:43:26 2026 -0700 PlotScalar: draw directly into the window on macOS and Windows. The Frm Mic plot repaints ten times a second while transmitting, and scrolling, converting and compositing PlotScalar's two bitmaps (the waveform and the gridline overlay) on every frame was the largest single cost on the GUI thread: - macOS: a wxBitmap is always sRGB and 1x while the window usually isn't, so CoreGraphics color matched and rescaled both bitmaps on every frame. - Windows: drawing the bitmaps through GDI+ took over twice as long as rasterizing the paths directly. On macOS and Windows, fill the waveform and stroke the gridlines straight into the window's context instead. Gridlines and the waveform land on the same pixels as before on both. Linux keeps the bitmap path. Measured over the rade_loss TX phase: - macOS CI runner: GUI thread 3.10 s -> 2.27 s; PlotScalar::draw ~640 ms -> ~58 ms. - Windows (200% scaling): GUI thread 7.6 s -> 6.6 s CPU; PlotScalar::draw -58%. - Linux: unchanged (bitmap path). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit acfeb524d7873109b1f7f0398983bcc049b646fa Author: Mooneer Salem Date: Sun Sep 27 09:56:50 2026 -0700 test/perf: accept xctrace's older "backtrace" element name. GitHub's macOS runners have an older xctrace that exports call stacks as rather than , so every main-thread rendering column in the CI summary came out as zero. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit 518c1971c0a7995dbec1e7aa03c6b6c56da371a0 Author: Mooneer Salem Date: Sun Sep 27 09:27:42 2026 -0700 CI: trim to a macOS-only PlotScalar performance experiment. Linux and Windows CI are skipped on this branch. macOS CI is replaced by one job that builds FreeDV with and without the PlotScalar change and alternates rade_loss runs of the two on the same runner, profiling each TX phase with the Time Profiler. A table of main-thread rendering costs, loss results and per-thread CPU goes to the job summary; traces are uploaded as an artifact. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit e514441b93b3ae8941bec2c51b8c84e3abcd398a Author: Mooneer Salem Date: Sun Sep 27 09:26:08 2026 -0700 PlotScalar: draw directly into the window on macOS. A wxBitmap is always sRGB and 1x, while the window is usually neither, so CoreGraphics color matched and rescaled both of PlotScalar's bitmaps (the waveform and the gridline overlay) on every frame. With the Frm Mic plot repainting ten times a second during TX, that was most of the GUI thread's time when rendering happens in-process (e.g. with the display asleep). On macOS, fill the waveform and stroke the gridlines straight into the window's context instead. Other platforms keep the bitmap path. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit 49c4255dedf3cc7f279343f81b82f140e1e2ae80 Author: Mooneer Salem Date: Sat Sep 26 17:52:42 2026 -0700 Fix wxWidgets fetch when pinned to a commit hash. GIT_SHALLOW clones with --depth 1, which only fetches branch tips, so checking out an arbitrary commit fails with "fatal: invalid reference". Disable shallow cloning and use the full commit hash. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit ae9d525aaf088960fe15f00540af64bda1664947 Author: Mooneer Salem Date: Sat Sep 26 17:52:29 2026 -0700 Only use system libraries that can actually be linked. find_library() can return libraries that can't be linked with the current toolchain, e.g. MacPorts' libhamlib.a when cross-compiling for Windows with LLVM-MinGW (resulting in undefined rig_* symbols at link time), or single-architecture libraries when building macOS universal binaries. Add CheckLibraryUsable(), which compiles and links a test program against the found library using the target toolchain, and fall back to building the dependency ourselves when it fails. Applied to hamlib, sndfile and PortAudio. PortAudio now also falls back to a static build instead of erroring out when no usable system copy is found. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxwHpb9KR3ymB15RFPGtR6 commit 2ae7718b41b40d0d2cbceac298c27e1fb1770fd9 Author: Mooneer Salem Date: Sat Sep 26 15:56:14 2026 -0700 Try enabling dark mode support on Windows. commit dea89d3383d6e1da2850224b67fa5ee04e7cd0b3 Author: Mooneer Salem Date: Sat Sep 26 14:53:10 2026 -0700 Fix compiler errors from merge. commit 26bd68e504323350a860145758d8ee3d6d98539c Author: Mooneer Salem Date: Sat Sep 26 21:49:50 2026 +0000 latest user manual PDF commit cc8b60b0f11c894d68bd5b8554062f72285d1385 Merge: 5d890993 c5385bda Author: Mooneer Salem Date: Sat Sep 26 14:47:49 2026 -0700 Merge branch 'v3.0-dev' into ms-rade-v2 commit c5385bdaab1d59f4976f9fc160f70c13f965b593 Author: Mooneer Salem Date: Sat Sep 26 14:45:56 2026 -0700 Setup Wizard: auto-select radio devices and import settings from WSJT-X/JTDX/JS8Call (#1513) * Setup Wizard: Auto-select first found radio device on first start. * Setup Wizard: prefer lowest-numbered DAX RX channel when auto-selecting. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LMBgG3zymVyaeK8vo5arWD * Setup Wizard: allow importing settings from WSJT-X, JTDX and JS8Call. Reads the program's QSettings INI file and fills in the radio audio devices, Hamlib/OmniRig/serial PTT rig control, callsign and grid square. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LMBgG3zymVyaeK8vo5arWD * Fix codespell failures in setup wizard. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LMBgG3zymVyaeK8vo5arWD * Setup Wizard: fix import of network and USB CAT settings from WSJT-X. The importer only treated "NET rigctl" and FLRig as network rigs, so other Hamlib network rigs (e.g. FlexRadio SmartSDR, GQRX) got the serial port instead of CATNetworkPort. Use the rig's Hamlib port type instead, as WSJT-X does, and also handle USB rigs (CATUSBPort). A blank network port now falls back to Hamlib's default address. Co-Authored-By: Claude Opus 5.5 * Setup Wizard: import TX audio source and data mode from WSJT-X. A Rear/Data TX audio source with CAT PTT now imports as "CAT via Data port". Adds a "Use USB instead of DIGU" checkbox to the radio control page, which is set from WSJT-X's Mode setting (USB checks it, Data/Pkt unchecks it, None leaves it unchanged). Co-Authored-By: Claude Opus 5.5 * Setup wizard: enable reporting if PSK Reporter is enabled in imported config. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017mSu6vi7WRbzLMWDN9u1Bz * Add PR #1513 to changelog. --------- Co-authored-by: Claude Opus 5.5 commit 7afc3b122f3049bf2b7a6e8056f496d4709a8baf Author: Mooneer Salem Date: Sat Sep 26 14:44:59 2026 -0700 Add PR #1513 to changelog. commit 71ae78fec3ebfccc11fe115b7f062e3c0324b37d Author: Mooneer Salem Date: Sat Sep 26 14:43:54 2026 -0700 Setup wizard: enable reporting if PSK Reporter is enabled in imported config. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017mSu6vi7WRbzLMWDN9u1Bz commit 0ab5becd70389d1314ea22ecabe5d3d8cdc88d54 Merge: f0c254aa e58da9e7 Author: Mooneer Salem Date: Sat Sep 26 14:40:23 2026 -0700 Merge branch 'v3.0-dev' into ms-setup-wizard-auto-select-dev commit e58da9e76424afb90f4bd2ff5924101c58e2a9d9 Merge: 8327aa70 cf60b7d4 Author: Mooneer Salem Date: Sat Sep 26 14:39:56 2026 -0700 Merge branch 'master' into v3.0-dev commit cf60b7d43939930dd3d1a26c54c6156fdbdc891a Author: Mooneer Salem Date: Sat Sep 26 14:38:32 2026 -0700 rade_reporting_awgn: Add a bit more buffer to avoid CI failures. (#1517) commit 248591a91753875ab3c1516ec63ab07c88c3c65e Author: Mooneer Salem Date: Sat Sep 26 14:25:32 2026 -0700 rade_reporting_awgn: Add a bit more buffer to avoid CI failures. commit 1798f00296193660a2cb93829f13dde958d909c5 Author: Mooneer Salem Date: Sat Sep 26 20:38:20 2026 +0000 latest user manual PDF commit 2d07912437a6178eaabe4fbe0e44565c6b1383c8 Author: Mooneer Salem Date: Sat Sep 26 13:35:53 2026 -0700 Reduce GUI thread paint and FreeDV Reporter overhead (#1515) * Waterfall: create bitmaps in the window's color space on macOS. wxGraphicsContext::CreateBitmapFromImage() goes through wxBitmap, which tags the image as sRGB. Since the window's backing store is normally in the display's color space (e.g. Display P3), CoreGraphics color matched every cached waterfall block on every frame it was on screen. Build the CGImage directly in the window's color space and in the backing store's 32-bit host-order layout so compositing is a straight copy. In the fullduplex_RADEV1 test this removed ~1.1 s of color conversion per 55 s from the GUI thread (~10% of its CPU time). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017mSu6vi7WRbzLMWDN9u1Bz * FreeDV Reporter: only refit columns when the widest text changes. On macOS, every updateHighlights() pass that touched a row pinned all columns, notified the view, then set them back to wxCOL_WIDTH_AUTOSIZE, which makes wxWidgets measure every cell of every row. Most updates (a new "last update" time, SNR, etc.) don't change any column's widest text, so that work was wasted. Cache each row's measured text width per column, re-measure only rows whose text changed or that became visible, and refit only when some column's widest text changes. Header/unit changes and newly shown columns still force a refit. Windows and Linux behavior is unchanged. In the fullduplex_RADEV1 test this cut the Reporter's GUI thread time by ~87% (~2.3 s per 55 s). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017mSu6vi7WRbzLMWDN9u1Bz * Waterfall: cache the graticule margins instead of redrawing them every frame. The axis labels and ticks only change with the control's size, colours or pixel density, but their text was laid out and rendered on every paint. All of it sits in the top and left margins, which have nothing but the background underneath, so render those once into opaque bitmaps (in the window's color space on macOS) and just copy them on each paint. The grid lines crossing the waterfall and the RX tuning markers are still drawn live; the solid ones are now stroked as a single path. A transparent overlay of the whole graticule was tried first, but alpha blending it every frame cost far more on macOS than it saved. In the fullduplex_RADEV1 test on macOS this cut drawGraticule() from ~550 ms to ~80 ms and glyph rendering by ~0.7 s per 55 s. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017mSu6vi7WRbzLMWDN9u1Bz * Waterfall: fix UBSan error and wxWidgets 3.0 build in graticule cache. wxImage::GetSubImage() offsets the image's alpha pointer even when it's null, which UBSan reports on the Windows sanitizer jobs; crop the margins by hand instead. Also use wxBitmap::CreateScaled() rather than CreateWithDIPSize(), which needs wxWidgets 3.1.6+ (Ubuntu 22.04 has 3.0). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017mSu6vi7WRbzLMWDN9u1Bz * Waterfall: size cached graticule labels for the window's DPI. wxGraphicsContext::CreateFont() sizes the font for its own context's DPI. The graticule cache drew its labels through a memory DC's context, which on Windows reports the default 96 DPI rather than the window's, so on scaled displays (e.g. 160%) the labels came out too small. Create the font from the window's paint context instead, as the uncached code did. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017mSu6vi7WRbzLMWDN9u1Bz * FreeDV Reporter: only refit columns on Windows when the widest text changes. Windows called autosizeColumns() on every updateHighlights() pass that touched a row, including colour-only highlight changes, which measures every cell of every row. Profiling the fullduplex_RADEV1 test on Windows showed it at ~25% of the GUI thread. Apply the same check macOS uses: refit only when some column's widest text changes, or when a refit is forced (layout/unit changes, a column being shown). Linux is unchanged: GTK sizes autosized columns itself, so no refit is ever issued there and the width measurement is skipped. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017mSu6vi7WRbzLMWDN9u1Bz * FreeDV Reporter: only declare columnsAutosized_ on macOS. It's only used by the macOS column-autosize toggling, so clang reports it as an unused private field (an error with -Werror) on other platforms. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017mSu6vi7WRbzLMWDN9u1Bz * FreeDV Reporter: pin column widths on Windows after autosizing. wxDataViewCtrl's generic implementation (used on Windows) invalidates column best widths on every item change and re-measures every row of wxCOL_WIDTH_AUTOSIZE columns at idle time. autosizeColumns() left the columns in AUTOSIZE mode, so with the Reporter's frequent highlight updates that idle work cost as much as the explicit refits it replaced. Pin each column at its fitted width instead; the model still refits when the widest text in a column changes. On Windows (fullduplex_RADEV1, ETW sampling) this cut the GUI thread's column measuring from ~380 ms to ~3 ms per 55 s; together with the rest of the branch, GUI thread time is ~30% lower than master. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017mSu6vi7WRbzLMWDN9u1Bz * Add PR #1515 to changelog. --------- Co-authored-by: Claude Opus 5.5 commit 5ef9d8c4e47477e3873dccbae1261cee0d9ec06f Author: Mooneer Salem Date: Sat Sep 26 12:14:59 2026 -0700 Add PR #1515 to changelog. commit f8fae8d328aa78b31cf0f8b148b14c2e54fafb53 Merge: bccf5c49 c08c4ae1 Author: Mooneer Salem Date: Sat Sep 26 12:08:44 2026 -0700 Merge branch 'master' into ms-waterfall-display-colorspace commit c08c4ae17f32e6dc785876dc287cfe320627885f Author: Mooneer Salem Date: Sat Sep 26 12:07:15 2026 -0700 CI: keep VB-Cable off the radio path (Windows and macOS) (#1516) * Windows CI: put the radio path on VAC and mic/speaker on VB-Cable. VB-Cable occasionally inserts ~10 ms of silence into its loop early in a stream (one extra period of latency). On the radio path that's a timing jump in the RADE signal, which makes the receiver lose sync briefly and fails fullduplex/rade_loss even though FreeDV's own audio paths are clean. Swapping the cables moves the glitch to the mic/speaker path, where it's harmless. On a local Windows 11 machine running fullduplex_RADEV1 alternately: radio on VB-Cable dropped sync in 10 of 12 runs, radio on VAC in 0 of 12. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017mSu6vi7WRbzLMWDN9u1Bz * macOS CI: put the radio path on BlackHole1 and the speaker on VB-Cable. Same idea as the Windows change: keep VB-Cable off the radio path. Radio in and out both use "BlackHole1 2ch". The fork's "2ch" -> "2ch 2" pairing drops most of the audio when both ends are AUHAL clients (as FreeDV's are), so FreeDV can't sync across it; one device loops back cleanly. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017mSu6vi7WRbzLMWDN9u1Bz --------- Co-authored-by: Claude Opus 5.5 commit 3df2e24b7db858b8566423497badf2ee4b396a0d Author: Mooneer Salem Date: Sat Sep 26 10:36:12 2026 -0700 macOS CI: put the radio path on BlackHole1 and the speaker on VB-Cable. Same idea as the Windows change: keep VB-Cable off the radio path. Radio in and out both use "BlackHole1 2ch". The fork's "2ch" -> "2ch 2" pairing drops most of the audio when both ends are AUHAL clients (as FreeDV's are), so FreeDV can't sync across it; one device loops back cleanly. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017mSu6vi7WRbzLMWDN9u1Bz commit bcac93a00f098194873661700b1f0878028a5f1f Author: Mooneer Salem Date: Sat Sep 26 08:26:04 2026 -0700 Windows CI: put the radio path on VAC and mic/speaker on VB-Cable. VB-Cable occasionally inserts ~10 ms of silence into its loop early in a stream (one extra period of latency). On the radio path that's a timing jump in the RADE signal, which makes the receiver lose sync briefly and fails fullduplex/rade_loss even though FreeDV's own audio paths are clean. Swapping the cables moves the glitch to the mic/speaker path, where it's harmless. On a local Windows 11 machine running fullduplex_RADEV1 alternately: radio on VB-Cable dropped sync in 10 of 12 runs, radio on VAC in 0 of 12. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017mSu6vi7WRbzLMWDN9u1Bz commit bccf5c49d2be4e1fc9a63d3d2a3df68ab9254f98 Author: Mooneer Salem Date: Fri Sep 25 19:55:31 2026 -0700 FreeDV Reporter: pin column widths on Windows after autosizing. wxDataViewCtrl's generic implementation (used on Windows) invalidates column best widths on every item change and re-measures every row of wxCOL_WIDTH_AUTOSIZE columns at idle time. autosizeColumns() left the columns in AUTOSIZE mode, so with the Reporter's frequent highlight updates that idle work cost as much as the explicit refits it replaced. Pin each column at its fitted width instead; the model still refits when the widest text in a column changes. On Windows (fullduplex_RADEV1, ETW sampling) this cut the GUI thread's column measuring from ~380 ms to ~3 ms per 55 s; together with the rest of the branch, GUI thread time is ~30% lower than master. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017mSu6vi7WRbzLMWDN9u1Bz commit 0f62c5027006d6e21ec46469ad3b4312c0b14cd1 Author: Mooneer Salem Date: Fri Sep 25 18:49:10 2026 -0700 FreeDV Reporter: only declare columnsAutosized_ on macOS. It's only used by the macOS column-autosize toggling, so clang reports it as an unused private field (an error with -Werror) on other platforms. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017mSu6vi7WRbzLMWDN9u1Bz commit 08fd391fbca9cd5df1f97466fe9d4e56e9074ea3 Author: Mooneer Salem Date: Fri Sep 25 18:44:35 2026 -0700 FreeDV Reporter: only refit columns on Windows when the widest text changes. Windows called autosizeColumns() on every updateHighlights() pass that touched a row, including colour-only highlight changes, which measures every cell of every row. Profiling the fullduplex_RADEV1 test on Windows showed it at ~25% of the GUI thread. Apply the same check macOS uses: refit only when some column's widest text changes, or when a refit is forced (layout/unit changes, a column being shown). Linux is unchanged: GTK sizes autosized columns itself, so no refit is ever issued there and the width measurement is skipped. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017mSu6vi7WRbzLMWDN9u1Bz commit 8f60da1f5406b8b8743abfbaeda5365ea80ca9cd Author: Mooneer Salem Date: Fri Sep 25 18:38:05 2026 -0700 Waterfall: size cached graticule labels for the window's DPI. wxGraphicsContext::CreateFont() sizes the font for its own context's DPI. The graticule cache drew its labels through a memory DC's context, which on Windows reports the default 96 DPI rather than the window's, so on scaled displays (e.g. 160%) the labels came out too small. Create the font from the window's paint context instead, as the uncached code did. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017mSu6vi7WRbzLMWDN9u1Bz commit c72511a6acbc46c8584c07450224e70e972dc3ab Author: Mooneer Salem Date: Fri Sep 25 15:18:22 2026 -0700 Waterfall: fix UBSan error and wxWidgets 3.0 build in graticule cache. wxImage::GetSubImage() offsets the image's alpha pointer even when it's null, which UBSan reports on the Windows sanitizer jobs; crop the margins by hand instead. Also use wxBitmap::CreateScaled() rather than CreateWithDIPSize(), which needs wxWidgets 3.1.6+ (Ubuntu 22.04 has 3.0). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017mSu6vi7WRbzLMWDN9u1Bz commit f016ae96c648623d84acdca7c17f420905c3ecb2 Author: Mooneer Salem Date: Fri Sep 25 14:30:55 2026 -0700 Waterfall: cache the graticule margins instead of redrawing them every frame. The axis labels and ticks only change with the control's size, colours or pixel density, but their text was laid out and rendered on every paint. All of it sits in the top and left margins, which have nothing but the background underneath, so render those once into opaque bitmaps (in the window's color space on macOS) and just copy them on each paint. The grid lines crossing the waterfall and the RX tuning markers are still drawn live; the solid ones are now stroked as a single path. A transparent overlay of the whole graticule was tried first, but alpha blending it every frame cost far more on macOS than it saved. In the fullduplex_RADEV1 test on macOS this cut drawGraticule() from ~550 ms to ~80 ms and glyph rendering by ~0.7 s per 55 s. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017mSu6vi7WRbzLMWDN9u1Bz commit 83f15d4fe47983f8748fb99cc7717558f8714cce Author: Mooneer Salem Date: Fri Sep 25 10:59:33 2026 -0700 FreeDV Reporter: only refit columns when the widest text changes. On macOS, every updateHighlights() pass that touched a row pinned all columns, notified the view, then set them back to wxCOL_WIDTH_AUTOSIZE, which makes wxWidgets measure every cell of every row. Most updates (a new "last update" time, SNR, etc.) don't change any column's widest text, so that work was wasted. Cache each row's measured text width per column, re-measure only rows whose text changed or that became visible, and refit only when some column's widest text changes. Header/unit changes and newly shown columns still force a refit. Windows and Linux behavior is unchanged. In the fullduplex_RADEV1 test this cut the Reporter's GUI thread time by ~87% (~2.3 s per 55 s). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017mSu6vi7WRbzLMWDN9u1Bz commit ea72edd9982e670ad361ef818c16ea8871d68718 Author: Mooneer Salem Date: Fri Sep 25 09:44:30 2026 -0700 Waterfall: create bitmaps in the window's color space on macOS. wxGraphicsContext::CreateBitmapFromImage() goes through wxBitmap, which tags the image as sRGB. Since the window's backing store is normally in the display's color space (e.g. Display P3), CoreGraphics color matched every cached waterfall block on every frame it was on screen. Build the CGImage directly in the window's color space and in the backing store's 32-bit host-order layout so compositing is a straight copy. In the fullduplex_RADEV1 test this removed ~1.1 s of color conversion per 55 s from the GUI thread (~10% of its CPU time). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017mSu6vi7WRbzLMWDN9u1Bz commit f0c254aa36e9cb492653dd916368fd9e1abe0107 Author: Mooneer Salem Date: Fri Sep 25 08:53:41 2026 -0700 Setup Wizard: import TX audio source and data mode from WSJT-X. A Rear/Data TX audio source with CAT PTT now imports as "CAT via Data port". Adds a "Use USB instead of DIGU" checkbox to the radio control page, which is set from WSJT-X's Mode setting (USB checks it, Data/Pkt unchecks it, None leaves it unchanged). Co-Authored-By: Claude Opus 5.5 commit 7a939ae1a61f8e163cf9d64aa3f3ff0db6c5a535 Author: Mooneer Salem Date: Fri Sep 25 08:46:09 2026 -0700 Setup Wizard: fix import of network and USB CAT settings from WSJT-X. The importer only treated "NET rigctl" and FLRig as network rigs, so other Hamlib network rigs (e.g. FlexRadio SmartSDR, GQRX) got the serial port instead of CATNetworkPort. Use the rig's Hamlib port type instead, as WSJT-X does, and also handle USB rigs (CATUSBPort). A blank network port now falls back to Hamlib's default address. Co-Authored-By: Claude Opus 5.5 commit 514ef2f23bd117cca67d38db18c86a7a4d33f93b Merge: 4053d2a7 8327aa70 Author: Mooneer Salem Date: Fri Sep 25 08:41:48 2026 -0700 Merge branch 'v3.0-dev' into ms-setup-wizard-auto-select-dev commit 8327aa703fe9dab2c40b2b714521b76b07fb3db0 Merge: 8d4c7f6e 7f19e7e1 Author: Mooneer Salem Date: Fri Sep 25 08:41:22 2026 -0700 Merge branch 'master' into v3.0-dev commit 7f19e7e1b6d2570f92d5fdad6d580aa01019e780 Author: Mooneer Salem Date: Fri Sep 25 15:36:46 2026 +0000 latest user manual PDF commit 1c6943cc06e27a78e8d9965af354e8d76c57ab91 Author: Mooneer Salem Date: Fri Sep 25 08:33:42 2026 -0700 Fix horizontal lines in waterfall on HiDPI/fractionally scaled Linux displays (#1514) * Fix horizontal lines in waterfall on HiDPI/fractionally scaled Linux displays. The waterfall is a stack of dy-tall blocks, each built as a 1x bitmap. On displays where GTK3 renders above 1x (e.g. GNOME at 125%, which GTK3 draws at 2x), Cairo upscales every block when compositing and dark seams appear between them. Confirmed with an on-screen test: 1x strips showed the lines, while the same strips built at the display's scale factor, and a single full-height image, did not. Setting the display to 100% also made the lines go away. Blocks are now built at GetContentScaleFactor() via wxImage::Scale() with nearest-neighbour sampling (matching what GTK3's StretchBlit did) and turned straight into a wxGraphicsBitmap, so they composite 1:1 with no upscaling. The per-block wxBitmap and memory DC blit are no longer needed. This is also much cheaper at 2x, since Cairo no longer upscales the whole plot on every paint (700x600 plot, offscreen wxGTK3/Cairo benchmark): 1x: 0.104 -> 0.096 ms per frame 2x: 8.9 -> 0.22 ms per frame GetContentScaleFactor() is 1 on Windows and 1x Linux, so behaviour there is unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LMBgG3zymVyaeK8vo5arWD * Add PR #1514 to changelog. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LMBgG3zymVyaeK8vo5arWD --------- Co-authored-by: Claude Opus 5.5 commit ab33e631d9e0d7fa259a97f399e3ad940427ac73 Author: Mooneer Salem Date: Thu Sep 24 22:56:09 2026 -0700 Add PR #1514 to changelog. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LMBgG3zymVyaeK8vo5arWD commit 7d718011ab9c858116bc5527cea6850cab4e8229 Author: Mooneer Salem Date: Thu Sep 24 22:55:47 2026 -0700 Fix horizontal lines in waterfall on HiDPI/fractionally scaled Linux displays. The waterfall is a stack of dy-tall blocks, each built as a 1x bitmap. On displays where GTK3 renders above 1x (e.g. GNOME at 125%, which GTK3 draws at 2x), Cairo upscales every block when compositing and dark seams appear between them. Confirmed with an on-screen test: 1x strips showed the lines, while the same strips built at the display's scale factor, and a single full-height image, did not. Setting the display to 100% also made the lines go away. Blocks are now built at GetContentScaleFactor() via wxImage::Scale() with nearest-neighbour sampling (matching what GTK3's StretchBlit did) and turned straight into a wxGraphicsBitmap, so they composite 1:1 with no upscaling. The per-block wxBitmap and memory DC blit are no longer needed. This is also much cheaper at 2x, since Cairo no longer upscales the whole plot on every paint (700x600 plot, offscreen wxGTK3/Cairo benchmark): 1x: 0.104 -> 0.096 ms per frame 2x: 8.9 -> 0.22 ms per frame GetContentScaleFactor() is 1 on Windows and 1x Linux, so behaviour there is unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LMBgG3zymVyaeK8vo5arWD commit 4053d2a75758058b40c97c0959bbb4cec64a1ae4 Author: Mooneer Salem Date: Thu Sep 24 19:17:12 2026 -0700 Fix codespell failures in setup wizard. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LMBgG3zymVyaeK8vo5arWD commit 5c56dbf28f8ae77594731ebc43ddf31ddcb58553 Author: Mooneer Salem Date: Thu Sep 24 18:43:16 2026 -0700 Setup Wizard: allow importing settings from WSJT-X, JTDX and JS8Call. Reads the program's QSettings INI file and fills in the radio audio devices, Hamlib/OmniRig/serial PTT rig control, callsign and grid square. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LMBgG3zymVyaeK8vo5arWD commit a12fe26f3177f5bdd10062014806973603bc49cb Author: Mooneer Salem Date: Thu Sep 24 17:14:49 2026 -0700 Setup Wizard: prefer lowest-numbered DAX RX channel when auto-selecting. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LMBgG3zymVyaeK8vo5arWD commit c0cc38f07b93315f359834cff8e80306c9ba6c50 Author: Mooneer Salem Date: Thu Sep 24 16:05:24 2026 -0700 Setup Wizard: Auto-select first found radio device on first start. commit 5d8909930f54206859a3b9f537876c9c9b730dae Author: Mooneer Salem Date: Thu Sep 24 13:13:13 2026 -0700 Revert "Try Qwen proposed change to WASAPIAudioDevice to improve ARM audio dropouts." This reverts commit 55852367dfedd0dc4f672d4ba4f7b8f0a0288886. commit 55852367dfedd0dc4f672d4ba4f7b8f0a0288886 Author: Mooneer Salem Date: Thu Sep 24 11:59:32 2026 -0700 Try Qwen proposed change to WASAPIAudioDevice to improve ARM audio dropouts. commit 0e96366817df8a50c8ecf7eca75d0e4d914e77b1 Merge: a876e06f 8d4c7f6e Author: Mooneer Salem Date: Thu Sep 24 11:58:58 2026 -0700 Merge branch 'v3.0-dev' into ms-rade-v2 commit 8d4c7f6ec21e35d3bcc6661155a0636f306ff2ff Author: Mooneer Salem Date: Thu Sep 24 17:39:02 2026 +0000 latest user manual PDF commit 0d8e555a72af0445468ef9ae158a93e1660bb6e1 Author: Mooneer Salem Date: Thu Sep 24 10:36:35 2026 -0700 Linux/PulseAudio: Show friendlier device names in configuration windows. (#1508) * Linux/PulseAudio: Show friendlier device names in configuration windows. * Add PR #1508 to changelog. commit 96027949d671ad2e80dd96ad1f4ed5c0d0160d08 Author: Mooneer Salem Date: Thu Sep 24 09:14:51 2026 -0700 Add PR #1508 to changelog. commit e6a6110361cf1d19c5b3370d6d1cdf6604dda62d Author: Mooneer Salem Date: Thu Sep 24 15:36:56 2026 +0000 latest user manual PDF commit 9ba3d68db4f0fa53beca234f66e8da297813dc1d Author: Mooneer Salem Date: Thu Sep 24 08:33:46 2026 -0700 Avoid duplicate initialization and deinitialization of audio engines. (#1511) * Avoid duplicate initialization and deinitialization of audio engines. * Add PR #1511 to changelog. commit 2ad18fd91f131faf3aed1acc4c4f0f5f4d237aad Author: Mooneer Salem Date: Thu Sep 24 08:33:31 2026 -0700 Add PR #1511 to changelog. commit a876e06f5dc8e81937851aa16ba4ba28738e18d9 Merge: 93de0a6b cdf987c9 Author: Mooneer Salem Date: Wed Sep 23 20:22:24 2026 -0700 Merge branch 'v3.0-dev' into ms-rade-v2 commit a69f370bd4a6c84fea5c94b5398fd114cb70e556 Author: Mooneer Salem Date: Wed Sep 23 17:14:13 2026 -0700 Avoid duplicate initialization and deinitialization of audio engines. commit cdf987c99678338495e87937c369b4f243e34993 Merge: b2e138a9 f904eda2 Author: Mooneer Salem Date: Wed Sep 23 17:13:41 2026 -0700 Merge branch 'master' into v3.0-dev commit f904eda2807d2095bf11f4219f6c2b7157abe656 Author: Mooneer Salem Date: Wed Sep 23 18:58:47 2026 +0000 latest user manual PDF commit 82787f1853d80cb8f61982079da373be4addabed Author: Mooneer Salem Date: Wed Sep 23 11:56:32 2026 -0700 Fix bundled libsndfile build on lib64 systems and strncpy build error (#1510) * Fix bundled libsndfile build on lib64 systems. On distros such as Fedora x86_64, GNUInstallDirs defaults CMAKE_INSTALL_LIBDIR to lib64, so libsndfile.a was installed to external/dist/lib64 instead of the expected external/dist/lib. Force CMAKE_INSTALL_LIBDIR=lib. Also link libsndfile.a before mpg123/mp3lame so that its references to those libraries resolve with static linking. Co-Authored-By: Claude Opus 5.5 * Fix unterminated strncpy in OnReliableTextRx_. strncpy() was bounded by the full destination size, so received text of RELIABLE_TEXT_FIFO_SIZE or more characters would leave tmpBuf without a null terminator. This also triggered -Werror=stringop-truncation in optimized GCC builds. Leave room for the terminator, matching OnRadeTextRx_. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LMBgG3zymVyaeK8vo5arWD * Add PR #1510 to changelog. --------- Co-authored-by: Claude Opus 5.5 commit 880e66d695f20e17f4a9313a079572b9ba0374bc Author: Mooneer Salem Date: Wed Sep 23 11:01:45 2026 -0700 Add PR #1510 to changelog. commit b17a158921e0596c263cc3e402c26c225d53bf5a Author: Mooneer Salem Date: Wed Sep 23 09:21:25 2026 -0700 Fix unterminated strncpy in OnReliableTextRx_. strncpy() was bounded by the full destination size, so received text of RELIABLE_TEXT_FIFO_SIZE or more characters would leave tmpBuf without a null terminator. This also triggered -Werror=stringop-truncation in optimized GCC builds. Leave room for the terminator, matching OnRadeTextRx_. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LMBgG3zymVyaeK8vo5arWD commit 552f7e2afb272ef05dc33a72e53067b9563d1770 Author: Mooneer Salem Date: Wed Sep 23 08:36:56 2026 -0700 Fix bundled libsndfile build on lib64 systems. On distros such as Fedora x86_64, GNUInstallDirs defaults CMAKE_INSTALL_LIBDIR to lib64, so libsndfile.a was installed to external/dist/lib64 instead of the expected external/dist/lib. Force CMAKE_INSTALL_LIBDIR=lib. Also link libsndfile.a before mpg123/mp3lame so that its references to those libraries resolve with static linking. Co-Authored-By: Claude Opus 5.5 commit 5d5009f5cee5b6b4392849c0b139559c2062264f Author: Mooneer Salem Date: Wed Sep 23 01:54:27 2026 -0700 Linux/PulseAudio: Show friendlier device names in configuration windows. commit 93de0a6bf277fffeb4c387de3e14e62a7bae03d7 Merge: 28652598 b2e138a9 Author: Mooneer Salem Date: Wed Sep 23 01:29:23 2026 -0700 Merge branch 'v3.0-dev' into ms-rade-v2 commit b2e138a9ff16828215cca7dd788068e43c406a0d Merge: 7236477e 37b2ddba Author: Mooneer Salem Date: Wed Sep 23 01:27:30 2026 -0700 Merge branch 'master' into v3.0-dev commit 37b2ddba0361cc6b617d765621043556742c2038 Author: Mooneer Salem Date: Wed Sep 23 01:11:41 2026 -0700 Stop playback of RX file before TX start. (#1507) * Stop playback of RX file before TX start. * [skip ci] Add PR #1507 to changelog. commit dc5ce3127b01bf4f28c9c7c4716f92204caaadbf Author: Mooneer Salem Date: Wed Sep 23 01:11:17 2026 -0700 [skip ci] Add PR #1507 to changelog. commit 31a6029a57a2c5c21012eb40c3faaef3dd80fb6d Author: Mooneer Salem Date: Wed Sep 23 00:55:05 2026 -0700 Stop playback of RX file before TX start. commit c282226f095a7590811dda1142e9f556759116bf Author: Mooneer Salem Date: Wed Sep 23 07:40:21 2026 +0000 latest user manual PDF commit cc5955dc3155d0ce3c1b789a0a6a27e1cfa42890 Author: Mooneer Salem Date: Wed Sep 23 00:37:04 2026 -0700 Port bugfixes from ms-rade-v2 to master. (#1506) * Port bugfixes from ms-rade-v2 to master. * RADEV2 is not supported in this branch. * Add PR #1506 to changelog. * Further relax AWGN test. commit 5454d549b5ca739e953ff8c1f387440c82898b4a Author: Mooneer Salem Date: Wed Sep 23 00:36:41 2026 -0700 Further relax AWGN test. commit 866419db98504bf8ca4746e01591f6726bb0434f Author: Mooneer Salem Date: Wed Sep 23 00:35:28 2026 -0700 Add PR #1506 to changelog. commit 917e045d06ffe74c713245bdcf31a0a26f91e7ed Author: Mooneer Salem Date: Tue Sep 22 16:36:32 2026 -0700 RADEV2 is not supported in this branch. commit d9d00c3fb73fe8422467ee3e5edaa84879fa7e10 Author: Mooneer Salem Date: Tue Sep 22 16:30:06 2026 -0700 Port bugfixes from ms-rade-v2 to master. commit c4ec703b73489081a882290d73714a748efb7e96 Author: Mooneer Salem Date: Tue Sep 22 16:04:43 2026 -0700 Fix autotools dependency configure failures on newer Xcode toolchains (#1505) * Fix autotools dependency configure failures on newer Xcode toolchains On recent Xcode toolchains, CMAKE_C_COMPILER can resolve to the raw absolute path inside XcodeDefault.xctoolchain rather than /usr/bin/cc or `xcrun cc`. When hamlib/lame/mpg123's own ./configure invokes that compiler to check it works, the driver no longer auto-discovers the default SDK and linking fails with "ld: library 'System' not found", breaking any full/clean build that builds these deps from source (BOOTSTRAP_WXWIDGETS / USE_STATIC_SNDFILE, or a missing system hamlib). Pass -isysroot explicitly via a shared EXTERNAL_PROJECT_APPLE_ISYSROOT variable to fix this, mirroring the same fix already applied to the RNNoise build in the freedv-backend repo (tmiw/freedv-backend#59). Also adds the -mmacosx-version-min=11.0/CFLAGS previously missing from the non-universal (BUILD_OSX_UNIVERSAL=0) branches of BuildLame.cmake and BuildMpg123.cmake, matching the flags already used in their universal-build branches and in BuildHamlib.cmake -- without any CFLAGS at all there, the same raw-CC-path SDK lookup failure hit those configure invocations regardless of deployment target. freedv-gui's own CMAKE_OSX_DEPLOYMENT_TARGET was already 11.0 (CMakeLists.txt), so no version bump was needed here. Verified by forcing source builds of hamlib, lame, and mpg123 (USE_STATIC_SNDFILE=1, hamlib not found on system) from a clean build directory: hamlib and mpg123 now configure and build successfully. lame's ./configure now succeeds too; its build fails for an unrelated, pre-existing reason -- LAME 3.100's bundled mpg123.h macro-redefines memcpy() via bcopy() without including , which newer Clang now treats as a hard error (implicit-function-declaration) instead of a warning. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi * Also pass -isysroot via CPPFLAGS to fix LAME build under newer Xcode The previous commit fixed autoconf's compiler-works check by passing -isysroot via CFLAGS/CXXFLAGS, but autoconf determines its own CPP command ("checking how to run the C preprocessor") as plain "$CC -E", independent of CFLAGS. Header checks that go through CPP directly (e.g. AC_HEADER_STDC's AC_EGREP_HEADER calls) therefore still failed to find SDK headers, silently reporting STDC_HEADERS as undefined even though string.h/stdlib.h etc. are all present. For LAME specifically, this broke the build: its bundled mpglib/ mpg123.h only includes under "#ifdef STDC_HEADERS" -- otherwise it falls back to K&R-style declarations and redefines memcpy()/memset() via bcopy() without a prototype, which newer Clang now treats as a hard error (implicit-function-declaration) instead of a warning, so the fallback path no longer compiles at all. Passing -isysroot via CPPFLAGS as well fixes CPP-based checks the same way CFLAGS fixed compiler-based ones, so STDC_HEADERS is correctly defined and the broken legacy fallback path in LAME's mpg123.h is never taken. LAME itself has had no upstream release since 3.100 (2017) to upgrade to; MacPorts still packages 3.100 with unrelated patches, so fixing the build here is the only option short of switching encoders. Verified by forcing source builds of hamlib, lame, and mpg123 from a clean build directory: all three now configure and build successfully end-to-end (previously lame's build step failed on the bcopy/ memcpy issue described above). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi * [skip ci] Add PR #1505 to changelog. --------- Co-authored-by: Claude Sonnet 5 commit a52b4c5bf632e973444888c023275e240062352f Author: Mooneer Salem Date: Tue Sep 22 16:01:18 2026 -0700 [skip ci] Add PR #1505 to changelog. commit 6ac950c21da5ddcaca4015e97a707261b0680ee8 Author: Mooneer Salem Date: Tue Sep 22 13:15:20 2026 -0700 Also pass -isysroot via CPPFLAGS to fix LAME build under newer Xcode The previous commit fixed autoconf's compiler-works check by passing -isysroot via CFLAGS/CXXFLAGS, but autoconf determines its own CPP command ("checking how to run the C preprocessor") as plain "$CC -E", independent of CFLAGS. Header checks that go through CPP directly (e.g. AC_HEADER_STDC's AC_EGREP_HEADER calls) therefore still failed to find SDK headers, silently reporting STDC_HEADERS as undefined even though string.h/stdlib.h etc. are all present. For LAME specifically, this broke the build: its bundled mpglib/ mpg123.h only includes under "#ifdef STDC_HEADERS" -- otherwise it falls back to K&R-style declarations and redefines memcpy()/memset() via bcopy() without a prototype, which newer Clang now treats as a hard error (implicit-function-declaration) instead of a warning, so the fallback path no longer compiles at all. Passing -isysroot via CPPFLAGS as well fixes CPP-based checks the same way CFLAGS fixed compiler-based ones, so STDC_HEADERS is correctly defined and the broken legacy fallback path in LAME's mpg123.h is never taken. LAME itself has had no upstream release since 3.100 (2017) to upgrade to; MacPorts still packages 3.100 with unrelated patches, so fixing the build here is the only option short of switching encoders. Verified by forcing source builds of hamlib, lame, and mpg123 from a clean build directory: all three now configure and build successfully end-to-end (previously lame's build step failed on the bcopy/ memcpy issue described above). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi commit 64cb8f36c31dd9e014f09086b2d297d8c33acf23 Author: Mooneer Salem Date: Tue Sep 22 13:01:28 2026 -0700 Fix autotools dependency configure failures on newer Xcode toolchains On recent Xcode toolchains, CMAKE_C_COMPILER can resolve to the raw absolute path inside XcodeDefault.xctoolchain rather than /usr/bin/cc or `xcrun cc`. When hamlib/lame/mpg123's own ./configure invokes that compiler to check it works, the driver no longer auto-discovers the default SDK and linking fails with "ld: library 'System' not found", breaking any full/clean build that builds these deps from source (BOOTSTRAP_WXWIDGETS / USE_STATIC_SNDFILE, or a missing system hamlib). Pass -isysroot explicitly via a shared EXTERNAL_PROJECT_APPLE_ISYSROOT variable to fix this, mirroring the same fix already applied to the RNNoise build in the freedv-backend repo (tmiw/freedv-backend#59). Also adds the -mmacosx-version-min=11.0/CFLAGS previously missing from the non-universal (BUILD_OSX_UNIVERSAL=0) branches of BuildLame.cmake and BuildMpg123.cmake, matching the flags already used in their universal-build branches and in BuildHamlib.cmake -- without any CFLAGS at all there, the same raw-CC-path SDK lookup failure hit those configure invocations regardless of deployment target. freedv-gui's own CMAKE_OSX_DEPLOYMENT_TARGET was already 11.0 (CMakeLists.txt), so no version bump was needed here. Verified by forcing source builds of hamlib, lame, and mpg123 (USE_STATIC_SNDFILE=1, hamlib not found on system) from a clean build directory: hamlib and mpg123 now configure and build successfully. lame's ./configure now succeeds too; its build fails for an unrelated, pre-existing reason -- LAME 3.100's bundled mpg123.h macro-redefines memcpy() via bcopy() without including , which newer Clang now treats as a hard error (implicit-function-declaration) instead of a warning. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi commit 286525982c3823e976ee7826da3e101c29346110 Merge: c4bd931d 4baeade1 Author: Mooneer Salem Date: Mon Sep 21 08:15:09 2026 -0700 Merge branch 'v3.0-dev' into ms-rade-v2 commit 7236477e1358439fb8539793a07c4574d4a483b0 Author: Mooneer Salem Date: Mon Sep 21 15:14:20 2026 +0000 latest user manual PDF commit 4baeade199886824ee1427b3135885ea11a835dd Author: Mooneer Salem Date: Mon Sep 21 08:11:18 2026 -0700 Add ability to record both the raw and decoded RX audio. (#1501) * Add ability to record both the raw and decoded RX audio. * Fix broken merge. * Try fixing it again. * Add missing declarations. * Add PR #1501 to changelog. * Add a bit more headroom for the AWGN test. commit 041c8010c36d264e73dbc47a2cba03acf203339a Author: Mooneer Salem Date: Mon Sep 21 08:10:04 2026 -0700 Add a bit more headroom for the AWGN test. commit 2b16300046d97ce0b9b858cec7914dd50b1497e7 Author: Mooneer Salem Date: Mon Sep 21 08:08:11 2026 -0700 Add PR #1501 to changelog. commit 18afa4c48bba471996412cf1c383c9f2f46a0507 Author: Mooneer Salem Date: Sun Sep 20 09:44:50 2026 -0700 Add missing declarations. commit e737b6e3aaf155a3e1a13959ee9a00db606e43b0 Author: Mooneer Salem Date: Sun Sep 20 09:38:05 2026 -0700 Try fixing it again. commit 7e0136f1a39bf44048f6f11e078e66d710dda876 Author: Mooneer Salem Date: Sun Sep 20 09:29:25 2026 -0700 Fix broken merge. commit c9b44f6ef1cc1a822694779f3db206ecdf330ef7 Merge: 9dfa9b6f a17f5013 Author: Mooneer Salem Date: Sun Sep 20 01:59:23 2026 -0700 Merge branch 'v3.0-dev' into ms-record-decode-and-raw commit a17f501332a3cd3d55a123f5869c09abc9e6128a Merge: 7b018da4 b392f3f2 Author: Mooneer Salem Date: Sun Sep 20 01:56:44 2026 -0700 Merge branch 'master' into v3.0-dev commit 50de1b6ebb535a547032084ed51c0c78616ef27c Author: Mooneer Salem Date: Sun Sep 20 08:55:29 2026 +0000 latest user manual PDF commit b392f3f238b752b598eada18ea685c376b0d41e6 Author: Mooneer Salem Date: Sun Sep 20 01:52:57 2026 -0700 Various data race cleanups. (#1502) * Various data race cleanups. * [skip CI] Add PR #1502 to changelog. commit 1b4ebeca1a20fc357fc9c6ccffcbd2426f9b6b8d Author: Mooneer Salem Date: Sun Sep 20 01:52:42 2026 -0700 [skip CI] Add PR #1502 to changelog. commit f95b2ca8eb1bd95712601951413f827ba8c54a5f Author: Mooneer Salem Date: Sat Sep 19 23:06:26 2026 -0700 Various data race cleanups. commit 9dfa9b6fa5e1d8954dfd8f567deb2cb99ca1b974 Author: Mooneer Salem Date: Sat Sep 19 11:24:38 2026 -0700 Add ability to record both the raw and decoded RX audio. commit 7b018da47e347191bc7d221342f0ea12364feca7 Merge: 5a71296b d0377829 Author: Mooneer Salem Date: Sat Sep 19 11:23:56 2026 -0700 Merge branch 'master' into v3.0-dev commit d0377829e08c99c61adf7f333877837011644730 Author: Mooneer Salem Date: Sat Sep 19 11:19:05 2026 -0700 Fix crash when testing Easy Setup with an invalid audio configuration (#1500) * Fix crash when testing Easy Setup with an invalid audio configuration WASAPIAudioEngine::start() can fail (e.g. Windows Audio service issue, no active endpoints, COM error) without any way to signal that to callers, since IAudioEngine::start() returns void. When that happens, inputDeviceList_/outputDeviceList_ (and devEnumerator_) are left null, but getAudioDeviceList()/getDefaultAudioDevice() unconditionally dereferenced them, so the very next call from EasySetupDialog::OnTest() (which starts the engine and immediately calls getAudioDevice()) would crash with a null pointer access violation instead of surfacing the existing "couldn't open device" error dialog. Also: - Show an error message when the analog playback test device fails to open, matching the existing behavior for the radio/TX test device. - Stop a running Easy Setup test (audio devices, rig control) when the dialog is closed via Cancel or the window close button while a test is active, instead of leaving the shared audio engine singleton started. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi * Handle IAudioDevice::start() failure in Easy Setup test The previous fix only covered IAudioEngine::start() failing (or the device not being found by getAudioDevice()). If getAudioDevice() successfully returns a device object but that device's own start() subsequently fails (e.g. WASAPI Initialize()/GetBufferSize()/Start() failing due to an exclusive-access conflict or unsupported format), none of the per-backend implementations propagate that failure back to the caller: start() returns void, and OnTest() never registered an error callback or checked isRunning(), so the dialog would report the test as running with no audio actually flowing and no error shown. Register IAudioDevice::setOnAudioError() on both test devices before calling start(), and use it to both surface the actual driver error message and flag the failure back to OnTest() via testAudioDeviceStartFailed_, checked immediately after start() returns (safe without extra synchronization since start() blocks via std::future::wait() until any error callback from that same call has already run -- verified for all four IAudioDevice backends). On failure, the radio/TX device aborts the test the same way an outright getAudioDevice() failure already did; the analog playback device treats it as non-fatal, matching its existing "device not found" handling. Also fixed stopTest_() to always stop the shared audio engine singleton, not just when a test device object exists -- otherwise the new failure paths (which call it after audioEngine->start() but before either test device object was successfully created) would leave the engine running. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi --------- Co-authored-by: Claude Sonnet 5 commit ff5ad7737ae95a42de6ff96a932f4d62fde73f10 Author: Mooneer Salem Date: Sat Sep 19 11:18:39 2026 -0700 [skip ci] Add PR #1500 to changelog. commit daf3ec4711ed5756743d53f77cf02792bd15f905 Author: Mooneer Salem Date: Fri Sep 18 23:20:39 2026 +0000 Enable RTSan on macOS CI. (#1499) RTSan build/test support already existed in build_osx.sh and CMakeLists.txt but wasn't exercised by CI. Adds WITH_RTSAN=1 to the macOS sanitizer test matrix, using Homebrew's llvm@20 clang/clang++ (RTSan needs LLVM Clang, not Apple's). Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi Co-authored-by: Claude Sonnet 5 commit c4bd931de793feff508dddf45e02c9c7594b65d2 Author: Mooneer Salem Date: Fri Sep 18 14:47:26 2026 -0700 Remove stale hamlibserver.py copy step from Windows audio test workflow hamlibserver.py was deleted from test/ in f6ab7193 since it's no longer used, but the CI workflow refactor on v3.0-dev (2125e5cd) reintroduced a reference to it when the Windows test steps were rewritten into the windows-audio-test.yml composite workflow. Merging v3.0-dev into ms-rade-v2 brought that reference back, breaking the "Copy test scripts to install folder" step with a "Cannot find path" error on every Windows test job. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi commit a1b0e10f7672d8a2bc60cda72072baf0cc1cb50d Author: Mooneer Salem Date: Fri Sep 18 14:42:03 2026 -0700 Handle IAudioDevice::start() failure in Easy Setup test The previous fix only covered IAudioEngine::start() failing (or the device not being found by getAudioDevice()). If getAudioDevice() successfully returns a device object but that device's own start() subsequently fails (e.g. WASAPI Initialize()/GetBufferSize()/Start() failing due to an exclusive-access conflict or unsupported format), none of the per-backend implementations propagate that failure back to the caller: start() returns void, and OnTest() never registered an error callback or checked isRunning(), so the dialog would report the test as running with no audio actually flowing and no error shown. Register IAudioDevice::setOnAudioError() on both test devices before calling start(), and use it to both surface the actual driver error message and flag the failure back to OnTest() via testAudioDeviceStartFailed_, checked immediately after start() returns (safe without extra synchronization since start() blocks via std::future::wait() until any error callback from that same call has already run -- verified for all four IAudioDevice backends). On failure, the radio/TX device aborts the test the same way an outright getAudioDevice() failure already did; the analog playback device treats it as non-fatal, matching its existing "device not found" handling. Also fixed stopTest_() to always stop the shared audio engine singleton, not just when a test device object exists -- otherwise the new failure paths (which call it after audioEngine->start() but before either test device object was successfully created) would leave the engine running. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi commit 08b2020a318fd0ad1fe01e7233599576af6159a1 Author: Mooneer Salem Date: Fri Sep 18 14:33:22 2026 -0700 Fix crash when testing Easy Setup with an invalid audio configuration WASAPIAudioEngine::start() can fail (e.g. Windows Audio service issue, no active endpoints, COM error) without any way to signal that to callers, since IAudioEngine::start() returns void. When that happens, inputDeviceList_/outputDeviceList_ (and devEnumerator_) are left null, but getAudioDeviceList()/getDefaultAudioDevice() unconditionally dereferenced them, so the very next call from EasySetupDialog::OnTest() (which starts the engine and immediately calls getAudioDevice()) would crash with a null pointer access violation instead of surfacing the existing "couldn't open device" error dialog. Also: - Show an error message when the analog playback test device fails to open, matching the existing behavior for the radio/TX test device. - Stop a running Easy Setup test (audio devices, rig control) when the dialog is closed via Cancel or the window close button while a test is active, instead of leaving the shared audio engine singleton started. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi commit 90f37ce27e81c0b4cd7974ccd71ac301c8a3ef01 Author: Mooneer Salem Date: Fri Sep 18 21:28:56 2026 +0000 Harden audio FIFO handling and WASAPI stop timing (#1496) * Handle GenericFIFO read() failures instead of replaying stale audio outfifo1/outfifo2 reads in the audio callbacks and PlaybackStep::execute() ignored read()'s return value. A read that numUsed()/canRead_ reported as available can still fail if it races with a concurrent reset() (e.g. on a TX/RX transition or resampler rebuild); when that happened, the code played out whatever was left in the reused scratch buffer from the previous callback instead of silence. Also fixes OnRxOutAudioData_ to zero-fill the remainder of the buffer on underflow, matching its sibling callbacks. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_018JYqaVNT7DbgmKePHQtNYv * Harden reliableTextFifo_ read and fix playbackResampler_ data race FreeDVInterface::getReliableText() ignored read()'s return value into an uninitialized stack buffer; a failed read (currently unreachable given the FIFO's one-record capacity and mutex-serialized consumer side, but fragile) would have built a std::string from uninitialized stack memory. Now zero-inits the buffer and only uses it on success. PlaybackStep::playbackResampler_ was a raw pointer whose lifecycle is owned exclusively by the file I/O thread (nonRtThreadEntry_), but reset() -- called on the real-time pipeline thread on every RX->TX transition -- read and dereferenced it without any synchronization. That's a torn-pointer read, a possible use-after-free (racing the file I/O thread's delete/recreate on a sample-rate change), and a possible concurrent mutation of the underlying libsamplerate state during a live execute(). Since the RT thread can't take a lock, reset() now just raises a lock-free atomic flag; the file I/O thread consumes it and performs the actual resampler reset itself. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_018JYqaVNT7DbgmKePHQtNYv * WASAPIAudioDevice: sleep-then-spin using NtQueryTimerResolution for stopRealTimeWork WaitForMultipleObjects on a high-resolution waitable timer can only wake on a scheduler tick, and once woken still has to wait to actually be scheduled -- both add jitter on top of the wait target itself. This is plausibly a contributor to the intermittent sync-loss failures seen on the windows-11-arm CI runner during recent audio-path investigation. Query the OS's current timer resolution via NtQueryTimerResolution (loaded dynamically since it isn't exported by any import library), sleep for everything except the last tick of that resolution, then spin-wait the remainder against steady_clock. The abort-early behavior is preserved at both stages via semaphore_: a blocking WaitForSingleObject during the sleep phase, and a non-blocking poll each iteration of the spin. Falls back to the original SetWaitableTimer + WaitForMultipleObjects approach if NtQueryTimerResolution can't be resolved. Untested on real hardware -- no Windows toolchain available locally. Needs CI/manual confirmation, ideally with wait-jitter instrumentation to check whether it actually tightens things up on windows-11-arm. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_018JYqaVNT7DbgmKePHQtNYv * Debugging -- try to force higher latency for VB-Cable. * More debugging--apply VB-Cable Registry settings across all Windows runners. * Override VB-Cable latency settings on macOS, too. * Might work better as a separate file instead. * Revert VB-Cable latency CI changes -- moved to ci-reusable-actions These workflow steps (VB-Cable latency plist on macOS, VB-Cable Registry settings on Windows) land at the exact points cmake-macos.yml and cmake-windows.yml are being consolidated into shared composite actions on ci-reusable-actions (setup-macos-virtual-audio and setup-windows-audio-drivers), so keeping them here too would conflict with that refactor on merge. Ported both changes into those composite actions instead, so this branch just picks them up once that PR lands. Reverts the .github/workflows/cmake-macos.yml, .github/workflows/cmake-windows.yml, and ci/com.vbaudio.vbcable.plist changes from: - c8474e96 Debugging -- try to force higher latency for VB-Cable. - 90f213b4 More debugging--apply VB-Cable Registry settings across all Windows runners. - 74705784 Override VB-Cable latency settings on macOS, too. - ac9a6f2d Might work better as a separate file instead. No source changes here -- src/audio/WASAPIAudioDevice.cpp and the rest of this branch's actual FIFO/WASAPI fixes are untouched. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi --------- Co-authored-by: Claude Sonnet 5 commit d7f9f505a13b50e11e8b3ff69e2daf72fa2da9c9 Author: Mooneer Salem Date: Fri Sep 18 14:28:38 2026 -0700 [skip ci] Add PR #1496 to changelog. commit 90384662cbfa3a3ee6e110afabdba626d937ba59 Merge: 8f8d14b7 2125e5cd Author: Mooneer Salem Date: Fri Sep 18 11:42:22 2026 -0700 Merge branch 'master' into ms-audio-fifo-wasapi-fixes commit f2e980a01ddf739dd4e55f485714cb0100f19bfc Author: Mooneer Salem Date: Fri Sep 18 11:32:16 2026 -0700 Enable RTSan on macOS CI. RTSan build/test support already existed in build_osx.sh and CMakeLists.txt but wasn't exercised by CI. Adds WITH_RTSAN=1 to the macOS sanitizer test matrix, using Homebrew's llvm@20 clang/clang++ (RTSan needs LLVM Clang, not Apple's). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi commit e6b0f49084a6e3243710851d3353d026c5daf05e Merge: 69473b05 5a71296b Author: Mooneer Salem Date: Fri Sep 18 11:19:06 2026 -0700 Merge v3.0-dev into ms-rade-v2 Brings in the CI composite-action refactor and ccache fixes from PR #1498 (merged into master, then into v3.0-dev). Conflicts were all in the three main workflow files, where ms-rade-v2 had independently evolved the same regions the refactor touched: - cmake-linux.yml: ms-rade-v2's old duplicated "Sanity check RADE" steps (one per OS branch) are replaced by the refactored single step + run-linux-gui-test composite action; ms-rade-v2's new RADE verification report generation (rade-loss-baseline job, "Generate/ Upload RADE verification report" steps) is unaffected and preserved. - cmake-macos.yml: kept ms-rade-v2's new WITH_RTSAN=1 sanitizer leg (needs Homebrew's llvm@20 clang, since Apple's clang lacks RTSan) as its own "Build freedv-gui" step, dropped its inline mic-permission/ kill-resource-hogs steps as redundant (setup-macos-virtual-audio already covers both earlier in the job), and kept the refactored setup-rade-python-env composite action call. - cmake-windows.yml / windows-audio-test.yml: ms-rade-v2 added RADE verification report generation (Windows-side, x86_64 leg only) and switched to testing RADEV2 instead of RADEV1. Ported both into the reusable workflow rather than re-inlining ms-rade-v2's old full job bodies: added mode-to-test (default RADEV1, so the other callers are unaffected), loss-baseline, rade-c-commit, and radae-commit inputs; added the "Copy RADE verification report script"/"Generate RADE verification report"/"Upload RADE verification report" steps, gated to the non-sanitizer, x86_64-only case that ms-rade-v2 used; both `test` and `test-sanitizer` callers now pass mode-to-test: RADEV2, matching what ms-rade-v2 had inlined in both jobs. Everything else (new composite actions, cleanup-caches.yml, prune-stale-caches.yml, the Hamlib/Lame/Mpg123/Sndfile ccache forwarding, ExternalProjectCCache.cmake) merged cleanly with no conflicts, since ms-rade-v2 never touched those. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi commit 5224aef6d7e9649ea7ec3447a5ee283db815cd1e Author: Mooneer Salem Date: Fri Sep 18 18:10:11 2026 +0000 latest user manual PDF commit 5a71296b8e15d8eac6495a689e5a2b37ce9bc245 Merge: ab701df5 2125e5cd Author: Mooneer Salem Date: Fri Sep 18 11:09:08 2026 -0700 Merge branch 'master' into v3.0-dev commit 2125e5cd7a8e02516084786f25e476e68aa014fc Author: Mooneer Salem Date: Fri Sep 18 18:07:41 2026 +0000 Refactor CI workflows into reusable composite actions; fix ccache gaps in ExternalProject deps (#1498) * Refactor CI workflows to use reusable composite actions Extracts duplicated step sequences from cmake-linux.yml, cmake-macos.yml, and cmake-windows.yml into shared composite actions under .github/actions/, and consolidates the near-identical Windows "test"/"test-sanitizer" jobs into a single parameterized reusable workflow (windows-audio-test.yml). New composite actions: - install-llvm-apt-pool: apt.llvm.org pool install (was duplicated 3x) - setup-rtkit-linux: rtkit/polkit setup for Ubuntu (was duplicated 3x) - setup-pulseaudio-legacy: PulseAudio config for Ubuntu 22.04 (was 3x) - setup-rade-python-env: RADE venv + torch/matplotlib install (was 5x) - run-linux-gui-test: Xvfb/dbus/metacity display setup (was 4x) - setup-macos-virtual-audio: VB-Cable + permission workarounds (was 3x) - download-mingw-llvm: pinned llvm-mingw toolchain download (was 3x) - setup-windows-audio-drivers / configure-windows-audio-devices: VAC/ VB-Cable install and Windows audio device configuration (was 3x) - configure-windows-crash-dumps: WER crash dump registry setup (was 2x) This reduces cmake-linux.yml, cmake-macos.yml, and cmake-windows.yml by about 820 lines combined while preserving existing job behavior. A few inert simplifications were made along the way (e.g. reordering some macOS setup steps, unifying near-identical if/else branches whose bodies differed only in already-duplicated display-setup code) and are called out in the accompanying summary. Co-Authored-By: Claude Sonnet 5 * Route Hamlib's autotools build through the configured ccache launcher Hamlib is built via ExternalProject_Add's own ./configure && make invocation, which CMake just shells out to. That means it never sees CMAKE_C_COMPILER_LAUNCHER/CMAKE_CXX_COMPILER_LAUNCHER (e.g. ccache) -- those only apply to CMake's own compile rules -- so Hamlib's compile was previously uncached everywhere except one macOS job that happened to have ccache-action's create-symlink PATH workaround enabled. Bake the launcher into CC/CXX for all three configure branches (mingw cross-compile, Apple, and Linux) instead, so Hamlib gets cached the same way as the rest of the project on every platform, without needing create-symlink enabled on every runner that builds it from source. Verified empirically with a standalone CMake script that the resulting CONFIGURE_COMMAND list element is exactly "CC=ccache " (one list element, not split) when a launcher is set, and plain "CC=" with no leading space when it isn't. Co-Authored-By: Claude Sonnet 5 * Extend ccache launcher forwarding to the other ExternalProject deps Reviewed every ExternalProject_Add-based dependency for the same gap found in Hamlib: FetchContent-based deps (freedv_backend, wxWidgets, portaudio) are compiled via add_subdirectory() as part of the same CMake generation, so they already inherit CMAKE_C_COMPILER_LAUNCHER/ CMAKE_CXX_COMPILER_LAUNCHER automatically -- no change needed there. The remaining ExternalProject_Add deps did need it: - Extract the CC/CXX-with-launcher logic out of BuildHamlib.cmake into a shared cmake/ExternalProjectCCache.cmake, defining EXTERNAL_PROJECT_CC/EXTERNAL_PROJECT_CXX for any autotools-based ExternalProject to reuse instead of duplicating the same if/else. - Apply it to BuildLame.cmake and BuildMpg123.cmake, which build via ./configure the same way Hamlib does and had the identical gap: neither previously set CC/CXX at all, so both relied entirely on autoconf's own PATH-based (or --host-triggered) compiler detection. - BuildCodec2.cmake and BuildSndfile.cmake build CMake-based deps as a *nested* ExternalProject cmake invocation (CMAKE_ARGS), which gets its own fresh CMakeCache.txt and doesn't inherit the parent's compiler-launcher settings either -- forward CMAKE_C_COMPILER_LAUNCHER/CMAKE_CXX_COMPILER_LAUNCHER there instead, since that's the correct mechanism for a nested CMake build (as opposed to baking it into CC/CXX, which is only needed for autotools). Verified against real generated build.make/CMakeCache output (not just the CMake list/string semantics) for both the launcher-set and launcher-unset cases, and for the mingw-cross-compile, Apple, and native-Linux configure branches. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi * Fix CI: composite action steps don't support timeout-minutes CI run on PR #1498 confirmed a real error: GitHub Actions rejected setup-macos-virtual-audio/action.yml with "Unexpected value 'timeout-minutes'" -- that key is only valid on a normal job step, not on a step inside a composite action's own `runs.steps` list. This broke every macOS job that calls the action (test, dist-pgo-instrument, and transitively test-app-folder/dist-pgo-build via needs:). Move the timeout from the inner "Workaround macOS permission issues" step (which can't have it) to the outer `uses:` call site in all three callers instead, covering the whole composite action's four steps. CI results otherwise confirmed the rest of the refactor and the ccache changes are working: every Linux and Windows job passed, including the new windows-audio-test.yml reusable workflow (test/test-sanitizer) and all the other extracted composite actions. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi * Add workflow to delete a PR branch's Actions caches when it closes While investigating whether the repo was at risk of hitting the 10GB Actions cache quota (it was: 10.03 GiB across 111 entries, meaning GitHub was almost certainly already silently evicting caches), found that 40% of that usage (4.06 GiB / 43 entries) was superseded duplicates -- ccache-action's default append-timestamp behavior writes a brand-new, uniquely-keyed cache blob on every run and never removes the previous one, so every active branch's cache footprint only grows for as long as the branch exists. Deleted those 43 stale entries (repo-wide, confirmed via the caches API), freeing the repo back down to ~5.98 GiB. That only clears the backlog once, though -- it doesn't stop the same accumulation from happening again on every other branch. This adds the other half: delete a PR branch's cache entries once the PR closes, so finished work stops permanently reserving quota that active branches need. Loops over `gh cache list`/`gh cache delete` rather than a single capped call, since a long-lived PR can accumulate more than one page of cache entries. Uses refs/heads/ (not refs/pull//merge, which is what GitHub's own docs example uses) for the BRANCH filter, since our build workflows trigger on push, not pull_request, so that's the ref every cache entry we inspected was actually scoped to -- the docs example wouldn't have matched anything in this repo. Runs on pull_request_target (not pull_request) so it also covers PRs from forks; safe here since the job never checks out or executes the PR's code, only calls the cache API for its branch name. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi * Add scheduled job to prune stale Actions caches on every branch cleanup-caches.yml only handles a branch's caches when its PR closes, which leaves the default branch (no PR to close) and any other long-lived branch's older cache generations unaddressed -- those keep accumulating for as long as the branch exists, since ccache-action writes a brand-new timestamped cache blob every run and nothing else ever removes the previous one. Adds a daily scheduled workflow (plus workflow_dispatch for manual runs) that lists every cache entry repo-wide, groups them by (ref, key with its trailing timestamp stripped), and deletes every entry in a group except the most recently created one. It never touches the newest cache for any (branch, job) pair, so nothing still reachable by ccache-action's restore step is removed. Verified the grouping/survivor logic against the repo's live cache list (94 entries, 68 distinct branch+job groups): every group retained exactly its newest entry with zero errors, and it correctly identified 26 already-superseded duplicates (~2.8 GiB) that have accumulated again since the one-time manual cleanup, which is exactly the ongoing drift this job is meant to catch automatically going forward. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi * Bring in VB-Cable latency tuning from ms-audio-fifo-wasapi-fixes That branch (PR #1496) independently added CI steps to override VB-Cable's default audio latency on both macOS and Windows, inserted at the same points in the audio setup sequence that are now consolidated into setup-macos-virtual-audio and setup-windows-audio-drivers here -- applying its changes on top of master directly would have conflicted with this refactor. Ported both changes into the composite actions instead, and the corresponding revert is going out on that branch separately so the two don't diverge or duplicate the work: - setup-macos-virtual-audio: adds "Override VB-Cable latency settings", copying the new ci/com.vbaudio.vbcable.plist into /Library/Preferences, right after "Install VB-Cable". - setup-windows-audio-drivers: adds "Set VB-Cable Registry settings" (VBAudioCableWDM/_Latency under HKLM\SOFTWARE\VB-Audio\Cable and the matching CurrentControlSet\Services key), right after "Install VAC" and before "Install VB-Cable", matching the original ordering. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi * Pin freedv_backend to last known-good commit -- RNNoise build regression CI on this PR surfaced a real failure: the macOS lint job's RNNoise ExternalProject build now fails with "C compiler cannot create executables" during ./configure. This traces to freedv-backend#55 (merged 2026-09-18, mine), which baked the ccache launcher into RNNoise's CC ("ccache ") -- something about that combination breaks RNNoise's own configure script on this macOS/LLVM setup in a way the earlier verification (checking the generated ExternalProject command file) didn't catch, since that only confirmed the argument was passed through correctly, not that RNNoise's ./configure would accept it. Ebur128 and RADE's launcher forwarding from the same PR use a different mechanism (CMAKE_ARGS on a nested cmake invocation, not CC) and aren't implicated by this failure -- but freedv-gui has no way to revert just one file's change from a merged commit in another repo, so pinning GIT_TAG back to the immediate parent commit is the only fix available without editing freedv-backend itself, which is out of scope here. The three application-level test failures on this same run (rade_reporting_awgn on Linux, TestFreeDVFullDuplex on both Windows runners) are the same pre-existing RADE audio-timing flakiness already seen on earlier runs, unrelated to this change or the CI refactor -- not touched. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi * Move freedv_backend back to main -- RNNoise regression fixed upstream freedv-backend#56 (merged) replaces the two-word CC/CXX that broke RNNoise's ./configure on macOS lint CI with a generated wrapper script, so the pin from the previous commit is no longer needed. main's HEAD is now 3d605c7, which includes both the original ccache-forwarding fix (#55: Ebur128/RADE/RNNoise) and the RNNoise follow-up fix (#56). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi * macOS lint: use Homebrew llvm@20 instead of install-llvm-action Diagnostic swap for the RNNoise ccache failure investigation. Two independent fix attempts in freedv-backend (two-word CC, then a wrapper script) both failed identically in this job, but the same ccache-launcher-forwarding mechanism works fine elsewhere: Linux's lint job uses the exact same explicit -DCMAKE_C_COMPILER_LAUNCHER=ccache forwarding (clang-21 from apt.llvm.org's llvm.sh) and passes; macOS's dist-pgo-instrument/dist-pgo-build jobs pass too, though those rely on ccache-action's create-symlink PATH trick rather than this job's explicit forwarding. The one thing unique to this failing job is the compiler binary itself: KyleMayes/install-llvm-action's LLVM 20.1. Swap that for `brew install llvm@20 lld@20` (matching dist-pgo-instrument/dist-pgo-build's own package list and comment about LLVM 21 incompatibilities) and prepend its bin dir to PATH via GITHUB_PATH, the same mechanism install-llvm-action used internally. CC=clang/CXX=clang++ and run-clang-tidy in the later steps are unchanged and will resolve through the new PATH entry. Deliberately not touching create-symlink on this job's ccache step -- keeping the explicit -DCMAKE_C_COMPILER_LAUNCHER=ccache forwarding as the only ccache-wiring mechanism in play isolates whether the compiler binary was actually the variable that mattered, since dist-pgo-instrument conflates "Homebrew LLVM" with "create-symlink" and doesn't answer that on its own. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi * macOS lint: fix clang-tidy sysroot detection with Homebrew llvm@20 Switching to Homebrew's llvm@20 fixed the RNNoise ccache failure, but surfaced a different problem: run-clang-tidy failed on all 106 files with errors like "no member named 'ldiv'" and "'TargetConditionals.h' file not found" -- Homebrew's llvm@20, unlike Apple's own clang or install-llvm-action's redistributable, doesn't auto-detect the active Xcode SDK. The actual build step doesn't hit this (CMake sets CMAKE_OSX_SYSROOT itself via its own xcrun-based detection, independent of which clang compiled it), but clang-tidy re-invoking the compiler directly needs the sysroot spelled out. Pass it explicitly via --extra-arg=-isysroot --extra-arg="$(xcrun --show-sdk-path)". Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi * Fix run-clang-tidy flag syntax: single-dash -extra-arg, not --extra-arg The previous commit used GNU-style --extra-arg=, but run-clang-tidy.py (confirmed against LLVM 20's actual source) defines this as a single-dash LLVM-style flag: parser.add_argument("-extra-arg", ...). The existing "-p ." usage in this same command is the same convention -- should have followed it. Python's argparse rejected the double-dash form outright: "unrecognized arguments: --extra-arg=-isysroot --extra-arg=...", never reaching clang-tidy at all. RNNoise's ccache build itself succeeded in this run (confirmed in the logs), so this was the only remaining problem. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi * Add PR #1498 to changelog. --------- Co-authored-by: Claude Sonnet 5 commit 96c1bce79911f26f8a40edf1f7cf7888eda0af54 Author: Mooneer Salem Date: Fri Sep 18 10:08:06 2026 -0700 Add PR #1498 to changelog. commit e5d16fcd60f57083b98e9cc9be2c6cf0082bb9e6 Author: Mooneer Salem Date: Fri Sep 18 08:39:03 2026 -0700 Fix run-clang-tidy flag syntax: single-dash -extra-arg, not --extra-arg The previous commit used GNU-style --extra-arg=, but run-clang-tidy.py (confirmed against LLVM 20's actual source) defines this as a single-dash LLVM-style flag: parser.add_argument("-extra-arg", ...). The existing "-p ." usage in this same command is the same convention -- should have followed it. Python's argparse rejected the double-dash form outright: "unrecognized arguments: --extra-arg=-isysroot --extra-arg=...", never reaching clang-tidy at all. RNNoise's ccache build itself succeeded in this run (confirmed in the logs), so this was the only remaining problem. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi commit 3c870779d81ed1843e65410e540e6bfde1c8d5d1 Author: Mooneer Salem Date: Fri Sep 18 08:29:03 2026 -0700 macOS lint: fix clang-tidy sysroot detection with Homebrew llvm@20 Switching to Homebrew's llvm@20 fixed the RNNoise ccache failure, but surfaced a different problem: run-clang-tidy failed on all 106 files with errors like "no member named 'ldiv'" and "'TargetConditionals.h' file not found" -- Homebrew's llvm@20, unlike Apple's own clang or install-llvm-action's redistributable, doesn't auto-detect the active Xcode SDK. The actual build step doesn't hit this (CMake sets CMAKE_OSX_SYSROOT itself via its own xcrun-based detection, independent of which clang compiled it), but clang-tidy re-invoking the compiler directly needs the sysroot spelled out. Pass it explicitly via --extra-arg=-isysroot --extra-arg="$(xcrun --show-sdk-path)". Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi commit b9d187d2720c9913c78c6d5f665786d22de53c03 Author: Mooneer Salem Date: Fri Sep 18 08:14:37 2026 -0700 macOS lint: use Homebrew llvm@20 instead of install-llvm-action Diagnostic swap for the RNNoise ccache failure investigation. Two independent fix attempts in freedv-backend (two-word CC, then a wrapper script) both failed identically in this job, but the same ccache-launcher-forwarding mechanism works fine elsewhere: Linux's lint job uses the exact same explicit -DCMAKE_C_COMPILER_LAUNCHER=ccache forwarding (clang-21 from apt.llvm.org's llvm.sh) and passes; macOS's dist-pgo-instrument/dist-pgo-build jobs pass too, though those rely on ccache-action's create-symlink PATH trick rather than this job's explicit forwarding. The one thing unique to this failing job is the compiler binary itself: KyleMayes/install-llvm-action's LLVM 20.1. Swap that for `brew install llvm@20 lld@20` (matching dist-pgo-instrument/dist-pgo-build's own package list and comment about LLVM 21 incompatibilities) and prepend its bin dir to PATH via GITHUB_PATH, the same mechanism install-llvm-action used internally. CC=clang/CXX=clang++ and run-clang-tidy in the later steps are unchanged and will resolve through the new PATH entry. Deliberately not touching create-symlink on this job's ccache step -- keeping the explicit -DCMAKE_C_COMPILER_LAUNCHER=ccache forwarding as the only ccache-wiring mechanism in play isolates whether the compiler binary was actually the variable that mattered, since dist-pgo-instrument conflates "Homebrew LLVM" with "create-symlink" and doesn't answer that on its own. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi commit 1f470ad3a9c05485b0a933cf1c54d53189ca6341 Author: Mooneer Salem Date: Thu Sep 17 22:54:50 2026 -0700 Move freedv_backend back to main -- RNNoise regression fixed upstream freedv-backend#56 (merged) replaces the two-word CC/CXX that broke RNNoise's ./configure on macOS lint CI with a generated wrapper script, so the pin from the previous commit is no longer needed. main's HEAD is now 3d605c7, which includes both the original ccache-forwarding fix (#55: Ebur128/RADE/RNNoise) and the RNNoise follow-up fix (#56). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi commit 1e5f767a7e167d51371ddd7ebecb4bd822aeac99 Author: Mooneer Salem Date: Thu Sep 17 22:41:09 2026 -0700 Pin freedv_backend to last known-good commit -- RNNoise build regression CI on this PR surfaced a real failure: the macOS lint job's RNNoise ExternalProject build now fails with "C compiler cannot create executables" during ./configure. This traces to freedv-backend#55 (merged 2026-09-18, mine), which baked the ccache launcher into RNNoise's CC ("ccache ") -- something about that combination breaks RNNoise's own configure script on this macOS/LLVM setup in a way the earlier verification (checking the generated ExternalProject command file) didn't catch, since that only confirmed the argument was passed through correctly, not that RNNoise's ./configure would accept it. Ebur128 and RADE's launcher forwarding from the same PR use a different mechanism (CMAKE_ARGS on a nested cmake invocation, not CC) and aren't implicated by this failure -- but freedv-gui has no way to revert just one file's change from a merged commit in another repo, so pinning GIT_TAG back to the immediate parent commit is the only fix available without editing freedv-backend itself, which is out of scope here. The three application-level test failures on this same run (rade_reporting_awgn on Linux, TestFreeDVFullDuplex on both Windows runners) are the same pre-existing RADE audio-timing flakiness already seen on earlier runs, unrelated to this change or the CI refactor -- not touched. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi commit 8f8d14b74d909a08d1a9450ef05a172ca41a9294 Author: Mooneer Salem Date: Thu Sep 17 20:51:43 2026 -0700 Revert VB-Cable latency CI changes -- moved to ci-reusable-actions These workflow steps (VB-Cable latency plist on macOS, VB-Cable Registry settings on Windows) land at the exact points cmake-macos.yml and cmake-windows.yml are being consolidated into shared composite actions on ci-reusable-actions (setup-macos-virtual-audio and setup-windows-audio-drivers), so keeping them here too would conflict with that refactor on merge. Ported both changes into those composite actions instead, so this branch just picks them up once that PR lands. Reverts the .github/workflows/cmake-macos.yml, .github/workflows/cmake-windows.yml, and ci/com.vbaudio.vbcable.plist changes from: - c8474e96 Debugging -- try to force higher latency for VB-Cable. - 90f213b4 More debugging--apply VB-Cable Registry settings across all Windows runners. - 74705784 Override VB-Cable latency settings on macOS, too. - ac9a6f2d Might work better as a separate file instead. No source changes here -- src/audio/WASAPIAudioDevice.cpp and the rest of this branch's actual FIFO/WASAPI fixes are untouched. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi commit b67a23c47ad4e00ba00f3fc7ea7598937ee547e1 Author: Mooneer Salem Date: Thu Sep 17 20:50:20 2026 -0700 Bring in VB-Cable latency tuning from ms-audio-fifo-wasapi-fixes That branch (PR #1496) independently added CI steps to override VB-Cable's default audio latency on both macOS and Windows, inserted at the same points in the audio setup sequence that are now consolidated into setup-macos-virtual-audio and setup-windows-audio-drivers here -- applying its changes on top of master directly would have conflicted with this refactor. Ported both changes into the composite actions instead, and the corresponding revert is going out on that branch separately so the two don't diverge or duplicate the work: - setup-macos-virtual-audio: adds "Override VB-Cable latency settings", copying the new ci/com.vbaudio.vbcable.plist into /Library/Preferences, right after "Install VB-Cable". - setup-windows-audio-drivers: adds "Set VB-Cable Registry settings" (VBAudioCableWDM/_Latency under HKLM\SOFTWARE\VB-Audio\Cable and the matching CurrentControlSet\Services key), right after "Install VAC" and before "Install VB-Cable", matching the original ordering. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi commit 42d7e82eda2c91e663685d6be98398615aaa8f8a Author: Mooneer Salem Date: Thu Sep 17 20:42:33 2026 -0700 Add scheduled job to prune stale Actions caches on every branch cleanup-caches.yml only handles a branch's caches when its PR closes, which leaves the default branch (no PR to close) and any other long-lived branch's older cache generations unaddressed -- those keep accumulating for as long as the branch exists, since ccache-action writes a brand-new timestamped cache blob every run and nothing else ever removes the previous one. Adds a daily scheduled workflow (plus workflow_dispatch for manual runs) that lists every cache entry repo-wide, groups them by (ref, key with its trailing timestamp stripped), and deletes every entry in a group except the most recently created one. It never touches the newest cache for any (branch, job) pair, so nothing still reachable by ccache-action's restore step is removed. Verified the grouping/survivor logic against the repo's live cache list (94 entries, 68 distinct branch+job groups): every group retained exactly its newest entry with zero errors, and it correctly identified 26 already-superseded duplicates (~2.8 GiB) that have accumulated again since the one-time manual cleanup, which is exactly the ongoing drift this job is meant to catch automatically going forward. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi commit 582bb89ebb75558b3c91c60f1cb650b3d33b02e3 Author: Mooneer Salem Date: Thu Sep 17 20:17:15 2026 -0700 Add workflow to delete a PR branch's Actions caches when it closes While investigating whether the repo was at risk of hitting the 10GB Actions cache quota (it was: 10.03 GiB across 111 entries, meaning GitHub was almost certainly already silently evicting caches), found that 40% of that usage (4.06 GiB / 43 entries) was superseded duplicates -- ccache-action's default append-timestamp behavior writes a brand-new, uniquely-keyed cache blob on every run and never removes the previous one, so every active branch's cache footprint only grows for as long as the branch exists. Deleted those 43 stale entries (repo-wide, confirmed via the caches API), freeing the repo back down to ~5.98 GiB. That only clears the backlog once, though -- it doesn't stop the same accumulation from happening again on every other branch. This adds the other half: delete a PR branch's cache entries once the PR closes, so finished work stops permanently reserving quota that active branches need. Loops over `gh cache list`/`gh cache delete` rather than a single capped call, since a long-lived PR can accumulate more than one page of cache entries. Uses refs/heads/ (not refs/pull//merge, which is what GitHub's own docs example uses) for the BRANCH filter, since our build workflows trigger on push, not pull_request, so that's the ref every cache entry we inspected was actually scoped to -- the docs example wouldn't have matched anything in this repo. Runs on pull_request_target (not pull_request) so it also covers PRs from forks; safe here since the job never checks out or executes the PR's code, only calls the cache API for its branch name. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi commit 71578ef69c514b4fc88ba061ba4af7ef79557be7 Author: Mooneer Salem Date: Thu Sep 17 18:25:19 2026 -0700 Fix CI: composite action steps don't support timeout-minutes CI run on PR #1498 confirmed a real error: GitHub Actions rejected setup-macos-virtual-audio/action.yml with "Unexpected value 'timeout-minutes'" -- that key is only valid on a normal job step, not on a step inside a composite action's own `runs.steps` list. This broke every macOS job that calls the action (test, dist-pgo-instrument, and transitively test-app-folder/dist-pgo-build via needs:). Move the timeout from the inner "Workaround macOS permission issues" step (which can't have it) to the outer `uses:` call site in all three callers instead, covering the whole composite action's four steps. CI results otherwise confirmed the rest of the refactor and the ccache changes are working: every Linux and Windows job passed, including the new windows-audio-test.yml reusable workflow (test/test-sanitizer) and all the other extracted composite actions. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi commit ff54b14f114c5120496f44e8b393fef3e9b09f2e Author: Mooneer Salem Date: Thu Sep 17 17:17:09 2026 -0700 Extend ccache launcher forwarding to the other ExternalProject deps Reviewed every ExternalProject_Add-based dependency for the same gap found in Hamlib: FetchContent-based deps (freedv_backend, wxWidgets, portaudio) are compiled via add_subdirectory() as part of the same CMake generation, so they already inherit CMAKE_C_COMPILER_LAUNCHER/ CMAKE_CXX_COMPILER_LAUNCHER automatically -- no change needed there. The remaining ExternalProject_Add deps did need it: - Extract the CC/CXX-with-launcher logic out of BuildHamlib.cmake into a shared cmake/ExternalProjectCCache.cmake, defining EXTERNAL_PROJECT_CC/EXTERNAL_PROJECT_CXX for any autotools-based ExternalProject to reuse instead of duplicating the same if/else. - Apply it to BuildLame.cmake and BuildMpg123.cmake, which build via ./configure the same way Hamlib does and had the identical gap: neither previously set CC/CXX at all, so both relied entirely on autoconf's own PATH-based (or --host-triggered) compiler detection. - BuildCodec2.cmake and BuildSndfile.cmake build CMake-based deps as a *nested* ExternalProject cmake invocation (CMAKE_ARGS), which gets its own fresh CMakeCache.txt and doesn't inherit the parent's compiler-launcher settings either -- forward CMAKE_C_COMPILER_LAUNCHER/CMAKE_CXX_COMPILER_LAUNCHER there instead, since that's the correct mechanism for a nested CMake build (as opposed to baking it into CC/CXX, which is only needed for autotools). Verified against real generated build.make/CMakeCache output (not just the CMake list/string semantics) for both the launcher-set and launcher-unset cases, and for the mingw-cross-compile, Apple, and native-Linux configure branches. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi commit c4d4b5e52147da30d252c6f606bfa6510595c2b5 Author: Mooneer Salem Date: Thu Sep 17 17:09:45 2026 -0700 Route Hamlib's autotools build through the configured ccache launcher Hamlib is built via ExternalProject_Add's own ./configure && make invocation, which CMake just shells out to. That means it never sees CMAKE_C_COMPILER_LAUNCHER/CMAKE_CXX_COMPILER_LAUNCHER (e.g. ccache) -- those only apply to CMake's own compile rules -- so Hamlib's compile was previously uncached everywhere except one macOS job that happened to have ccache-action's create-symlink PATH workaround enabled. Bake the launcher into CC/CXX for all three configure branches (mingw cross-compile, Apple, and Linux) instead, so Hamlib gets cached the same way as the rest of the project on every platform, without needing create-symlink enabled on every runner that builds it from source. Verified empirically with a standalone CMake script that the resulting CONFIGURE_COMMAND list element is exactly "CC=ccache " (one list element, not split) when a launcher is set, and plain "CC=" with no leading space when it isn't. Co-Authored-By: Claude Sonnet 5 commit 29c8d698b268751f8ff8c191eee13ea0a36b8f47 Author: Mooneer Salem Date: Thu Sep 17 16:56:10 2026 -0700 Refactor CI workflows to use reusable composite actions Extracts duplicated step sequences from cmake-linux.yml, cmake-macos.yml, and cmake-windows.yml into shared composite actions under .github/actions/, and consolidates the near-identical Windows "test"/"test-sanitizer" jobs into a single parameterized reusable workflow (windows-audio-test.yml). New composite actions: - install-llvm-apt-pool: apt.llvm.org pool install (was duplicated 3x) - setup-rtkit-linux: rtkit/polkit setup for Ubuntu (was duplicated 3x) - setup-pulseaudio-legacy: PulseAudio config for Ubuntu 22.04 (was 3x) - setup-rade-python-env: RADE venv + torch/matplotlib install (was 5x) - run-linux-gui-test: Xvfb/dbus/metacity display setup (was 4x) - setup-macos-virtual-audio: VB-Cable + permission workarounds (was 3x) - download-mingw-llvm: pinned llvm-mingw toolchain download (was 3x) - setup-windows-audio-drivers / configure-windows-audio-devices: VAC/ VB-Cable install and Windows audio device configuration (was 3x) - configure-windows-crash-dumps: WER crash dump registry setup (was 2x) This reduces cmake-linux.yml, cmake-macos.yml, and cmake-windows.yml by about 820 lines combined while preserving existing job behavior. A few inert simplifications were made along the way (e.g. reordering some macOS setup steps, unifying near-identical if/else branches whose bodies differed only in already-duplicated display-setup code) and are called out in the accompanying summary. Co-Authored-By: Claude Sonnet 5 commit ac9a6f2d541aafe5d7e8ce307a300141a45192aa Author: Mooneer Salem Date: Thu Sep 17 09:22:28 2026 -0700 Might work better as a separate file instead. commit 7470578416c757b44f74da90a6f4dd8f4921677e Author: Mooneer Salem Date: Thu Sep 17 02:52:51 2026 -0700 Override VB-Cable latency settings on macOS, too. commit 90f213b4c21b53ad8969eafa5ba74bc782707f9d Author: Mooneer Salem Date: Thu Sep 17 02:27:18 2026 -0700 More debugging--apply VB-Cable Registry settings across all Windows runners. commit c8474e96a3606605b22117a2bb2a4eeeec173c33 Author: Mooneer Salem Date: Thu Sep 17 01:34:40 2026 -0700 Debugging -- try to force higher latency for VB-Cable. commit 592f5313b0e58dea3d70c5bbb8c1241ba196a0ee Author: Mooneer Salem Date: Wed Sep 16 22:41:09 2026 -0700 Fix Reporter window text not scaling with Windows display scaling ReportMessageRenderer draws the FreeDV Reporter list's Call/Version/ Message columns through a hand-rolled Direct2D path on Windows (added to support color emoji) instead of the normal wxDC::DrawText() used elsewhere in the app. wxGraphicsRenderer::CreateContextFromUnknownDC() doesn't reliably propagate the owning control's per-monitor DPI scale factor to the resulting context, so text drawn through it can stay pinned to 96 DPI and fail to grow when Windows display scaling is set above 100%, even though the rest of the DPI-aware UI scales normally. Explicitly set the graphics context's content scale factor from the owning wxDataViewCtrl before drawing. Fixes #1491. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_018JYqaVNT7DbgmKePHQtNYv commit b751c68f1a95215113fbf1ed358df5ed453f767a Author: Mooneer Salem Date: Wed Sep 16 08:25:48 2026 -0700 WASAPIAudioDevice: sleep-then-spin using NtQueryTimerResolution for stopRealTimeWork WaitForMultipleObjects on a high-resolution waitable timer can only wake on a scheduler tick, and once woken still has to wait to actually be scheduled -- both add jitter on top of the wait target itself. This is plausibly a contributor to the intermittent sync-loss failures seen on the windows-11-arm CI runner during recent audio-path investigation. Query the OS's current timer resolution via NtQueryTimerResolution (loaded dynamically since it isn't exported by any import library), sleep for everything except the last tick of that resolution, then spin-wait the remainder against steady_clock. The abort-early behavior is preserved at both stages via semaphore_: a blocking WaitForSingleObject during the sleep phase, and a non-blocking poll each iteration of the spin. Falls back to the original SetWaitableTimer + WaitForMultipleObjects approach if NtQueryTimerResolution can't be resolved. Untested on real hardware -- no Windows toolchain available locally. Needs CI/manual confirmation, ideally with wait-jitter instrumentation to check whether it actually tightens things up on windows-11-arm. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_018JYqaVNT7DbgmKePHQtNYv commit 4b8c38e4402e433566c1cbd433402771dbee9dc0 Author: Mooneer Salem Date: Tue Sep 15 22:29:23 2026 -0700 Harden reliableTextFifo_ read and fix playbackResampler_ data race FreeDVInterface::getReliableText() ignored read()'s return value into an uninitialized stack buffer; a failed read (currently unreachable given the FIFO's one-record capacity and mutex-serialized consumer side, but fragile) would have built a std::string from uninitialized stack memory. Now zero-inits the buffer and only uses it on success. PlaybackStep::playbackResampler_ was a raw pointer whose lifecycle is owned exclusively by the file I/O thread (nonRtThreadEntry_), but reset() -- called on the real-time pipeline thread on every RX->TX transition -- read and dereferenced it without any synchronization. That's a torn-pointer read, a possible use-after-free (racing the file I/O thread's delete/recreate on a sample-rate change), and a possible concurrent mutation of the underlying libsamplerate state during a live execute(). Since the RT thread can't take a lock, reset() now just raises a lock-free atomic flag; the file I/O thread consumes it and performs the actual resampler reset itself. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_018JYqaVNT7DbgmKePHQtNYv commit ac5adb295fb8bf5321d00c835ff5b4b8d904e342 Author: Mooneer Salem Date: Tue Sep 15 21:02:59 2026 -0700 Handle GenericFIFO read() failures instead of replaying stale audio outfifo1/outfifo2 reads in the audio callbacks and PlaybackStep::execute() ignored read()'s return value. A read that numUsed()/canRead_ reported as available can still fail if it races with a concurrent reset() (e.g. on a TX/RX transition or resampler rebuild); when that happened, the code played out whatever was left in the reused scratch buffer from the previous callback instead of silence. Also fixes OnRxOutAudioData_ to zero-fill the remainder of the buffer on underflow, matching its sibling callbacks. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_018JYqaVNT7DbgmKePHQtNYv commit 69473b0561e4aad8ae3d1b1dd0b86a7bee051bed Author: Mooneer Salem Date: Wed Sep 16 08:25:48 2026 -0700 WASAPIAudioDevice: sleep-then-spin using NtQueryTimerResolution for stopRealTimeWork WaitForMultipleObjects on a high-resolution waitable timer can only wake on a scheduler tick, and once woken still has to wait to actually be scheduled -- both add jitter on top of the wait target itself. This is plausibly a contributor to the intermittent sync-loss failures seen on the windows-11-arm CI runner during recent audio-path investigation. Query the OS's current timer resolution via NtQueryTimerResolution (loaded dynamically since it isn't exported by any import library), sleep for everything except the last tick of that resolution, then spin-wait the remainder against steady_clock. The abort-early behavior is preserved at both stages via semaphore_: a blocking WaitForSingleObject during the sleep phase, and a non-blocking poll each iteration of the spin. Falls back to the original SetWaitableTimer + WaitForMultipleObjects approach if NtQueryTimerResolution can't be resolved. Untested on real hardware -- no Windows toolchain available locally. Needs CI/manual confirmation, ideally with wait-jitter instrumentation to check whether it actually tightens things up on windows-11-arm. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_018JYqaVNT7DbgmKePHQtNYv commit 5d73ce3748886087e04b667efe5646beb075c331 Author: Mooneer Salem Date: Tue Sep 15 22:29:23 2026 -0700 Harden reliableTextFifo_ read and fix playbackResampler_ data race FreeDVInterface::getReliableText() ignored read()'s return value into an uninitialized stack buffer; a failed read (currently unreachable given the FIFO's one-record capacity and mutex-serialized consumer side, but fragile) would have built a std::string from uninitialized stack memory. Now zero-inits the buffer and only uses it on success. PlaybackStep::playbackResampler_ was a raw pointer whose lifecycle is owned exclusively by the file I/O thread (nonRtThreadEntry_), but reset() -- called on the real-time pipeline thread on every RX->TX transition -- read and dereferenced it without any synchronization. That's a torn-pointer read, a possible use-after-free (racing the file I/O thread's delete/recreate on a sample-rate change), and a possible concurrent mutation of the underlying libsamplerate state during a live execute(). Since the RT thread can't take a lock, reset() now just raises a lock-free atomic flag; the file I/O thread consumes it and performs the actual resampler reset itself. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_018JYqaVNT7DbgmKePHQtNYv commit ce42017a152e893873ed399453557340d3da6965 Author: Mooneer Salem Date: Tue Sep 15 21:02:59 2026 -0700 Handle GenericFIFO read() failures instead of replaying stale audio outfifo1/outfifo2 reads in the audio callbacks and PlaybackStep::execute() ignored read()'s return value. A read that numUsed()/canRead_ reported as available can still fail if it races with a concurrent reset() (e.g. on a TX/RX transition or resampler rebuild); when that happened, the code played out whatever was left in the reused scratch buffer from the previous callback instead of silence. Also fixes OnRxOutAudioData_ to zero-fill the remainder of the buffer on underflow, matching its sibling callbacks. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_018JYqaVNT7DbgmKePHQtNYv commit 97e8cbc279fc46a74c62e84e6112d15fc4e11bf5 Author: Mooneer Salem Date: Tue Sep 15 16:50:44 2026 -0700 s/RADEV1/RADEV2/g in test files. commit 1f690e2dbdbdc436510bbc1f266ccae01ce5e658 Merge: 5b50954a ab701df5 Author: Mooneer Salem Date: Tue Sep 15 13:02:34 2026 -0700 Merge branch 'v3.0-dev' into ms-agc-tweaks-test commit d3fe1001c3b3e3dc0a70e828c8325b6e4d2474c5 Merge: 24c12b4c ab701df5 Author: Mooneer Salem Date: Tue Sep 15 13:02:15 2026 -0700 Merge branch 'v3.0-dev' into ms-rade-v2 commit ab701df5527c7239c750b5e3b604acb70bd41852 Merge: 79951b37 6e975f0d Author: Mooneer Salem Date: Tue Sep 15 13:01:58 2026 -0700 Merge branch 'master' into v3.0-dev commit 6e975f0d31946870c00f4aa38c0bd385937d878c Author: Mooneer Salem Date: Tue Sep 15 13:01:02 2026 -0700 Fix Ubuntu compiler error from latest changes. (#1493) * Fix Ubuntu compiler error from latest changes. * Add PR #1493 to changelog. commit 1acaf935bc9f51eaf588cae1172ddc5bebc432ac Author: Mooneer Salem Date: Tue Sep 15 13:00:42 2026 -0700 [skip ci] Add PR #1493 to changelog. commit 3d43134cd8e61d5cfa1cba95214c2ee6cd26771e Merge: b3af4577 e56570a8 Author: Mooneer Salem Date: Tue Sep 15 12:59:12 2026 -0700 Merge branch 'master' into ms-freedv-interface-compiler-err commit e56570a8f5ec7229362b7c5e3fc91eacaca509d7 Author: Mooneer Salem Date: Tue Sep 15 12:58:42 2026 -0700 Port ms-rade-v2 audio class fixes. (#1494) * Port ms-rade-v2 audio class fixes. * [skip ci] Add PR #1494 to changelog. commit eced0a5f846c7e90a5f66ee819153177ee3db5b4 Author: Mooneer Salem Date: Tue Sep 15 12:58:19 2026 -0700 [skip ci] Add PR #1494 to changelog. commit 9742ff9bc6b13417aaf9dac749ca0401ef9011d1 Author: Mooneer Salem Date: Tue Sep 15 08:46:27 2026 -0700 Port ms-rade-v2 audio class fixes. commit b3af4577cd02479605c7b9a4cecaa61f1ae844f1 Author: Mooneer Salem Date: Tue Sep 15 08:42:57 2026 -0700 Fix Ubuntu compiler error from latest changes. commit 31c514161e2f24986ed7b5f8860784631fa5c3f8 Merge: 24c12b4c 4438bd0a Author: Mooneer Salem Date: Tue Sep 15 07:51:03 2026 -0700 WIP on ms-rade-v2: 24c12b4c MacAudioDevice: anchor wait compensation to startTime_, not last cycle's debt commit 4438bd0ad7f843fbe15be15ab30ef5d64bac67e1 Author: Mooneer Salem Date: Tue Sep 15 07:51:03 2026 -0700 index on ms-rade-v2: 24c12b4c MacAudioDevice: anchor wait compensation to startTime_, not last cycle's debt commit 742f66c56b70070c19bd26ac994b2fdfa4cd9dc9 Author: Mooneer Salem Date: Mon Sep 14 15:46:09 2026 -0700 Add wx<3.1 compatibility shim so FromDIP() doesn't break Ubuntu 22.04 wxWindow::FromDIP() was added in wxWidgets 3.1.0, but Ubuntu 22.04 CI (ubuntu-22.04, ubuntu-22.04-arm) builds against the system libwxgtk3.0 package, and CMakeLists.txt happily uses whatever wxWidgets >= 3.0.0 it finds on the system rather than always bootstrapping a newer bundled copy. Without a fallback, the previous commit's FromDIP() calls would fail to compile there. Add src/gui/util/DipCompat.h: under wx < 3.1.0 it declares global FromDIP() overloads (no-op passthrough) that unqualified calls inside wxWindow-derived classes fall through to via ordinary C++ lookup rules, since those classes have no such member on older wx. On wx >= 3.1.0 the real member always wins over the global fallback, so behavior there is unchanged. A WX_STATIC_FROM_DIP macro covers the handful of call sites that use the qualified static wxWindow::FromDIP(size, window) form as a constructor default argument, which can't rely on that same fallback. Verified by temporarily forcing the pre-3.1 branch to compile against the real (3.3.3) wx headers here: no ambiguity, no unused-function warnings under -Werror, full app builds and links. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 91fa8c4bf9f386dd00ea263daf9326d5391cfbdd Author: Mooneer Salem Date: Mon Sep 14 15:35:36 2026 -0700 Scale hardcoded UI sizes for DPI awareness on Windows The Windows resource template (contrib/freedv.rc.in) already declares Per-Monitor-V2 DPI awareness via wxUSE_DPI_AWARE_MANIFEST 2, but no code used wxWidgets' FromDIP() to scale the ~170 hardcoded pixel sizes used across the main window and dialogs. Because the app claims DPI awareness, Windows will not bitmap-stretch it to compensate, so on a high-DPI display everything rendered undersized/cramped. Wrap all literal wxSize()/SetSize()/SetMinSize()/SetSizeHints() pixel values with FromDIP() so layouts scale with the monitor's DPI. Default constructor size arguments (which can't reference the parent window) use the static wxWindow::FromDIP(size, nullptr) form; everything else uses the unqualified member form since it runs after the owning window is constructed. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 24c12b4c13fff933945101712e4f6c6b605013be Author: Mooneer Salem Date: Mon Sep 14 13:49:27 2026 -0700 MacAudioDevice: anchor wait compensation to startTime_, not last cycle's debt Ports WASAPIAudioDevice's stopRealTimeWork() fix (36db96e5) to MacAudioDevice, which still had the original single-cycle-lagged extraTimeMs_ debt from 5a2d84cf: it corrected this cycle's wait for last cycle's overrun instead of this cycle's own, which overcorrects/undercorrects whenever processing time varies cycle to cycle. Now measures this cycle's own elapsed processing time directly against startTime_ before deciding how long to wait, and tracks only the wait's own overshoot separately (waitOvershootUs_) so a scheduling overshoot still can't accumulate into drift. Likely contributor to the macOS CI rade_loss flakiness seen on ms-rade-v2 (loss spiking to 9-21x threshold on some runs vs. a clean baseline-level result on others) -- the signature of an occasional dropout/desync rather than steady-state distortion. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 0b12785267623cf205478600004f7dc4387a2217 Author: Mooneer Salem Date: Mon Sep 14 10:53:47 2026 -0700 WASAPIAudioDevice: raise MMCSS thread priority to Critical AvSetMmThreadCharacteristics() alone only enrolls a thread in the "Pro Audio" MMCSS class at that class's default (Normal) priority band. The render/capture thread and the TxRxThread TX/RX worker threads (both routed through this shared setHelperRealTime()) sit on the critical path for stopRealTimeWork()'s wait, so bump them to the top of the band via AvSetMmThreadPriority(..., AVRT_PRIORITY_CRITICAL) to reduce post-wake scheduling delay -- the likely source of the wait stdev/max jitter seen in CI, especially on windows-11-arm. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit f4d5efccfe884c6bcc379586467650ad659eeaac Author: Mooneer Salem Date: Mon Sep 14 17:26:58 2026 +0000 latest user manual PDF commit f0cb2a399602bf83310050ac8fade3b3dd924827 Author: Mooneer Salem Date: Mon Sep 14 10:24:37 2026 -0700 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 , 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 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk * Add PR #1489 to changelog. --------- Co-authored-by: Claude Sonnet 5 commit 697d7ace8e183af8acca8fcff65e6732e0d1583f Author: Mooneer Salem Date: Mon Sep 14 10:24:21 2026 -0700 Add PR #1489 to changelog. commit 379d497fbecc545180d36b1457d6491c50e92818 Author: Mooneer Salem Date: Mon Sep 14 08:28:16 2026 -0700 Add Ubuntu gcc-mingw-w64 cross-compile toolchain for Windows builds Adds a CMake toolchain file (cross-compile/freedv-mingw-gcc-x86_64.cmake) to build FreeDV for Windows using Ubuntu's packaged gcc-mingw-w64 instead of llvm-mingw, plus the supporting changes needed to make it work: - CMakeLists.txt: set CMAKE_C/CXX_STANDARD_LIBRARIES (-lssp) after project() so it isn't clobbered by CMake's Windows-GNU platform defaults, and gate the .pdb install on FREEDV_USING_LLVM_MINGW. - cmake/BuildWxWidgets.cmake: disable wxUSE_STD_IOSTREAM for this toolchain to avoid a libstdc++.a/mingw-w64 header ABI mismatch (fpos vs fpos<_Mbstatet>) in wx's explicit template instantiations. - cmake/GetDependencies.cmake.in: locate GCC's runtime DLLs (libstdc++-6.dll, libwinpthread-1.dll) dynamically via `${CMAKE_CXX_COMPILER} -print-file-name=` for CPack packaging, since this toolchain has no fixed install layout to rely on the way llvm-mingw's does. - src/rig_control/omnirig/CMakeLists.txt: silence a GCC 13 warning ("initialized and declared 'extern'") in MIDL-generated OmniRig_i.c that -Werror would otherwise trip. - src/audio/WASAPIAudioEngine.cpp: use wxString::ToStdString() at two call sites that relied on wxString's ostream operator<<, which wxUSE_STD_IOSTREAM=OFF removes. The toolchain file itself deliberately avoids UCRT (-D_UCRT/-lucrt/ -lucrtbase): mixing UCRT and classic-msvcrt CRT internals in one statically-linked binary is not safe (confirmed by a real crash -- msvcrt's internal _lock() received a garbage index from UCRT-side code, corrupting the next indirect call). -static-libgcc -static-libstdc++ -static and -Wl,--allow-multiple-definition are both still needed independent of that: the former avoids a real cross-DLL static-init/TLS risk for the threaded C++ runtime, and the latter works around an LTO/COMDAT-folding issue with wx's virtual method thunks. CI workflow changes (matrix/job restructuring to use this toolchain) are intentionally excluded from this commit. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 312eb10f2e8f9c98852efba291a9eeb5863bb2e0 Author: Mooneer Salem Date: Sun Sep 13 22:54:09 2026 -0700 Restore -Wl,--allow-multiple-definition, needed for an unrelated LTO issue CI (which builds with -DENABLE_LTO=1) failed after the previous commit removed this flag: ld reported "multiple definition" of wxTextCtrlBase virtual-method thunks (SetValue/GetValue) across LTO-plugin objects with different vtable-offset ("Thn") adjustments. This is a genuine LTO/COMDAT-folding issue in wx's own virtual dispatch, completely unrelated to the CRT-mixing bug the previous commit fixed -- it reproduced identically with -D_UCRT/-lucrt/-lucrtbase already removed. The flag's original comment only mentioned the (now-eliminated) msvcrt/ucrt wide-char overlap as justification, which is why it looked safe to remove; it was actually covering two unrelated problems at once. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit ae57a4c1d785e29be82b25e0587dddd3d6404c7d Author: Mooneer Salem Date: Sun Sep 13 22:27:17 2026 -0700 Stop mixing UCRT and classic msvcrt in the gcc-mingw-w64 toolchain -D_UCRT plus -lucrt/-lucrtbase were added to satisfy one C11 timespec_get() call in freedv-backend's logging code, papering over the resulting msvcrt.a/ucrt.a symbol overlap with -Wl,--allow-multiple-definition. That mixing turned out to be unsafe: statically linking both CRTs let UCRT-side code (behind ucrtbase's __acrt_iob_func) call into msvcrt's incompatible internal _lock() with a garbage table index, corrupting the next indirect call and crashing freedv.exe -- this is what the wxVListBox::DoSelectAll / libstdc++ crashes seen in CI actually were, not a wx or cross-DLL static-init bug. Confirmed via Wine relay tracing and reproduced in under 2 seconds with a ~40-line wx test program. freedv-backend's ulog_async.cpp now uses clock_gettime() instead, which mingw-w64 provides via winpthreads on the classic msvcrt target with no UCRT involved, so none of this is needed anymore: -D_UCRT, -lucrt, -lucrtbase, and -Wl,--allow-multiple-definition (whose only purpose was tolerating the resulting msvcrt/ucrt overlap) are all removed. -static-libgcc -static-libstdc++ -static is kept since it's still worth having, though it turned out not to be the fix for the crash it was originally added for. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit cdd764b0dad64c2a018893225c8d74186911a889 Author: Mooneer Salem Date: Sun Sep 13 20:16:43 2026 -0700 Statically link libstdc++/libgcc/libwinpthread to fix a real crash The build and packaging now succeed end to end (previous commits), but CI's runtime test job crashed freedv.exe on every run. Pulled the crash dumps CI captured and parsed them directly (no Windows debugger available): STATUS_ACCESS_VIOLATION (0xc0000005), writing to address 0x8 -- a null-pointer member write -- with the faulting address resolving into libstdc++-6.dll itself, per the dump's module list. This is a known class of bug with mingw-w64's threaded C++ runtime split across separate DLLs (libstdc++-6.dll, libgcc_s_seh-1.dll, libwinpthread-1.dll): cross-DLL static-initialization-order/TLS issues can corrupt or race iostream/locale globals. The standard fix is to statically link the C++ runtime into the executable instead. Verified directly: relinked the already-built freedv.exe on the test VM with -static-libgcc -static-libstdc++ -static added (no rebuild needed, only the link command changes) and confirmed via objdump -p that libstdc++-6.dll/libgcc_s_seh-1.dll/libwinpthread-1.dll/libssp-0.dll all disappear from its dependency list. Also confirmed librade.dll and libhamlib-4.dll are pure C (no libstdc++ dependency), so this doesn't risk two coexisting C++ runtime instances -- limited to CMAKE_EXE_LINKER_FLAGS for that reason, not CMAKE_SHARED_LINKER_FLAGS. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 2c6ebed82206b41323216f37e7a9bffbe22ae078 Author: Mooneer Salem Date: Sun Sep 13 19:49:53 2026 -0700 Skip Linux/macOS CI on this branch while iterating on Windows toolchain This branch only touches the Windows build (Ubuntu's gcc-mingw-w64 instead of llvm-mingw); the Linux/macOS workflows are unrelated and were competing with the Windows job for what appears to be a limited shared runner-concurrency budget on this repo, causing multi-hour queuing delays for all three. Exclude this branch from their triggers while iterating -- remove the exclusion before merging. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 4848906080696e13e7ea8abb1dd89c8f7645b8ac Author: Mooneer Salem Date: Sun Sep 13 19:47:25 2026 -0700 Fix CPack packaging for gcc-mingw-w64: missing DLLs and .pdb install freedv.exe itself now compiles and links cleanly (previous commits), but `make package` still failed for two packaging-only reasons: 1. install(FILES .../freedv.pdb) was unconditional on WIN32, but a .pdb is only ever produced by llvm-mingw's lld (via -Wl,--pdb= in that toolchain file) -- gcc-mingw-w64 embeds DWARF debug info in the binary instead, so the file never exists there. Gated on FREEDV_USING_LLVM_MINGW too. 2. GetDependencies.cmake.in's runtime-DLL bundling only knew how to find llvm-mingw's self-contained bin/ directory. Ubuntu's gcc-mingw-w64 splits its runtime across two separate locations: libstdc++-6.dll/libssp-0.dll/libgcc_s_seh-1.dll live next to the compiler (in a GCC-version- and threading-variant-specific path, e.g. .../gcc/x86_64-w64-mingw32/13-posix/), while libwinpthread-1.dll (needed since this branch uses the posix-threading variant) lives in the target sysroot's lib/ instead. CPack failed outright on the latter: "Could not find RUNTIME_libwinpthread-1.dll". Added a parallel branch that asks the compiler where each DLL actually is via -print-file-name=, rather than hardcoding either path (both vary by GCC version/threading model/distro) -- confirmed directly against Ubuntu 24.04's gcc-mingw-w64 13-posix via a standalone cmake -P run. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 9054d658653559ace394152e8cff7458684fe13c Author: Mooneer Salem Date: Sun Sep 13 19:09:51 2026 -0700 Run gcc-mingw-w64 build job on ubuntu-24.04, not ubuntu-22.04 ubuntu-22.04 is a leftover from the llvm-mingw workflow, where the host Ubuntu version didn't matter since llvm-mingw was a self-contained, version-pinned download unrelated to apt. For this Ubuntu-packaged gcc-mingw-w64 approach it matters a great deal: 22.04 ships gcc-mingw-w64-x86-64 10.3.0 / mingw-w64-x86-64-dev 8.0.0, which is old enough that some networking headers aren't fully declared -- confirmed via CI: freedv-backend's pskreporter.cpp failed with "'AI_V4MAPPED' was not declared in this scope" (a constant), a symbol gap that never showed up in any of this branch's local verification because that was all done on Ubuntu 24.04 (13.2.0 / 11.0.1). Every gcc-mingw-w64-specific fix in this branch (UCRT libs, libssp, --allow-multiple-definition, the OmniRig_i.c -Werror exception, the wxUSE_STD_IOSTREAM libstdc++ ABI workaround) was verified against 24.04's toolchain versions, not 22.04's -- align CI with that rather than re-deriving a second set of fixes for an older, more limited mingw-w64 release. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit abd99a51e34198602438eca2ec19990ca8a98e0b Author: Mooneer Salem Date: Sun Sep 13 18:50:32 2026 -0700 Fix ambiguous operator<< for wxString after disabling wxUSE_STD_IOSTREAM wxString's std::ostream operator<< overload is itself gated behind wxUSE_STD_IOSTREAM, so disabling that flag (previous commit, to work around a gcc-mingw-w64 libstdc++ ABI skew) turned these two direct `stringstream << spec.name` calls ambiguous: the compiler was left choosing between wxString's implicit conversions to bool/const void*/ const char*/const wchar_t* (deleted), with no exact match winning. Use .ToStdString() (the existing convention elsewhere in this file's neighbors, e.g. main.cpp/ongui.cpp) instead of relying on wxString's own stream operator, which doesn't depend on wxUSE_STD_IOSTREAM at all. Checked every other std::stringstream/ostringstream use in code this Windows build actually compiles (TxRxThread.cpp, freedv_reporter.cpp, HamlibRigController.cpp, OmniRigController.cpp, WASAPIAudioDevice.cpp) -- none of them stream a wxString directly, so this is the only place that needed it. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit cf492f569fb4480ef50c42ac82240ceee19881a3 Author: Mooneer Salem Date: Sun Sep 13 18:43:17 2026 -0700 Disable wxUSE_STD_IOSTREAM for gcc-mingw-w64 (libstdc++ ABI skew) Ubuntu's gcc-mingw-w64 13.2.0 package ships a prebuilt libstdc++.a whose explicit instantiation of std::basic_streambuf::seekpos (and friends) uses std::fpos, while the currently-installed mingw-w64-x86-64-dev headers (which is what wxWidgets itself compiles against) define std::fpos<_Mbstatet> with _Mbstatet as a struct (wchar.h), not the int typedef a legacy compatibility header (yvals.h) also provides. This is a real ABI/version skew between the two separately-versioned Ubuntu packages, not anything specific to this project or fixable from a toolchain file -- confirmed via `nm` on libstdc++.a: it only defines the fpos overload, and wx's compiled objects only reference the fpos<_Mbstatet> one, so the two never match regardless of what's linked or in what order. wxTextCtrlBase/wxComboCtrlTextCtrl/wxPrintPageTextCtrl (see wxUSE_STD_IOSTREAM in ) inherit from std::streambuf to redirect C++ iostreams through a text control -- a feature FreeDV doesn't use. Disabling it removes that inheritance (and the mismatched vtable reference) entirely, without needing to touch the actual toolchain packaging bug. Confirmed the exact symbol mismatch via `x86_64-w64-mingw32-nm libstdc++.a | c++filt`: only std::fpos overloads are defined there, never std::fpos<_Mbstatet>. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit d6ba1d5623eb92474a480031469303f350c349b3 Author: Mooneer Salem Date: Sun Sep 13 18:33:51 2026 -0700 Silence -Werror on MIDL-generated OmniRig_i.c under gcc-mingw-w64 GCC 13's mingw-w64 target warns on every GUID definition in this file ("'X' initialized and declared 'extern'", from the standard MIDL EXTERN_C + __declspec(selectany) idiom), with no dedicated -W flag to disable individually -- confirmed via `gcc -Q --help=warning`, nothing matches. This is vendored, MIDL-generated COM interface glue, not hand-written code to "fix", so turn -Werror back into a warning for just this one file rather than relaxing it for the whole target (or project-wide). Confirmed via a full remote rebuild of just this target on Ubuntu 24.04 with gcc-mingw-w64 13-posix. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 4e533424c9d0371d8a07ac8a377af1c7e988bff5 Author: Mooneer Salem Date: Sun Sep 13 18:22:58 2026 -0700 Move extra runtime libs to CMakeLists.txt; add missing -D_UCRT Empirically confirmed (via a throwaway CMake project on the same Ubuntu 24.04 toolchain) that CMake's Windows-GNU platform file unconditionally repopulates CMAKE_C_STANDARD_LIBRARIES/CMAKE_CXX_ STANDARD_LIBRARIES (e.g. with "-lkernel32 -luser32 ...") during language enablement, which runs *after* the toolchain file and silently discards whatever it set there -- explaining why -lssp/-lucrt/ -lucrtbase never actually made it into linkLibs.rsp despite being "set" in the toolchain file (confirmed missing entirely from a user's build tree). Setting the same variables in CMakeLists.txt right after project() instead works: at that point the variable already holds the platform's populated default, so appending to it survives. This also correctly propagates to plain add_subdirectory()/FetchContent subprojects (confirmed: a nested project() call for an already-enabled language doesn't re-trigger platform-default population) -- just not to genuinely separate ExternalProject_Add invocations like freedv-backend's build_rade, which needs an explicit forward (see the freedv-backend fix forwarding these same two variables via CMAKE_ARGS). Also adds -D_UCRT, which turned out to have never actually been in this toolchain file -- it was a local addition made (before this back-and-forth started) directly in a tester's checkout, never committed here. It's genuinely required: confirmed mingw-w64 doesn't even declare timespec_get()/TIME_UTC without it, regardless of what's linked. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit a0e5e3b05799ea622ca4a191824da5e40f18e4ac Author: Mooneer Salem Date: Sun Sep 13 17:47:49 2026 -0700 Allow multiple definitions to resolve libucrt/libmsvcrt overlap Now that -lucrt actually links (previous commit fixed its ordering), its objects get pulled in alongside g++'s own unconditionally-appended -lmsvcrt (there's no UCRT-native mingw-w64 triple on Ubuntu the way MSYS2 packages one separately -- libucrt.a/libucrtbase.a here are just additional opt-in import libraries layered onto the classic msvcrt-default toolchain). Both archives provide a handful of overlapping legacy wide-char functions (wcsrtombs, mbsrtowcs so far), which is a multiple-definition error even though the implementations are functionally interchangeable for these. -Wl,--allow-multiple-definition is the standard workaround for this known mingw-w64 msvcrt+ucrt overlap. Unlike the -l flags added in the previous commit, this is a global linker policy rather than a library reference, so it doesn't need CMAKE_*_STANDARD_LIBRARIES positioning -- CMAKE_EXE_LINKER_FLAGS's early placement doesn't matter here. Confirmed via a user's local build: switching to CMAKE_*_STANDARD_ LIBRARIES already resolved the __imp__timespec64_get and __intrinsic_setjmpex errors; this is the next error in the same run. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit dee72c44fb63d07044605ee23c8e6e1efb439e00 Author: Mooneer Salem Date: Sun Sep 13 17:35:53 2026 -0700 Use CMAKE_*_STANDARD_LIBRARIES so extra runtime libs actually link CMAKE_EXE_LINKER_FLAGS/CMAKE_SHARED_LINKER_FLAGS content gets placed *before* the target's object files and libraries in CMake's default link-executable rule, but ld only pulls a static archive's symbols in to satisfy references it already knows are outstanding at that point in its left-to-right scan -- it never backtracks. Confirmed directly from a user's local build log: .../g++ ... -lucrt -lssp -lucrt -lssp -mwindows -Wl,--whole-archive CMakeFiles/freedv.dir/objects.a -Wl,--no-whole-archive ... @CMakeFiles/freedv.dir/linkLibs.rsp -lucrt/-lssp were scanned before anything had asked for their symbols, so they silently contributed nothing -- explaining why the previous __memcpy_chk fix's mechanism was already wrong in principle (it likely only appeared to work for lpcnet_demo, a smaller/simpler link line, by coincidence) and why adding -lucrtbase the same way didn't help either. CMAKE__STANDARD_LIBRARIES is CMake's own mechanism for exactly this: it's a separate placeholder in the link rule positioned *after* the target's libraries, which is why the toolchain's existing -lkernel32 -luser32 etc. (set by CMake's own Windows-GNU platform file via this same variable) have always resolved fine. (Duplicated -lucrt/-lssp entries in that log are a separate, harmless artifact of reconfiguring the same build_windows directory across multiple toolchain-file edits without a clean rebuild -- worth a `rm -rf build_windows` to get a clean signal, but not the cause of the failures.) Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 926ca8ff411a50ef6470e994fb9e336d4875dc48 Author: Mooneer Salem Date: Sun Sep 13 13:45:37 2026 -0700 Link libssp to fix undefined __memcpy_chk with Ubuntu's gcc-mingw-w64 Ubuntu's gcc bakes in _FORTIFY_SOURCE hardening by default even when cross-compiling, so calls like memcpy() with a compile-time-known destination size (RADE/opus's dnn/nnet.c hit this) get rewritten to __memcpy_chk() etc. On Linux those come from glibc; mingw-w64 has no libc of its own providing them, so linking failed: undefined reference to `__memcpy_chk' libssp.a, shipped by gcc-mingw-w64-x86-64, provides the fortify-source runtime for mingw targets. Confirmed via the PR's second CI run. If this recurs for one of the autotools-driven sub-builds (hamlib, mp3lame, mpg123 -- these don't inherit CMAKE_EXE_LINKER_FLAGS since they're not CMake-configured), they'll need -lssp added to their own LDFLAGS instead. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 58c0f8cf262b007aca1805e8b3d3739a79825847 Author: Mooneer Salem Date: Sun Sep 13 13:20:37 2026 -0700 Fix missing osslsigncode on PATH during signed configure step CMakeLists.txt's find_program(OSSLSIGNCODE_BIN osslsigncode REQUIRED) runs at CMake configure time, not build time, so the "Configure freedv-gui (signed)" step needs osslsigncode's build directory on PATH before invoking cmake -- not just the later build step. Dropped when the llvm-mingw PATH export was removed from this step; the llvm-mingw part was correctly removed (apt installs gcc-mingw-w64 straight onto system PATH) but took the still-needed osslsigncode entry with it. Confirmed via the PR's first CI run: "Could not find OSSLSIGNCODE_BIN using the following names: osslsigncode" at CMakeLists.txt:244. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 74a505d0ebb7a0baeeeea03036b7827b0f8bf537 Author: Mooneer Salem Date: Sun Sep 13 13:00:20 2026 -0700 Windows CI: build with Ubuntu's gcc-mingw-w64 instead of llvm-mingw Adds cross-compile/freedv-mingw-gcc-x86_64.cmake, a toolchain file using Ubuntu's packaged x86_64-w64-mingw32-gcc/g++ (posix-threading variant -- the default win32-threading alternative doesn't implement C++11 //, which FreeDV's real-time audio pipeline depends on throughout). Unlike the llvm-mingw toolchain files, this one sets CMAKE_FIND_ROOT_PATH to Ubuntu's mingw-w64 sysroot so find_library()/find_path() calls can't accidentally pick up a same-named host library. CMakeLists.txt: the Windows installer filename customization was gated on FREEDV_USING_LLVM_MINGW (only set by the llvm-mingw toolchain files); broadened to apply unconditionally within the existing if(WIN32) packaging block so it also applies to this toolchain. cmake-windows.yml: switched the build job to install gcc-mingw-w64-x86-64 via apt instead of downloading llvm-mingw, and dropped: - Windows ARM (aarch64): Ubuntu's mingw-w64 packages don't provide an aarch64 cross-compiler. - PGO instrumentation: llvm-mingw's -ftemporal-profile and the llvm-profdata merge step are LLVM-specific, with no GCC equivalent used here. - ASan/UBSan sanitizer coverage: no mingw sanitizer runtimes ship with Ubuntu's GCC. The three build/profile/rebuild jobs this used to take (build-pgo-inst, collect-pgo-profile, build-pgo-use-profile) collapse into one plain build job; build-sanitizer and test-sanitizer are removed entirely. Draft: I could only verify the toolchain file's CMake syntax locally (no Linux/Docker environment on this machine to actually exercise Ubuntu's mingw-w64 gcc), unlike the llvm-mingw toolchain, which I cross-compiled and linked freedv.exe with directly. This needs a real CI run to confirm the gcc-mingw-w64 build actually succeeds. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 7c491cb8aa1e280a356219acd93bb8a623c915d3 Author: Mooneer Salem Date: Sun Sep 13 11:45:15 2026 -0700 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 , 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 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 01822a45894b1b6eb9b796dd10f15eec472a95ba Author: Mooneer Salem Date: Sun Sep 13 11:45:15 2026 -0700 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 , 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 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit ae8aa49b2dc0169f538dc2b02ec280182bbb63b4 Merge: 36db96e5 79951b37 Author: Mooneer Salem Date: Sun Sep 13 10:59:38 2026 -0700 Merge branch 'v3.0-dev' into ms-rade-v2 commit 79951b3712d14781df47ad150e697b03dddf3bac Merge: 48eb4923 2c7aa5ff Author: Mooneer Salem Date: Sun Sep 13 10:55:13 2026 -0700 Merge branch 'master' into v3.0-dev commit 2c7aa5ff8277b7a12681922addb15b0f83dc283f Author: Mooneer Salem Date: Sun Sep 13 10:53:43 2026 -0700 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 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 -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 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 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 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 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 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 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk * master still uses RADEV1, not V2. --------- Co-authored-by: Claude Sonnet 5 commit dfb7cee40de53a590cf94fdffc33aed423477205 Merge: 6cc390e7 07a51dc6 Author: Mooneer Salem Date: Sun Sep 13 09:17:53 2026 -0700 Merge branch 'master' into windows-asan-ubsan-ci commit 07a51dc6caf835f43d688bf9588bbac22c0a3d3f Author: Mooneer Salem Date: Sun Sep 13 16:11:14 2026 +0000 latest user manual PDF commit 5f39abb700a60fd6da3c04086cd9c7916b1ac42d Author: Mooneer Salem Date: Sun Sep 13 09:09:03 2026 -0700 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 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 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 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 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 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk * Add PR #1488 to changelog. --------- Co-authored-by: Claude Sonnet 5 commit d005ce53a8e840dff8e007e14e3043ec99f8acda Author: Mooneer Salem Date: Sun Sep 13 01:53:31 2026 -0700 Add PR #1488 to changelog. commit 6cc390e78230b20d4c2bb72c86d403ecef26aec5 Author: Mooneer Salem Date: Sun Sep 13 01:49:25 2026 -0700 master still uses RADEV1, not V2. commit bbfce036ee6354af3921fe9f839df5b3a1683394 Author: Mooneer Salem Date: Sat Sep 12 22:04:41 2026 -0700 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 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 78959bd0d3655e106fca4ee23a826b9477a3868f Author: Mooneer Salem Date: Sat Sep 12 18:37:36 2026 -0700 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 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 857e327132f6fb44a04334bf8816a6c0d628c1d6 Author: Mooneer Salem Date: Sat Sep 12 16:29:28 2026 -0700 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 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 73d03f83bb50d8b3ea9cce104ea94d595d167229 Author: Mooneer Salem Date: Sat Sep 12 13:43:20 2026 -0700 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 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 299b44eb23465e9005b34ff7d75df231425556dc Author: Mooneer Salem Date: Sat Sep 12 09:45:28 2026 -0700 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 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 36db96e5271dc15af71b8978b84f44f0c0ac62dc Author: Mooneer Salem Date: Sat Sep 12 22:04:41 2026 -0700 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 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 25ed4c6bfbabf0f1ddddaa0115bf6a9fbe73eee3 Author: Mooneer Salem Date: Sat Sep 12 19:46:07 2026 -0700 Fix RADE warm-up regression: use a throwaway instance, not the live one CI on Windows caught this: the previous commit's rade_tx()/rade_rx() warm-up calls ran directly against the live rade_ instance, but rade_rx()'s sync detector keeps IIR-smoothed state (Ry_smooth, frame_sync_odd/even, alpha=0.95/ beta=0.999) inside struct rade that rade_open() zeroes but a live call does not. Feeding it one warmup frame of silence nudged that state off its pristine zeroed start before real acquisition ever began. Confirmed via the fullduplex_RADEV1 CI test (requires exactly one sync acquisition over a 60s run, no drops): it passed on every prior commit, then started failing -- consistently on windows-11-arm, intermittently on windows-2022 x86_64 -- starting with the warm-up commit alone, before the unrelated WASAPI timer commit on top of it. Fix: warm up a separate rade_open()/rade_close() throwaway instance instead. It still touches the same static, read-only weight-table memory the real rade_ instance reads (so the first-frame page-fault stall is still avoided), but never touches rade_'s own state. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit db5ab52cd94bddf34feb03d38c018e440bf73a81 Author: Mooneer Salem Date: Sat Sep 12 18:37:36 2026 -0700 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 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 2960fd819035f6b122dfb957ee6301755dd4657f Author: Mooneer Salem Date: Sat Sep 12 18:23:54 2026 -0700 Pre-warm RADE inference paths to avoid first-frame page-fault stalls rade_open() only points internal structs at RADE's statically-compiled neural net weight tables -- it doesn't touch that memory. The first code path to actually read every byte of it is the first real rade_tx() or rade_rx() call, which on a loaded CI runner has been observed to stall the real-time TX/RX thread by 300ms-1s (traced via CI log timestamps to precisely the first frame after PTT and the first frame after initial sync). Run one dummy rade_tx()/rade_rx() call with zeroed input right after rade_open(), during mode setup and off the real-time thread, so that one-time cost is paid before the real-time loop ever starts. Follows the same "prime with zeros" pattern already used for fargan_cont() just below. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 456528130e1d640a86269935318d2da2f963a701 Author: Mooneer Salem Date: Sat Sep 12 16:43:49 2026 -0700 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 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 54aa69885b434417bef2f36f80ed68b87902344a Author: Mooneer Salem Date: Sat Sep 12 16:43:13 2026 -0700 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 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 5a2d84cff43efaea279e27c67d2aa2d64af9a9a3 Author: Mooneer Salem Date: Sat Sep 12 16:29:28 2026 -0700 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 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit a34adbaa73093e10d1be6a4faba2d2063dad8b51 Author: Mooneer Salem Date: Sat Sep 12 13:43:20 2026 -0700 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 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 1498db854990e784b50e0f8a6fca30d1dbb18803 Author: Mooneer Salem Date: Sat Sep 12 12:39:46 2026 -0700 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 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 8e3e4bd6a775bfe9418d22786ad8b95883aba479 Author: Mooneer Salem Date: Sat Sep 12 11:42:05 2026 -0700 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 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit bfb0bda9f6e1d41e57797d836a1497314abf3c7d Author: Mooneer Salem Date: Fri Sep 11 22:28:44 2026 -0700 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 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 167f863b15ca3e7fab184129d3cc6a70c1d8684a Author: Mooneer Salem Date: Fri Sep 11 22:00:23 2026 -0700 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 -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 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit fa9348684761ec9d1f3b0f558c416535f0b44687 Author: Mooneer Salem Date: Sat Sep 12 13:15:20 2026 -0700 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 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 178f81142439a2f6690b0e71b2bf7a9f734e8a8c Author: Mooneer Salem Date: Sat Sep 12 12:39:46 2026 -0700 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 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit bacd58063d1e3ed926647b1c03f74928082ee96e Author: Mooneer Salem Date: Sat Sep 12 11:42:05 2026 -0700 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 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit a290d16f848126881fc4261519564decde157ffb Author: Mooneer Salem Date: Sat Sep 12 09:45:28 2026 -0700 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 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 47bfd79e7fa6b0fa6fa54680e1ee867754f875d7 Author: Mooneer Salem Date: Fri Sep 11 22:28:44 2026 -0700 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 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 8cdce134aba5a1f3ca69ea381ee7f6a451d24839 Author: Mooneer Salem Date: Fri Sep 11 22:00:23 2026 -0700 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 -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 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit f013450b8969a9d74cbb0190ef89c41e6d1eb928 Author: Mooneer Salem Date: Fri Sep 11 19:35:40 2026 -0700 FreeDVReporter: only force column relayout on visibility/highlight changes setColumnAutosize_() forces a full column relayout (SetWidth() on every column) on macOS, worked around a wxWidgets rendering bug (see https://github.com/wxWidgets/wxWidgets/issues/25972). It was firing whenever any row's data changed at all (isPendingUpdate), not just when a row's visibility or highlight color changed. On a busy reporter network, isPendingUpdate can be true on effectively every 250ms highlight-timer tick, so this was running continuously rather than only when column widths could plausibly need to change -- matching an observed symptom of the Reporter window's headers redrawing themselves very frequently. Plain data refreshes still go through ItemsChanged() so cell content stays live; only the column-width relayout is now skipped for them. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 76165aad2cc6af339b511d74c6edb27968074ec5 Author: Mooneer Salem Date: Fri Sep 11 17:31:36 2026 -0700 Add input-side FIFO starvation counters (infifo1/infifo2 empty) The existing outfifo1/outfifo2 counters only see the speaker/radio-output side going hungry. Tracing TxRxThread::rxProcessing_/txProcessing_ showed the mirror-image case -- infifo1/infifo2 not having a full frame ready -- was completely invisible: the read loops just silently exit for that wake cycle with no counter and no log line. That's exactly the kind of gap that could explain a modem losing sync without any of our current counters showing anything (as seen on the windows-11-arm CI failure this round: loss 0.653 with two sync drops, yet Audio1/Audio2 and outfifo*_empty were all clean). Only count a wake cycle as starved if it produced literally zero frames of new input all cycle -- draining whatever was buffered down to less than one frame is the loop's normal, designed exit condition and happens on effectively every wake, so counting every failed read (as a first attempt here did) just produces noise indistinguishable from healthy operation. Caveat: unlike outfifo*_empty, this counter won't sit at zero on a healthy run. MacAudioDevice::stopRealTimeWork() halves its wait timeout whenever the output FIFO is under half full, so the thread often wakes via timeout faster than its producer actually delivers new samples, and legitimately finds nothing yet. Read it as a relative/trend signal across runs, not an absolute health check the way the output counters are. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 46aff386b19ab39020496e50a251e3cda3fe58b4 Author: Mooneer Salem Date: Fri Sep 11 15:19:17 2026 -0700 Log audio under/overflow and FIFO counters at test teardown These previously only fed the GUI diagnostics panel and were invisible in headless -ut/CI runs. Logging them at end-of-test gives rade_loss (and other -ut runs) hard numbers on whether a failure was caused by CoreAudio itself missing a deadline (Audio1/Audio2 counters) versus our own TxRxThread falling behind and starving outfifo1/outfifo2 (Fifos counters), instead of guessing after the fact. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Jz1Nz4hmnEzwQ7hjQtCBhk commit 260f861b011ed105030d2d657e47513ea2fe1572 Author: Mooneer Salem Date: Fri Sep 11 02:03:47 2026 -0700 Fix typo. commit fd26949546df6a0c50b49524595baa8ff4fcf302 Merge: 1f5f78f0 b8f7a4b9 Author: Mooneer Salem Date: Fri Sep 11 01:40:53 2026 -0700 Merge branch 'ms-rade-v2' of github.com:drowe67/freedv-gui into ms-rade-v2 commit 1f5f78f05fa511932151459f1b301f1cceb2d467 Author: Mooneer Salem Date: Fri Sep 11 01:36:06 2026 -0700 Another attempt at enabling RTSan on macOS. commit b8f7a4b99e35fb25b14ce7093fb1e8f074d866e5 Author: Mooneer Salem Date: Fri Sep 11 01:27:14 2026 -0700 Revert previous changes. commit 3552f4b0b5eb806965491698ef3a430b97895f36 Author: Mooneer Salem Date: Thu Sep 10 22:58:47 2026 -0700 TxRxThread: retry mic FIFO read for up to 1ms before giving up on TX pass infifo2 is fed by a separate sound card thread, so a single empty read is often just a transient underrun rather than genuinely nothing to encode. When not ending TX, spin re-attempting the read for up to 1ms (no sleeping -- this runs on the real-time TX thread) before falling back to the existing break. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01AYJXLpaEyPTsegX93QzE1a commit c88167d4d9e6f049e2540497825ae0fd2a4a8fc1 Author: Mooneer Salem Date: Thu Sep 10 21:45:27 2026 -0700 Move break further up. commit d5dc4b59302e3df3f79b3cc1d185e4dba0fba794 Author: Mooneer Salem Date: Thu Sep 10 20:34:56 2026 -0700 TX should be continuously fed if under 1/4 capacity. commit 2e86e943fc7ea66d4c5a658d2941f0ddfc8f1bde Author: Mooneer Salem Date: Thu Sep 10 20:27:35 2026 -0700 Disable RTSan again, no time to debug it ATM. commit de098bbbe19bd0ac18e98d2f61fa5ba213f1b7d9 Author: Mooneer Salem Date: Thu Sep 10 17:18:21 2026 -0700 Use correct sanitizer library name. commit cbd77005cfac95226ea3e4d7345684f396e18ee5 Author: Mooneer Salem Date: Thu Sep 10 17:12:27 2026 -0700 Add debugging. commit cf4a3153f93b2c418ad9b8d4cfb65d1029c74aba Author: Mooneer Salem Date: Thu Sep 10 16:46:06 2026 -0700 Update rpath clear script to handle multiple different dupe rpaths. commit facf403731d6fbdd1b106b5e185e64c9c636332a Author: Mooneer Salem Date: Thu Sep 10 14:38:44 2026 -0700 Need LLVM Clang for RTSan on macOS. commit 5b50954a1ca0c7caa8266a695c5442ab3254bd18 Author: Mooneer Salem Date: Thu Sep 10 14:31:32 2026 -0700 Command line arguments were accidentally gated to unit test logic. commit d748073fd0b32e5ca9385cf9c4cc1465eeb47f64 Author: Mooneer Salem Date: Thu Sep 10 13:20:02 2026 -0700 Add -agcleveler and -agclimiter command line arguments. commit 46b7f74eee8256fbdaef967367d11847940ac4ef Author: Mooneer Salem Date: Thu Sep 10 13:06:27 2026 -0700 fullduplex_RADE wasn't outputting logs. commit b1fa3228593e4570fc87a0d2bdf7e4ea30e045ed Author: Mooneer Salem Date: Thu Sep 10 13:06:06 2026 -0700 rade_loss is now safe to run in RTSan. commit 24fcc862454a82b45a1f6fbc639a79c435e7ff68 Author: Mooneer Salem Date: Thu Sep 10 13:05:53 2026 -0700 Enable RTSan on macOS CI. commit 62e1db808ca3e57ed0fdcab21a8944bd44542e80 Author: Mooneer Salem Date: Thu Sep 10 08:13:07 2026 -0700 Reenable processing stats. commit fa8708812886a907077feacc49b548f8c8e811f3 Author: Mooneer Salem Date: Wed Sep 9 22:07:23 2026 -0700 Switch branch back now that the PR has merged. commit b5dbdd5795c48a1d874530809e4411357288038f Author: Mooneer Salem Date: Wed Sep 9 20:01:49 2026 -0700 CI: re-test against updated ms-rt-safe-logging backend No source change. Triggers a rebuild now that the backend branch carries the async-logging fixes: ulog.level made atomic (TSan data race with ulog_set_level), ulog_async_flush() waits for emission, and the always-on consumer lifecycle. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01AYJXLpaEyPTsegX93QzE1a commit 4cfb67bb3d8af5bfa187dea280faadcb127bc00f Author: Mooneer Salem Date: Wed Sep 9 18:58:17 2026 -0700 Test new async logging implementation. commit d2a6d6aa6f0a153ed11f83f3dec1b002ae989e11 Author: Mooneer Salem Date: Wed Sep 9 17:18:27 2026 -0700 Update comment. commit 8641a37f8fd30a4ffd4c238485cf1a150416bdf1 Merge: e7a322b2 48eb4923 Author: Mooneer Salem Date: Wed Sep 9 08:46:03 2026 -0700 Merge branch 'v3.0-dev' into ms-agc-tweaks-test commit 219ab320438df83dad29b965e4713168d1a45edb Merge: 40f2c54e 48eb4923 Author: Mooneer Salem Date: Wed Sep 9 08:43:53 2026 -0700 Merge branch 'v3.0-dev' into ms-rade-v2 commit ae1b7d44356615a65145930c5dd01719ea02e8b6 Author: Mooneer Salem Date: Wed Sep 9 15:43:36 2026 +0000 latest user manual PDF commit 48eb49233c13c156d2eff88cb4347cee06a6d0a6 Merge: 37de15d9 da2fb5b0 Author: Mooneer Salem Date: Wed Sep 9 08:43:25 2026 -0700 Merge branch 'master' into v3.0-dev commit da2fb5b0af088c3d5e0ad632c175e295388ea695 Author: Mooneer Salem Date: Wed Sep 9 08:40:24 2026 -0700 Remove reliable text lock from audio threads. (#1486) commit af3956340c2e7e3c690dcffe6cfff24585bf017d Author: Mooneer Salem Date: Wed Sep 9 08:40:17 2026 -0700 Add PR #1486 to changelog. commit 6380f5263bbf1e6cfb45b554e5c1ab3904df5cfc Author: Mooneer Salem Date: Wed Sep 9 01:50:38 2026 -0700 Remove reliable text lock from audio threads. commit 40f2c54eace9584a612d82a38226c636f6f4c037 Author: Mooneer Salem Date: Tue Sep 8 15:38:43 2026 -0700 Temporary revert of ulog_async to ensure it's not causing problems. commit e7a322b2b179f671fed0e60fe22a869be87082ba Author: Mooneer Salem Date: Tue Sep 8 09:08:45 2026 -0700 Test freedv-backend AGC changes. commit 76d75cf35424f734289bc01165f8a3a0b1ab4476 Merge: 0ad63afd 37de15d9 Author: Mooneer Salem Date: Tue Sep 8 09:00:54 2026 -0700 Merge branch 'v3.0-dev' into ms-rade-v2 commit 37de15d998047aa9ef9bc4761f234f3149a867db Merge: f865ec49 c97b73c7 Author: Mooneer Salem Date: Tue Sep 8 08:59:52 2026 -0700 Merge branch 'master' into v3.0-dev commit c97b73c7549768b7abf08189d68c8005e0714f6b Author: Mooneer Salem Date: Tue Sep 8 07:30:36 2026 -0700 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 Claude-Session: https://claude.ai/code/session_01AYJXLpaEyPTsegX93QzE1a * Increase noise slightly. * Oops, accidentally deleted the - sign. --------- Co-authored-by: Claude Sonnet 5 commit 0ad63afd141f1ef9c3e88377ffc5ff6d280c1381 Author: Mooneer Salem Date: Tue Sep 8 01:21:19 2026 -0700 Revert MPP noise adjustment and add a bit more AWGN noise. commit e6398be2caa39652b145c76fc67c29cc6e847492 Merge: 9ac08861 1665b53e Author: Mooneer Salem Date: Tue Sep 8 01:14:03 2026 -0700 Merge branch 'master' into ms-fix-rade-reporting-awgn-flake commit 9ac08861c28f3c80e682ceee88b039f089e1aac8 Author: Mooneer Salem Date: Tue Sep 8 01:14:00 2026 -0700 Oops, accidentally deleted the - sign. commit 1665b53ef739a51ecb4205203b2cd4f97dc018c5 Author: Mooneer Salem Date: Mon Sep 7 23:57:12 2026 -0700 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 commit 4e731b0bfbac60a54641d889876e031d74fa9d48 Author: Mooneer Salem Date: Mon Sep 7 23:56:30 2026 -0700 Increase noise slightly. commit e0dcd530f11224f68d48f6e20104b88315f2b4dc Author: Mooneer Salem Date: Mon Sep 7 22:20:49 2026 -0700 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 Claude-Session: https://claude.ai/code/session_01AYJXLpaEyPTsegX93QzE1a commit 097bb4134f495589c8ea4911105e2f869a1549ac Author: Mooneer Salem Date: Mon Sep 7 21:01:03 2026 -0700 CI (Windows): make collect-pgo-profile wait for real audio endpoints "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. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01AYJXLpaEyPTsegX93QzE1a commit 0fb09eee0e4c55373b9e7362b5ce0da2a66f3d18 Author: Mooneer Salem Date: Mon Sep 7 20:40:35 2026 -0700 CI (Windows): fix PowerShell parse error in PGO retry block The retry loop added in 8db9c498 was inline in the workflow YAML and had `"... attempt $i: $_"` -- in a PowerShell double-quoted string `$i:` is parsed as a scope-qualified variable, so the whole step failed to parse before running anything ("Variable reference is not valid"). Move the retry logic into ci/Invoke-PgoProfileCollection.ps1 (delimited as `${attempt}:`, reviewable as a whole) and reduce the workflow step to a one-line call. Behaviour is unchanged: wait for endpoints, run GeneratePGOProfiles.ps1, retry up to 3x, clearing partial .profraw between attempts. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01AYJXLpaEyPTsegX93QzE1a commit 8db9c49800018c1a242800196892fe73852b8f54 Author: Mooneer Salem Date: Mon Sep 7 20:03:04 2026 -0700 CI (Windows): gate PGO/test jobs on real audio-endpoint readiness collect-pgo-profile intermittently fails in "Generate PGO data" 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. Seen on master too. The old "Start Windows Audio Service" step only waited on WMI Win32_SoundDevice (driver nodes, not MMDevice endpoints), had an operator-precedence bug in its until-condition, ran once minutes before the test, and never checked the specific endpoints FreeDV needs. * Add ci/Wait-AudioDevices.ps1: restarts AudioEndpointBuilder/audiosrv and polls "Get-AudioDevice -List" (the active-endpoint surface FreeDV actually uses) until every required playback/recording endpoint is present, nudging the audio stack halfway through and dumping full diagnostics before throwing on timeout. * Use it as a pre-flight gate in both the collect-pgo-profile and test jobs, keyed on exact endpoint name + data-flow direction. * Wrap "Generate PGO data" in a 3x retry that re-checks endpoints each attempt, since an endpoint can also drop mid-run right after SoX releases the capture device (no up-front wait can prevent that). * GeneratePGOProfiles.ps1: best-effort wait for the endpoints before each of the TX and RX passes. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01AYJXLpaEyPTsegX93QzE1a commit a043af7a15f1f5d7f557236e31e2fe56f63202f1 Author: Mooneer Salem Date: Mon Sep 7 19:17:32 2026 -0700 Pin freedv-backend to the ulog_async clang-tidy fix Bumps the FetchContent pin to tmiw/freedv-backend@61ed161, which fixes the two clang-tidy warnings-as-errors (clang-analyzer-valist.Uninitialized in ulog.c, performance-no-int-to-ptr in ulog_async.cpp) that broke the Linux lint job. Pinned to a SHA so CI is deterministic; still to be reverted to radev2-dev once tmiw/freedv-backend#48 merges. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01AYJXLpaEyPTsegX93QzE1a commit ee81118150c412dd4c32712cdf41ee0015818ffe Author: Mooneer Salem Date: Mon Sep 7 18:14:21 2026 -0700 Route real-time thread logging through the async ulog front end Pairs with tmiw/freedv-backend#48. Enables -DULOG_ASYNC (must match the backend, which changes ulog_Event's layout), starts/stops the async log consumer in MainApp, and marks the TxRxThread real-time thread so its log_*() calls are captured without locking or allocating on the audio path. BuildFreeDVBackend.cmake points at the ms-rt-safe-logging branch until tmiw/freedv-backend#48 merges. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01AYJXLpaEyPTsegX93QzE1a commit af833957fbf970c780162ab6febc719bcc43b84a Author: Mooneer Salem Date: Mon Sep 7 17:25:50 2026 -0700 test_rade_reporting.sh: match test_rade_loss.sh tee pattern The `(FreeDV ... 2>&1 | tee tmp.log) &` form made $! the subshell/tee PID, so `wait $FDV_PID; FREEDV_EXIT_CODE=$?` captured tee's exit code (~always 0) rather than FreeDV's -- a FreeDV crash would be masked and the script would exit 0. Switch to `> >(tee tmp.log) 2>&1` process substitution so $! stays FreeDV, keeping the log visible in CI while restoring a correct FREEDV_EXIT_CODE. Matches the fix in test_rade_loss.sh. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01AYJXLpaEyPTsegX93QzE1a commit 8213ba317f5b8020728ab5e05aee733f939978dc Author: Mooneer Salem Date: Mon Sep 7 17:22:03 2026 -0700 test_rade_loss.sh: restore visible FreeDV log output de8a1264 ("Revert audio dropout investigation instrumentation") reverted the `| tee tmp.log` on both FreeDV invocations back to `>tmp.log 2>&1`, so FreeDV's log (device errors, sync transitions, etc.) is no longer visible in ctest / CI output -- a failing rade_loss run just shows loss.py's FAIL with no indication why. Restore the tee, but via process substitution (`> >(tee tmp.log) 2>&1`) rather than a plain pipe: this keeps $! pointing at FreeDV so FREEDV_EXIT_CODE stays FreeDV's real exit code. The pre-revert `(... 2>&1 | tee tmp.log) &` form had $! = tee, making FREEDV_EXIT_CODE always 0. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01AYJXLpaEyPTsegX93QzE1a commit 3275f47b6c4952163b5531541a8832a0bfe9f968 Author: Mooneer Salem Date: Mon Sep 7 13:16:03 2026 -0700 Back off RADE reporting test noise by 3 dB to stop intermittent failures d71f32bc set the awgn/mpp noise to the RADEV2 decode minimum (awgn --No -13, mpp --No -18). ch fixes the noise density, not the SNR, and its input (test.wav) is a live virtual-cable capture whose level varies run to run with residual silence, monitor-capture dropouts and the sox silence trim -- so the effective SNR floats and, sitting exactly on the decode cliff, rade_reporting_awgn (and mpp) fail intermittently on the non-sanitizer Linux legs. Move awgn to --No -16 and mpp to --No -21: ~3 dB of headroom above the RADEV2 minimum, still well into noise and harder than the pre-d71f32bc values. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01AYJXLpaEyPTsegX93QzE1a commit e5da4b463f800b4f1d66522cac42bab4631e00d6 Author: Mooneer Salem Date: Mon Sep 7 12:28:38 2026 -0700 Fix RadeVerificationReport.ps1 variable clashes; report from macos-26 PowerShell variable names are case-insensitive, so the internal $level1Log clobbered the -Level1Log parameter before it was read: on Windows CI the script looked for its own default log path, never found it, and reported Level 1 as ERROR. Rename the colliding locals ($level1Log -> $level1LogFile, $repoRoot -> $repoDir, $radaeCommit -> $radaeHash, $radeCCommit -> $radeCHash) and fix the two-statement $radaeCommit fallback that the clash had also broken. cmake-macos.yml: generate/upload the verification report on macos-26 instead of macos-15. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01AYJXLpaEyPTsegX93QzE1a commit fbfa93f769fb5af8e7899a9d0a479f55fe79d4ce Author: Mooneer Salem Date: Mon Sep 7 01:25:49 2026 -0700 CI: generate the RADE verification report per OS on the PGO build Each platform workflow gains a rade-loss-baseline job that builds rade_c and computes the software-only --v2 feature-loss baseline (deterministic / model-driven, so one Linux computation serves every platform's report), exposing baseline, threshold and the rade_c / radae commit hashes as job outputs. On one leg per OS -- ubuntu-24.04, macos-15, windows-2022 x86_64, all running the PGO-optimized build users actually get -- the existing RADE loss step's log is fed to rade_verification_report.sh / RadeVerificationReport.ps1, which fill in the Application Under Test, Signal Path Declaration, Baseline Loss (Step 1) and Level 1 - Software Loopback sections and upload rade_verification_report.md as the rade-verification-report-{linux,macos,windows} artifact. Level 2/3 stay blank for the tester. Report scripts gain the plumbing for this: - RADE_LEVEL1_LOG / -Level1Log: parse an existing test_rade_loss run instead of running the loopback again. - RADE_REPO_ROOT / -RepoRoot: point at the FreeDV checkout for the version / git hash (the Windows test runs from the install dir). - RADE_C_COMMIT / RADE_RADAE_COMMIT / -RadeCCommit / -RadaeCommit: fill the commit-hash fields from the baseline job. - Skip rade_c tool discovery and the radae clone when the baseline is supplied; treat a missing RADE_LEVEL1_LOG as an ERROR rather than silently running the loopback. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01AYJXLpaEyPTsegX93QzE1a commit 0e30302f80ab0219ecc506d85e21aaa150605d64 Author: Mooneer Salem Date: Mon Sep 7 01:07:12 2026 -0700 Add RADE verification report scripts; use --v2 for the baseline This branch runs RADEV2 (src/freedv_interface.cpp opens RADE with RADE_MODE_V2 unconditionally, whatever the -utmode label), so the software-only baseline must be measured with rade_tx_wav --v2 / rade_rx_wav --v2. With V2 the baseline is ~0.080 (matching the RADE verification procedure) and the derived threshold ~0.088. - test_rade_loss.sh, TestFreeDVRadeLoss.ps1, cmake-windows.yml (rade-loss-baseline job): pass --v2 to the wav tools when computing the loss threshold. - test/rade_verification_report.sh, test/RadeVerificationReport.ps1: new scripts that begin populating the RADE integration verification report (https://github.com/drowe67/radae/blob/dr-tx-bpf/doc/verification/template.md). They fill in the Application Under Test, Signal Path Declaration, Baseline Loss (Step 1) and Level 1 - Software Loopback sections automatically; Level 1 is produced by the existing rade_loss test (the same path CI uses). Level 2/3 are left as blank template fields. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01AYJXLpaEyPTsegX93QzE1a commit ee2fb1a1b257c1f192c6276ab2dd2ece486d42bb Author: Mooneer Salem Date: Mon Sep 7 00:43:45 2026 -0700 RADE loss test: derive threshold from software-only baseline Instead of hard-coding LOSS_THRESHOLD=0.0891, compute the maximum allowed RADE feature loss from a software-only baseline as described in the RADE integration verification procedure: run all.wav through rade_tx_wav + rade_rx_wav from the rade_c build, measure the loss with loss.py, and require the hardware round trip to stay within +10% of that baseline. - test_rade_loss.sh: locate rade_tx_wav/rade_rx_wav in the rade_c build tree (overridable via RADE_C_TOOLS_DIR), compute the baseline, and set LOSS_THRESHOLD = baseline * 1.10. Falls back to 0.0891 with a warning if the tools/loss.py are unavailable; honors a RADE_LOSS_THRESHOLD override. - cmake-windows.yml: rade_tx_wav/rade_rx_wav are excluded from the Windows build (if(NOT WIN32) in rade_c), so add a rade-loss-baseline job that builds rade_c on Linux, computes the threshold, and exposes it as a job output the Windows test job consumes via -LossThreshold. - TestFreeDVRadeLoss.ps1: -LossThreshold now defaults to 0 (derive it); add Get-RadeLossThreshold plus a -RadeCToolsDir param mirroring the shell logic, with the same 0.0891 fallback. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01AYJXLpaEyPTsegX93QzE1a commit 8e80519b14f7c2d0dfc912dd10a66e95b04c05f0 Author: Mooneer Salem Date: Mon Sep 7 00:18:03 2026 -0700 Go through manually and revert unneeded changes. commit c0c9c06f0cd657c72ca2c7b8bc87579ecd79a361 Merge: de8a1264 f865ec49 Author: Mooneer Salem Date: Mon Sep 7 00:12:58 2026 -0700 Merge branch 'v3.0-dev' into ms-rade-v2 commit f865ec491e28d2d6d766c158ad4ddd778fe301ce Merge: 833790e1 f3159ca3 Author: Mooneer Salem Date: Mon Sep 7 00:12:48 2026 -0700 Merge branch 'master' into v3.0-dev commit f3159ca346c555993bb6b771728527657e426599 Author: Mooneer Salem Date: Mon Sep 7 06:02:39 2026 +0000 latest user manual PDF commit 4ca76ddf00b62750d6d7447bc7bfde30b307c796 Author: Mooneer Salem Date: Sun Sep 6 23:00:20 2026 -0700 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 Claude-Session: https://claude.ai/code/session_01ADMCssMazpW1igtvMHXNWZ * Add PR #1482 to changelog. --------- Co-authored-by: Claude Opus 5 commit 9fda3525b328087cf7348ad65ce32805ff8a7024 Author: Mooneer Salem Date: Sun Sep 6 21:36:23 2026 -0700 Add PR #1482 to changelog. commit 271cc146f1bafeb25a32cffdcc3adec38b512229 Author: Mooneer Salem Date: Sun Sep 6 20:29:36 2026 -0700 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 Claude-Session: https://claude.ai/code/session_01ADMCssMazpW1igtvMHXNWZ commit 071d233539a0222e16b563bedfaa322226e5ab40 Author: Mooneer Salem Date: Sun Sep 6 23:31:10 2026 +0000 latest user manual PDF commit 97fe8214f9fada1d0ae0d939b925d409921f9696 Author: Mooneer Salem Date: Sun Sep 6 16:29:03 2026 -0700 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 Claude-Session: https://claude.ai/code/session_01ADMCssMazpW1igtvMHXNWZ * Add PR #1481 to changelog. --------- Co-authored-by: Claude Opus 5 commit c88b01665577a400bb8feb640748f87590b14043 Author: Mooneer Salem Date: Sun Sep 6 16:27:39 2026 -0700 Add PR #1481 to changelog. commit 40881530411bde580e81a2f75a822da8cd37ec93 Author: Mooneer Salem Date: Sun Sep 6 15:11:44 2026 -0700 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 Claude-Session: https://claude.ai/code/session_01ADMCssMazpW1igtvMHXNWZ commit de8a1264e1267f4fcbd67f92c06397e4be173ef0 Author: Mooneer Salem Date: Sun Sep 6 10:30:42 2026 -0700 Revert audio dropout investigation instrumentation, keep the bug fixes. The investigation into the intermittent rade_loss failures is done, so the instrumentation it needed comes back out: * The CI-gated RX retry (RETRY_ON_HAL_SKIP) and the input timestamp continuity check that fed it. The check answered its question -- CoreAudio skips input outright on some runners, so those frames never reach FreeDV -- but neither belongs in the tree now that it has. * The IO buffer frame size negotiation diagnostics, which established that negotiation succeeds and is not the cause. * All of the main.cpp sync/SNR and audio counter logging. * The RADE encoder/decoder input recording feature: the -txradeinfile and -rxradeinfile UT options, the two recording taps in TxRxThread's pipeline, the playrec.cpp globals behind them, the test script wiring on all three platforms, and the rade-io-* CI artifact uploads. Three fixes found along the way are real defects and stay: * TxRxThread retries the EOO write rather than dropping it. execute() pops every EOO sample unconditionally, so a failed all-or-nothing write to outfifo1 discarded a whole End of Over with only a warning. * RecordStep no longer drops writes silently, and no longer writes a buffer a failed all-or-nothing read left untouched, which spliced audio into the recording that never went through the pipeline. * MacAudioDevice restores mDataByteSize before every AudioUnitRender. AudioUnitRender overwrites it with the amount rendered, so once shrunk by a short render it stayed shrunk while the copy still read the full request. Independent of the dropout hunt, and the copy is now clamped to what was actually rendered. Reverting ef552cb8 also wanted to restore the phase-shift retry that a528750f deliberately removed from TestFreeDVRadeLoss.ps1; that deletion is kept. It likewise restores >tmp.log in place of | tee tmp.log in the shell tests, which also restores $! naming the FreeDV process rather than a subshell -- the kill $FDV_PID that follows was targeting the wrong process. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ADMCssMazpW1igtvMHXNWZ commit a528750fc6e08f16850e55651c0aa54e62856212 Author: Mooneer Salem Date: Sun Sep 6 02:03:27 2026 -0700 Remove second change execution from Windows rade_loss test. commit a32c54d1f5c1715e6b45dbee6b8698bad2b42477 Author: Mooneer Salem Date: Sun Sep 6 08:59:43 2026 +0000 latest user manual PDF commit 35a3ad65d7bbff3585644419f5bfa148522cf54c Merge: 1189f02f 833790e1 Author: Mooneer Salem Date: Sun Sep 6 01:58:04 2026 -0700 Merge branch 'v3.0-dev' into ms-rade-v2 commit 833790e12410ed14d7c3fee9d9f2d26482a6fbbd Merge: 87fc3a31 9e803577 Author: Mooneer Salem Date: Sun Sep 6 01:57:51 2026 -0700 Merge branch 'master' into v3.0-dev commit 9e803577bc3411c767ae2a96deeff0c68ac0a1c4 Author: Mooneer Salem Date: Sun Sep 6 01:57:26 2026 -0700 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. commit 6e6df3c828c97d430cd8cfa3f39c3b44b1f3aaaa Author: Mooneer Salem Date: Sun Sep 6 01:57:11 2026 -0700 Add PR #1480 to changelog. commit 489ba1cea7960d6f98adeecdbe57256580e39c1f Author: Mooneer Salem Date: Sat Sep 5 21:57:50 2026 -0700 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. commit 1189f02f3d231a71bc15f12e6b9a03ecc2311f73 Author: Mooneer Salem Date: Sat Sep 5 13:57:40 2026 -0700 Retry the RADE loss RX pass when CoreAudio skipped input, in CI only. The timestamp instrumentation settled where the dropouts come from. On the failing runner CoreAudio skipped input outright -- 4096 frames on VB-Cable in one pass, 6404 in another, and 5720 on the mic cable -- while the runner that passed reported zero skips on every device. That audio never reaches FreeDV at all, so those attempts measured the machine, not the code. Retry the RX pass when frames were skipped on the device carrying the signal under test. Skips on the other inputs cannot affect a decode and are not counted. Bounded at three attempts, and exhausting them still fails -- this drops attempts that measured nothing useful, it does not excuse a failure. Gated behind RETRY_ON_HAL_SKIP, set only on the macOS test-app-folder job. Run locally the variable is unset, so a failure stays a failure on the first attempt. Detection is unconditional either way, so a local failure still reports whether it was environmental. The loss verdict is no longer echoed from inside the attempt function. CI decides pass/fail by grepping the output for PASS, so a discarded attempt printing PASS would have satisfied that grep and quietly weakened the gate; only the accepted attempt's verdict now reaches stdout. Device name added to the stop summary so the harness can tell which stream lost audio. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_013SnS9T3qzKT1QnytAFUxU4 commit ef1dad2c7f6454e87992e6b08822b499c209d3cf Author: Mooneer Salem Date: Fri Sep 4 18:53:00 2026 -0700 macOS: check input timestamp continuity to find skipped HAL input. The captured RX audio shows two different defects that look identical once recorded: 1024-sample zero-fills (a buffer delivered full of silence) and outright deletions (this run, one step of 6880 samples = 143ms, which is not a multiple of the 1024 buffer or the 960-sample pipeline chunk). Everything instrumented so far reads clean on the failing runs -- infull1 is 0, the record FIFO logs nothing, no short renders, no device over/underflow -- so neither mode is attributable yet. CoreAudio stamps each input callback with the device sample time it corresponds to. Consecutive callbacks must advance by exactly the frames delivered; a forward jump means the HAL skipped input we never see, which is the deletion mode, whereas continuous timestamps with silent content is the zero-fill mode. Checking that here separates the two at the point where the difference is still visible, instead of inferring it from the recording afterwards. Each jump is logged with its size (capped at 20 per session), and the totals are reported unconditionally on stop, so a clean run is positive evidence that every input frame arrived rather than merely an absence of warnings. Diagnostic only; nothing about how audio is handled changes. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_013SnS9T3qzKT1QnytAFUxU4 commit b8d8276f8cad5d880488322c9fd640a718ca4037 Author: Mooneer Salem Date: Fri Sep 4 16:19:58 2026 -0700 macOS: restore mDataByteSize before each input render; drop witness. The witness recording added in d1934aeb answered its question. Two clients capturing the same virtual device simultaneously: FreeDV's capture had four zero-gaps (4096 and 8192 samples among them) while sox's recording of that same device over the same interval had none. The audio was on the cable; FreeDV's capture path is where it goes missing. InputProc_ never restored mDataByteSize before AudioUnitRender, which overwrites it with the amount actually rendered. Once shrunk by a short render it stays shrunk, so later renders fill only part of the buffer while the copy loop still read inNumberFrames worth -- surfacing the untouched tail as audio, with err == noErr and no counter moving. That matches every property observed: zeros rather than deletion (timeline preserved), quantized to buffer-size multiples, macOS only, and invisible to instrumentation. Restore the capacity before every render, clamp the copy and the delivered frame count to what was rendered, and calloc the buffers so a partial render can't surface heap contents. A short render is now logged, rate limited per episode, which will confirm or rule out this mechanism directly on the next failure. The witness is reverted in the same commit: it perturbed the test it was measuring (all three runners failed, at 1.810/0.779/0.584 against a 0.082 baseline, with four gaps each rather than the usual one), so leaving it in would distort the very signal being tracked. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_013SnS9T3qzKT1QnytAFUxU4 commit d1934aebfacedc8c448ce12b759640f34b38e413 Author: Mooneer Salem Date: Fri Sep 4 15:03:04 2026 -0700 Record an independent witness of the RX loopback; soften counter claims. The RX capture keeps losing audio with every FreeDV-side counter reading zero -- infull1 is 0, the record FIFO logs nothing, CoreAudio reports no over/underflow -- so nothing in the app can say whether the samples were dropped by our capture path or never arrived on the cable. Record the same input device with a second sox process in parallel with FreeDV's own capture during the RX pass. Comparing the two against test.wav splits it directly: a dropout in both was introduced by the playback side or the virtual cable, one in only FreeDV's capture was introduced by its capture path. Killed and ignored unconditionally after the run so it can never affect the test result. Also demote the delivery counter added in 27cca746 to what it actually is. It reported -214ms on a run whose capture was provably intact -- virtual-device clock offset alone runs 140-540 ppm between runners, which is the same order as the dropouts being chased. Cross-correlating the capture against the played file measures lost content directly and is immune to clock offset; that is the real instrument, and the comments now say so rather than presenting the counter as a discriminator. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_013SnS9T3qzKT1QnytAFUxU4 commit 27cca746ac10faa1923f2837aa8cc8c1ff6568a0 Author: Mooneer Salem Date: Fri Sep 4 12:19:25 2026 -0700 Account for RX capture delivery against elapsed time. Every FreeDV-side counter reads zero on the runs where the RX capture is corrupted -- infull1 is 0, the record FIFO logs nothing, and CoreAudio reports no over/underflow -- yet the captured waveform is provably missing audio (this run: a 960-sample gap plus a 7848-sample deletion on the failing runner, against zero drift on the passing one). So the loss is happening somewhere none of the existing instrumentation can see. Count the samples the capture callback is actually handed and compare that against elapsed wall-clock time in the end-of-pass summary. That splits the two remaining candidates directly: delta ~= 0 capture stream was continuous, so the missing audio was never played -- look at the playback side delta << 0 the device dropped input buffers before we saw them Counted before the FIFO write so it reflects what the sound layer delivered rather than what we kept, and timestamped from the first callback rather than stream start so device startup latency (which is the same order as the deletions we are chasing) doesn't skew it. Resolution is roughly one buffer period, so the ~150ms events seen so far are well clear of the noise floor. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_013SnS9T3qzKT1QnytAFUxU4 commit 2ef6e86584ab010f6f04e47a118b409eb6077581 Author: Mooneer Salem Date: Fri Sep 4 11:04:19 2026 -0700 Report audio counters as one end-of-pass summary, not per change. The per-change reporting added in 72e10862 emitted ~3,494 lines per job: outfifo1 and infifo2 tick on every callback whenever the TX side is idle, which is the entire RX pass, so "did any counter move" was true on nearly every 20ms poll. That buried the signal it was meant to surface. Replace it with a single summary logged at the end of the TX and RX passes. The per-event case that actually carries information -- an underrun or dropped buffer during a transmission -- is already covered by the rate-limited warnings in the audio callbacks, which are unchanged. Also reverts the fixed-duration TX sleep back to a single sleep_for; it had only been split into a polling loop to drive the per-tick reporting. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_013SnS9T3qzKT1QnytAFUxU4 commit 72e1086220050f44d89c74feb81f71a98c9fc61f Author: Mooneer Salem Date: Fri Sep 4 09:21:04 2026 -0700 Log FIFO counters and restore TX outfifo1 underrun warnings. Two of the macOS artifacts have a ~170ms near-silent stretch in test.wav itself -- present before playback, so produced on the transmit side. It was missed earlier because sox dithers, so the gap reads as +/-1 rather than bit-zero. At the same timestamps rade_encoder_input.wav carries full-amplitude speech, so the silence appears downstream of the RADE encoder, and 170ms at 48kHz is 8 x 1024 -- eight output callbacks at the negotiated buffer size. OnTxOutAudioData_ explains that shape: on underrun it increments g_outfifo1_empty and returns without writing audioData at all, leaving whatever the driver had in the buffer to go on the air (silence on this HAL). The RX path handles the same case by explicitly zero-filling. Logging only for now, no change to what is transmitted: restore the per-event rate-limited underrun warning that fdc3b26e reverted (grep for "underrun" in main.cpp currently finds nothing), and add the FIFO counters to the periodic counter logging, now also polled during the TX pass -- previously it ran only in the RX branch, so a TX-side stall could never have been reported. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_013SnS9T3qzKT1QnytAFUxU4 commit 6943b3a3416a9987bc502b97dc79be8c5288abd7 Author: Mooneer Salem Date: Thu Sep 3 23:25:46 2026 -0700 Log audio device errors and dropped input buffers. Analysis of the macos-14 test-app-folder failure (loss 2.217 vs 0.082 on the runners that passed) found the RX capture was missing exactly 12288 samples -- 12 x 1024, one discrete 256ms deletion, with correct amplitude and zero drift before or after. The demodulator reported -28 dB SNR just before losing sync, against +17 dB on the passing runners. A deletion of whole 1024-frame device buffers points at the sound layer dropping input, but nothing in a headless run can confirm that: the counters that track it (g_infifo1_full at the RX callback, and the device over/underflow counters) are incremented and then only ever rendered in the Options dialog, which CI never displays. So: log infifo1/infifo2 drops from the audio callbacks, rate-limited per drop event like the existing outfifo underrun logging, and report the device over/underflow counters from the UT polling loop whenever they move. The counters are polled rather than logged in the device callbacks because those run on the audio thread on some platforms (the PulseAudio mainloop, for one). Note the RecordStep drop logging added earlier did NOT fire on that run, so the loss was upstream of the record tap -- the decoder really did lose that audio rather than it being a recording artifact. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_013SnS9T3qzKT1QnytAFUxU4 commit fa5b90f3b4d2982996a03ecbd013eb9ae09b4c77 Author: Mooneer Salem Date: Thu Sep 3 22:02:22 2026 -0700 Log SNR and frequency offset alongside UT sync transitions. The Windows full-duplex CI test flaps sync several times per second with no FIFO underrun, overflow, or drop warning logged anywhere, so the plumbing is delivering a continuous stream and the cause is upstream of it. "Sync changed from X to Y" alone can't distinguish marginal acquisition (SNR sitting near the decision threshold) from a discontinuity in the demodulator's input (SNR healthy right until sync vanishes); those look very different once SNR and frequency offset are attached to each transition. getSNREstimate() returns 0 as soon as sync drops, so the last in-sync values are carried forward -- reading it after a 1->0 transition would only ever report 0. The two identical polling loops now share one helper. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_013SnS9T3qzKT1QnytAFUxU4 commit 8249e5d55e641950d4f0d8efe3cef47bc1d9e039 Author: Mooneer Salem Date: Thu Sep 3 20:08:53 2026 -0700 RecordStep: stop silently dropping and fabricating recorded audio. Both GenericFIFO calls in RecordStep ignored their return values, and GenericFIFO is all-or-nothing on both ends: - execute() ignored write() failure, so a full FIFO silently omitted the entire chunk from the recording. The FIFO is drained by a Utility-QoS thread, which is what gets starved first under load -- exactly when we most need the recording to be faithful. Now logs the start and end of a drop episode, rate-limited like the existing outfifo underrun logging. - fileIoThreadEntry_() ignored read() failure while writing numInputSamples samples anyway from an untouched buffer. A failed read (e.g. a concurrent reset() landing between numUsed() and read()) would splice uninitialized memory -- or a stale repeat of earlier audio -- into the WAV, fabricating content that never went through the pipeline. Now only writes what was actually read, and buf is zero-initialized as defense in depth. The loop body and the shutdown drain were identical, so both now share drainFifoToFile_() and the fix covers the shutdown path too. These are recording-fidelity fixes; per cross-correlation of the CI artifacts they are not the source of the buffer-quantized gaps seen in rade_decoder_input.wav, which enter at the CoreAudio capture boundary. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_013SnS9T3qzKT1QnytAFUxU4 commit d9dd118d6286dfd4118f8c56b26d7886ac5e49e0 Author: Mooneer Salem Date: Thu Sep 3 15:13:53 2026 -0700 Revert FIFO size changes. commit 99d0b866db2fe46771efd9d2176e0423af08e699 Author: Mooneer Salem Date: Thu Sep 3 14:10:32 2026 -0700 macOS: revert time quantum change back to 60ms. commit f296eb6539588739d1cc3505d73343f6984877b0 Author: Mooneer Salem Date: Thu Sep 3 13:04:13 2026 -0700 RecordStep: write whatever's left in the FIFO on exit. commit c9b40598d6061df4293f327f72db42a44d06646f Author: Mooneer Salem Date: Thu Sep 3 11:52:52 2026 -0700 Non-Linux FIFO sizes should match existing minimum FIFO gating. commit 6837379b9ef9b21c98a338671099fabc396ab376 Merge: ba0d48cc 87fc3a31 Author: Mooneer Salem Date: Thu Sep 3 10:42:38 2026 -0700 Merge branch 'v3.0-dev' into ms-rade-v2 commit 87fc3a31f15188a80994ec31e6923ec02e25ff22 Author: Mooneer Salem Date: Thu Sep 3 10:41:19 2026 -0700 Update ignored words list. commit ba0d48cc4a7bc9aba4dacb3f265fc13b0bdce09f Merge: a29f6eee ef46c60d Author: Mooneer Salem Date: Thu Sep 3 09:51:30 2026 -0700 Merge branch 'v3.0-dev' into ms-rade-v2 commit ef46c60dce809452ef0216a76e28bbe9e9542e71 Merge: 9e4b4a4f 48c007d5 Author: Mooneer Salem Date: Thu Sep 3 09:50:38 2026 -0700 Merge branch 'master' into v3.0-dev commit a29f6eeea8d6b87cccec91b33a3ead224be9e272 Author: Mooneer Salem Date: Thu Sep 3 08:02:32 2026 -0700 Fix compiler error. commit a1d15dec974d48a131c9eec191e0777fe63301ef Author: Mooneer Salem Date: Thu Sep 3 01:29:49 2026 -0700 Use smaller input FIFO sizes on non-Linux platforms. commit 48c007d552ae24d5016d7eb9f41f98cee69e7c20 Author: Mooneer Salem Date: Thu Sep 3 04:50:01 2026 +0000 latest user manual PDF commit fe0986d497155b52640b36b7f25a5260d16f7555 Author: Mooneer Salem Date: Wed Sep 2 21:46:35 2026 -0700 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. commit 1f0b6c19c5134d7585f6302be1110f05990e1aa6 Author: Mooneer Salem Date: Thu Sep 3 01:16:13 2026 +0000 latest user manual PDF commit 6dfd5083201ef65e96543e7b26e13ef2cb8baff4 Merge: 87420bda 1bfb8f2c Author: Mooneer Salem Date: Wed Sep 2 18:14:41 2026 -0700 Merge branch 'master' into tmiw-patch-1 commit 1bfb8f2cb7e4721f802ae06f1c9e758724fda279 Author: Mooneer Salem Date: Wed Sep 2 18:13:56 2026 -0700 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. commit 87420bda3468b40496288de9a216d487e3f54fb7 Merge: 5e60b938 397e55b0 Author: Mooneer Salem Date: Wed Sep 2 16:31:15 2026 -0700 Merge branch 'master' into tmiw-patch-1 commit 046b98259aeb054052183c259830712923e982d2 Author: Mooneer Salem Date: Wed Sep 2 16:30:14 2026 -0700 Add PR #1478 to changelog. commit e415aaaafd11537f8b29dc0d7975746f5d4c224f Merge: ad36f2d7 397e55b0 Author: Mooneer Salem Date: Wed Sep 2 16:29:33 2026 -0700 Merge branch 'master' into ms-dylib-macos-26 commit 397e55b069159c036847c50ea302768cf734c6f4 Author: Mooneer Salem Date: Wed Sep 2 16:29:08 2026 -0700 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. commit 5e60b9384c4f8e28ab0e6bbcc415c094dac2bdd9 Author: Mooneer Salem Date: Wed Sep 2 15:12:52 2026 -0700 Fix typo. commit 44950856637b009cdc5bb6b47ae6659166982ee4 Author: Mooneer Salem Date: Wed Sep 2 15:08:26 2026 -0700 Fix misspellings due to switch to 26.04. commit aaf9ceb703ae2d28c19ba0601a52917569cb576e Author: Mooneer Salem Date: Wed Sep 2 15:05:41 2026 -0700 CI: Add Ubuntu 26.04 LTS to tests. commit 6d70e120134d150ec2f0b7384787165301e46722 Author: Mooneer Salem Date: Wed Sep 2 13:14:57 2026 -0700 Add PR #1477 to changelog. commit 4f1bcae1c8698cda55bdd7c492effec928170458 Author: Mooneer Salem Date: Wed Sep 2 13:14:04 2026 -0700 Update CMake to explicitly look for rig_state.h. commit 9e4b4a4f5ec2a04c66bdb42555ed537ba348bee4 Author: Mooneer Salem Date: Wed Sep 2 20:06:20 2026 +0000 latest user manual PDF commit f2a16a20b954baf6e858f4323c3fdc66a2e1266f Author: Mooneer Salem Date: Wed Sep 2 12:46:34 2026 -0700 Additional improvements for the Level slider (#1464) * Mic level bar should show level before AGC. * Use log scale for Level meter. * Use -40 to 0 dB for the Level scale. * Add PR #1464 to changelog. * Merge down from master to mic level PR (#1466) * latest user manual PDF * 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. * latest user manual PDF * 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 Co-authored-by: Mooneer Salem --------- Co-authored-by: Barry Jackson Co-authored-by: Barry Jackson * Revert "Merge down from master to mic level PR (#1466)" (#1467) This reverts commit 39e769a85469de2e1d0ddd4238a41c4a8dd2d585. * latest user manual PDF * Have equalization happen before AGC. * Adjust audio level guidance in the user manual. * Increase decay rate for Level meter. * From Mic plot wasn't actually before AGC. * Test not resetting AGC gain on pipeline reset. * Level meter should be pre-equalizer. * Add 'To Mod' tab to show audio post-AGC. * Update user manual guidance. * Adjust level meter tooltip. * Quick test -- see if cache miss improvements help with CI. * More tooltip tweaks. * Move Frm Mic back to after AGC, remove To Mod tab. * Mic level is no longer adjustable from the main window. * Enabling AGC resets mic gain to 0 dB and disables control in Filter window. * Adjust Level gauge scaling to -30 - 0 dB. * Port @barjac's level meter coloring and adjust for log scale. * Add threshold for adopting new maximum value for level meter. Requires that at least 5% of samples are above the previous maximum sample value before a new maximum is adopted. This is intended to filter out brief spikes that should have no impact on audio quality. * Revert filtering added in previous commit. * Need to grab audio before RNNoise. * Decay constant was slightly off (thanks Claude!) * freedv-backend should be using main again. * Fix line spacing. --------- Co-authored-by: Barry Jackson Co-authored-by: Barry Jackson commit efdd8c080d90791caac98940a4146a5968603836 Author: Mooneer Salem Date: Wed Sep 2 12:45:17 2026 -0700 Fix line spacing. commit ad36f2d793af925a4e865881922cfccb86260b63 Author: Mooneer Salem Date: Tue Sep 1 22:21:45 2026 -0700 Readd macOS 14 and 15 to tests. commit bf82814a077ea499e8d0db5ee512a6d3273f2665 Author: Mooneer Salem Date: Tue Sep 1 13:21:11 2026 -0700 Work around dyld error when enabling sanitizers. commit 1e1f061aac0ce1751e1ce91ac7168a4667541ea2 Author: Mooneer Salem Date: Tue Sep 1 12:50:40 2026 -0700 Remove deprecated macos-14, test only on macos-26 due to GitHub issues. commit c7dab1f1dc4e1ae80c89c0421bada377fc95513a Author: Mooneer Salem Date: Tue Sep 1 23:43:35 2026 -0700 Bring back old code for Ubuntu 22.04. commit 80428b3e62490c4381ef2c304e8eb26ebb31da45 Author: Mooneer Salem Date: Tue Sep 1 22:53:21 2026 -0700 Fix typo. commit 8d72e033de7288ba05a41ddd7169f52d6c57bdcf Author: Mooneer Salem Date: Tue Sep 1 22:39:32 2026 -0700 Fix compiler error with Hamlib 5.0. commit 6280d7a8b1b026e2185feb89398dfaa4e755b2a2 Author: Mooneer Salem Date: Tue Sep 1 22:21:45 2026 -0700 Readd macOS 14 and 15 to tests.