Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qwindowsaudiodevice.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
5
6#include <QtCore/qdebug.h>
7#include <QtCore/qloggingcategory.h>
8#include <QtCore/qthreadpool.h>
9#include <QtCore/qt_windows.h>
10#include <QtCore/private/qsystemerror_p.h>
11#include <QtCore/qapplicationstatic.h>
12
13#include <QtMultimedia/private/qaudioformat_p.h>
14#include <QtMultimedia/private/qcominitializer_p.h>
15#include <QtMultimedia/private/qcomtaskresource_p.h>
16#include <QtMultimedia/private/qwindows_propertystore_p.h>
17#include <QtMultimedia/private/qwindowsaudioutils_p.h>
18
19#include <audioclient.h>
20#include <mmdeviceapi.h>
21#include <propkeydef.h>
22
23#include <future>
24#include <set>
25
26QT_BEGIN_NAMESPACE
27
28using QtMultimediaPrivate::PropertyStoreHelper;
29using namespace Qt::Literals;
30
31namespace {
32
33Q_STATIC_LOGGING_CATEGORY(qLcAudioDeviceProbes, "qt.multimedia.audiodevice.probes")
34
35std::optional<EndpointFormFactor> inferFormFactor(PropertyStoreHelper &propertyStore)
36{
37 std::optional<uint32_t> val = propertyStore.getUInt32(PKEY_AudioEndpoint_FormFactor);
38 if (val == EndpointFormFactor::UnknownFormFactor)
39 return EndpointFormFactor(*val);
40
41 return std::nullopt;
42}
43
44std::optional<QAudioFormat::ChannelConfig>
45inferChannelConfiguration(PropertyStoreHelper &propertyStore, int maximumChannelCount)
46{
47 std::optional<uint32_t> val = propertyStore.getUInt32(PKEY_AudioEndpoint_PhysicalSpeakers);
48 if (val && val != 0)
49 return QWindowsAudioUtils::maskToChannelConfig(*val, maximumChannelCount);
50
51 return std::nullopt;
52}
53
54int maxChannelCountForFormFactor(EndpointFormFactor formFactor)
55{
56 switch (formFactor) {
57 case EndpointFormFactor::Headphones:
58 case EndpointFormFactor::Headset:
59 return 2;
60 case EndpointFormFactor::SPDIF:
61 return 6; // SPDIF can have 2 channels of uncompressed or 6 channels of compressed audio
62
63 case EndpointFormFactor::DigitalAudioDisplayDevice:
64 return 8; // HDMI can have max 8 channels
65
66 case EndpointFormFactor::Microphone:
67 return 32; // 32 channels should be more than enough for real-world microphones
68
69 default:
70 return 128;
71 }
72}
73
74struct FormatProbeResult
75{
76 void update(const QAudioFormat &fmt)
77 {
78 supportedSampleFormats.insert(fmt.sampleFormat());
79 updateChannelCount(fmt.channelCount());
80 updateSamplingRate(fmt.sampleRate());
81 }
82
83 void updateChannelCount(int channelCount)
84 {
85 if (channelCount < channelCountRange.first)
86 channelCountRange.first = channelCount;
87 if (channelCount > channelCountRange.second)
88 channelCountRange.second = channelCount;
89 }
90
91 void updateSamplingRate(int samplingRate)
92 {
93 if (samplingRate < sampleRateRange.first)
94 sampleRateRange.first = samplingRate;
95 if (samplingRate > sampleRateRange.second)
96 sampleRateRange.second = samplingRate;
97 }
98
99 std::set<QAudioFormat::SampleFormat> supportedSampleFormats;
100 std::pair<int, int> channelCountRange{ std::numeric_limits<int>::max(), 0 };
101 std::pair<int, int> sampleRateRange{ std::numeric_limits<int>::max(), 0 };
102
103 [[maybe_unused]]
104 friend QDebug operator<<(QDebug dbg, const FormatProbeResult &self)
105 {
106 QDebugStateSaver saver(dbg);
107 dbg.nospace();
108
109 dbg << "FormatProbeResult{supportedSampleFormats: " << self.supportedSampleFormats
110 << ", channelCountRange: " << self.channelCountRange.first << " - " << self.channelCountRange.second
111 << ", sampleRateRange: " << self.sampleRateRange.first << "-" << self.sampleRateRange.second
112 << "}";
113 return dbg;
114 }
115};
116
117std::optional<QAudioFormat> performIsFormatSupportedWithClosestMatch(const ComPtr<IAudioClient> &audioClient,
118 const QAudioFormat &fmt)
119{
120 using namespace QWindowsAudioUtils;
121 std::optional<WAVEFORMATEXTENSIBLE> formatEx = toWaveFormatExtensible(fmt);
122 if (!formatEx) {
123 qCWarning(qLcAudioDeviceProbes) << "toWaveFormatExtensible failed" << fmt;
124 return std::nullopt;
125 }
126
127 qCDebug(qLcAudioDeviceProbes) << "performIsFormatSupportedWithClosestMatch for" << fmt;
128 QComTaskResource<WAVEFORMATEX> closestMatch;
129 HRESULT result = audioClient->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, &formatEx->Format,
130 closestMatch.address());
131
132 if (FAILED(result)) {
133 qCDebug(qLcAudioDeviceProbes) << "performIsFormatSupportedWithClosestMatch: error" << QSystemError::windowsComString(result);
134 return std::nullopt;
135 }
136
137 if (closestMatch) {
138 QAudioFormat closestMatchFormat = waveFormatExToFormat(*closestMatch);
139 qCDebug(qLcAudioDeviceProbes) << "performProbe returned closest match" << closestMatchFormat;
140 return closestMatchFormat;
141 }
142
143 qCDebug(qLcAudioDeviceProbes) << "performProbe successful";
144
145 return fmt;
146}
147
148std::optional<FormatProbeResult> probeFormats(const ComPtr<IAudioClient> &audioClient,
149 PropertyStoreHelper &propertyStore,
150 const QAudioFormat &preferredFormat)
151{
152 using namespace QWindowsAudioUtils;
153
154 // probing formats is a bit slow, so we limit the number of channels of we can
155 std::optional<EndpointFormFactor> formFactor = inferFormFactor(propertyStore);
156 int maxChannelsForFormFactor = formFactor ? maxChannelCountForFormFactor(*formFactor) : 128;
157
158 qCDebug(qLcAudioDeviceProbes) << "probing: maxChannelsForFormFactor" << maxChannelsForFormFactor << formFactor;
159
160 std::optional<FormatProbeResult> limits;
161
162 // Note: probing for AUDCLNT_SHAREMODE_SHARED, float32 seems to be the preferred format for all
163 // devices.
164 constexpr QAudioFormat::SampleFormat initialSampleFormat = QAudioFormat::SampleFormat::Float;
165
166 // we initially probe for the maximum channel count for the format.
167 // wasapi will typically recommend a "closest" match, containing the max number of channels
168 // we can probe for.
169 QAudioFormat initialProbeFormat;
170 initialProbeFormat.setSampleFormat(initialSampleFormat);
171 initialProbeFormat.setSampleRate(preferredFormat.sampleRate());
172 initialProbeFormat.setChannelCount(maxChannelsForFormFactor);
173
174 qCDebug(qLcAudioDeviceProbes) << "probeFormats: probing for" << initialProbeFormat;
175
176 std::optional<QAudioFormat> initialProbeResult =
177 performIsFormatSupportedWithClosestMatch(audioClient, initialProbeFormat);
178
179 int maxChannelForFormat;
180 if (initialProbeResult) {
181 if (initialProbeResult->sampleRate() != preferredFormat.sampleRate()) {
182 qCDebug(qLcAudioDeviceProbes)
183 << "probing: returned a different sample rate as closest match ..."
184 << *initialProbeResult;
185 return std::nullopt;
186 }
187
188 maxChannelForFormat = initialProbeResult->channelCount();
189 } else {
190 // some drivers seem to not report any closest match, but simply fail.
191 // in this case we need to brute-force enumerate the formats
192 // however probing is rather expensive, so we limit our probes to a maxmimum of 2
193 // channels
194 maxChannelForFormat = std::min(maxChannelsForFormFactor, 2);
195 }
196
197 // we rely on wasapi's AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM for format conversion, so we only
198 // need to check the closest match format
199 QAudioFormat::SampleFormat probeSampleFormat =
200 initialProbeResult ? initialProbeResult->sampleFormat() : initialSampleFormat;
201
202 for (int channelCount = 1; channelCount != maxChannelForFormat + 1; ++channelCount) {
203 QAudioFormat fmt;
204 fmt.setSampleFormat(probeSampleFormat);
205 fmt.setSampleRate(preferredFormat.sampleRate());
206 fmt.setChannelCount(channelCount);
207
208 std::optional<WAVEFORMATEXTENSIBLE> formatEx = toWaveFormatExtensible(fmt);
209 if (!formatEx)
210 continue;
211
212 qCDebug(qLcAudioDeviceProbes) << "probing" << fmt;
213
214 QComTaskResource<WAVEFORMATEX> closestMatch;
215 HRESULT result = audioClient->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, &formatEx->Format,
216 closestMatch.address());
217
218 if (FAILED(result)) {
219 qCDebug(qLcAudioDeviceProbes)
220 << "probing format failed" << QSystemError::windowsComString(result);
221 continue;
222 }
223
224 if (closestMatch) {
225 qCDebug(qLcAudioDeviceProbes) << "probing format reported a closest match"
226 << waveFormatExToFormat(*closestMatch);
227 continue; // we don't have an exact match, but just something close by
228 }
229
230 if (!limits)
231 limits = FormatProbeResult{};
232
233 qCDebug(qLcAudioDeviceProbes) << "probing format successful" << fmt;
234 limits->update(fmt);
235 }
236
237 qCDebug(qLcAudioDeviceProbes) << "probing successful" << limits;
238
239 return limits;
240}
241
242std::optional<QAudioFormat> probePreferredFormat(const ComPtr<IAudioClient> &audioClient)
243{
244 using namespace QWindowsAudioUtils;
245
246 static const QAudioFormat preferredFormat = [] {
247 QAudioFormat fmt;
248 fmt.setSampleRate(44100);
249 fmt.setChannelCount(2);
250 fmt.setSampleFormat(QAudioFormat::Int16);
251 return fmt;
252 }();
253
254 std::optional<WAVEFORMATEXTENSIBLE> formatEx = toWaveFormatExtensible(preferredFormat);
255 if (!formatEx)
256 return std::nullopt;
257
258 QComTaskResource<WAVEFORMATEX> closestMatch;
259 HRESULT result = audioClient->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, &formatEx->Format,
260 closestMatch.address());
261
262 if (FAILED(result))
263 return std::nullopt;
264 if (!closestMatch)
265 return preferredFormat;
266
267 QAudioFormat closestMatchFormat = waveFormatExToFormat(*closestMatch);
268 if (closestMatchFormat.isValid())
269 return closestMatchFormat;
270 return std::nullopt;
271}
272
273struct WindowsFormatResult
274{
275 QAudioDevicePrivate::AudioDeviceFormat format;
276 QtWASAPI::WindowsProbeData probeData;
277};
278
279WindowsFormatResult performFormatProbe(ComPtr<IMMDevice> immDev)
280{
281 QAudioDevicePrivate::AudioDeviceFormat format;
282 QtWASAPI::WindowsProbeData probeData{
283 { 1, 2 },
284 { QtMultimediaPrivate::allSupportedSampleRates.front(),
285 QtMultimediaPrivate::allSupportedSampleRates.back() },
286 };
287
288 ComPtr<IAudioClient> audioClient;
289 HRESULT hr = immDev->Activate(__uuidof(IAudioClient), CLSCTX_INPROC_SERVER, nullptr,
290 reinterpret_cast<void **>(audioClient.GetAddressOf()));
291
292 if (SUCCEEDED(hr)) {
293 QComTaskResource<WAVEFORMATEX> mixFormat;
294 hr = audioClient->GetMixFormat(mixFormat.address());
295 if (SUCCEEDED(hr))
296 format.preferredFormat = QWindowsAudioUtils::waveFormatExToFormat(*mixFormat);
297 } else {
298 qWarning() << "QWindowsAudioDeviceInfo: could not activate audio client:"
299 << QSystemError::windowsComString(hr);
300 return {format, probeData};
301 }
302
303 auto propStoreHelper = PropertyStoreHelper::open(immDev);
304 if (!propStoreHelper) {
305 qWarning() << "QWindowsAudioDeviceInfo: could not open property store:"
306 << propStoreHelper.error();
307 return {format, probeData};
308 }
309
310 qCDebug(qLcAudioDeviceProbes) << "probing formats";
311
312 std::optional<FormatProbeResult> probedFormats =
313 probeFormats(audioClient, *propStoreHelper, format.preferredFormat);
314 if (probedFormats) {
315 // wasapi does sample format conversion for us: AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM
316 format.supportedSampleFormats = qAllSupportedSampleFormats();
317
318 // this is a bit of a lie:
319 // for sources, WASAPI only supports a single sample rate, but we inject a resampler
320 // for sinks, WASAPI resamples internally
321 format.minimumSampleRate = QtMultimediaPrivate::allSupportedSampleRates.front();
322 format.maximumSampleRate = QtMultimediaPrivate::allSupportedSampleRates.back();
323
324 format.minimumChannelCount = 1; // we are lying here, but expect the QWASAPIAudioSinkStream to
325 // perform format conversion
326 format.maximumChannelCount = probedFormats->channelCountRange.second;
327
328 probeData.channelCountRange = probedFormats->channelCountRange;
329 probeData.sampleRateRange = probedFormats->sampleRateRange;
330 }
331
332 if (!format.preferredFormat.isValid()) {
333 std::optional<QAudioFormat> probedFormat = probePreferredFormat(audioClient);
334 if (probedFormat)
335 format.preferredFormat = *probedFormat;
336 }
337
338 std::optional<QAudioFormat::ChannelConfig> config =
339 inferChannelConfiguration(*propStoreHelper, format.maximumChannelCount);
340
341 format.channelConfiguration = config
342 ? *config
343 : QAudioFormat::defaultChannelConfigForChannelCount(format.maximumChannelCount);
344
345 return {format, probeData};
346}
347
348struct WasapiProbeThreadpool final : public QThreadPool
349{
350 WasapiProbeThreadpool()
351 {
352 setObjectName(u"WasapiProbeThreadpool"_s);
353 setMaxThreadCount(2);
354 setThreadPriority(QThread::LowPriority);
355 setServiceLevel(QThread::QualityOfService::Eco);
356 setExpiryTimeout(500 /*ms*/);
357 }
358
359 ~WasapiProbeThreadpool()
360 {
361 // Ensure all threads submitted tasks are canceled before destruction
362 clear();
363 }
364};
365
366Q_APPLICATION_STATIC(WasapiProbeThreadpool, wasapiProbeThreadpool)
367
368QtWASAPI::WindowsFormatResultFutures probeWindowsAudioDeviceFormatAsync(ComPtr<IMMDevice> immDev)
369{
370 std::promise<QAudioDevicePrivate::AudioDeviceFormat> formatPromise;
371 std::promise<QtWASAPI::WindowsProbeData> probePromise;
372
374 formatPromise.get_future(),
375 probePromise.get_future(),
376 };
377
378 wasapiProbeThreadpool->start([immDev = std::move(immDev),
379 formatPromise = std::move(formatPromise),
380 probePromise = std::move(probePromise)]() mutable {
381 auto result = performFormatProbe(immDev);
382 formatPromise.set_value(result.format);
383 probePromise.set_value(result.probeData);
384 });
385
386 return ret;
387}
388
389} // namespace
390
391QWindowsAudioDevice::QWindowsAudioDevice(QByteArray id, ComPtr<IMMDevice> immDev, QString desc,
392 QUuid containerId, EndpointFormFactor formFactor,
393 QAudioDevice::Mode mode)
397{}
398
399QWindowsAudioDevice::QWindowsAudioDevice(QByteArray deviceId, QString description,
400 QUuid containerId, EndpointFormFactor formFactor,
401 QAudioDevice::Mode mode,
407 },
410 },
413 }
414{
415}
416
417ComPtr<IMMDevice> QWindowsAudioDevice::open() const
418{
419 QComInitializer init;
420 ComPtr<IMMDeviceEnumerator> deviceEnumerator;
421 HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL,
422 IID_PPV_ARGS(&deviceEnumerator));
423 if (FAILED(hr)) {
424 qWarning() << "Failed to create device enumerator" << hr;
425 return nullptr;
426 }
427
428 auto deviceId = QString::fromUtf8(id);
429
430 ComPtr<IMMDevice> device;
431 HRESULT result =
432 deviceEnumerator->GetDevice(deviceId.toStdWString().c_str(), device.GetAddressOf());
433 if (FAILED(result)) {
434 qWarning() << "IMMDeviceEnumerator::GetDevice failed" << id
435 << QSystemError::windowsComString(result);
436 return nullptr;
437 }
438 return device;
439}
440
441QWindowsAudioDevice::~QWindowsAudioDevice() = default;
442
443std::unique_ptr<QAudioDevicePrivate> QWindowsAudioDevice::clone() const
444{
445 return std::unique_ptr<QAudioDevicePrivate>(new QWindowsAudioDevice{ *this });
446}
447
448QT_END_NAMESPACE
std::unique_ptr< QAudioDevicePrivate > clone() const
QWindowsAudioDevice(QByteArray deviceId, QString description, QUuid containerId, EndpointFormFactor, QAudioDevice::Mode, QtWASAPI::WindowsFormatResultFutures)
QWindowsAudioDevice(QByteArray deviceId, ComPtr< IMMDevice >, QString description, QUuid containerId, EndpointFormFactor, QAudioDevice::Mode)
ComPtr< IMMDevice > open() const