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