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