Skip to content

Commit 623a97a

Browse files
nigelfentonclaudeten9876
authored
feat(audio): compile the PortAudio CW sidetone sink on Windows (#5200) (#5201)
Fixes #5200. ## What Stock Windows/MSVC builds have never compiled `CwSidetonePortAudioSink.cpp`: PortAudio detection ran exclusively through pkg-config, which does not resolve on Windows, so `HAVE_PORTAUDIO` was never defined and every Windows build silently took the push-model `CwSidetoneQAudioSink` sidetone (2 ms timer). The WASAPI host-API preference added in #3193 *specifically for Windows CW jitter* has consequently never been active in any shipped build. Windows is the build-configuration route to the same push-path landing spot as #4978's Linux runtime route (per the split documented in #4890). **Build plumbing** mirrors the established FFTW3/hidapi Windows-dependency pattern: - **`scripts/setup/setup-portaudio.ps1`** — downloads PortAudio v19.7.0 (pinned, SHA256-verified, same `_verify_sha256.ps1` helper as the other setup scripts), builds the static lib with MSVC (WASAPI requested explicitly; WDM-KS / DirectSound / MME from upstream's Windows defaults), installs into `third_party/portaudio/`. - **`CMakeLists.txt`** — a `WIN32` detection branch that soft-detects `third_party/portaudio/` (absent → build proceeds, with a `message(WARNING)` naming the script and the consequence), checking both the header and the static lib it links, and naming that lib's system dependencies from PortAudio's own CMake export. - **CI wiring** — the Windows CI job runs the script (after `msvc-dev-cmd`, same as hidapi), plus an assert step that fails the job if CMake did not actually find PortAudio, so this job cannot go green having compiled nothing new. **Sink fixes.** Merely compiling the sink was NOT enough. Live A/B testing by ear (FLEX-6300 into a dummy load, keyboard iambic paddles) found it audibly broken on Windows, three defects deep — all in code that had only ever run against CoreAudio: 1. **Windows friendly names are not unique.** The test box has **three active endpoints all named "TOSHIBA-TV (NVIDIA High Definition Audio)"** (one per HDMI connector; five including unplugged ones). Name matching selected a live-but-unwired port that accepted the stream and played it into nothing — instrumentation showed 50k callbacks rendering a clean 0.566-peak tone into an inaudible endpoint. The fix matches the Qt device to the PortAudio WASAPI device **by endpoint ID** (`PaWasapi_GetIMMDevice` → `IMMDevice::GetId` compared against `QAudioDevice::id()`), demoting name matching to fallback. This is the Windows analog of the name-match fragility skerker is fixing on Linux in #5123/#5135 — same disease, platform-appropriate cure. 2. **The exact-match branch defeated #3193.** `findPortAudioOutputDevice()` returned the *first* exact name match in enumeration order — DirectSound on Windows — so the WASAPI preference (written only into the partial-match branch) never ran. Exact matches are now collected and the same WASAPI preference applied; a multi-exact match with no WASAPI candidate names the losing candidates rather than silently taking the first. 3. **`suggestedLatency = 0.0` is a CoreAudio-ism.** DirectSound built an unservable buffer ring: the stream ran, the callback rendered a clean tone, and the speaker output was garbled crackle. Windows now requests the device's `defaultLowOutputLatency` (22 ms reported on WASAPI shared for this endpoint); other platforms unchanged. **Observability**, because the diagnosis above needed instrumentation the start line lacked — `hostApi=` on the started line, a stopping line with `callbacks=` / `peak=` / `underflows=` / `overflows=`, and the endpoint-ID match. All on `lcAudioSummary` so they reach a **default** support bundle; `lcAudio` sits at `QtWarningMsg`, so a `qCInfo` on it would not. A started stream that renders silence or garbage is now distinguishable from a working one without asking the operator to re-run with debug logging. Underflow counting exempts the first couple of stream-prime callbacks, which report `paOutputUnderflow` on essentially every host and are not deadline misses. **`src/core/CwSidetoneEdgeProbe.h`** (new) is the bench instrument behind the timing tables in the thread: `AETHER_CW_EDGE_PROBE=1` makes **both** sidetone sinks capture envelope transitions at the sink boundary with a running sample counter, dumped at `stop()` as `EDGEPROBE` lines. Positions are stream-sample-exact, so element durations and onset spacing are measured on the stream's own clock — no loopback recording, no wall-clock jitter in the instrument. Disabled it costs one bool test per rendered buffer and allocates nothing. `CwSidetoneQAudioSink` is wired up too, which is what makes the push-vs-callback A/B a like-for-like comparison; that is the only change here to a file that runs on every platform. `tests/cw_sidetone_edge_probe_test.cpp` covers the probe: the empty-stream reset (reverting it fails the test with `got 480001, want 0`), the zero-crossing rule the falling-edge detection rests on, short-element detection, and the absolute-threshold limitation — pinned so it stays a deliberate property. ## Shipped Windows builds switch to the callback sink This is the maintainer decision #5200 asked to be made explicitly, and it has been made: `windows-installer.yml` runs the setup script, so **shipped Windows installers now build with `HAVE_PORTAUDIO` and ship the PortAudio callback sidetone** — the same transport macOS and Linux already ship, and what #3193 intended but never delivered. Note there is no per-platform default being changed: `CwSidetoneBackend` has always defaulted to `"PortAudio"` on every platform. On Windows that default was simply unsatisfiable, because the sink was never compiled. This makes it reachable. **The trade being accepted**, from the element timing further down this thread: - **Latency (the win):** ~22 ms buffered depth to the DAC against the push sink's ~50 ms — roughly **28 ms less key-to-ear delay**. That is what a CW operator feels at the key and what #3193 was chasing. - **Rhythm (the cost):** a small jitter tail at 30 WPM that two independent Windows boxes both show and neither explains — displacements quantized around 10.3–10.8 ms, with `underflows= 0` across 169,523 callbacks, so it is *not* a device deadline miss. At 20 WPM the tail is absent (SD 0.262 ms, 0 of 176 elements beyond 3SD). The in-stream rhythm of the push path remains tighter (SD 0.26 ms vs 0.96 ms on the originating box). Latency wins; the tail is tracked rather than dismissed, and the instrumentation in this PR is what will explain it. The installer job also gains an assert mirroring the GPU-spectrum guard — the `WIN32` detection is soft-optional, and this is the workflow that produces what users install, so a silent skip would ship the push sidetone while the release notes claimed otherwise. It fails the build instead. `CwSidetoneBackend=QAudioSink` remains the escape hatch, and the PortAudio-start-failure → QAudioSink fallback (with the consequence-naming log from the #4978 fixes) is unchanged. ## Verified on real hardware (Windows 11, MSVC, Qt 6.10.3, FLEX-6300 on a dummy load) Full build from clean configure; app run, connected, keyboard iambic paddles keyed by both an operator and synthesized key events; every claim below is **by ear**, A/B on the same physical endpoint: - **QAudioSink control:** clean tones (establishes the endpoint + gate path). - **Sink as first compiled** (DirectSound selected by defect 2, garbled by defect 3): audibly broken — first reported as "sounds crap", then near-silent on a different endpoint pick (defect 1). - **Sink with all three fixes:** `matched WASAPI endpoint by ID "{0.0.0.00000000}.{35f3f303-…}"` → `hostApi= Windows WASAPI … outputLatency= 22 ms` → **clean tones**, operator-confirmed, radio keying verified via `cw key` wire traces and clean unkey after every run. Element-timing measurement per @williamscody's #4890 methodology is in the thread below, as is @skerker's independent replication on macOS, Windows and Linux — the macOS arm is the regression check that mattered, since that platform already ships this sink. A process note worth stating plainly: the PR as first opened claimed the sink "works" from a clean start line and low reported latency. That claim was wrong — a started stream was rendering garbage. The by-ear A/B is what caught it, and the `callbacks=` / `peak=` instrumentation is there so the next person can catch it from a log instead. ## Not exercised - MME-truncation partial-match path (all live selections here resolved exact or by ID). - PortAudio start-failure → QAudioSink fallback on Windows. - The Windows-only `#ifdef Q_OS_WIN` paths have no automated coverage — `findPortAudioOutputDevice()` needs live device enumeration, and a synthetic peer would be worse than nothing. They rest on the hardware A/B above and @skerker's Windows bench. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Jeremy [KK7GWY] <kk7gwy@aethersdr.com>
1 parent 2bed375 commit 623a97a

13 files changed

Lines changed: 825 additions & 17 deletions

.github/workflows/ci.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,15 @@ jobs:
522522
shell: pwsh
523523
run: .\scripts\setup\setup-hidapi.ps1
524524

525+
- name: Setup PortAudio (CW sidetone callback sink)
526+
# Without this, HAVE_PORTAUDIO is undefined and
527+
# CwSidetonePortAudioSink never compiles on Windows — the CW
528+
# sidetone silently takes the push-model QAudioSink path and CI
529+
# would miss real compile errors in the PortAudio sink (#5200).
530+
# Runs after msvc-dev-cmd for cl.exe + link.exe on PATH.
531+
shell: pwsh
532+
run: .\scripts\setup\setup-portaudio.ps1
533+
525534
# zlib is bundled under third_party/zlib (1.3.1) — no vcpkg install
526535
# needed; CMake builds zlibstatic from source. (#2651)
527536

@@ -612,6 +621,31 @@ jobs:
612621
}
613622
Select-String -Path configure.log -Pattern 'GPU spectrum rendering enabled'
614623
624+
# The WIN32 PortAudio detection keys on EXISTS third_party/portaudio and
625+
# is soft-optional — if the setup step's output ever goes missing (tag
626+
# moved, cache restored a partial tree, output rename in a bump), CMake
627+
# would skip CwSidetonePortAudioSink.cpp without a word and this job
628+
# would go green having compiled nothing new. Assert the found line so
629+
# the coverage this job exists to buy cannot evaporate silently (#5200).
630+
- name: Assert PortAudio sidetone sink actually enabled
631+
shell: pwsh
632+
run: |
633+
if (-not (Select-String -Path configure.log -Pattern 'PortAudio \(third_party\) found' -Quiet)) {
634+
Write-Host "ERROR: setup-portaudio.ps1 ran but CMake did not find third_party/portaudio."
635+
# NOT Select-String on configure.log: the not-found leg is a
636+
# message(WARNING), which CMake writes to stderr, and the configure
637+
# step tees stdout only (see the note there). Show the tree the
638+
# detection actually keys on instead.
639+
Write-Host "--- third_party/portaudio ---"
640+
if (Test-Path third_party\portaudio) {
641+
Get-ChildItem -Recurse third_party\portaudio | Select-Object -ExpandProperty FullName
642+
} else {
643+
Write-Host "(third_party/portaudio does not exist)"
644+
}
645+
exit 1
646+
}
647+
Select-String -Path configure.log -Pattern 'PortAudio \(third_party\) found'
648+
615649
- name: Build Opus (RADE dependency)
616650
# ExternalProject dependency ordering via BUILD_BYPRODUCTS is correct,
617651
# but a separate step gives clearer CI failure attribution and avoids

