Add CommonRadioAudio / Float32 virtual driver support (#1070)#1090
Add CommonRadioAudio / Float32 virtual driver support (#1070)#1090aethersdr-agent[bot] wants to merge 1 commit into
Conversation
…ers (aethersdr#1070) Virtual audio drivers (CommonRadioAudio, BlackHole, Loopback) operate natively in Float32 at the CoreAudio HAL level. The previous macOS RX path relied on isFormatSupported() which can misreport these formats, causing a CAException on the CoreAudio render-callback thread and a silent crash with no error dialog. Replace the isFormatSupported() check in startRxStream() with a direct try-and-open loop that falls back through 24kHz Int16 -> 48kHz Int16 -> 48kHz Float32 -> 44.1kHz Float32. Extend the TX format negotiation loop to also probe Float32 after Int16 for each rate/channel combination. Add m_rxFloat32/m_txFloat32 flags and corresponding Int16<->Float32 conversion so the rest of the pipeline (VITA-49, DSP, resampler) remains Int16-based. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
51f5d32 to
695bbcb
Compare
There was a problem hiding this comment.
Thanks for the Float32 virtual driver fix — the root cause analysis is correct and the RX try-and-open loop is a better approach than trusting isFormatSupported() on CoreAudio. A few items worth addressing before merge:
Out-of-scope changes — DFNR (DeepFilterNet3)
A significant portion of this diff adds a complete DFNR feature that has nothing to do with Float32 virtual driver support: DeepFilterFilter forward declaration, m_dfnr/m_dfnrEnabled members, setDfnrEnabled() / setDfnrAttenLimit() / setDfnrPostFilterBeta() / dfnrAttenLimit(), the dfnrEnabledChanged signal, DeepFilterFilter.h include, the feedAudioData() DFNR branch, and mutual-exclusion wiring in all four other NR paths. This should be a separate PR so it can be reviewed on its own merits and the Float32 fix can land independently.
TX still uses isFormatSupported()
The PR description correctly identifies that isFormatSupported() can lie for HAL-level virtual drivers and trigger a CAException on the render thread. The RX path was updated to avoid this. But startTxStream() still probes with isFormatSupported() for the Float32 candidate:
for (auto sampleFmt : {QAudioFormat::Int16, QAudioFormat::Float32}) {
...
if (dev.isFormatSupported(fmt)) { // ← same unreliable probe on TXIf isFormatSupported() can misreport on the output (sink) side, it can equally misreport on the input (source) side, meaning TX through CommonRadioAudio could still crash. The try-and-open approach used for RX (QAudioSource::start() + state check) should be applied consistently to TX as well.
#ifdef HAVE_DFNR wrapper in setRadeMode is unnecessary
The !HAVE_DFNR stub for setDfnrEnabled(bool) is already defined unconditionally, so the #ifdef guard around setDfnrEnabled(false) in setRadeMode is dead — it compiles fine either way. Minor, but worth cleaning up if the DFNR changes come back in a follow-up.
The conversion helpers (convertInt16ToFloat32 / convertFloat32ToInt16) are correct — clamping, scaling, and the buffer-size accounting in feedAudioData (bytesPerSample for the latency cap) all look right. The stopRxStream reset of m_rxFloat32 and m_resampleTo48k is good hygiene. Core approach is sound; just needs the scope split and the TX-side follow-through.
CI Build Failure —
|
CI Build Failure —
|
CI Build Failure Analysis — Run #24209639869Thanks for this PR — Float32 virtual driver support (CommonRadioAudio, BlackHole, Loopback) is a real pain point and the approach here is sound. The CI failure is in the Build step; Configure passed, so CMake found all dependencies correctly. The Most likely cause —
|
CI Build Failure — AnalysisThanks for this contribution! The CI run #24209639869 failed at the Build step (both the main build job and the CodeQL analysis job hit the same failure). The checkout, CMake configure, and DeepFilterNet3 setup all passed, so the failure is a compiler error in the changed source, not a CI infrastructure issue. What the diff changesThe PR touches
Likely suspects to check
How to reproduce locallycmake -B build -DCMAKE_BUILD_TYPE=Debug # add -DENABLE_DFNR=OFF to test without DFNR
cmake --build build 2>&1 | grep -A5 "error:"Full error outputThe raw compiler errors are in the Build step log — click Build to expand it and you'll see the exact file/line. That's the fastest path to the fix. Once you've identified the line, feel free to push a follow-up commit to this branch — the CI will re-run automatically. Thanks again for tackling the virtual-driver Float32 path; this is a real pain point for CommonRadioAudio / BlackHole users and the approach is solid! |
CI Build Failure AnalysisThanks for this contribution — the Float32 virtual driver support and DFNR mutual-exclusion cleanup look well-thought-out. Here's what I found digging into the failure. What failed
The CMake configuration succeeded, so this is a C++ compilation error, not a build-system issue. The full error text is in the Actions run log — click build → Build to expand the step. Most likely causeLooking at the The key area to verify in // Make sure the NR4 #ifdef wraps only the NR4 else-if arm,
// and not the entire if/else chain that includes BNR below it.
#ifdef HAVE_NR4
} else if (m_nr4Enabled && m_nr4) {
// ...
#endif // ← should close HERE, before the DFNR block
#ifdef HAVE_DFNR
} else if (m_dfnrEnabled && m_dfnr) {
// ...
#endif
} else if (m_bnrEnabled && m_bnr && m_bnr->isConnected()) {If the original code had the How to reproduce locallycmake -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build build --target AetherSDR 2>&1 | grep "error:"Other things look good
Once you check the Again, appreciate the work on the virtual driver audio path — this is a real pain point for users on macOS with BlackHole/CommonRadioAudio. 73! |
CI build failure — Run #24209639869Thanks for this PR! The approach looks solid — Float32 fallback for virtual audio drivers (#1070) and the DFNR integration are both real gaps. Unfortunately the Build step failed; here's what I can see and where to look. What failed
The configure step passing means cmake is happy with the Most likely causes to check
How to reproduce locally./setup-deepfilter.sh
cmake -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build --target AetherSDR 2>&1 | grep -E "error:|warning:" | head -40The full log is at the Actions run linked above — the exact compiler error will be in the Build step output. This is good work — the Float32 virtual driver fix and the DFNR mutual-exclusion plumbing are both things we need. Once you track down the compiler error it should be a quick fix. Happy to take another look when you push a fixup commit. |
|
Thanks for putting this together — the Float32 virtual driver support is a real fix for a nasty silent-crash path, and the design (try-and-open loop instead of What failed and whyThe CI Configure step passed but the Build step failed. Based on the check run (run This branch was created from a point in The PR branch includes This is not a problem with the Float32 logic — the new How to fix itRebase the branch onto current git fetch upstream
git rebase upstream/mainAfter the rebase, Relevant upstream files added by
Once rebased, re-push the branch and the CI should go green. The Float32 support itself looks solid — just needs the history untangled. Thanks again for the contribution! |
CI Build Failure — Run #24209639869Hi! I checked the CI run for commit I wasn't able to retrieve the full compiler error text via the log API, so I reviewed the diff directly to identify the most likely cause. Most likely issue:
|
CI Build Failure — Missing
|
CI Build Failure Analysis —
|
|
Claude here — closing this PR as part of a review pass. Issue #1070 remains open for future implementation. Thanks pi-claude! |
…#2973) ## Summary Implements #2971 by adding a dedicated default-on `aether.audio.summary` logging path for support-bundle-friendly audio routing and negotiation summaries. This adds compact, deduped summary blocks for: - startup audio environment: selected/default input and output devices, saved-device presence, PC Audio state, and TX mic route intent - RX sink startup: actual output device, sample rate, channel count, sample format, resampling state, and fallback/substitution history - TX source startup: actual input device, negotiated rate/channels, mono/stereo state, sample format, resampling-to-24 kHz state, and fallback history - CW sidetone startup: backend, actual/backend-selected device, rate, backend substitution, and fallback history - auxiliary local sinks when instantiated: Quindar local sink and Aetherial monitor playback sink - final open failures: attempted rates/channels/formats, backend, device, failure reason, and fallback history ## Why The normal support log already caught many warning paths, but it did not reliably capture successful runtime sink/source negotiation. That left recurring audio/CW reports without the details needed to distinguish wrong endpoint, sample-rate mismatch, resampler path, backend fallback, and complete open failure cases. ## Important Safety Detail The startup summary uses a new shallow `DeviceDiagnostics::buildAudioStartupSnapshot(...)` helper. It intentionally does not call support-bundle-grade capability probes such as `preferredFormat()` or `isFormatSupported()` across devices at app startup. Full device capability probing remains in `buildAudioDevicesSnapshot(...)` for explicit diagnostics/support flows. Failure summaries only report negotiation work the audio path already attempted; they do not add extra probing. ## Historical Cases Covered The summary path is designed around recurring issue/PR classes including: - macOS Bluetooth/HFP route substitution and 24 kHz vs 48 kHz behavior (#1486, #2634) - CommonRadioAudio / Float32 vs Int16 format issues (#1070, #1090) - CW sidetone endpoint/backend routing (#2072, #2075, #2899, #2901) - CW sidetone missing/distorted reports (#2629, #2694) - no-audio-with-active-meters / output negotiation support gaps (#1855) ## Implementation Notes - Adds `AudioSummaryLogger` with canonical one-block formatters and dedupe by event key. - Adds `aether.audio.summary` as a default-on info category without enabling the full `aether.audio` info/debug stream. - Keeps detailed existing `aether.audio` diagnostics intact. - Documents the summary logging policy in `docs/audio-pipeline.md`. ## Validation - `cmake --build build -j22` - `./build/device_diagnostics_test` - `./build/cw_sidetone_test` - `git diff --check` - Built and deployed a macOS test bundle; the primary test-machine replacement path is currently blocked by a Desktop delete ACL, so a separate quarantine-cleared test bundle was prepared. 👨🏼💻 Generated with OpenAI Codex (GPT-5.5 Pro 4/23) and tested by @jensenpat --------- Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Jeremy Fielder <kk7gwy@aethersdr.com>
…3397) ## What First, **pure-addition** step of the consolidated audio sink factory (#3306): a single shared, OS-parameterized format/rate **negotiation policy** that every audio sink and source will migrate onto, replacing the ~9 divergent per-sink fallback ladders and per-OS `#ifdef` branches that are the root of a recurring class of platform audio bugs. Zero behaviour change — this is the maintainer-endorsed *"land the helper first, migrate sinks incrementally"* approach (#3306, and the #3194 thread's "sensitive audio-stack changes must be separate PRs with soak time"). ## Why this shape The historical bugs escaped CI because the per-OS rate ladders were compiled behind `Q_OS_*`, so a Linux runner could only ever exercise the Linux path. The new policy is a **pure function over an injected `DeviceCaps` snapshot with `TargetOs` as a parameter, not an `#ifdef`** — so one headless, hardware-free test binary exercises the Windows, macOS *and* Linux ladders on any runner. ## Contents - **`src/core/AudioFormatNegotiator.{h,cpp}`** — dependency-free policy (links only `Qt6::Core`). One ladder owns: - Windows force-48k + r8brain (#2120/#2123), macOS 48k/A2DP (#1705), Linux native-24k — divergences preserved as **data**, not `#ifdef`. - The **universal 44.1 kHz rung** that RX/Quindar/QSO are missing today (#3385). - `Int16`↔`Float32` + `preferredFormat()` catch-all (#2669 / #1090 / #3231). - macOS mic preferred-rate-first (#2930) and Bluetooth-HFP native rate (#2615); Windows probe-at-open (#2929). - The two stereo resampler strategies — **PreservePan** (RX/QSO) vs **MonoCollapse** (TCI-TX/RADE) — kept deliberately distinct (#2403/#2459). - **`tests/audio_format_negotiation_test.cpp`** — table-driven golden matrix, registered under CTest. **25/25 pass**, including the 44.1k-only-device regression guard. - **`docs/audio-sink-factory.md`** — the full three-layer design (pure policy → live Qt wrapper → device-ownership router), the device-following **"uncoupling"** fix (CW / RADE / Aetherial-Audio "Pudu" recorder following the selected output), the regression-guard invariants, and the one-sink-per-PR migration plan. ## Test ``` $ ./build/audio_format_negotiation_test ... 25/25 checks passed ``` ## Follow-ups (per the migration plan in the doc) Live Qt wrapper → migrate RX speaker → migrate PC mic → `AudioOutputRouter` (closes the uncoupling class) → TCI-TX client-rate → PipeWire DAX shared resampler. Each a separate, soakable PR. Refs #3306 💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat Co-authored-by: Codex <noreply@openai.com>
…aethersdr#2973) ## Summary Implements aethersdr#2971 by adding a dedicated default-on `aether.audio.summary` logging path for support-bundle-friendly audio routing and negotiation summaries. This adds compact, deduped summary blocks for: - startup audio environment: selected/default input and output devices, saved-device presence, PC Audio state, and TX mic route intent - RX sink startup: actual output device, sample rate, channel count, sample format, resampling state, and fallback/substitution history - TX source startup: actual input device, negotiated rate/channels, mono/stereo state, sample format, resampling-to-24 kHz state, and fallback history - CW sidetone startup: backend, actual/backend-selected device, rate, backend substitution, and fallback history - auxiliary local sinks when instantiated: Quindar local sink and Aetherial monitor playback sink - final open failures: attempted rates/channels/formats, backend, device, failure reason, and fallback history ## Why The normal support log already caught many warning paths, but it did not reliably capture successful runtime sink/source negotiation. That left recurring audio/CW reports without the details needed to distinguish wrong endpoint, sample-rate mismatch, resampler path, backend fallback, and complete open failure cases. ## Important Safety Detail The startup summary uses a new shallow `DeviceDiagnostics::buildAudioStartupSnapshot(...)` helper. It intentionally does not call support-bundle-grade capability probes such as `preferredFormat()` or `isFormatSupported()` across devices at app startup. Full device capability probing remains in `buildAudioDevicesSnapshot(...)` for explicit diagnostics/support flows. Failure summaries only report negotiation work the audio path already attempted; they do not add extra probing. ## Historical Cases Covered The summary path is designed around recurring issue/PR classes including: - macOS Bluetooth/HFP route substitution and 24 kHz vs 48 kHz behavior (aethersdr#1486, aethersdr#2634) - CommonRadioAudio / Float32 vs Int16 format issues (aethersdr#1070, aethersdr#1090) - CW sidetone endpoint/backend routing (aethersdr#2072, aethersdr#2075, aethersdr#2899, aethersdr#2901) - CW sidetone missing/distorted reports (aethersdr#2629, aethersdr#2694) - no-audio-with-active-meters / output negotiation support gaps (aethersdr#1855) ## Implementation Notes - Adds `AudioSummaryLogger` with canonical one-block formatters and dedupe by event key. - Adds `aether.audio.summary` as a default-on info category without enabling the full `aether.audio` info/debug stream. - Keeps detailed existing `aether.audio` diagnostics intact. - Documents the summary logging policy in `docs/audio-pipeline.md`. ## Validation - `cmake --build build -j22` - `./build/device_diagnostics_test` - `./build/cw_sidetone_test` - `git diff --check` - Built and deployed a macOS test bundle; the primary test-machine replacement path is currently blocked by a Desktop delete ACL, so a separate quarantine-cleared test bundle was prepared. 👨🏼💻 Generated with OpenAI Codex (GPT-5.5 Pro 4/23) and tested by @jensenpat --------- Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Jeremy Fielder <kk7gwy@aethersdr.com>
…#3306) (aethersdr#3397) ## What First, **pure-addition** step of the consolidated audio sink factory (aethersdr#3306): a single shared, OS-parameterized format/rate **negotiation policy** that every audio sink and source will migrate onto, replacing the ~9 divergent per-sink fallback ladders and per-OS `#ifdef` branches that are the root of a recurring class of platform audio bugs. Zero behaviour change — this is the maintainer-endorsed *"land the helper first, migrate sinks incrementally"* approach (aethersdr#3306, and the aethersdr#3194 thread's "sensitive audio-stack changes must be separate PRs with soak time"). ## Why this shape The historical bugs escaped CI because the per-OS rate ladders were compiled behind `Q_OS_*`, so a Linux runner could only ever exercise the Linux path. The new policy is a **pure function over an injected `DeviceCaps` snapshot with `TargetOs` as a parameter, not an `#ifdef`** — so one headless, hardware-free test binary exercises the Windows, macOS *and* Linux ladders on any runner. ## Contents - **`src/core/AudioFormatNegotiator.{h,cpp}`** — dependency-free policy (links only `Qt6::Core`). One ladder owns: - Windows force-48k + r8brain (aethersdr#2120/aethersdr#2123), macOS 48k/A2DP (aethersdr#1705), Linux native-24k — divergences preserved as **data**, not `#ifdef`. - The **universal 44.1 kHz rung** that RX/Quindar/QSO are missing today (aethersdr#3385). - `Int16`↔`Float32` + `preferredFormat()` catch-all (aethersdr#2669 / aethersdr#1090 / aethersdr#3231). - macOS mic preferred-rate-first (aethersdr#2930) and Bluetooth-HFP native rate (aethersdr#2615); Windows probe-at-open (aethersdr#2929). - The two stereo resampler strategies — **PreservePan** (RX/QSO) vs **MonoCollapse** (TCI-TX/RADE) — kept deliberately distinct (aethersdr#2403/aethersdr#2459). - **`tests/audio_format_negotiation_test.cpp`** — table-driven golden matrix, registered under CTest. **25/25 pass**, including the 44.1k-only-device regression guard. - **`docs/audio-sink-factory.md`** — the full three-layer design (pure policy → live Qt wrapper → device-ownership router), the device-following **"uncoupling"** fix (CW / RADE / Aetherial-Audio "Pudu" recorder following the selected output), the regression-guard invariants, and the one-sink-per-PR migration plan. ## Test ``` $ ./build/audio_format_negotiation_test ... 25/25 checks passed ``` ## Follow-ups (per the migration plan in the doc) Live Qt wrapper → migrate RX speaker → migrate PC mic → `AudioOutputRouter` (closes the uncoupling class) → TCI-TX client-rate → PipeWire DAX shared resampler. Each a separate, soakable PR. Refs aethersdr#3306 💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat Co-authored-by: Codex <noreply@openai.com>
Fixes #1070
Summary
CommonRadioAudio (and other HAL-level virtual audio drivers like BlackHole and Loopback) operate natively in Float32. The previous macOS RX path used
isFormatSupported()to probe Int16 formats only; if the driver incorrectly reported Int16 as supported, Qt would attempt to open a CoreAudio AudioUnit stream in Int16, causing the HAL plugin to throw aCAExceptionon the render callback thread. Because Qt never catches exceptions on CoreAudio threads, the process terminated silently — the "simply closes with no error" symptom.Changes
startRxStream()(macOS/Linux) — ReplaceisFormatSupported()probe with a direct try-and-open loop: 24 kHz Int16 → 48 kHz Int16 → 48 kHz Float32 → 44.1 kHz Float32. First format that opens successfully wins.startTxStream()(macOS/Linux) — Extend the format negotiation inner loop to also probeQAudioFormat::Float32afterInt16for each rate/channel combination, so CommonRadioAudio input (mic) is also supported.feedAudioData()writeAudiolambda — Whenm_rxFloat32is set, convert Int16 PCM (from the radio) to Float32 before writing to the sink.onTxAudioReady()— Whenm_txFloat32is set, convert Float32 captured audio to Int16 before the resampler and VITA-49 encoding path.AudioEngine.h— Addm_rxFloat32andm_txFloat32atomic flags.Test plan
🤖 Generated with Claude Code