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
qwindowsaudiodevices.cpp
Go to the documentation of this file.
1// Copyright (C) 2021 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 <QtMultimedia/private/qmultimedia_ranges_p.h>
8#include <QtCore/private/qcomobject_p.h>
9#include <QtCore/private/qsystemerror_p.h>
10
11#include <QtMultimedia/qmediadevices.h>
12#include <QtMultimedia/private/qcomtaskresource_p.h>
13#include <QtMultimedia/private/qwindowsaudiodevice_p.h>
14#include <QtMultimedia/private/qwindowsaudiosink_p.h>
15#include <QtMultimedia/private/qwindowsaudiosource_p.h>
16#include <QtMultimedia/private/qwindows_propertystore_p.h>
17
18#include <audioclient.h>
19#include <functiondiscoverykeys_devpkey.h>
20#include <mmdeviceapi.h>
21
22#include <map>
23
24QT_BEGIN_NAMESPACE
25namespace ranges = QtMultimediaPrivate::ranges;
26
27// older mingw does not have PKEY_Device_ContainerId defined
28// https://github.com/mingw-w64/mingw-w64/commit/7e6eca69655c81976acfd7cd6a1ed25e7961e8c7
29// defining it here to avoid depending on the mingw version
30DEFINE_PROPERTYKEY(PKEY_Device_ContainerIdQt, 0x8c7ed206, 0x3f8a, 0x4827, 0xb3, 0xab, 0xae, 0x9e,
31 0x1f, 0xae, 0xfc, 0x6c, 2);
32
33namespace QtWASAPI {
34
35namespace {
36
37enum class DeviceState : uint8_t {
38 active,
39 disabled,
40 notPresent,
41 unplugged,
42};
43
44constexpr DeviceState asDeviceState(DWORD state)
45{
46 switch (state) {
47 case DEVICE_STATE_ACTIVE:
48 return DeviceState::active;
49 case DEVICE_STATE_DISABLED:
50 return DeviceState::disabled;
51 case DEVICE_STATE_NOTPRESENT:
52 return DeviceState::notPresent;
53 case DEVICE_STATE_UNPLUGGED:
54 return DeviceState::unplugged;
55 default:
56 Q_UNREACHABLE_RETURN(DeviceState::notPresent);
57 }
58}
59
60} // namespace
61
63{
64 Q_OBJECT
65
67
68 struct DeviceRecord
69 {
70 ComPtr<IMMDevice> device;
71 DeviceState state;
72 };
73
74 std::map<QString, DeviceRecord> m_deviceMap;
75
76public:
77 explicit CMMNotificationClient(ComPtr<IMMDeviceEnumerator> enumerator)
79 {
80 ComPtr<IMMDeviceCollection> devColl;
81 UINT count = 0;
82
83 if (SUCCEEDED(m_enumerator->EnumAudioEndpoints(EDataFlow::eAll, DEVICE_STATEMASK_ALL,
84 devColl.GetAddressOf()))
85 && SUCCEEDED(devColl->GetCount(&count))) {
86 for (UINT i = 0; i < count; i++) {
87 ComPtr<IMMDevice> device;
88 if (FAILED(devColl->Item(i, device.GetAddressOf())))
89 continue;
90
91 auto enumerateResult = enumerateDevice(device);
92 if (!enumerateResult)
93 continue;
94
95 auto idResult = deviceId(enumerateResult->device);
96 if (!idResult)
97 continue;
98
99 m_deviceMap.emplace(std::move(*idResult), std::move(*enumerateResult));
100 }
101 }
102
103 // Does not seem to be necessary, but also won't do any harm
104 qRegisterMetaType<ComPtr<IMMDevice>>();
105 }
106
107signals:
112
113private:
114 HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role,
116 {
117 ComPtr device = [&] {
119 if (it != std::end(m_deviceMap))
120 return it->second.device;
121
122 return ComPtr<IMMDevice>{};
123 }();
124
125 if (role == ERole::eMultimedia) {
126 switch (flow) {
127 case EDataFlow::eCapture:
129 break;
130 case EDataFlow::eRender:
132 break;
133 case EDataFlow::eAll:
134 // Not expected, but handle it anyway
137 break;
138 default:
140 }
141 }
142
143 return S_OK;
144 }
145
162
175
195
205
207 {
210 if (FAILED(deviceStatus))
211 return q23::unexpected{ deviceStatus };
212 return enumerateDevice(device);
213 }
214
216 {
217 DWORD state = 0;
218
220 if (FAILED(stateStatus))
221 return q23::unexpected{ stateStatus };
222 return DeviceRecord{
223 device,
225 };
226 }
228 {
230 auto idStatus = device->GetId(id.address());
231 if (FAILED(idStatus))
232 return q23::unexpected{ idStatus };
233 return QString::fromWCharArray(id.get());
234 }
235
236 // Destructor is not public. Caller should call Release.
238};
239
240} // namespace QtWASAPI
241
244{
245 using namespace QtWASAPI;
246
247 // Debounce device change notifications. Bluetooth devices (and some USB
248 // devices) may generate rapid bursts of IMMNotificationClient callbacks
249 // during a single connect/disconnect cycle. Without coalescing, each
250 // callback invalidates the device cache and notifies consumers, which
251 // may trigger re-enumeration involving synchronous COM RPC calls
252 // (EnumAudioEndpoints, OpenPropertyStore) that may stall while the
253 // Windows Audio Service is still processing the change.
254 constexpr auto kDebounceInterval = std::chrono::milliseconds{200};
255
256 m_audioInputsDebounce.setSingleShot(true);
257 m_audioInputsDebounce.setInterval(kDebounceInterval);
258 m_audioInputsDebounce.callOnTimeout(this, &QWindowsAudioDevices::onAudioInputsChanged);
259
260 m_audioOutputsDebounce.setSingleShot(true);
261 m_audioOutputsDebounce.setInterval(kDebounceInterval);
262 m_audioOutputsDebounce.callOnTimeout(this, &QWindowsAudioDevices::onAudioOutputsChanged);
263
264 auto hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER,
265 IID_PPV_ARGS(&m_deviceEnumerator));
266
267 if (FAILED(hr)) {
268 qWarning("Failed to instantiate IMMDeviceEnumerator (%s)."
269 "Audio device change notification will be disabled",
270 qPrintable(QSystemError::windowsComString(hr)));
271 return;
272 }
273
274 m_notificationClient = makeComObject<QtWASAPI::CMMNotificationClient>(m_deviceEnumerator);
275 m_deviceEnumerator->RegisterEndpointNotificationCallback(m_notificationClient.Get());
276
277 connect(m_notificationClient.Get(), &QtWASAPI::CMMNotificationClient::audioDeviceAdded, this,
278 [this] {
279 scheduleAudioInputsChanged();
280 scheduleAudioOutputsChanged();
281 });
282 connect(m_notificationClient.Get(), &QtWASAPI::CMMNotificationClient::audioDeviceRemoved, this,
283 [this](ComPtr<IMMDevice> device) {
284 {
285 std::lock_guard lock(m_cacheMutex);
286 m_cachedDevices.erase(device);
287 }
288 scheduleAudioInputsChanged();
289 scheduleAudioOutputsChanged();
290 });
291 connect(m_notificationClient.Get(), &QtWASAPI::CMMNotificationClient::audioDeviceDefaultChanged,
292 this, [this](QAudioDevice::Mode mode, ComPtr<IMMDevice> device) {
293 {
294 std::lock_guard lock(m_cacheMutex);
295
296 for (auto &entry : m_cachedDevices) {
297 if (entry.second.mode() != mode)
298 continue;
299
300 auto handle = QAudioDevicePrivate::handle<QWindowsAudioDevice>(entry.second);
301 Q_PRESUME(handle);
302
303 std::unique_ptr<QAudioDevicePrivate> newPrivate = handle->clone();
304 newPrivate->isDefault = entry.first == device;
305
306 entry.second = QAudioDevicePrivate::createQAudioDevice(std::move(newPrivate));
307 }
308 }
309
310 switch (mode) {
311 case QAudioDevice::Input:
312 scheduleAudioInputsChanged();
313 break;
314 case QAudioDevice::Output:
315 scheduleAudioOutputsChanged();
316 break;
317 default:
318 break;
319 }
320 });
321 connect(m_notificationClient.Get(),
322 &QtWASAPI::CMMNotificationClient::audioDevicePropertyChanged, this,
323 [this](ComPtr<IMMDevice> device) {
324 {
325 std::lock_guard lock(m_cacheMutex);
326 m_cachedDevices.erase(device);
327 }
328
329 scheduleAudioInputsChanged();
330 scheduleAudioOutputsChanged();
331 });
332}
333
334void QWindowsAudioDevices::scheduleAudioInputsChanged()
335{
336 m_audioInputsDebounce.start();
337}
338
339void QWindowsAudioDevices::scheduleAudioOutputsChanged()
340{
341 m_audioOutputsDebounce.start();
342}
343
345{
346 if (m_deviceEnumerator) {
347 // Note: Calling UnregisterEndpointNotificationCallback after CoUninitialize
348 // will abruptly terminate application, preventing remaining destructors from
349 // being called (QTBUG-120198).
350 m_deviceEnumerator->UnregisterEndpointNotificationCallback(m_notificationClient.Get());
351 }
352
353 m_deviceEnumerator.Reset();
354 m_notificationClient.Reset();
355}
356
357static std::optional<QString> getDeviceId(const ComPtr<IMMDevice> &dev)
358{
359 Q_ASSERT(dev);
360 QComTaskResource<WCHAR> id;
361 HRESULT status = dev->GetId(id.address());
362 if (FAILED(status)) {
363 qWarning() << "IMMDevice::GetId failed" << QSystemError::windowsComString(status);
364 return {};
365 }
366 return QString::fromWCharArray(id.get());
367}
368
369static std::optional<QAudioDevice> asQAudioDevice(ComPtr<IMMDevice> device, QAudioDevice::Mode mode,
370 std::optional<QString> defaultAudioDeviceID)
371{
372 using QtMultimediaPrivate::PropertyStoreHelper;
373
374 std::optional<QString> deviceId = getDeviceId(device);
375 if (!deviceId)
376 return std::nullopt;
377
378 q23::expected<PropertyStoreHelper, QString> props = PropertyStoreHelper::open(device);
379 if (!props) {
380 qWarning() << "OpenPropertyStore failed" << props.error();
381 return std::nullopt;
382 }
383
384 std::optional<QString> friendlyName = props->getString(PKEY_Device_FriendlyName);
385 if (!friendlyName) {
386 qWarning() << "Cannot read property store";
387 return std::nullopt;
388 }
389
390 std::optional<QUuid> deviceContainerId = props->getGUID(PKEY_Device_ContainerIdQt);
391 if (!deviceContainerId) {
392 qWarning() << "Cannot read property store";
393 return std::nullopt;
394 }
395
396 std::optional<uint32_t> formFactor = props->getUInt32(PKEY_AudioEndpoint_FormFactor);
397 if (!formFactor) {
398 qWarning() << "Cannot infer form factor";
399 return std::nullopt;
400 }
401
402 auto dev = std::make_unique<QWindowsAudioDevice>(deviceId->toUtf8(), device, *friendlyName,
403 *deviceContainerId,
404 EndpointFormFactor(*formFactor), mode);
405 dev->isDefault = deviceId == defaultAudioDeviceID;
406 return QAudioDevicePrivate::createQAudioDevice(std::move(dev));
407}
408
409QList<QAudioDevice> QWindowsAudioDevices::availableDevices(QAudioDevice::Mode mode) const
410{
411 if (!m_deviceEnumerator)
412 return {};
413
414 const bool audioOut = mode == QAudioDevice::Output;
415 const auto dataFlow = audioOut ? EDataFlow::eRender : EDataFlow::eCapture;
416
417 const auto defaultAudioDeviceID = [&, this]() -> std::optional<QString> {
418 ComPtr<IMMDevice> dev;
419 if (SUCCEEDED(m_deviceEnumerator->GetDefaultAudioEndpoint(dataFlow, ERole::eMultimedia,
420 dev.GetAddressOf())))
421 return getDeviceId(dev);
422
423 return std::nullopt;
424 }();
425
426 QList<QAudioDevice> devices;
427
428 ComPtr<IMMDeviceCollection> allActiveDevices;
429 HRESULT result = m_deviceEnumerator->EnumAudioEndpoints(dataFlow, DEVICE_STATE_ACTIVE,
430 allActiveDevices.GetAddressOf());
431
432 if (FAILED(result)) {
433 qWarning() << "IMMDeviceEnumerator::EnumAudioEndpoints failed"
434 << QSystemError::windowsComString(result);
435 return devices;
436 }
437
438 UINT numberOfDevices;
439 result = allActiveDevices->GetCount(&numberOfDevices);
440 if (FAILED(result)) {
441 qWarning() << "IMMDeviceCollection::GetCount failed"
442 << QSystemError::windowsComString(result);
443 return devices;
444 }
445
446 for (UINT index = 0; index != numberOfDevices; ++index) {
447 ComPtr<IMMDevice> device;
448 result = allActiveDevices->Item(index, device.GetAddressOf());
449 if (FAILED(result)) {
450 qWarning() << "IMMDeviceCollection::Item" << QSystemError::windowsComString(result);
451 continue;
452 }
453
454 {
455 std::lock_guard lock(m_cacheMutex);
456 auto cachedDevice = m_cachedDevices.find(device);
457 if (cachedDevice != m_cachedDevices.end()) {
458 devices.append(cachedDevice->second);
459 continue;
460 }
461 }
462
463 std::optional<QAudioDevice> audioDevice =
464 asQAudioDevice(device, mode, defaultAudioDeviceID);
465
466 if (audioDevice) {
467 devices.append(*audioDevice);
468 std::lock_guard lock(m_cacheMutex);
469 m_cachedDevices.emplace(device, *audioDevice);
470 }
471 }
472
473 ranges::sort(devices, [](const QAudioDevice &lhs, const QAudioDevice &rhs) {
474 auto lhsHandle = QAudioDevicePrivate::handle<QWindowsAudioDevice>(lhs);
475 auto rhsHandle = QAudioDevicePrivate::handle<QWindowsAudioDevice>(rhs);
476 auto lhsKey = std::tie(lhsHandle->m_device_ContainerId, lhsHandle->m_formFactor,
477 lhsHandle->description);
478 auto rhsKey = std::tie(rhsHandle->m_device_ContainerId, rhsHandle->m_formFactor,
479 rhsHandle->description);
480 return lhsKey < rhsKey;
481 });
482 return devices;
483}
484
486{
487 return availableDevices(QAudioDevice::Input);
488}
489
491{
492 return availableDevices(QAudioDevice::Output);
493}
494
496 const QAudioFormat &fmt,
497 QObject *parent)
498{
499 return new QtWASAPI::QWindowsAudioSource(device, fmt, parent);
500}
501
503 const QAudioFormat &fmt, QObject *parent)
504{
505 return new QtWASAPI::QWindowsAudioSink(device, fmt, parent);
506}
507
508QT_END_NAMESPACE
509
510#include "qwindowsaudiodevices.moc"
QList< QAudioDevice > findAudioInputs() const override
QPlatformAudioSource * createAudioSource(const QAudioDevice &, const QAudioFormat &, QObject *parent) override
QPlatformAudioSink * createAudioSink(const QAudioDevice &, const QAudioFormat &, QObject *parent) override
QList< QAudioDevice > findAudioOutputs() const override
void audioDeviceDefaultChanged(QAudioDevice::Mode, ComPtr< IMMDevice >)
void audioDeviceRemoved(ComPtr< IMMDevice >)
CMMNotificationClient(ComPtr< IMMDeviceEnumerator > enumerator)
void audioDevicePropertyChanged(ComPtr< IMMDevice >)
QT_BEGIN_NAMESPACE DEFINE_PROPERTYKEY(PKEY_Device_ContainerIdQt, 0x8c7ed206, 0x3f8a, 0x4827, 0xb3, 0xab, 0xae, 0x9e, 0x1f, 0xae, 0xfc, 0x6c, 2)
static std::optional< QString > getDeviceId(const ComPtr< IMMDevice > &dev)
static std::optional< QAudioDevice > asQAudioDevice(ComPtr< IMMDevice > device, QAudioDevice::Mode mode, std::optional< QString > defaultAudioDeviceID)