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
qwindows_wasapi_warmup_client.cpp
Go to the documentation of this file.
1// Copyright (C) 2025 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/qapplicationstatic.h>
7#include <QtCore/qcoreapplication.h>
8#include <QtCore/qdebug.h>
9#include <QtCore/qspan.h>
10#include <QtCore/qthread.h>
11#include <QtCore/qtimer.h>
12#include <QtCore/quuid.h>
13#include <QtCore/private/qsystemerror_p.h>
14#include <QtMultimedia/private/qcominitializer_p.h>
15#include <QtMultimedia/private/qcomtaskresource_p.h>
16#include <QtMultimedia/private/qwindowsaudioutils_p.h>
17
18#include <audioclient.h>
19#include <guiddef.h>
20#include <mmdeviceapi.h>
21#include <powrprof.h>
22
24
25namespace QtMultimediaPrivate {
26
27using namespace std::chrono_literals;
28
29namespace {
30
31///////////////////////////////////////////////////////////////////////////////////////////////////
32
33// The "warm-up" audio client is required to run in the background in order to keep audio engine
34// ready for audio output immediately after creating any other subsequent audio client.
35class QWasapiWarmupClient
36{
37public:
38 QWasapiWarmupClient();
39 ~QWasapiWarmupClient();
40
41private:
42 ComPtr<IAudioClient3> m_warmupClient;
43};
44
45QWasapiWarmupClient::QWasapiWarmupClient()
46{
47 using namespace QWindowsAudioUtils;
48 ensureComInitializedOnThisThread();
49
50 ComPtr<IMMDeviceEnumerator> deviceEnumerator;
51 HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL,
52 IID_PPV_ARGS(&deviceEnumerator));
53 if (FAILED(hr)) {
54 qWarning() << "Failed to create device enumerator" << audioClientErrorString(hr);
55 return;
56 }
57
58 ComPtr<IMMDevice> device;
59 hr = deviceEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, device.GetAddressOf());
60 if (FAILED(hr)) {
61 if (hr != E_NOTFOUND)
62 qWarning() << "Failed to retrieve default audio endpoint" << audioClientErrorString(hr);
63 return;
64 }
65
66 hr = device->Activate(__uuidof(IAudioClient3), CLSCTX_ALL, nullptr,
67 reinterpret_cast<void **>(m_warmupClient.GetAddressOf()));
68 if (FAILED(hr)) {
69 qWarning() << "Failed to activate audio engine" << audioClientErrorString(hr);
70 return;
71 }
72
73 QComTaskResource<WAVEFORMATEX> deviceFormat;
74 UINT32 currentPeriodInFrames = 0;
75 hr = m_warmupClient->GetCurrentSharedModeEnginePeriod(deviceFormat.address(),
76 &currentPeriodInFrames);
77 if (FAILED(hr)) {
78 qWarning() << "Failed to retrieve the current format and periodicity of the audio engine"
79 << audioClientErrorString(hr);
80 return;
81 }
82
83 UINT32 defaultPeriodInFrames = 0;
84 UINT32 fundamentalPeriodInFrames = 0;
85 UINT32 minPeriodInFrames = 0;
86 UINT32 maxPeriodInFrames = 0;
87 hr = m_warmupClient->GetSharedModeEnginePeriod(deviceFormat.get(), &defaultPeriodInFrames,
88 &fundamentalPeriodInFrames, &minPeriodInFrames,
89 &maxPeriodInFrames);
90 if (FAILED(hr)) {
91 qWarning() << "Failed to retrieve the range of periodicities supported by the audio engine"
92 << audioClientErrorString(hr);
93 return;
94 }
95
96 audioClientSetRole(m_warmupClient, QtMultimediaPrivate::AudioEndpointRole::SoundEffect);
97
98 hr = m_warmupClient->InitializeSharedAudioStream(
99 AUDCLNT_SHAREMODE_SHARED, defaultPeriodInFrames, deviceFormat.get(), nullptr);
100 if (FAILED(hr)) {
101 qWarning() << "Failed to initialize audio engine stream" << audioClientErrorString(hr);
102 return;
103 }
104
105 hr = m_warmupClient->Start();
106 if (FAILED(hr))
107 qWarning() << "Failed to start audio engine" << audioClientErrorString(hr);
108}
109
110QWasapiWarmupClient::~QWasapiWarmupClient()
111{
112 using namespace QWindowsAudioUtils;
113
114 if (m_warmupClient) {
115 HRESULT hr = m_warmupClient->Stop();
116 if (FAILED(hr))
117 qWarning() << "Failed to stop audio engine" << audioClientErrorString(hr);
118 }
119}
120
121} // namespace
122
123///////////////////////////////////////////////////////////////////////////////////////////////////
124
125// https://learn.microsoft.com/en-us/windows-hardware/customize/power-settings/sleep-settings-sleep-idle-timeout
126DEFINE_GUID(GUID_SLEEP_TIMEOUT, 0x29f6c1db, 0x86da, 0x48c5, 0x9f, 0xdb, 0xf2, 0xb6, 0x7b, 0x1f,
127 0x44, 0xda);
128
130{
132
133public:
136
137signals:
139
140private:
141 static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
142 static LRESULT handlePowerSettingsChanged(HWND hwnd, POWERBROADCAST_SETTING *pbs);
143
144 std::wstring m_windowClass;
145 HWND m_offscreenWindow{};
146 HPOWERNOTIFY m_powerNotifySleepTimeout;
147};
148
149SleepTimeoutMonitor::SleepTimeoutMonitor()
150{
151 m_windowClass = QUuid::createUuid().toString(QUuid::WithoutBraces).toStdWString()
152 + L"PowerNotificationWindowClass";
153
154 WNDCLASSEX wc{};
155 wc.cbSize = sizeof(WNDCLASSEX);
156 wc.lpfnWndProc = WndProc;
157 wc.hInstance = nullptr;
158 wc.lpszClassName = m_windowClass.c_str();
159 wc.hCursor = nullptr;
160
161 if (!RegisterClassEx(&wc)) {
162 qWarning() << "RegisterClassEx failed" << QSystemError::windowsString();
163 m_windowClass.clear();
164 return;
165 }
166
167 m_offscreenWindow = CreateWindowEx(0, m_windowClass.c_str(), L"Power Notification Window", 0, 0,
168 0, 0, 0, HWND_MESSAGE, nullptr, nullptr, this);
169
170 if (!m_offscreenWindow) {
171 qWarning() << "CreateWindowEx failed" << QSystemError::windowsString();
172 return;
173 }
174
175 SetWindowLongPtr(m_offscreenWindow, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this));
176
177 m_powerNotifySleepTimeout = RegisterPowerSettingNotification(
178 m_offscreenWindow, &GUID_SLEEP_TIMEOUT, DEVICE_NOTIFY_WINDOW_HANDLE);
179 if (!m_powerNotifySleepTimeout) {
180 qWarning() << "RegisterPowerSettingNotification failed" << QSystemError::windowsString();
181 }
182}
183
185{
186 if (m_powerNotifySleepTimeout)
187 UnregisterPowerSettingNotification(m_powerNotifySleepTimeout);
188
189 if (m_offscreenWindow)
190 DestroyWindow(m_offscreenWindow);
191 if (!m_windowClass.empty())
192 UnregisterClass(m_windowClass.c_str(), GetModuleHandle(nullptr));
193}
194
195LRESULT SleepTimeoutMonitor::WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
196{
197 switch (msg) {
198 case WM_POWERBROADCAST: {
199 switch (wParam) {
200 case PBT_POWERSETTINGCHANGE: {
201 POWERBROADCAST_SETTING *pbs = reinterpret_cast<POWERBROADCAST_SETTING *>(lParam);
202 return handlePowerSettingsChanged(hwnd, pbs);
203 }
204 default:
205 break;
206 }
207 break;
208 }
209 default:
210 return DefWindowProc(hwnd, msg, wParam, lParam);
211 }
212
213 return 0;
214}
215
216LRESULT SleepTimeoutMonitor::handlePowerSettingsChanged(HWND hwnd, POWERBROADCAST_SETTING *pbs)
217{
218 if (pbs->PowerSetting == GUID_SLEEP_TIMEOUT) {
219 Q_ASSERT(pbs->DataLength == 4);
220 union UnpackUnion {
221 UCHAR data[4];
222 DWORD sleepTimeoutInSeconds;
223 } helper;
224
225 std::copy_n(pbs->Data, pbs->DataLength, helper.data);
226 auto self = reinterpret_cast<SleepTimeoutMonitor *>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
227
228 emit self->sleepTimeoutChanged(std::chrono::seconds{ helper.sleepTimeoutInSeconds });
229 }
230 return 0;
231}
232
233///////////////////////////////////////////////////////////////////////////////////////////////////
234
235namespace {
236
237// The wasapi warmup client prevents excessive fade-in times on some systems. However keeping it
238// around prevents the system from going to sleep.
239// so we don't keep it around forever, but only for half the time before the system may go to sleep
240// (or 5 minutes max)
241class QWasapiWarmupClientHelper
242{
243public:
244 QWasapiWarmupClientHelper()
245 {
246 m_cleanupTimer.setInterval(120s);
247 m_cleanupTimer.setTimerType(Qt::TimerType::VeryCoarseTimer);
248 m_cleanupTimer.setSingleShot(true);
249 m_cleanupTimer.callOnTimeout(&m_cleanupTimer, [this] {
250 m_warmupClient = {};
251 });
252
253 if (!QThread::isMainThread())
254 m_cleanupTimer.moveToThread(qApp->thread());
255
256 QObject::connect(qApp, &QCoreApplication::aboutToQuit, qApp, [this] {
257 m_warmupClient = {};
258 });
259
260 QObject::connect(&m_sleepTimeoutMonitor, &SleepTimeoutMonitor::sleepTimeoutChanged,
261 &m_cleanupTimer, [this](std::chrono::seconds sleepTimeout) {
262 m_cleanupTimer.setInterval(std::min<std::chrono::milliseconds>(sleepTimeout / 2, 5min));
263 });
264 }
265
266 void refresh()
267 {
268 Q_ASSERT(QThread::isMainThread());
269
270 if (!m_warmupClient)
271 m_warmupClient = std::make_unique<QWasapiWarmupClient>();
272
273 m_cleanupTimer.start();
274 }
275
276private:
277 QTimer m_cleanupTimer;
278 SleepTimeoutMonitor m_sleepTimeoutMonitor;
279 std::unique_ptr<QWasapiWarmupClient> m_warmupClient;
280};
281
282Q_APPLICATION_STATIC(QWasapiWarmupClientHelper, warmupClient);
283
284} // namespace
285
286///////////////////////////////////////////////////////////////////////////////////////////////////
287
289{
290 const static bool useWarmupClient = [] {
291 bool envVarSet = false;
292 int disableWarmup = qEnvironmentVariableIntValue("QT_DISABLE_AUDIO_PREPARE", &envVarSet);
293 return !envVarSet || disableWarmup == 0;
294 }();
295
296 if (!useWarmupClient)
297 return;
298
299 QMetaObject::invokeMethod(qApp, [] {
300 warmupClient->refresh();
301 });
302}
303
304} // namespace QtMultimediaPrivate
305
306QT_END_NAMESPACE
307
308#include "qwindows_wasapi_warmup_client.moc"
DEFINE_GUID(GUID_SLEEP_TIMEOUT, 0x29f6c1db, 0x86da, 0x48c5, 0x9f, 0xdb, 0xf2, 0xb6, 0x7b, 0x1f, 0x44, 0xda)