.github/workflows/windows-installer.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,24 @@ jobs:
131131
if: steps.cache-hidapi.outputs.cache-hit != 'true'
132132
run: .\scripts\setup\setup-hidapi.ps1
133133

134+
# PortAudio is what makes the CW sidetone run on the callback ("pull")
135+
# path with WASAPI, the same transport macOS and Linux already ship.
136+
# Without it HAVE_PORTAUDIO is undefined, CwSidetonePortAudioSink.cpp is
137+
# not compiled, and the shipped installer silently falls through to the
138+
# push-model QAudioSink — which is what every Windows build did before
139+
# #5200, including the #3193 WASAPI preference that was never active.
140+
# Static lib, so nothing to bundle in the Deploy step. (#5200)
141+
- name: Cache PortAudio
142+
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
143+
id: cache-portaudio
144+
with:
145+
path: third_party/portaudio
146+
key: portaudio-${{ runner.os }}-${{ hashFiles('scripts/setup/setup-portaudio.ps1') }}
147+
148+
- name: Setup PortAudio (CW sidetone callback sink)
149+
if: steps.cache-portaudio.outputs.cache-hit != 'true'
150+
run: .\scripts\setup\setup-portaudio.ps1
151+
134152
- name: Cache DeepFilterNet3
135153
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
136154
id: cache-deepfilter
@@ -239,6 +257,28 @@ jobs:
239257
}
240258
Select-String -Path configure.log -Pattern 'GPU spectrum rendering enabled'
241259
260+
# Same shape as the GPU-spectrum guard above, and for the same reason:
261+
# the WIN32 PortAudio detection is soft-optional, so a missing or
262+
# half-restored third_party/portaudio makes CMake skip the callback sink
263+
# without failing. This workflow is the one place that matters most --
264+
# it produces what users install, so a silent skip would ship the
265+
# push-model QAudioSink while the release notes claim the callback path.
266+
# Fail here instead. (#5200)
267+
- name: Assert PortAudio sidetone sink actually enabled
268+
run: |
269+
if (-not (Select-String -Path configure.log -Pattern 'PortAudio \(third_party\) found' -Quiet)) {
270+
Write-Host "ERROR: setup-portaudio.ps1 ran but CMake did not find third_party/portaudio."
271+
Write-Host "The installer would ship the push-model QAudioSink sidetone. Refusing."
272+
Write-Host "--- third_party/portaudio ---"
273+
if (Test-Path third_party\portaudio) {
274+
Get-ChildItem -Recurse third_party\portaudio | Select-Object -ExpandProperty FullName
275+
} else {
276+
Write-Host "(third_party/portaudio does not exist)"
277+
}
278+
exit 1
279+
}
280+
Select-String -Path configure.log -Pattern 'PortAudio \(third_party\) found'
281+
242282
- name: Build Opus (RADE dependency)
243283
# ExternalProject dependency ordering via BUILD_BYPRODUCTS is correct,
244284
# but a separate step gives clearer CI failure attribution and avoids

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ third_party/onnxruntime-*.zip
4141
third_party/sherpa-onnx/
4242
third_party/sherpa-onnx-*.tar.bz2
4343

