Revision control

++ // Copyright (C) 2025 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 "qwindows_wasapi_warmup_client_p.h"
++
++ #include <QtCore/qapplicationstatic.h>
++ #include <QtCore/qchronotimer.h>
++ #include <QtCore/qcoreapplication.h>
++ #include <QtCore/qdebug.h>
++ #include <QtCore/qspan.h>
++ #include <QtCore/qthread.h>
++ #include <QtCore/quuid.h>
++ #include <QtCore/private/qfunctions_win_p.h>
++ #include <QtCore/private/qsystemerror_p.h>
++ #include <QtMultimedia/private/qcomtaskresource_p.h>
++ #include <QtMultimedia/private/qwindowsaudioutils_p.h>
++
++ #include <audioclient.h>
++ #include <guiddef.h>
++ #include <mmdeviceapi.h>
++ #include <powrprof.h>
++
++ QT_BEGIN_NAMESPACE
++
++ namespace QtMultimediaPrivate {
++
++ using namespace std::chrono_literals;
++
++ namespace {
++
++ ///////////////////////////////////////////////////////////////////////////////////////////////////
++
++ // The "warm-up" audio client is required to run in the background in order to keep audio engine
++ // ready for audio output immediately after creating any other subsequent audio client.
++ class QWasapiWarmupClient
++ {
++ public:
++ QWasapiWarmupClient();
++ ~QWasapiWarmupClient();
++
++ private:
++ ComPtr<IAudioClient3> m_warmupClient;
++ };
++
++ QWasapiWarmupClient::QWasapiWarmupClient()
++ {
++ using namespace QWindowsAudioUtils;
++ qt_win_ensureComInitializedOnThisThread();
++
++ ComPtr<IMMDeviceEnumerator> deviceEnumerator;
++ HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL,
++ IID_PPV_ARGS(&deviceEnumerator));
++ if (FAILED(hr)) {
++ qWarning() << "Failed to create device enumerator" << audioClientErrorString(hr);
++ return;
++ }
++
++ ComPtr<IMMDevice> device;
++ hr = deviceEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, device.GetAddressOf());
++ if (FAILED(hr)) {
++ if (hr != E_NOTFOUND)
++ qWarning() << "Failed to retrieve default audio endpoint" << audioClientErrorString(hr);
++ return;
++ }
++
++ hr = device->Activate(__uuidof(IAudioClient3), CLSCTX_ALL, nullptr,
++ reinterpret_cast<void **>(m_warmupClient.GetAddressOf()));
++ if (FAILED(hr)) {
++ qWarning() << "Failed to activate audio engine" << audioClientErrorString(hr);
++ return;
++ }
++
++ QComTaskResource<WAVEFORMATEX> deviceFormat;
++ UINT32 currentPeriodInFrames = 0;
++ hr = m_warmupClient->GetCurrentSharedModeEnginePeriod(deviceFormat.address(),
++ &currentPeriodInFrames);
++ if (FAILED(hr)) {
++ qWarning() << "Failed to retrieve the current format and periodicity of the audio engine"
++ << audioClientErrorString(hr);
++ return;
++ }
++
++ UINT32 defaultPeriodInFrames = 0;
++ UINT32 fundamentalPeriodInFrames = 0;
++ UINT32 minPeriodInFrames = 0;
++ UINT32 maxPeriodInFrames = 0;
++ hr = m_warmupClient->GetSharedModeEnginePeriod(deviceFormat.get(), &defaultPeriodInFrames,
++ &fundamentalPeriodInFrames, &minPeriodInFrames,
++ &maxPeriodInFrames);
++ if (FAILED(hr)) {
++ qWarning() << "Failed to retrieve the range of periodicities supported by the audio engine"
++ << audioClientErrorString(hr);
++ return;
++ }
++
++ audioClientSetRole(m_warmupClient, QtMultimediaPrivate::AudioEndpointRole::SoundEffect);
++
++ hr = m_warmupClient->InitializeSharedAudioStream(
++ AUDCLNT_SHAREMODE_SHARED, defaultPeriodInFrames, deviceFormat.get(), nullptr);
++ if (FAILED(hr)) {
++ qWarning() << "Failed to initialize audio engine stream" << audioClientErrorString(hr);
++ return;
++ }
++
++ hr = m_warmupClient->Start();
++ if (FAILED(hr))
++ qWarning() << "Failed to start audio engine" << audioClientErrorString(hr);
++ }
++
++ QWasapiWarmupClient::~QWasapiWarmupClient()
++ {
++ using namespace QWindowsAudioUtils;
++
++ if (m_warmupClient) {
++ HRESULT hr = m_warmupClient->Stop();
++ if (FAILED(hr))
++ qWarning() << "Failed to stop audio engine" << audioClientErrorString(hr);
++ }
++ }
++
++ } // namespace
++
++ ///////////////////////////////////////////////////////////////////////////////////////////////////
++
++ DEFINE_GUID(GUID_SLEEP_TIMEOUT, 0x29f6c1db, 0x86da, 0x48c5, 0x9f, 0xdb, 0xf2, 0xb6, 0x7b, 0x1f,
++ 0x44, 0xda);
++
++ class SleepTimeoutMonitor : public QObject
++ {
++ Q_OBJECT
++
++ public:
++ SleepTimeoutMonitor();
++ ~SleepTimeoutMonitor();
++
++ Q_SIGNALS:
++ void sleepTimeoutChanged(std::chrono::seconds);
++
++ private:
++ static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
++ static LRESULT handlePowerSettingsChanged(HWND hwnd, POWERBROADCAST_SETTING *pbs);
++
++ std::wstring m_windowClass;
++ HWND m_offscreenWindow{};
++ HPOWERNOTIFY m_powerNotifySleepTimeout;
++ };
++
++ SleepTimeoutMonitor::SleepTimeoutMonitor()
++ {
++ m_windowClass = QUuid::createUuid().toString(QUuid::WithoutBraces).toStdWString()
++ + L"PowerNotificationWindowClass";
++
++ WNDCLASSEX wc{};
++ wc.cbSize = sizeof(WNDCLASSEX);
++ wc.lpfnWndProc = WndProc;
++ wc.hInstance = nullptr;
++ wc.lpszClassName = m_windowClass.c_str();
++ wc.hCursor = nullptr;
++
++ if (!RegisterClassEx(&wc)) {
++ qWarning() << "RegisterClassEx failed" << QSystemError::windowsString();
++ m_windowClass.clear();
++ return;
++ }
++
++ m_offscreenWindow = CreateWindowEx(0, m_windowClass.c_str(), L"Power Notification Window", 0, 0,
++ 0, 0, 0, HWND_MESSAGE, nullptr, nullptr, this);
++
++ if (!m_offscreenWindow) {
++ qWarning() << "CreateWindowEx failed" << QSystemError::windowsString();
++ return;
++ }
++
++ SetWindowLongPtr(m_offscreenWindow, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this));
++
++ m_powerNotifySleepTimeout = RegisterPowerSettingNotification(
++ m_offscreenWindow, &GUID_SLEEP_TIMEOUT, DEVICE_NOTIFY_WINDOW_HANDLE);
++ if (!m_powerNotifySleepTimeout) {
++ qWarning() << "RegisterPowerSettingNotification failed" << QSystemError::windowsString();
++ }
++ }
++
++ SleepTimeoutMonitor::~SleepTimeoutMonitor()
++ {
++ if (m_powerNotifySleepTimeout)
++ UnregisterPowerSettingNotification(m_powerNotifySleepTimeout);
++
++ if (m_offscreenWindow)
++ DestroyWindow(m_offscreenWindow);
++ if (!m_windowClass.empty())
++ UnregisterClass(m_windowClass.c_str(), GetModuleHandle(nullptr));
++ }
++
++ LRESULT SleepTimeoutMonitor::WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
++ {
++ switch (msg) {
++ case WM_POWERBROADCAST: {
++ switch (wParam) {
++ case PBT_POWERSETTINGCHANGE: {
++ POWERBROADCAST_SETTING *pbs = reinterpret_cast<POWERBROADCAST_SETTING *>(lParam);
++ return handlePowerSettingsChanged(hwnd, pbs);
++ }
++ default:
++ break;
++ }
++ break;
++ }
++ default:
++ return DefWindowProc(hwnd, msg, wParam, lParam);
++ }
++
++ return 0;
++ }
++
++ LRESULT SleepTimeoutMonitor::handlePowerSettingsChanged(HWND hwnd, POWERBROADCAST_SETTING *pbs)
++ {
++ if (pbs->PowerSetting == GUID_SLEEP_TIMEOUT) {
++ Q_ASSERT(pbs->DataLength == 4);
++ union UnpackUnion {
++ UCHAR data[4];
++ DWORD sleepTimeoutInSeconds;
++ } helper;
++
++ std::copy_n(pbs->Data, pbs->DataLength, helper.data);
++ auto self = reinterpret_cast<SleepTimeoutMonitor *>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
++
++ Q_EMIT self->sleepTimeoutChanged(std::chrono::seconds{ helper.sleepTimeoutInSeconds });
++ }
++ return 0;
++ }
++
++ ///////////////////////////////////////////////////////////////////////////////////////////////////
++
++ namespace {
++
++ // The wasapi warmup client prevents excessive fade-in times on some systems. However keeping it
++ // around prevents the system from going to sleep.
++ // so we don't keep it around forever, but only for half the time before the system may go to sleep
++ // (or 5 minutes max)
++ class QWasapiWarmupClientHelper
++ {
++ public:
++ QWasapiWarmupClientHelper()
++ {
++ m_cleanupTimer.setInterval(120s);
++ m_cleanupTimer.setTimerType(Qt::TimerType::VeryCoarseTimer);
++ m_cleanupTimer.setSingleShot(true);
++ m_cleanupTimer.callOnTimeout(&m_cleanupTimer, [this] {
++ m_warmupClient = {};
++ });
++
++ if (!QThread::isMainThread())
++ m_cleanupTimer.moveToThread(qApp->thread());
++
++ QObject::connect(qApp, &QCoreApplication::aboutToQuit, qApp, [this] {
++ m_cleanupTimer.stop();
++ m_inShutdown = true;
++ m_warmupClient = {};
++ m_sleepTimeoutMonitor = std::nullopt;
++ });
++
++ m_sleepTimeoutMonitor.emplace();
++ QObject::connect(&*m_sleepTimeoutMonitor, &SleepTimeoutMonitor::sleepTimeoutChanged,
++ &m_cleanupTimer, [this](std::chrono::seconds sleepTimeout) {
++ m_cleanupTimer.setInterval(std::min<std::chrono::milliseconds>(sleepTimeout / 2, 5min));
++ });
++ }
++
++ void refresh()
++ {
++ Q_ASSERT(QThread::isMainThread());
++ if (m_inShutdown)
++ return;
++
++ if (!m_warmupClient)
++ m_warmupClient = std::make_unique<QWasapiWarmupClient>();
++
++ m_cleanupTimer.start();
++ }
++
++ private:
++ QChronoTimer m_cleanupTimer;
++ std::optional<SleepTimeoutMonitor> m_sleepTimeoutMonitor;
++ std::unique_ptr<QWasapiWarmupClient> m_warmupClient;
++ bool m_inShutdown{};
++ };
++
++ Q_APPLICATION_STATIC(QWasapiWarmupClientHelper, warmupClient);
++
++ } // namespace
++
++ ///////////////////////////////////////////////////////////////////////////////////////////////////
++
++ void refreshWarmupClient()
++ {
++ const static bool useWarmupClient = [] {
++ bool envVarSet = false;
++ int disableWarmup = qEnvironmentVariableIntValue("QT_DISABLE_AUDIO_PREPARE", &envVarSet);
++ return !envVarSet || disableWarmup == 0;
++ }();
++
++ if (!useWarmupClient)
++ return;
++
++ QMetaObject::invokeMethod(qApp, [] {
++ warmupClient->refresh();
++ });
++ }
++
++ } // namespace QtMultimediaPrivate
++
++ QT_END_NAMESPACE
++
++ #include "qwindows_wasapi_warmup_client.moc"