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 <QtCore/qapplicationstatic.h>
7#include <QtCore/qcoreapplication.h>
8#include <QtCore/qdebug.h>
9#include <QtCore/qeventloop.h>
10#include <QtCore/qfile.h>
11#include <QtCore/qfuturewatcher.h>
12#include <QtCore/qloggingcategory.h>
13#include <QtConcurrent/qtconcurrentrun.h>
14
15#if QT_CONFIG(network)
16# include <QtNetwork/qnetworkaccessmanager.h>
17# include <QtNetwork/qnetworkreply.h>
18# include <QtNetwork/qnetworkrequest.h>
19#endif
20
21#include "dr_wav.h"
22
23#include <utility>
24
25Q_STATIC_LOGGING_CATEGORY(qLcSampleCache, "qt.multimedia.samplecache")
26
27#if !QT_CONFIG(thread)
28# define thread_local
29#endif
30
31QT_BEGIN_NAMESPACE
32
33Q_APPLICATION_STATIC(QSampleCache, sampleCache)
34
35QSampleCache *QSampleCache::instance()
36{
37 return sampleCache();
38}
39
40#if QT_CONFIG(thread)
41QThreadPool *QSampleCache::threadPool()
42{
43#ifdef Q_OS_WASM
44 return QThreadPool::globalInstance();
45#else
46 return &m_threadPool;
47#endif
48}
49#endif
50
51QSampleCache::QSampleCache(QObject *parent) : QObject(parent)
52{
53#if QT_CONFIG(thread)
54 if (!thread()->isMainThread())
55 moveToThread(qApp->thread());
56
57# if !defined(Q_OS_WASM)
58 // we limit the number of loader threads to avoid thread explosion
59 static constexpr int loaderThreadLimit = 8;
60 m_threadPool.setObjectName("QSampleCachePool");
61 m_threadPool.setMaxThreadCount(loaderThreadLimit);
62 m_threadPool.setExpiryTimeout(15);
63 m_threadPool.setThreadPriority(QThread::LowPriority);
64 m_threadPool.setServiceLevel(QThread::QualityOfService::Eco);
65
66 qAddPostRoutine([] {
67 // HACK: we need to stop the thread pool before qApp is nulled, otherwise some threads might still try construct
68 // some Q_APPLICATION_STATIC instances, causing assertion failures inside QNetworkAccessManager
69 Q_ASSERT(qApp && "QApplication is still valid");
70
71 QSampleCache *instance = sampleCache();
72
73 instance->m_threadPool.clear();
74 instance->m_threadPool.waitForDone();
75 });
76
77# endif // Q_OS_WASM
78#endif // QT_CONFIG(thread)
79}
80
81QSampleCache::~QSampleCache()
82{
83#if QT_CONFIG(thread) && !defined(Q_OS_WASM)
84 m_threadPool.clear();
85 m_threadPool.waitForDone();
86#endif
87
88 for (auto &entry : m_loadedSamples) {
89 auto samplePtr = entry.second.lock();
90 if (samplePtr)
91 samplePtr->clearParent();
92 }
93
94 for (auto &entry : m_pendingSamples) {
95 auto samplePtr = entry.second.first;
96 if (samplePtr)
97 samplePtr->clearParent();
98 }
99}
100
101QSampleCache::SampleLoadResult QSampleCache::loadSample(QSpan<const char> data)
102{
103 using namespace QtPrivate;
104
105 drwav wavParser;
106 bool success = drwav_init_memory(&wavParser, data.data(), data.size(), nullptr);
107 if (!success)
108 return std::nullopt;
109
110 auto cleanup = qScopeGuard([&] {
111 drwav_uninit(&wavParser);
112 });
113
114 // using float as internal format. one could argue to use int16 and save half the ram at the
115 // cost of potential run-time conversions
116 QAudioFormat audioFormat;
117 audioFormat.setChannelCount(wavParser.channels);
118 audioFormat.setSampleFormat(QAudioFormat::Float);
119 audioFormat.setSampleRate(wavParser.sampleRate);
120 audioFormat.setChannelConfig(
121 QAudioFormat::defaultChannelConfigForChannelCount(wavParser.channels));
122
123 QByteArray sampleData;
124 sampleData.resizeForOverwrite(qsizetype(sizeof(float) * wavParser.channels
125 * wavParser.totalPCMFrameCount));
126 uint64_t framesRead = drwav_read_pcm_frames_f32(&wavParser, wavParser.totalPCMFrameCount,
127 reinterpret_cast<float *>(sampleData.data()));
128
129 if (framesRead != wavParser.totalPCMFrameCount)
130 return std::nullopt;
131
132 return std::pair{
133 std::move(sampleData),
134 audioFormat,
135 };
136}
137
138#if QT_CONFIG(network)
139
140namespace {
141
142Q_CONSTINIT thread_local std::optional<QNetworkAccessManager> g_networkAccessManager;
143QNetworkAccessManager &threadLocalNetworkAccessManager()
144{
145 if (!g_networkAccessManager.has_value()) {
146 g_networkAccessManager.emplace();
147
148 if (QThread::isMainThread()) {
149 // poor man's Q_APPLICATION_STATIC
150 qAddPostRoutine([] {
151 g_networkAccessManager.reset();
152 });
153 }
154 }
155
156 return *g_networkAccessManager;
157}
158
159} // namespace
160
161#endif
162
163#if QT_CONFIG(thread)
164
165QSampleCache::SampleLoadResult
166QSampleCache::loadSample(const QUrl &url, std::optional<SampleSourceType> forceSourceType)
167{
168 using namespace Qt::Literals;
169
170 bool errorOccurred = false;
171
172 if (url.scheme().isEmpty())
173 // exit early, to avoid QNetworkAccessManager trying to construct a default ssl
174 // configuration, which tends to cause timeouts on CI on macos.
175 // catch this case and exit early.
176 return std::nullopt;
177
178 std::unique_ptr<QIODevice> decoderInput;
179 SampleSourceType realSourceType =
180 forceSourceType.value_or(url.scheme() == u"qrc"_s || url.scheme() == u"file"_s
181 ? SampleSourceType::File
182 : SampleSourceType::NetworkManager);
183 if (realSourceType == SampleSourceType::File) {
184 QString locationString =
185 url.isLocalFile() ? url.toLocalFile() : u":" + url.toString(QUrl::RemoveScheme);
186
187 auto *file = new QFile(locationString);
188 bool opened = file->open(QFile::ReadOnly);
189 if (!opened)
190 errorOccurred = true;
191 decoderInput.reset(file);
192 } else {
193#if QT_CONFIG(network)
194 QNetworkReply *reply = threadLocalNetworkAccessManager().get(QNetworkRequest(url));
195
196 if (reply->error() != QNetworkReply::NoError)
197 errorOccurred = true;
198
199 connect(reply, &QNetworkReply::errorOccurred, reply,
200 [&]([[maybe_unused]] QNetworkReply::NetworkError errorCode) {
201 errorOccurred = true;
202 });
203
204 decoderInput.reset(reply);
205#else
206 return std::nullopt;
207#endif
208 }
209
210 if (!decoderInput->isOpen())
211 return std::nullopt;
212
213 QByteArray data = decoderInput->readAll();
214 if (data.isEmpty() || errorOccurred)
215 return std::nullopt;
216
217 return loadSample(data);
218}
219
220#endif
221
222QFuture<QSampleCache::SampleLoadResult> QSampleCache::loadSampleAsync(const QUrl &url)
223{
224 auto promise = std::make_shared<QPromise<QSampleCache::SampleLoadResult>>();
225 auto future = promise->future();
226
227 auto fulfilPromise = [&](auto &&result) mutable {
228 promise->start();
229 promise->addResult(result);
230 promise->finish();
231 };
232
233 using namespace Qt::Literals;
234
235 SampleSourceType realSourceType = (url.scheme() == u"qrc"_s || url.scheme() == u"file"_s)
236 ? SampleSourceType::File
237 : SampleSourceType::NetworkManager;
238 if (realSourceType == SampleSourceType::File) {
239 QString locationString = url.toString(QUrl::RemoveScheme);
240 if (url.scheme() == u"qrc"_s)
241 locationString = u":" + locationString;
242 QFile file{ locationString };
243 bool opened = file.open(QFile::ReadOnly);
244 if (!opened) {
245 fulfilPromise(std::nullopt);
246 return future;
247 }
248
249 QByteArray data = file.readAll();
250 if (data.isEmpty()) {
251 fulfilPromise(std::nullopt);
252 return future;
253 }
254
255 fulfilPromise(loadSample(data));
256 return future;
257 }
258
259#if QT_CONFIG(network)
260
261 QNetworkReply *reply = threadLocalNetworkAccessManager().get(QNetworkRequest(url));
262
263 if (reply->error() != QNetworkReply::NoError) {
264 fulfilPromise(std::nullopt);
265 reply->deleteLater();
266 return future;
267 }
268
269 connect(reply, &QNetworkReply::errorOccurred, reply,
270 [reply, promise]([[maybe_unused]] QNetworkReply::NetworkError errorCode) {
271 promise->start();
272 promise->addResult(std::nullopt);
273 promise->finish();
274 reply->deleteLater(); // we cannot delete immediately
275 });
276
277 connect(reply, &QNetworkReply::finished, reply, [promise, reply] {
278 promise->start();
279 QByteArray data = reply->readAll();
280 if (data.isEmpty())
281 promise->addResult(std::nullopt);
282 else
283 promise->addResult(loadSample(data));
284 promise->finish();
285 reply->deleteLater(); // we cannot delete immediately
286 });
287#else
288 fulfilPromise(std::nullopt);
289#endif
290 return future;
291}
292
293bool QSampleCache::isCached(const QUrl &url) const
294{
295 std::lock_guard guard(m_mutex);
296
297 return m_loadedSamples.find(url) != m_loadedSamples.end()
298 || m_pendingSamples.find(url) != m_pendingSamples.end();
299}
300
301QFuture<SharedSamplePtr> QSampleCache::requestSampleFuture(const QUrl &url)
302{
303 std::lock_guard guard(m_mutex);
304
305 auto promise = std::make_shared<QPromise<SharedSamplePtr>>();
306 auto future = promise->future();
307
308 // found and ready
309 auto found = m_loadedSamples.find(url);
310 if (found != m_loadedSamples.end()) {
311 SharedSamplePtr foundSample = found->second.lock();
312 Q_ASSERT(foundSample);
313 Q_ASSERT(foundSample->state() == QSample::Ready);
314 promise->start();
315 promise->addResult(std::move(foundSample));
316 promise->finish();
317 return future;
318 }
319
320 // already in the process of being loaded
321 auto pending = m_pendingSamples.find(url);
322 if (pending != m_pendingSamples.end()) {
323 pending->second.second.append(promise);
324 return future;
325 }
326
327 // we need to start a new load process
328 SharedSamplePtr sample = std::make_shared<QSample>(url, this);
329 m_pendingSamples.emplace(url, std::pair{ sample, QList<SharedSamplePromise>{ promise } });
330
331 QFuture<SampleLoadResult> futureResult = [&] {
332#if QT_CONFIG(thread)
333 if (threadPool()->maxThreadCount() > 0)
334 return QtConcurrent::run(threadPool(), [url, type = m_sampleSourceType] {
335 return loadSample(url, type);
336 });
337#endif
338 return loadSampleAsync(url);
339 }();
340
341 futureResult.then(this,
342 [this, url, sample = std::move(sample)](SampleLoadResult loadResult) mutable {
343 if (loadResult)
344 sample->setData(loadResult->first, loadResult->second);
345 else
346 sample->setError();
347
348 std::lock_guard guard(m_mutex);
349
350 auto pending = m_pendingSamples.find(url);
351 if (pending != m_pendingSamples.end()) {
352 for (auto &promise : pending->second.second) {
353 promise->start();
354 promise->addResult(loadResult ? sample : nullptr);
355 promise->finish();
356 }
357 }
358
359 if (loadResult)
360 m_loadedSamples.emplace(url, sample);
361
362 if (pending != m_pendingSamples.end())
363 m_pendingSamples.erase(pending);
364 sample = {};
365 });
366
367 return future;
368}
369
370QSample::~QSample()
371{
372 // Remove ourselves from our parent
373 if (m_parent)
374 m_parent->removeUnreferencedSample(m_url);
375
376 qCDebug(qLcSampleCache) << "~QSample" << this << ": deleted [" << m_url << "]" << QThread::currentThread();
377}
378
379void QSampleCache::removeUnreferencedSample(const QUrl &url)
380{
381 std::lock_guard guard(m_mutex);
382 m_loadedSamples.erase(url);
383}
384
385void QSample::setError()
386{
387 m_state = State::Error;
388}
389
390void QSample::setData(QByteArray data, QAudioFormat format)
391{
392 m_state = State::Ready;
393 m_soundData = std::move(data);
394 m_audioFormat = format;
395}
396
397QSample::State QSample::state() const
398{
399 return m_state;
400}
401
402QSample::QSample(QUrl url, QSampleCache *parent) : m_parent(parent), m_url(std::move(url)) { }
403
404void QSample::clearParent()
405{
406 m_parent = nullptr;
407}
408
409QT_END_NAMESPACE
410
411#if !QT_CONFIG(thread)
412# undef thread_local
413#endif
QT_BEGIN_NAMESPACE Q_STATIC_LOGGING_CATEGORY(lcSynthesizedIterableAccess, "qt.iterable.synthesized", QtWarningMsg)