44+
# Built by scripts/setup/setup-portaudio.ps1 on Windows (#5200); the tarball
45+
# only survives a run that failed partway.
46+
third_party/portaudio/
47+
third_party/portaudio-*.tar.gz
48+
4449
# Claude Code / Browser-pane dev-server config. Per-developer and per-worktree:
4550
# it names a local build path and the entry is generated by whoever opens the
4651
# preview, so a committed copy is wrong for everybody else. `.claude/commands/`

CMakeLists.txt

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -296,10 +296,43 @@ else()
296296
message(STATUS "Qt6Keychain not found — credential persistence disabled")
297297
endif()
298298

299-
# PortAudio (optional fallback for audio)
300-
find_package(PkgConfig QUIET)
301-
if(PkgConfig_FOUND)
302-
pkg_check_modules(PORTAUDIO portaudio-2.0)
299+
# PortAudio (optional fallback for audio; on Windows it is the CW sidetone's
300+
# callback-model path — without it the sidetone runs on the push-model
301+
# QAudioSink and the WASAPI-preference logic from #3193 never compiles, #5200)
302+
# Windows: run scripts/setup/setup-portaudio.ps1 first to build the static lib
303+
# Linux: apt install portaudio19-dev
304+
# macOS: brew install portaudio
305+
if(WIN32)
306+
set(PORTAUDIO_ROOT "${CMAKE_SOURCE_DIR}/third_party/portaudio")
307+
# Check BOTH artefacts the link below depends on. Testing only the header
308+
# let a half-populated third_party/portaudio (interrupted setup run, cache
309+
# restored without lib/) configure as "found", satisfy the CI assert, and
310+
# then die at link. setup-portaudio.ps1's own early-exit guard tests the
311+
# .lib, so this keeps the two in agreement. (#5200)
312+
if(EXISTS "${PORTAUDIO_ROOT}/include/portaudio.h"
313+
AND EXISTS "${PORTAUDIO_ROOT}/lib/portaudio_static_x64.lib")
314+
set(PORTAUDIO_FOUND TRUE)
315+
set(PORTAUDIO_INCLUDE_DIRS "${PORTAUDIO_ROOT}/include")
316+
# Static lib, so its host-API system dependencies must be named here
317+
# (matches INTERFACE_LINK_LIBRARIES of PortAudio's own CMake export).
318+
set(PORTAUDIO_LIBRARIES
319+
"${PORTAUDIO_ROOT}/lib/portaudio_static_x64.lib"
320+
winmm dsound ole32 uuid setupapi)
321+
# CI greps configure output for this exact line — a soft-optional
322+
# dependency that vanishes must fail the gate, not silently skip
323+
# compiling the sink (same rule as the GPU-spectrum assert).
324+
message(STATUS "PortAudio (third_party) found — CW sidetone callback sink enabled")
325+
else()
326+
# Soft-optional (build proceeds), but VISIBLY: without this the
327+
# sidetone silently lands on the push-model QAudioSink path.
328+
message(WARNING "PortAudio not found — run scripts/setup/setup-portaudio.ps1; "
329+
"the CW sidetone will use the push-model QAudioSink")
330+
endif()
331+
else()
332+
find_package(PkgConfig QUIET)
333+
if(PkgConfig_FOUND)
334+
pkg_check_modules(PORTAUDIO portaudio-2.0)
335+
endif()
303336
endif()
304337

