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
qwindowsaudiosink.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 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/private/qsystemerror_p.h>
7#include <QtCore/private/qfunctions_win_p.h>
8#include <QtMultimedia/private/qmemory_resource_tlsf_p.h>
9#include <QtMultimedia/private/qwindowsaudiodevice_p.h>
10#include <QtMultimedia/private/qwindowsresampler_p.h>
11
12#include <audioclient.h>
13#include <mmdeviceapi.h>
14
16
17namespace QtWASAPI {
18
20using namespace std::chrono_literals;
21
22namespace {
23
24QAudioFormat makeHostFormatForSink(const QAudioDevice &device, const QAudioFormat &format)
25{
26 const QWindowsAudioDevice *winDevice = QAudioDevicePrivate::handle<QWindowsAudioDevice>(device);
27
28 auto status = winDevice->m_probeDataFuture.wait_for(QAudioDevicePrivate::formatProbeTimeout);
29 switch (status) {
30 case std::future_status::ready:
31 case std::future_status::deferred:
32 break;
33 case std::future_status::timeout:
34 return QAudioFormat{};
35 default:
36 Q_UNREACHABLE();
37 }
38
39 auto [minProbedChannels, maxProbedChannels] = winDevice->m_probeDataFuture.get().channelCountRange;
40
41 QAudioFormat hostFormat = format;
42 const int requestedChannelCount = format.channelCount();
43 if (requestedChannelCount < device.minimumChannelCount()) {
44 hostFormat.setChannelCount(minProbedChannels);
45 hostFormat.setChannelConfig(
46 QAudioFormat::defaultChannelConfigForChannelCount(minProbedChannels));
47 } else if (requestedChannelCount > device.maximumChannelCount()) {
48 hostFormat.setChannelCount(maxProbedChannels);
49 hostFormat.setChannelConfig(
50 QAudioFormat::defaultChannelConfigForChannelCount(maxProbedChannels));
51 }
52
53 return hostFormat;
54}
55
56} // namespace
57
58QWASAPIAudioSinkStream::QWASAPIAudioSinkStream(QAudioDevice device, const QAudioFormat &format,
59 std::optional<qsizetype> ringbufferSize,
60 QWindowsAudioSink *parent, float volume,
61 std::optional<NativePeriodFrames> nativePeriodFrames,
62 AudioEndpointRole role)
65 },
66 m_role{
67 role,
68 },
70 CreateEvent(0, false, false, nullptr),
71 },
72 m_parent{ parent },
75 }
76{
77}
78
80{
81 return true;
82}
83
84bool QWASAPIAudioSinkStream::start(QIODevice *ioDevice)
85{
86 auto immDevice = QAudioDevicePrivate::handle<QWindowsAudioDevice>(m_audioDevice)->open();
87 if (!immDevice)
88 return false;
89
90 bool clientOpen = openAudioClient(std::move(immDevice), m_role);
91 if (!clientOpen)
92 return false;
93
94 setQIODevice(ioDevice);
95 createQIODeviceConnections(ioDevice);
96 pullFromQIODevice();
97
98 return startAudioClient(StreamType::Ringbuffer);
99}
100
102{
103 auto immDevice = QAudioDevicePrivate::handle<QWindowsAudioDevice>(m_audioDevice)->open();
104 if (!immDevice)
105 return nullptr;
106
107 bool clientOpen = openAudioClient(std::move(immDevice), m_role);
108 if (!clientOpen)
109 return nullptr;
110
111 QIODevice *ioDevice = createRingbufferWriterDevice();
112
113 m_parent->updateStreamIdle(true, QWindowsAudioSink::EmitStateSignal::False);
114
115 setQIODevice(ioDevice);
116 createQIODeviceConnections(ioDevice);
117
118 bool started = startAudioClient(StreamType::Ringbuffer);
119 return started ? ioDevice : nullptr;
120}
121
122bool QWASAPIAudioSinkStream::start(AudioCallback audioCallback)
123{
124 auto immDevice = QAudioDevicePrivate::handle<QWindowsAudioDevice>(m_audioDevice)->open();
125 if (!immDevice)
126 return false;
127
128 bool clientOpen = openAudioClient(std::move(immDevice), m_role);
129 if (!clientOpen)
130 return false;
131
132 m_audioCallback = std::move(audioCallback);
133
134 return startAudioClient(StreamType::Callback);
135}
136
138{
139 m_suspended = true;
140 QWindowsAudioUtils::audioClientStop(m_audioClient);
141}
142
144{
145 m_suspended = false;
146 QWindowsAudioUtils::audioClientStart(m_audioClient);
147}
148
149void QWASAPIAudioSinkStream::stop(ShutdownPolicy shutdownPolicy)
150{
151 using namespace QWindowsAudioUtils;
152
153 m_parent = nullptr;
154 m_shutdownPolicy = shutdownPolicy;
155
156 requestStop();
157 switch (shutdownPolicy) {
158 case ShutdownPolicy::DiscardRingbuffer: {
159 audioClientStop(m_audioClient);
160 joinWorkerThread();
161 audioClientReset(m_audioClient);
162
163 return;
164 }
165 case ShutdownPolicy::DrainRingbuffer: {
166 m_ringbufferDrained.callOnActivated([self = shared_from_this()]() mutable {
167 self->joinWorkerThread();
168 self = {};
169 });
170 return;
171 }
172 default:
173 Q_UNREACHABLE_RETURN();
174 }
175}
176
178{
179 if (m_parent)
180 m_parent->updateStreamIdle(streamIsIdle);
181}
182
183bool QWASAPIAudioSinkStream::openAudioClient(ComPtr<IMMDevice> device, AudioEndpointRole role)
184{
185 using namespace QWindowsAudioUtils;
186
187 std::optional<AudioClientCreationResult> clientData = createAudioClient(
188 device, m_hostFormat, m_nativePeriodFrames, m_audioClientEventHandle, role);
189
190 if (!clientData)
191 return false;
192
193 m_audioClient = std::move(clientData->client);
194 m_periodSize = clientData->periodSize;
195 m_audioClientFrames = clientData->audioClientFrames;
196
197 HRESULT hr = m_audioClient->GetService(IID_PPV_ARGS(m_renderClient.GetAddressOf()));
198 if (FAILED(hr)) {
199 qWarning() << "IAudioClient3::GetService failed to obtain IAudioRenderClient"
200 << audioClientErrorString(hr);
201 return false;
202 }
203
204 if (m_audioDevice.preferredFormat().sampleRate() != m_hostFormat.sampleRate())
205 audioClientSetRate(m_audioClient, m_hostFormat.sampleRate());
206
207 return true;
208}
209
210bool QWASAPIAudioSinkStream::startAudioClient(StreamType streamType)
211{
212 using namespace QWindowsAudioUtils;
213 m_workerThread.reset(QThread::create([this, streamType] {
214 const auto mmcssRegistration = registerMMCSSThreadForPeriod(m_periodSize);
215 fillInitialHostBuffer(streamType);
216 std::optional<QComHelper> m_comHelper;
217
218 if (m_hostFormat != m_format) {
219 m_comHelper.emplace();
220 m_resampler = std::make_unique<QWindowsResampler>();
221 m_resampler->setup(m_format, m_hostFormat);
222
223 m_memoryResource = std::make_unique<QTlsfMemoryResource>(512 * 1024);
224 }
225
226 switch (streamType) {
227 case StreamType::Ringbuffer:
228 return runProcessRingbufferLoop();
229 case StreamType::Callback:
230 return runProcessCallbackLoop();
231 }
232 }));
233 m_workerThread->setObjectName(u"QWASAPIAudioSinkStream");
234 m_workerThread->start();
235
236 bool started = QWindowsAudioUtils::audioClientStart(m_audioClient);
237 if (!started) {
238 joinWorkerThread();
239 return false;
240 }
241 return true;
242}
243
244void QWASAPIAudioSinkStream::fillInitialHostBuffer(StreamType streamType)
245{
246 switch (streamType) {
248 processRingbuffer();
249 return;
250 }
252 processCallback();
253 return;
254 }
255 default:
256 Q_UNREACHABLE_RETURN();
257 }
258}
259
260void QWASAPIAudioSinkStream::runProcessRingbufferLoop()
261{
262 using namespace QWindowsAudioUtils;
263
264 for (;;) {
265 constexpr std::chrono::milliseconds timeout = 2s;
266 const bool wasSuspended = m_suspended;
267 DWORD retval = WaitForSingleObject(m_audioClientEventHandle.get(), timeout.count());
268 if (retval != WAIT_OBJECT_0) {
269 if (wasSuspended || m_suspended)
270 continue;
271
272 handleAudioClientError();
273 return;
274 }
275
276 if (isStopRequested()) {
277 switch (m_shutdownPolicy.load(std::memory_order_relaxed)) {
278 case ShutdownPolicy::DiscardRingbuffer:
279 return;
280 case ShutdownPolicy::DrainRingbuffer: {
281 bool bufferDrained = visitRingbuffer([](const auto &ringbuffer) {
282 return ringbuffer.used() == 0;
283 });
284 if (bufferDrained) {
285 audioClientStop(m_audioClient);
286 audioClientReset(m_audioClient);
287
288 m_ringbufferDrained.set();
289 return;
290 }
291 break;
292 }
293 default:
294 Q_UNREACHABLE_RETURN();
295 }
296 }
297
298 bool success = processRingbuffer();
299 if (!success) {
300 handleAudioClientError();
301 return;
302 }
303 }
304}
305
306void QWASAPIAudioSinkStream::runProcessCallbackLoop()
307{
308 using namespace QWindowsAudioUtils;
309
310 for (;;) {
311 constexpr std::chrono::milliseconds timeout = 2s;
312 const bool wasSuspended = m_suspended;
313 DWORD retval = WaitForSingleObject(m_audioClientEventHandle.get(), timeout.count());
314 if (retval != WAIT_OBJECT_0) {
315 if (wasSuspended || m_suspended)
316 continue;
317
318 handleAudioClientError();
319 return;
320 }
321
322 if (isStopRequested())
323 return;
324
325 bool success = processCallback();
326 if (!success) {
327 handleAudioClientError();
328 return;
329 }
330 }
331}
332
333template <typename Functor>
334bool QWASAPIAudioSinkStream::visitAudioClientBuffer(Functor &&f)
335{
336 uint32_t numFramesPadding;
337 HRESULT hr = m_audioClient->GetCurrentPadding(&numFramesPadding);
338 if (FAILED(hr)) {
339 qWarning() << "IAudioClient3::GetCurrentPadding failed" << audioClientErrorString(hr);
340 return false;
341 }
342
343 const uint32_t requiredFrames = m_audioClientFrames - numFramesPadding;
344 if (requiredFrames == 0)
345 return true;
346
347 // Grab the next empty buffer from the audio device.
348 unsigned char *hostBuffer{};
349 hr = m_renderClient->GetBuffer(requiredFrames, &hostBuffer);
350 if (FAILED(hr)) {
351 qWarning() << "IAudioRenderClient::getBuffer failed" << audioClientErrorString(hr);
352 return false;
353 }
354
355 QSpan<std::byte> hostBufferSpan{
356 reinterpret_cast<std::byte *>(hostBuffer),
357 m_hostFormat.bytesForFrames(requiredFrames),
358 };
359
360 uint64_t consumedFrames;
361 if (m_resampler) {
362 Q_UNLIKELY_BRANCH;
363
364 std::pmr::vector<std::byte> resampleBuffer{
365 size_t(m_format.bytesForFrames(requiredFrames)),
366 m_memoryResource.get(),
367 };
368 consumedFrames = f(as_writable_bytes(QSpan{ resampleBuffer }), requiredFrames);
369
370 auto resampledBuffer = m_resampler->resample(resampleBuffer, m_memoryResource.get());
371
372 Q_ASSERT(resampledBuffer.size() == size_t(hostBufferSpan.size()));
373 std::copy_n(resampledBuffer.data(), resampledBuffer.size(), hostBufferSpan.data());
374 } else {
375 consumedFrames = f(hostBufferSpan, requiredFrames);
376 }
377
378 const DWORD flags = consumedFrames != 0 ? 0 : AUDCLNT_BUFFERFLAGS_SILENT;
379
380 hr = m_renderClient->ReleaseBuffer(requiredFrames, flags);
381 if (FAILED(hr)) {
382 qWarning() << "IAudioRenderClient::ReleaseBuffer failed" << audioClientErrorString(hr);
383 return false;
384 }
385
386 return true;
387}
388
390{
393 return consumedFrames;
394 });
395}
396
398{
401 return requiredFrames;
402 });
403}
404
406{
407 requestStop();
408 ::SetEvent(m_audioClientEventHandle.get()); // force wakeup
410 m_workerThread = {};
411}
412
414{
415 using namespace QWindowsAudioUtils;
416 requestStop();
419
421 if (self->m_parent && self->m_parent->m_stream == self)
423 });
424}
425
431
433 = default;
434
435} // namespace QtWASAPI
436
437QT_END_NAMESPACE
Combined button and popup list for selecting options.
QWASAPIAudioSinkStream(QAudioDevice, const QAudioFormat &, std::optional< qsizetype > ringbufferSize, QWindowsAudioSink *parent, float volume, std::optional< NativePeriodFrames > nativePeriodFrames, AudioEndpointRole)
QAudioFormat::SampleFormat SampleFormat