Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 62 additions & 19 deletions src/core/CwSidetonePortAudioSink.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,45 +30,68 @@ PaDeviceIndex findPortAudioOutputDevice(const QAudioDevice& device)
return paNoDevice;
}

PaDeviceIndex partialMatch = paNoDevice;
QString partialMatchName;
bool partialMatchAmbiguous = false;
// Collect all partial-match candidates. On Windows a single physical
// device appears under multiple host APIs (MME, DirectSound, WASAPI);
// the candidate list lets us prefer WASAPI instead of giving up when
// more than one partial match is found. (#3193)
struct Candidate { PaDeviceIndex idx; QString rawName; PaHostApiTypeId apiType; };
QList<Candidate> partials;

for (PaDeviceIndex i = 0; i < count; ++i) {
const PaDeviceInfo* info = Pa_GetDeviceInfo(i);
if (!info || info->maxOutputChannels <= 0 || !info->name)
continue;

const QString rawName = QString::fromUtf8(info->name);
const QString candidate =
normalizedDeviceName(rawName);
const QString candidate = normalizedDeviceName(rawName);
if (candidate == target)
return i;

if (candidate.contains(target) || target.contains(candidate)) {
if (partialMatch == paNoDevice) {
partialMatch = i;
partialMatchName = rawName;
} else {
partialMatchAmbiguous = true;
}
// paInDevelopment (0) is used as a safe "unknown" sentinel when
// Pa_GetHostApiInfo returns null — it will never equal paWASAPI.
PaHostApiTypeId apiType = paInDevelopment;
if (const PaHostApiInfo* api = Pa_GetHostApiInfo(info->hostApi))
apiType = api->type;
partials.append({i, rawName, apiType});
}
}

if (partialMatchAmbiguous) {
qCWarning(lcAudio) << "CwSidetonePortAudioSink: selected Qt output device"
<< device.description()
<< "matched multiple PortAudio outputs";
if (partials.isEmpty())
return paNoDevice;
}

if (partialMatch != paNoDevice) {
if (partials.size() == 1) {
qCWarning(lcAudio) << "CwSidetonePortAudioSink: selected Qt output device"
<< device.description()
<< "partially matched PortAudio output"
<< partialMatchName
<< partials[0].rawName
<< "by name substring";
return partials[0].idx;
}

#ifdef Q_OS_WIN
// Multiple matches — the same physical device enumerated under different
// host APIs. Prefer WASAPI (~10 ms shared-mode latency) over MME or
// DirectSound (50–150 ms) to reduce CW timing jitter. (#3193)
QList<Candidate> wasapiCandidates;
for (const Candidate& c : partials) {
if (c.apiType == paWASAPI)
wasapiCandidates.append(c);
}
if (wasapiCandidates.size() == 1) {
qCInfo(lcAudio) << "CwSidetonePortAudioSink: selected Qt output device"
<< device.description()
<< "resolved to WASAPI output"
<< wasapiCandidates[0].rawName
<< "(preferred over" << partials.size() - 1 << "other host API(s))";
return wasapiCandidates[0].idx;
}
return partialMatch;
#endif

qCWarning(lcAudio) << "CwSidetonePortAudioSink: selected Qt output device"
<< device.description()
<< "matched multiple PortAudio outputs";
return paNoDevice;
}

PaDeviceIndex defaultPortAudioOutputDevice()
Expand All @@ -89,6 +112,26 @@ PaDeviceIndex defaultPortAudioOutputDevice()
}
}
}
#endif
#ifdef Q_OS_WIN
// Pa_GetDefaultOutputDevice() on Windows typically returns an MME device
// (the first enumerated host API), which has 50–150 ms OS-level buffering.
// Prefer WASAPI shared mode (~10 ms) to reduce CW timing jitter on fast
// keying. Mirrors the Linux JACK preference above. (#3193)
if (devIdx == paNoDevice) {
const PaHostApiIndex apiCount = Pa_GetHostApiCount();
for (PaHostApiIndex i = 0; i < apiCount; ++i) {
const PaHostApiInfo* api = Pa_GetHostApiInfo(i);
if (!api || !api->name) continue;
if (qstrncmp(api->name, "Windows WASAPI", 14) == 0
&& api->defaultOutputDevice != paNoDevice) {
devIdx = api->defaultOutputDevice;
qCInfo(lcAudio) << "CwSidetonePortAudioSink: using WASAPI host API"
<< "(device" << devIdx << ")";
break;
}
}
}
#endif
if (devIdx == paNoDevice)
devIdx = Pa_GetDefaultOutputDevice();
Expand Down
Loading