305338
# FFTW3 (required by vendored WDSP; also used by NR2 spectral noise reduction)

scripts/setup/setup-portaudio.ps1

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<#
2+
.SYNOPSIS
3+
Download and build PortAudio for Windows x64.
4+
5+
.DESCRIPTION
6+
Downloads PortAudio v19.7.0 source from GitHub, builds the static library
7+
with CMake + MSVC, and places headers/lib in third_party/portaudio/ ready
8+
for CMake. WASAPI is requested EXPLICITLY (-DPA_USE_WASAPI=ON) because the
9+
sidetone sink's whole reason to exist is #3193's WASAPI preference — a
10+
future PortAudio bump must not be able to drop it silently. WDM-KS /
11+
DirectSound / MME come from upstream's Windows defaults.
12+
13+
Required for the callback-model CW sidetone sink (CwSidetonePortAudioSink)
14+
and its WASAPI host-API preference (#3193). Without it the Windows build
15+
silently falls back to the push-model QAudioSink sidetone path.
16+
17+
.EXAMPLE
18+
.\setup-portaudio.ps1
19+
#>
20+
21+
$ErrorActionPreference = "Stop"
22+
. "$PSScriptRoot\_verify_sha256.ps1"
23+
24+
$PaVersion = "19.7.0"
25+
$PaUrl = "https://github.com/PortAudio/portaudio/archive/refs/tags/v${PaVersion}.tar.gz"
26+
# SHA256 of the GitHub source archive. Bump alongside the version.
27+
$PaSha256 = "5af29ba58bbdbb7bbcefaaecc77ec8fc413f0db6f4c4e286c40c3e1b83174fa0"
28+
$OutDir = "third_party\portaudio"
29+
$TarFile = "third_party\portaudio-${PaVersion}.tar.gz"
30+
31+
# ── Check if already set up ──────────────────────────────────────────────
32+
if (Test-Path "$OutDir\lib\portaudio_static_x64.lib") {
33+
Write-Host "PortAudio already set up in $OutDir" -ForegroundColor Green
34+
exit 0
35+
}
36+
37+
# ── Create directories ───────────────────────────────────────────────────
38+
New-Item -ItemType Directory -Force -Path "third_party" | Out-Null
39+
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
40+
New-Item -ItemType Directory -Force -Path "$OutDir\lib" | Out-Null
41+
New-Item -ItemType Directory -Force -Path "$OutDir\include" | Out-Null
42+
43+
# ── Download source ─────────────────────────────────────────────────────
44+
if (-not (Test-Path $TarFile)) {
45+
Write-Host "Downloading PortAudio ${PaVersion} source..." -ForegroundColor Cyan
46+
Invoke-WebRequest -Uri $PaUrl -OutFile $TarFile
47+
}
48+
# Verify OUTSIDE the download guard. This script deletes the tarball on
49+
# success, so a tarball that survives to a later run is by definition from a
50+
# run that failed partway — possibly mid-download. Verifying only what we just
51+
# fetched would consume that one unchecked.
52+
Confirm-Sha256 -Path $TarFile -Expected $PaSha256
53+
54+
# ── Extract ──────────────────────────────────────────────────────────────
55+
Write-Host "Extracting..." -ForegroundColor Cyan
56+
$tempDir = "third_party\portaudio-temp"
57+
if (Test-Path $tempDir) { Remove-Item -Recurse -Force $tempDir }
58+
New-Item -ItemType Directory -Force -Path $tempDir | Out-Null
59+
tar -xzf $TarFile -C $tempDir 2>$null
60+
61+
$srcDir = Get-ChildItem "$tempDir\portaudio-*" -Directory | Select-Object -First 1
62+
if (-not $srcDir) {
63+
Write-Error "Failed to locate extracted PortAudio source"
64+
exit 1
65+
}
66+
67+
# ── Build with CMake + MSVC ──────────────────────────────────────────────
68+
Write-Host "Building PortAudio from source with MSVC..." -ForegroundColor Cyan
69+
70+
$buildDir = "$($srcDir.FullName)\build"
71+
# CMAKE_POLICY_VERSION_MINIMUM: v19.7.0's CMakeLists declares a
72+
# cmake_minimum_required below 3.5, which CMake 4.x refuses outright — same
73+
# situation and same fix as setup-hidapi.ps1. The flag MUST be quoted:
74+
# PowerShell's native-argument tokenizer splits an unquoted -Dkey=3.5 at
75+
# the dot, so CMake receives "3" and rejects it.
76+
cmake -B $buildDir -S $srcDir.FullName -G "Ninja" `
77+
-DCMAKE_BUILD_TYPE=Release `
78+
-DPA_BUILD_SHARED=OFF `
79+
-DPA_BUILD_STATIC=ON `
80+
-DPA_USE_WASAPI=ON `
81+
-DPA_BUILD_EXAMPLES=OFF `
82+
-DPA_BUILD_TESTS=OFF `
83+
"-DCMAKE_POLICY_VERSION_MINIMUM=3.5"
84+
85+
cmake --build $buildDir --config Release -j $env:NUMBER_OF_PROCESSORS
86+
87+
# ── Find and copy built artifacts ────────────────────────────────────────
88+
$libFile = Get-ChildItem "$buildDir" -Recurse -Filter "portaudio_static_x64.lib" | Select-Object -First 1
89+
if (-not $libFile) {
90+
Write-Error "Failed to build portaudio_static_x64.lib"
91+
exit 1
92+
}
93+
94+
Copy-Item $libFile.FullName "$OutDir\lib\portaudio_static_x64.lib"
95+
# Public header plus the pa_win_* host-API headers (WASAPI stream options etc.)
96+
Copy-Item "$($srcDir.FullName)\include\*.h" "$OutDir\include\"
97+
98+
# ── Cleanup ──────────────────────────────────────────────────────────────
99+
Remove-Item -Recurse -Force $tempDir
100+
Remove-Item -Force $TarFile
101+
102+
Write-Host "PortAudio ready in $OutDir" -ForegroundColor Green
103+
Write-Host " Header: $OutDir\include\portaudio.h"
104+
Write-Host " Lib: $OutDir\lib\portaudio_static_x64.lib"

src/core/AudioEngine.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4113,8 +4113,10 @@ void AudioEngine::setMuted(bool muted)
41134113
}
41144114

41154115
// Pick the sidetone backend based on build flag + AppSettings override.
4116-
// PortAudio when available (lower latency on Linux/macOS); QAudioSink
4117-
// fallback otherwise or when explicitly requested by the user.
4116+
// PortAudio when available (the callback path: lower latency on every
4117+
// platform that builds it — Windows joined Linux/macOS in #5200, where the
4118+
// shipped installer started providing it); QAudioSink fallback otherwise or
4119+
// when explicitly requested by the user.
41184120
static std::unique_ptr<CwSidetoneSinkBackend> makeSidetoneBackend(QObject* qparent)
41194121
{
41204122
const QString pref =

0 commit comments

Comments
 (0)