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