Revision control

++ // Copyright (C) 2021 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++
++ #include "qwindowsaudiodevices_p.h"
++
++ #include <QtCore/qdebug.h>
++ #include <QtCore/private/qcomobject_p.h>
++ #include <QtCore/private/qfunctions_win_p.h>
++ #include <QtCore/private/qsystemerror_p.h>
++
++ #include <QtMultimedia/qmediadevices.h>
++ #include <QtMultimedia/private/qcomtaskresource_p.h>
++ #include <QtMultimedia/private/qmultimedia_ranges_p.h>
++ #include <QtMultimedia/private/qwindowsaudiodevice_p.h>
++ #include <QtMultimedia/private/qwindowsaudiosink_p.h>
++ #include <QtMultimedia/private/qwindowsaudiosource_p.h>
++ #include <QtMultimedia/private/qwindows_propertystore_p.h>
++
++ #include <audioclient.h>
++ #include <functiondiscoverykeys_devpkey.h>
++ #include <mmdeviceapi.h>
++
++ #include <map>
++
++ QT_BEGIN_NAMESPACE
++ namespace ranges = QtMultimediaPrivate::ranges;
++
++ // older mingw does not have PKEY_Device_ContainerId defined
++ // defining it here to avoid depending on the mingw version
++ DEFINE_PROPERTYKEY(PKEY_Device_ContainerIdQt, 0x8c7ed206, 0x3f8a, 0x4827, 0xb3, 0xab, 0xae, 0x9e,
++ 0x1f, 0xae, 0xfc, 0x6c, 2);
++
++ namespace QtWASAPI {
++
++ namespace {
++
++ enum class DeviceState : uint8_t {
++ active,
++ disabled,
++ notPresent,
++ unplugged,
++ };
++
++ constexpr DeviceState asDeviceState(DWORD state)
++ {
++ switch (state) {
++ case DEVICE_STATE_ACTIVE:
++ return DeviceState::active;
++ case DEVICE_STATE_DISABLED:
++ return DeviceState::disabled;
++ case DEVICE_STATE_NOTPRESENT:
++ return DeviceState::notPresent;
++ case DEVICE_STATE_UNPLUGGED:
++ return DeviceState::unplugged;
++ default:
++ Q_UNREACHABLE_RETURN(DeviceState::notPresent);
++ }
++ }
++
++ } // namespace
++
++ class CMMNotificationClient : public QObject, public QComObject<IMMNotificationClient>
++ {
++ Q_OBJECT
++
++ ComPtr<IMMDeviceEnumerator> m_enumerator;
++
++ struct DeviceRecord
++ {
++ ComPtr<IMMDevice> device;
++ DeviceState state;
++ };
++
++ std::map<QString, DeviceRecord> m_deviceMap;
++
++ public:
++ explicit CMMNotificationClient(ComPtr<IMMDeviceEnumerator> enumerator)
++ : m_enumerator(enumerator)
++ {
++ ComPtr<IMMDeviceCollection> devColl;
++ UINT count = 0;
++
++ if (SUCCEEDED(m_enumerator->EnumAudioEndpoints(EDataFlow::eAll, DEVICE_STATEMASK_ALL,
++ devColl.GetAddressOf()))
++ && SUCCEEDED(devColl->GetCount(&count))) {
++ for (UINT i = 0; i < count; i++) {
++ ComPtr<IMMDevice> device;
++ if (FAILED(devColl->Item(i, device.GetAddressOf())))
++ continue;
++
++ auto enumerateResult = enumerateDevice(device);
++ if (!enumerateResult)
++ continue;
++
++ auto idResult = deviceId(enumerateResult->device);
++ if (!idResult)
++ continue;
++
++ m_deviceMap.emplace(std::move(*idResult), std::move(*enumerateResult));
++ }
++ }
++
++ // Does not seem to be necessary, but also won't do any harm
++ qRegisterMetaType<ComPtr<IMMDevice>>();
++ }
++
++ Q_SIGNALS:
++ void audioDeviceAdded(ComPtr<IMMDevice>);
++ void audioDeviceRemoved(ComPtr<IMMDevice>);
++ void audioDevicePropertyChanged(ComPtr<IMMDevice>);
++ void audioDeviceDefaultChanged(QAudioDevice::Mode, ComPtr<IMMDevice>);
++
++ private:
++ HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role,
++ LPCWSTR deviceID) override
++ {
++ ComPtr device = [&] {
++ auto it = m_deviceMap.find(QString::fromWCharArray(deviceID));
++ if (it != std::end(m_deviceMap))
++ return it->second.device;
++
++ return ComPtr<IMMDevice>{};
++ }();
++
++ if (role == ERole::eMultimedia) {
++ switch (flow) {
++ case EDataFlow::eCapture:
++ Q_EMIT audioDeviceDefaultChanged(QAudioDevice::Input, device);
++ break;
++ case EDataFlow::eRender:
++ Q_EMIT audioDeviceDefaultChanged(QAudioDevice::Output, device);
++ break;
++ case EDataFlow::eAll:
++ // Not expected, but handle it anyway
++ Q_EMIT audioDeviceDefaultChanged(QAudioDevice::Input, device);
++ Q_EMIT audioDeviceDefaultChanged(QAudioDevice::Output, device);
++ break;
++ default:
++ Q_UNREACHABLE_RETURN(S_OK);
++ }
++ }
++
++ return S_OK;
++ }
++
++ HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR deviceID) override
++ {
++ auto it = m_deviceMap.find(QString::fromWCharArray(deviceID));
++ if (it == std::end(m_deviceMap)) {
++ auto enumerateResult = enumerateDevice(deviceID);
++ if (!enumerateResult)
++ return S_OK;
++
++ m_deviceMap.emplace(QString::fromWCharArray(deviceID), *enumerateResult);
++
++ if (enumerateResult->state == DeviceState::active)
++ Q_EMIT audioDeviceAdded(enumerateResult->device);
++ }
++
++ return S_OK;
++ }
++
++ HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR deviceID) override
++ {
++ auto key = QString::fromWCharArray(deviceID);
++ auto it = m_deviceMap.find(key);
++ if (it != std::end(m_deviceMap)) {
++ if (it->second.state == DeviceState::active)
++ Q_EMIT audioDeviceRemoved(it->second.device);
++ m_deviceMap.erase(key);
++ }
++
++ return S_OK;
++ }
++
++ HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(LPCWSTR deviceID, DWORD state) override
++ {
++ const DeviceState newState = asDeviceState(state);
++
++ if (auto it = m_deviceMap.find(QString::fromWCharArray(deviceID));
++ it != std::end(m_deviceMap)) {
++ if (it->second.state == newState)
++ return S_OK;
++
++ if (newState == DeviceState::active && it->second.state != DeviceState::active)
++ Q_EMIT audioDeviceAdded(it->second.device);
++ else if (newState != DeviceState::active && it->second.state == DeviceState::active)
++ Q_EMIT audioDeviceRemoved(it->second.device);
++
++ it->second.state = newState;
++ }
++
++ return S_OK;
++ }
++
++ HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(LPCWSTR deviceID, const PROPERTYKEY) override
++ {
++ if (auto it = m_deviceMap.find(QString::fromWCharArray(deviceID));
++ it != std::end(m_deviceMap)) {
++ Q_EMIT audioDevicePropertyChanged(it->second.device);
++ }
++
++ return S_OK;
++ }
++
++ q23::expected<DeviceRecord, HRESULT> enumerateDevice(LPCWSTR deviceID)
++ {
++ ComPtr<IMMDevice> device;
++ auto deviceStatus = m_enumerator->GetDevice(deviceID, device.GetAddressOf());
++ if (FAILED(deviceStatus))
++ return q23::unexpected{ deviceStatus };
++ return enumerateDevice(device);
++ }
++
++ q23::expected<DeviceRecord, HRESULT> enumerateDevice(const ComPtr<IMMDevice> &device)
++ {
++ DWORD state = 0;
++
++ auto stateStatus = device->GetState(&state);
++ if (FAILED(stateStatus))
++ return q23::unexpected{ stateStatus };
++ return DeviceRecord{
++ device,
++ asDeviceState(state),
++ };
++ }
++ q23::expected<QString, HRESULT> deviceId(const ComPtr<IMMDevice> &device)
++ {
++ QComTaskResource<WCHAR> id;
++ auto idStatus = device->GetId(id.address());
++ if (FAILED(idStatus))
++ return q23::unexpected{ idStatus };
++ return QString::fromWCharArray(id.get());
++ }
++
++ // Destructor is not public. Caller should call Release.
++ ~CMMNotificationClient() override = default;
++ };
++
++ } // namespace QtWASAPI
++
++ QWindowsAudioDevices::QWindowsAudioDevices()
++ {
++ qt_win_ensureComInitializedOnThisThread();
++
++ using namespace QtWASAPI;
++
++ // Debounce device change notifications. Bluetooth devices (and some USB
++ // devices) may generate rapid bursts of IMMNotificationClient callbacks
++ // during a single connect/disconnect cycle. Without coalescing, each
++ // callback invalidates the device cache and notifies consumers, which
++ // may trigger re-enumeration involving synchronous COM RPC calls
++ // (EnumAudioEndpoints, OpenPropertyStore) that may stall while the
++ // Windows Audio Service is still processing the change.
++ constexpr auto kDebounceInterval = std::chrono::milliseconds{200};
++
++ m_audioInputsDebounce.setSingleShot(true);
++ m_audioInputsDebounce.setInterval(kDebounceInterval);
++ m_audioInputsDebounce.callOnTimeout(this, &QWindowsAudioDevices::onAudioInputsChanged);
++
++ m_audioOutputsDebounce.setSingleShot(true);
++ m_audioOutputsDebounce.setInterval(kDebounceInterval);
++ m_audioOutputsDebounce.callOnTimeout(this, &QWindowsAudioDevices::onAudioOutputsChanged);
++
++ auto hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER,
++ IID_PPV_ARGS(&m_deviceEnumerator));
++
++ if (FAILED(hr)) {
++ qWarning("Failed to instantiate IMMDeviceEnumerator (%s)."
++ "Audio device change notification will be disabled",
++ qPrintable(QSystemError::windowsComString(hr)));
++ return;
++ }
++
++ m_notificationClient = makeComObject<QtWASAPI::CMMNotificationClient>(m_deviceEnumerator);
++ m_deviceEnumerator->RegisterEndpointNotificationCallback(m_notificationClient.Get());
++
++ connect(m_notificationClient.Get(), &QtWASAPI::CMMNotificationClient::audioDeviceAdded, this,
++ [this] {
++ scheduleAudioInputsChanged();
++ scheduleAudioOutputsChanged();
++ });
++ connect(m_notificationClient.Get(), &QtWASAPI::CMMNotificationClient::audioDeviceRemoved, this,
++ [this](ComPtr<IMMDevice> device) {
++ {
++ std::lock_guard lock(m_cacheMutex);
++ m_cachedDevices.erase(device);
++ }
++ scheduleAudioInputsChanged();
++ scheduleAudioOutputsChanged();
++ });
++ connect(m_notificationClient.Get(), &QtWASAPI::CMMNotificationClient::audioDeviceDefaultChanged,
++ this, [this](QAudioDevice::Mode mode, ComPtr<IMMDevice> device) {
++ {
++ std::lock_guard lock(m_cacheMutex);
++
++ for (auto &entry : m_cachedDevices) {
++ if (entry.second.mode() != mode)
++ continue;
++
++ auto handle = QAudioDevicePrivate::handle<QWindowsAudioDevice>(entry.second);
++ Q_PRESUME(handle);
++
++ std::unique_ptr<QAudioDevicePrivate> newPrivate = handle->clone();
++ newPrivate->isDefault = entry.first == device;
++
++ entry.second = QAudioDevicePrivate::createQAudioDevice(std::move(newPrivate));
++ }
++ }
++
++ switch (mode) {
++ case QAudioDevice::Input:
++ scheduleAudioInputsChanged();
++ break;
++ case QAudioDevice::Output:
++ scheduleAudioOutputsChanged();
++ break;
++ default:
++ break;
++ }
++ });
++ connect(m_notificationClient.Get(),
++ &QtWASAPI::CMMNotificationClient::audioDevicePropertyChanged, this,
++ [this](ComPtr<IMMDevice> device) {
++ {
++ std::lock_guard lock(m_cacheMutex);
++ m_cachedDevices.erase(device);
++ }
++
++ scheduleAudioInputsChanged();
++ scheduleAudioOutputsChanged();
++ });
++ }
++
++ void QWindowsAudioDevices::scheduleAudioInputsChanged()
++ {
++ m_audioInputsDebounce.start();
++ }
++
++ void QWindowsAudioDevices::scheduleAudioOutputsChanged()
++ {
++ m_audioOutputsDebounce.start();
++ }
++
++ QWindowsAudioDevices::~QWindowsAudioDevices()
++ {
++ if (m_deviceEnumerator) {
++ // Note: Calling UnregisterEndpointNotificationCallback after CoUninitialize
++ // will abruptly terminate application, preventing remaining destructors from
++ // being called (QTBUG-120198).
++ m_deviceEnumerator->UnregisterEndpointNotificationCallback(m_notificationClient.Get());
++ }
++
++ m_deviceEnumerator.Reset();
++ m_notificationClient.Reset();
++ }
++
++ static std::optional<QString> getDeviceId(const ComPtr<IMMDevice> &dev)
++ {
++ Q_ASSERT(dev);
++ QComTaskResource<WCHAR> id;
++ HRESULT status = dev->GetId(id.address());
++ if (FAILED(status)) {
++ qWarning() << "IMMDevice::GetId failed" << QSystemError::windowsComString(status);
++ return {};
++ }
++ return QString::fromWCharArray(id.get());
++ }
++
++ static std::optional<QAudioDevice> asQAudioDevice(ComPtr<IMMDevice> device, QAudioDevice::Mode mode,
++ std::optional<QString> defaultAudioDeviceID)
++ {
++ using QtMultimediaPrivate::PropertyStoreHelper;
++
++ std::optional<QString> deviceId = getDeviceId(device);
++ if (!deviceId)
++ return std::nullopt;
++
++ q23::expected<PropertyStoreHelper, QString> props = PropertyStoreHelper::open(device);
++ if (!props) {
++ qWarning() << "OpenPropertyStore failed" << props.error();
++ return std::nullopt;
++ }
++
++ std::optional<QString> friendlyName = props->getString(PKEY_Device_FriendlyName);
++ if (!friendlyName) {
++ qWarning() << "Cannot read property store";
++ return std::nullopt;
++ }
++
++ std::optional<QUuid> deviceContainerId = props->getGUID(PKEY_Device_ContainerIdQt);
++ if (!deviceContainerId) {
++ qWarning() << "Cannot read property store";
++ return std::nullopt;
++ }
++
++ std::optional<uint32_t> formFactor = props->getUInt32(PKEY_AudioEndpoint_FormFactor);
++ if (!formFactor) {
++ qWarning() << "Cannot infer form factor";
++ return std::nullopt;
++ }
++
++ auto dev = std::make_unique<QWindowsAudioDevice>(deviceId->toUtf8(), device, *friendlyName,
++ *deviceContainerId,
++ EndpointFormFactor(*formFactor), mode);
++ dev->isDefault = deviceId == defaultAudioDeviceID;
++ return QAudioDevicePrivate::createQAudioDevice(std::move(dev));
++ }
++
++ QList<QAudioDevice> QWindowsAudioDevices::availableDevices(QAudioDevice::Mode mode) const
++ {
++ if (!m_deviceEnumerator)
++ return {};
++
++ const bool audioOut = mode == QAudioDevice::Output;
++ const auto dataFlow = audioOut ? EDataFlow::eRender : EDataFlow::eCapture;
++
++ const auto defaultAudioDeviceID = [&, this]() -> std::optional<QString> {
++ ComPtr<IMMDevice> dev;
++ if (SUCCEEDED(m_deviceEnumerator->GetDefaultAudioEndpoint(dataFlow, ERole::eMultimedia,
++ dev.GetAddressOf())))
++ return getDeviceId(dev);
++
++ return std::nullopt;
++ }();
++
++ QList<QAudioDevice> devices;
++
++ ComPtr<IMMDeviceCollection> allActiveDevices;
++ HRESULT result = m_deviceEnumerator->EnumAudioEndpoints(dataFlow, DEVICE_STATE_ACTIVE,
++ allActiveDevices.GetAddressOf());
++
++ if (FAILED(result)) {
++ qWarning() << "IMMDeviceEnumerator::EnumAudioEndpoints failed"
++ << QSystemError::windowsComString(result);
++ return devices;
++ }
++
++ UINT numberOfDevices;
++ result = allActiveDevices->GetCount(&numberOfDevices);
++ if (FAILED(result)) {
++ qWarning() << "IMMDeviceCollection::GetCount failed"
++ << QSystemError::windowsComString(result);
++ return devices;
++ }
++
++ for (UINT index = 0; index != numberOfDevices; ++index) {
++ ComPtr<IMMDevice> device;
++ result = allActiveDevices->Item(index, device.GetAddressOf());
++ if (FAILED(result)) {
++ qWarning() << "IMMDeviceCollection::Item" << QSystemError::windowsComString(result);
++ continue;
++ }
++
++ {
++ std::lock_guard lock(m_cacheMutex);
++ auto cachedDevice = m_cachedDevices.find(device);
++ if (cachedDevice != m_cachedDevices.end()) {
++ devices.append(cachedDevice->second);
++ continue;
++ }
++ }
++
++ std::optional<QAudioDevice> audioDevice =
++ asQAudioDevice(device, mode, defaultAudioDeviceID);
++
++ if (audioDevice) {
++ devices.append(*audioDevice);
++ std::lock_guard lock(m_cacheMutex);
++ m_cachedDevices.emplace(device, *audioDevice);
++ }
++ }
++
++ ranges::sort(devices, [](const QAudioDevice &lhs, const QAudioDevice &rhs) {
++ auto lhsHandle = QAudioDevicePrivate::handle<QWindowsAudioDevice>(lhs);
++ auto rhsHandle = QAudioDevicePrivate::handle<QWindowsAudioDevice>(rhs);
++ auto lhsKey = std::tie(lhsHandle->m_device_ContainerId, lhsHandle->m_formFactor,
++ lhsHandle->description);
++ auto rhsKey = std::tie(rhsHandle->m_device_ContainerId, rhsHandle->m_formFactor,
++ rhsHandle->description);
++ return lhsKey < rhsKey;
++ });
++ return devices;
++ }
++
++ QList<QAudioDevice> QWindowsAudioDevices::findAudioInputs() const
++ {
++ return availableDevices(QAudioDevice::Input);
++ }
++
++ QList<QAudioDevice> QWindowsAudioDevices::findAudioOutputs() const
++ {
++ return availableDevices(QAudioDevice::Output);
++ }
++
++ QPlatformAudioSource *QWindowsAudioDevices::createAudioSource(const QAudioDevice &device,
++ const QAudioFormat &fmt,
++ QObject *parent)
++ {
++ return new QtWASAPI::QWindowsAudioSource(device, fmt, parent);
++ }
++
++ QPlatformAudioSink *QWindowsAudioDevices::createAudioSink(const QAudioDevice &device,
++ const QAudioFormat &fmt, QObject *parent)
++ {
++ return new QtWASAPI::QWindowsAudioSink(device, fmt, parent);
++ }
++
++ QT_END_NAMESPACE
++
++ #include "qwindowsaudiodevices.moc"