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
qsamplecache_p.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 <QtMultimedia/qaudiobuffer.h>
7#include <QtMultimedia/qaudiodecoder.h>
8#include <QtMultimedia/private/qaudiohelpers_p.h>
9#include <QtMultimedia/private/qdrawavaudiodecoder_p.h>
10#include <QtMultimedia/private/qmultimediautils_p.h>
11#include <QtConcurrent/qtconcurrentrun.h>
12#include <QtCore/qapplicationstatic.h>
13#include <QtCore/qbuffer.h>
14#include <QtCore/qcoreapplication.h>
15#include <QtCore/qdebug.h>
16#include <QtCore/qeventloop.h>
17#include <QtCore/qfile.h>
18#include <QtCore/qfuturewatcher.h>
19#include <QtCore/qloggingcategory.h>
20#include <QtCore/qmutex.h>
21
22#if QT_CONFIG(network)
23# include <QtNetwork/qnetworkaccessmanager.h>
24# include <QtNetwork/qnetworkreply.h>
25# include <QtNetwork/qnetworkrequest.h>
26#endif
27
28#include "dr_wav.h"
29
30#include <utility>
31
32Q_STATIC_LOGGING_CATEGORY(qLcSampleCache, "qt.multimedia.samplecache")
33
34#if !QT_CONFIG(thread)
35# define thread_local
36#endif
37
38QT_BEGIN_NAMESPACE
39
40QSample::QSample(QUrl url, QSampleCache *parent) : m_parent(parent), m_url(std::move(url)) { }
41
42QSample::QSample(QUrl url, QSampleCache *parent, std::optional<SampleRate> targetSampleRate)
43 : m_parent(parent), m_url(std::move(url)), m_targetSampleRate(targetSampleRate)
44{
45}
46
47QSample::~QSample()
48{
49 // Remove ourselves from our parent
50 if (m_parent)
51 m_parent->removeUnreferencedSample(m_url, m_targetSampleRate);
52
53 qCDebug(qLcSampleCache) << "~QSample" << this << ": deleted [" << m_url << "]" << QThread::currentThread();
54}
55
56void QSample::setError()
57{
58 m_state = State::Error;
59}
60
61void QSample::setData(QByteArray data, QAudioFormat format)
62{
63 m_state = State::Ready;
64 m_soundData = std::move(data);
65 m_audioFormat = format;
66}
67
68QSample::State QSample::state() const
69{
70 return m_state;
71}
72
73void QSample::clearParent()
74{
75 m_parent = nullptr;
76}
77
78///////////////////////////////////////////////////////////////////////////////////////////////////
79
80Q_APPLICATION_STATIC(QSampleCache, sampleCache)
81
82QSampleCache *QSampleCache::instance()
83{
84 return sampleCache();
85}
86
87#if QT_CONFIG(thread)
88QThreadPool *QSampleCache::threadPool()
89{
90#ifdef Q_OS_WASM
91 return QThreadPool::globalInstance();
92#else
93 return &m_threadPool;
94#endif
95}
96#endif
97
98QSampleCache::QSampleCache(QObject *parent) : QObject(parent)
99{
100#if QT_CONFIG(thread)
101 if (!thread()->isMainThread())
102 moveToThread(qApp->thread());
103
104# if !defined(Q_OS_WASM)
105 // we limit the number of loader threads to avoid thread explosion
106 static constexpr int loaderThreadLimit = 8;
107 m_threadPool.setObjectName("QSampleCachePool");
108 m_threadPool.setMaxThreadCount(loaderThreadLimit);
109 m_threadPool.setExpiryTimeout(15);
110 m_threadPool.setThreadPriority(QThread::LowPriority);
111 m_threadPool.setServiceLevel(QThread::QualityOfService::Eco);
112
113 qAddPostRoutine([] {
114 // HACK: we need to stop the thread pool before qApp is nulled, otherwise some threads might still try construct
115 // some Q_APPLICATION_STATIC instances, causing assertion failures inside QNetworkAccessManager
116 Q_ASSERT(qApp && "QApplication is still valid");
117
118 QSampleCache *instance = sampleCache();
119
120 instance->m_threadPool.clear();
121 instance->m_threadPool.waitForDone();
122 });
123
124# endif // Q_OS_WASM
125#endif // QT_CONFIG(thread)
126}
127
128QSampleCache::~QSampleCache()
129{
130#if QT_CONFIG(thread) && !defined(Q_OS_WASM)
131 m_threadPool.clear();
132 m_threadPool.waitForDone();
133#endif
134
135 for (auto &entry : m_loadedSamples) {
136 auto samplePtr = entry.second.lock();
137 if (samplePtr)
138 samplePtr->clearParent();
139 }
140
141 for (auto &entry : m_pendingSamples) {
142 auto samplePtr = entry.second.first;
143 if (samplePtr)
144 samplePtr->clearParent();
145 }
146}
147
148QSampleCache::SampleLoadResult QSampleCache::loadSample(QSpan<const char> data)
149{
150 using namespace QtPrivate;
151
152 drwav wavParser;
153 bool success = drwav_init_memory(&wavParser, data.data(), data.size(), nullptr);
154 if (!success)
155 return q23::unexpected(QSampleLoadError::FormatError);
156
157 auto cleanup = qScopeGuard([&] {
158 drwav_uninit(&wavParser);
159 });
160
161 // using float as internal format. one could argue to use int16 and save half the ram at the
162 // cost of potential run-time conversions
163 QAudioFormat audioFormat;
164 audioFormat.setChannelCount(wavParser.channels);
165 audioFormat.setSampleFormat(QAudioFormat::Float);
166 audioFormat.setSampleRate(wavParser.sampleRate);
167 audioFormat.setChannelConfig(
168 QAudioFormat::defaultChannelConfigForChannelCount(wavParser.channels));
169
170 std::optional<QByteArray> sampleData = QtMultimediaPrivate::readFramesInChunks(
171 wavParser.totalPCMFrameCount, sizeof(float) * wavParser.channels,
172 [&](uint64_t frameCount, char *out) -> uint64_t {
173 return drwav_read_pcm_frames_f32(&wavParser, frameCount, reinterpret_cast<float *>(out));
174 });
175 if (!sampleData)
176 return q23::unexpected(QSampleLoadError::FormatError);
177
178 return std::pair{
179 std::move(*sampleData),
180 audioFormat,
181 };
182}
183
184namespace {
185
186QByteArray convertToFloat32(const QByteArray &data, const QAudioFormat &fmt)
187{
188 if (fmt.sampleFormat() == QAudioFormat::Float)
189 return data;
190
191 int totalSamples = fmt.framesForBytes(data.size()) * fmt.channelCount();
192
193 QByteArray result{
194 totalSamples * int(sizeof(float)),
195 Qt::Initialization::Uninitialized,
196 };
197
198 using namespace QAudioHelperInternal;
199 convertSampleFormat(as_bytes(QSpan{ data }), toNativeSampleFormat(fmt.sampleFormat()),
200 as_writable_bytes(QSpan{ result }), NativeSampleFormat::float32_t);
201
202 return result;
203}
204
205QSampleCache::SampleLoadResult runDecoderLoop(QAudioDecoder &decoder, QEventLoop &loop)
206{
207 using SampleLoadResult = QSampleCache::SampleLoadResult;
208
209 QByteArray accumulated;
210 QAudioFormat fmt;
211 SampleLoadResult result = q23::unexpected(QSampleLoadError::DecoderError);
212
213 QObject::connect(&decoder, &QAudioDecoder::bufferReady, &loop, [&] {
214 QAudioBuffer buf = decoder.read();
215 if (!buf.isValid())
216 return;
217 if (!fmt.isValid())
218 fmt = buf.format();
219 accumulated.append(buf.constData<char>(), buf.byteCount());
220 });
221
222 QObject::connect(&decoder, &QAudioDecoder::finished, &loop, [&] {
223 QByteArray floatData = convertToFloat32(accumulated, fmt);
224 QAudioFormat floatFmt = fmt;
225 floatFmt.setSampleFormat(QAudioFormat::Float);
226 result = std::pair{ std::move(floatData), floatFmt };
227 loop.quit();
228 });
229
230 QObject::connect(&decoder, qOverload<QAudioDecoder::Error>(&QAudioDecoder::error),
231 &loop, [&](QAudioDecoder::Error) {
232 result = q23::unexpected(QSampleLoadError::DecoderError);
233 loop.quit();
234 });
235
236 decoder.start();
237 if (decoder.error() != QAudioDecoder::NoError)
238 return q23::unexpected(QSampleLoadError::DecoderError);
239
240 loop.exec(QEventLoop::ExcludeUserInputEvents);
241
242 return result;
243}
244
245} // unnamed namespace
246
247QSampleCache::SampleLoadResult
248QSampleCache::loadSampleViaDecoder(std::variant<QUrl, QByteArray> arg)
249{
250 // caveat: we run our own event loop, so this function should ideally not be run from the main thread.
251 using namespace QtMultimediaPrivate;
252
253 QAudioDecoder decoder;
254 if (!decoder.isSupported())
255 return q23::unexpected(QSampleLoadError::NotSupported);
256
257 QEventLoop loop;
258 // clang-format off
259 return std::visit(qOverloadedVisitor([&](QUrl url) -> SampleLoadResult {
260 decoder.setSource(url);
261 return runDecoderLoop(decoder, loop);
262 }, [&](QByteArray data) -> SampleLoadResult {
263 QBuffer buffer;
264 buffer.setData(data);
265 if (!buffer.open(QIODevice::ReadOnly))
266 return q23::unexpected(QSampleLoadError::IoError);
267 decoder.setSourceDevice(&buffer);
268
269 return runDecoderLoop(decoder, loop);
270 }), std::move(arg));
271 // clang-format on
272}
273
274#if QT_CONFIG(network)
275
276namespace {
277
278Q_CONSTINIT thread_local std::optional<QNetworkAccessManager> g_networkAccessManager;
279QNetworkAccessManager &threadLocalNetworkAccessManager()
280{
281 if (!g_networkAccessManager.has_value()) {
282 g_networkAccessManager.emplace();
283
284 if (QThread::isMainThread()) {
285 // poor man's Q_APPLICATION_STATIC
286 qAddPostRoutine([] {
287 g_networkAccessManager.reset();
288 });
289 }
290 }
291
292 return *g_networkAccessManager;
293}
294
295} // namespace
296
297#endif
298
299#if QT_CONFIG(thread)
300
301QSampleCache::SampleLoadResult
302QSampleCache::loadSample(const QUrl &url, std::optional<SampleSourceType> forceSourceType)
303{
304 using namespace Qt::Literals;
305
306 SampleSourceType realSourceType =
307 forceSourceType.value_or(url.scheme() == u"qrc"_s || url.scheme() == u"file"_s
308 ? SampleSourceType::File
309 : SampleSourceType::NetworkManager);
310
311 if (realSourceType == SampleSourceType::AudioDecoder)
312 return loadSampleViaDecoder(url);
313
314 if (url.scheme().isEmpty())
315 // exit early, to avoid QNetworkAccessManager trying to construct a default ssl
316 // configuration, which tends to cause timeouts on CI on macos.
317 // catch this case and exit early.
318 return q23::unexpected(QSampleLoadError::IoError);
319
320 bool errorOccurred = false;
321
322 std::unique_ptr<QIODevice> decoderInput;
323 if (realSourceType == SampleSourceType::File) {
324 QString locationString =
325 url.isLocalFile() ? url.toLocalFile() : u":" + url.toString(QUrl::RemoveScheme);
326
327 auto *file = new QFile(locationString);
328 bool opened = file->open(QFile::ReadOnly);
329 if (!opened)
330 errorOccurred = true;
331 decoderInput.reset(file);
332 } else {
333#if QT_CONFIG(network)
334 QNetworkReply *reply = threadLocalNetworkAccessManager().get(QNetworkRequest(url));
335
336 if (reply->error() != QNetworkReply::NoError)
337 errorOccurred = true;
338
339 connect(reply, &QNetworkReply::errorOccurred, reply,
340 [&]([[maybe_unused]] QNetworkReply::NetworkError errorCode) {
341 errorOccurred = true;
342 });
343
344 decoderInput.reset(reply);
345#else
346 return q23::unexpected(QSampleLoadError::IoError);
347#endif
348 }
349
350 if (!decoderInput->isOpen())
351 return q23::unexpected(QSampleLoadError::IoError);
352
353 QByteArray data = decoderInput->readAll();
354 if (data.isEmpty() || errorOccurred)
355 return q23::unexpected(QSampleLoadError::IoError);
356
357 SampleLoadResult result = loadSample(data);
358 if (!result && result.error() == QSampleLoadError::FormatError && !forceSourceType) {
359 qCDebug(qLcSampleCache) << "drwav failed, retrying via QAudioDecoder";
360 result = loadSampleViaDecoder(data);
361 if (!result) {
362 qCDebug(qLcSampleCache) << "device-based decode failed, retrying via URL";
363 result = loadSampleViaDecoder(url);
364 }
365 }
366 return result;
367}
368
369#endif
370
371QFuture<QSampleCache::SampleLoadResult>
372QSampleCache::loadSampleAsync(const QUrl &url, std::optional<SampleSourceType> forceSourceType)
373{
374 auto promise = std::make_shared<QPromise<QSampleCache::SampleLoadResult>>();
375 auto future = promise->future();
376
377 auto fulfilPromise = [&](auto &&result) mutable {
378 promise->start();
379 promise->addResult(result);
380 promise->finish();
381 };
382
383 using namespace Qt::Literals;
384
385 SampleSourceType realSourceType = (url.scheme() == u"qrc"_s || url.scheme() == u"file"_s)
386 ? SampleSourceType::File
387 : SampleSourceType::NetworkManager;
388 if (realSourceType == SampleSourceType::File) {
389 QString locationString = url.toString(QUrl::RemoveScheme);
390 if (url.scheme() == u"qrc"_s)
391 locationString = u":" + locationString;
392 QFile file{ locationString };
393 bool opened = file.open(QFile::ReadOnly);
394 if (!opened) {
395 fulfilPromise(q23::unexpected(QSampleLoadError::IoError));
396 return future;
397 }
398
399 QByteArray data = file.readAll();
400 if (data.isEmpty()) {
401 fulfilPromise(q23::unexpected(QSampleLoadError::IoError));
402 return future;
403 }
404
405 if (forceSourceType == SampleSourceType::AudioDecoder)
406 return loadSampleAsyncViaDecoder(data);
407
408 auto drwavResult = loadSample(data);
409 if (drwavResult || drwavResult.error() != QSampleLoadError::FormatError) {
410 fulfilPromise(drwavResult);
411 return future;
412 }
413 return loadSampleAsyncViaDecoder(data);
414 }
415
416#if QT_CONFIG(network)
417
418 QNetworkReply *reply = threadLocalNetworkAccessManager().get(QNetworkRequest(url));
419
420 if (reply->error() != QNetworkReply::NoError) {
421 fulfilPromise(q23::unexpected(QSampleLoadError::IoError));
422 reply->deleteLater();
423 return future;
424 }
425
426 connect(reply, &QNetworkReply::errorOccurred, reply,
427 [reply, promise]([[maybe_unused]] QNetworkReply::NetworkError errorCode) {
428 promise->start();
429 promise->addResult(q23::unexpected(QSampleLoadError::IoError));
430 promise->finish();
431 reply->deleteLater(); // we cannot delete immediately
432 });
433
434 connect(reply, &QNetworkReply::finished, reply,
435 [promise, reply, fulfilPromise = std::move(fulfilPromise), forceSourceType]() mutable {
436 QByteArray data = reply->readAll();
437 if (data.isEmpty()) {
438 promise->start();
439 promise->addResult(q23::unexpected(QSampleLoadError::IoError));
440 promise->finish();
441 } else if (forceSourceType == SampleSourceType::AudioDecoder) {
442 auto decoderFuture = loadSampleAsyncViaDecoder(data);
443 decoderFuture.then([promise](SampleLoadResult result) {
444 promise->start();
445 promise->addResult(std::move(result));
446 promise->finish();
447 });
448 } else {
449 auto drwavResult = loadSample(data);
450 if (drwavResult || drwavResult.error() != QSampleLoadError::FormatError) {
451 fulfilPromise(drwavResult);
452 } else {
453 auto decoderFuture = loadSampleAsyncViaDecoder(data);
454 decoderFuture.then([promise](SampleLoadResult result) {
455 promise->start();
456 promise->addResult(std::move(result));
457 promise->finish();
458 });
459 }
460 }
461 reply->deleteLater(); // we cannot delete immediately
462 });
463#else
464 fulfilPromise(q23::unexpected(QSampleLoadError::IoError));
465#endif
466 return future;
467}
468
469namespace {
470
471// keeps the only reference to the QAudioDecoder instances alive until they finish decoding.
472// We cannot keep the QAudioDecoder instances in the the decoder connections, as they may not
473// fire if the application is shutting down before the decoder terminates
474struct QStaticDecoderSingleton
475{
476 void addDecoder(std::shared_ptr<QAudioDecoder> decoder)
477 {
478 std::lock_guard guard(mutex);
479 decoders.insert(std::move(decoder));
480 }
481
482 void removeDecoder(std::shared_ptr<QAudioDecoder> decoder)
483 {
484 std::lock_guard guard(mutex);
485 decoders.erase(decoder);
486 }
487
488 std::set<std::shared_ptr<QAudioDecoder>> decoders;
489 QBasicMutex mutex;
490};
491
492Q_APPLICATION_STATIC(QStaticDecoderSingleton, decoders);
493
494} // namespace
495
496QFuture<QSampleCache::SampleLoadResult>
497QSampleCache::loadSampleAsyncViaDecoder(QByteArray data)
498{
499 // NB: we heap-allocate the QAudioDecoder and keep it alive until decoding finishes or an error
500 // occurs. however we cannot keep the QAudioDecoder alive in the lambda captures of the decoder
501 // signals, as they might not fire if the application is shutting down before the decoder
502 // terminates, causing a potential leak. Instead, we keep the decoders in a singleton set until
503 // they finish decoding, and capture weak pointers to them in the lambda captures of the
504 // finished/error signals.
505
506 auto promise = std::make_shared<QPromise<SampleLoadResult>>();
507 auto future = promise->future();
508
509 auto decoder = QtMultimediaPrivate::makeSharedDeleteLater<QAudioDecoder>();
510 if (!decoder->isSupported()) {
511 promise->start();
512 promise->addResult(q23::unexpected(QSampleLoadError::NotSupported));
513 promise->finish();
514 return future;
515 }
516
517 auto *buffer = new QBuffer(decoder.get());
518 buffer->setData(data);
519 buffer->open(QIODevice::ReadOnly);
520
521 auto accum = std::make_shared<QByteArray>();
522 auto fmt = std::make_shared<QAudioFormat>();
523
524 decoders->addDecoder(decoder);
525
526 QObject::connect(decoder.get(), &QAudioDecoder::bufferReady, decoder.get(),
527 [weakDecoder = std::weak_ptr<QAudioDecoder>(decoder), accum, fmt] {
528 auto decoder = weakDecoder.lock();
529 if (!decoder)
530 return;
531
532 QAudioBuffer buf = decoder->read();
533 if (!buf.isValid())
534 return;
535 if (!fmt->isValid())
536 *fmt = buf.format();
537 accum->append(buf.constData<char>(), buf.byteCount());
538 });
539
540 QObject::connect(decoder.get(), &QAudioDecoder::finished, decoder.get(),
541 [promise, accum, fmt, weakDecoder = std::weak_ptr<QAudioDecoder>(decoder)] {
542 auto decoder = weakDecoder.lock();
543 if (!decoder)
544 return;
545
546 QByteArray floatData = convertToFloat32(*accum, *fmt);
547 QAudioFormat floatFmt = *fmt;
548 floatFmt.setSampleFormat(QAudioFormat::Float);
549 promise->start();
550 promise->addResult(std::pair{ std::move(floatData), floatFmt });
551 promise->finish();
552
553 decoders->removeDecoder(decoder);
554 });
555
556 QObject::connect(
557 decoder.get(), qOverload<QAudioDecoder::Error>(&QAudioDecoder::error), decoder.get(),
558 [promise, weakDecoder = std::weak_ptr<QAudioDecoder>(decoder)](QAudioDecoder::Error) {
559 auto decoder = weakDecoder.lock();
560 if (!decoder)
561 return;
562
563 promise->start();
564 promise->addResult(q23::unexpected(QSampleLoadError::DecoderError));
565 promise->finish();
566 decoders->removeDecoder(decoder);
567 });
568
569 decoder->setSourceDevice(buffer);
570 decoder->start();
571 return future;
572}
573
574bool QSampleCache::isCached(const QUrl &url, std::optional<int> targetSampleRate) const
575{
576 std::lock_guard guard(m_mutex);
577 using namespace QtMultimediaPrivate;
578
579 const SampleKey key{ url, asSampleRate(targetSampleRate) };
580 return m_loadedSamples.find(key) != m_loadedSamples.end()
581 || m_pendingSamples.find(key) != m_pendingSamples.end();
582}
583
584QFuture<SharedSamplePtr>
585QSampleCache::requestSampleFuture(const QUrl &url, std::optional<int> targetSampleRate,
586 std::optional<SampleSourceType> forceSourceType)
587{
588 std::lock_guard guard(m_mutex);
589 using namespace QtMultimediaPrivate;
590
591 auto targetRate = asSampleRate(targetSampleRate);
592 const SampleKey key{ url, targetRate };
593
594 auto promise = std::make_shared<QPromise<SharedSamplePtr>>();
595 auto future = promise->future();
596
597 // found and ready
598 auto found = m_loadedSamples.find(key);
599 if (found != m_loadedSamples.end()) {
600 SharedSamplePtr foundSample = found->second.lock();
601 Q_ASSERT(foundSample);
602 Q_ASSERT(foundSample->state() == QSample::Ready);
603 promise->start();
604 promise->addResult(std::move(foundSample));
605 promise->finish();
606 return future;
607 }
608
609 // already in the process of being loaded
610 auto pending = m_pendingSamples.find(key);
611 if (pending != m_pendingSamples.end()) {
612 pending->second.second.append(promise);
613 return future;
614 }
615
616 // we need to start a new load process
617 SharedSamplePtr sample = std::make_shared<QSample>(url, this, targetRate);
618 m_pendingSamples.emplace(key, std::pair{ sample, QList<SharedSamplePromise>{ promise } });
619
620 QFuture<SampleLoadResult> futureResult = [&] {
621#if QT_CONFIG(thread)
622 if (threadPool()->maxThreadCount() > 0)
623 return QtConcurrent::run(threadPool(), [url, type = forceSourceType] {
624 return loadSample(url, type);
625 });
626#endif
627 return loadSampleAsync(url, forceSourceType);
628 }();
629
630 futureResult.then(this,
631 [this, key, targetRate,
632 sample = std::move(sample)](SampleLoadResult loadResult) mutable {
633 if (loadResult) {
634 QByteArray sampleData = loadResult->first;
635 QAudioFormat sampleFormat = loadResult->second;
636
637 if (targetRate && sampleFormat.sampleRate() != qToUnderlying(*targetRate)) {
638 const int rate = qToUnderlying(*targetRate);
639 const qsizetype totalFloats = sampleData.size() / qsizetype(sizeof(float));
640 QSpan<const float> inputSpan{
641 reinterpret_cast<const float *>(sampleData.constData()),
642 totalFloats
643 };
644 sampleData = QAudioHelperInternal::resampleAudioCatmullRom(
645 inputSpan, sampleFormat.channelCount(),
646 sampleFormat.sampleRate(), rate);
647 sampleFormat.setSampleRate(rate);
648 }
649
650 sample->setData(std::move(sampleData), sampleFormat);
651 } else {
652 sample->setError();
653 }
654
655 std::lock_guard guard(m_mutex);
656
657 auto pending = m_pendingSamples.find(key);
658 if (pending != m_pendingSamples.end()) {
659 for (auto &promise : pending->second.second) {
660 promise->start();
661 promise->addResult(loadResult ? sample : nullptr);
662 promise->finish();
663 }
664 }
665
666 if (loadResult)
667 m_loadedSamples.emplace(key, sample);
668
669 if (pending != m_pendingSamples.end())
670 m_pendingSamples.erase(pending);
671 sample = {};
672 });
673
674 return future;
675}
676
677void QSampleCache::removeUnreferencedSample(const QUrl &url,
678 std::optional<SampleRate> targetSampleRate)
679{
680 std::lock_guard guard(m_mutex);
681 m_loadedSamples.erase(SampleKey{ url, targetSampleRate });
682}
683
684QT_END_NAMESPACE
685
686#if !QT_CONFIG(thread)
687# undef thread_local
688#endif
QT_BEGIN_NAMESPACE Q_STATIC_LOGGING_CATEGORY(lcSynthesizedIterableAccess, "qt.iterable.synthesized", QtWarningMsg)