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 // using float as internal format. one could argue to use int16 and save half the ram at the
111 // cost of potential run-time conversions
112 QAudioFormat audioFormat;
113 audioFormat.setChannelCount(wavParser.channels);
114 audioFormat.setSampleFormat(QAudioFormat::Float);
115 audioFormat.setSampleRate(wavParser.sampleRate);
116 audioFormat.setChannelConfig(
117 QAudioFormat::defaultChannelConfigForChannelCount(wavParser.channels));
118
119 QByteArray sampleData;
120 sampleData.resizeForOverwrite(sizeof(float) * wavParser.channels
121 * wavParser.totalPCMFrameCount);
122 uint64_t framesRead = drwav_read_pcm_frames_f32(&wavParser, wavParser.totalPCMFrameCount,
123 reinterpret_cast<float *>(sampleData.data()));
124
125 if (framesRead != wavParser.totalPCMFrameCount)
126 return std::nullopt;
127
128 return std::pair{
129 std::move(sampleData),
130 audioFormat,
131 };
132}
133
134#if QT_CONFIG(network)
135
136namespace {
137
138Q_CONSTINIT thread_local std::optional<QNetworkAccessManager> g_networkAccessManager;
139QNetworkAccessManager &threadLocalNetworkAccessManager()
140{
141 if (!g_networkAccessManager.has_value()) {
142 g_networkAccessManager.emplace();
143
144 if (QThread::isMainThread()) {
145 // poor man's Q_APPLICATION_STATIC
146 qAddPostRoutine([] {
147 g_networkAccessManager.reset();
148 });
149 }
150 }
151
152 return *g_networkAccessManager;
153}
154
155} // namespace
156
157#endif
158
159#if QT_CONFIG(thread)
160
161QSampleCache::SampleLoadResult
162QSampleCache::loadSample(const QUrl &url, std::optional<SampleSourceType> forceSourceType)
163{
164 using namespace Qt::Literals;
165
166 bool errorOccurred = false;
167
168 if (url.scheme().isEmpty())
169 // exit early, to avoid QNetworkAccessManager trying to construct a default ssl
170 // configuration, which tends to cause timeouts on CI on macos.
171 // catch this case and exit early.
172 return std::nullopt;
173
174 std::unique_ptr<QIODevice> decoderInput;
175 SampleSourceType realSourceType =
176 forceSourceType.value_or(url.scheme() == u"qrc"_s || url.scheme() == u"file"_s
177 ? SampleSourceType::File
178 : SampleSourceType::NetworkManager);
179 if (realSourceType == SampleSourceType::File) {
180 QString locationString =
181 url.isLocalFile() ? url.toLocalFile() : u":" + url.toString(QUrl::RemoveScheme);
182
183 auto *file = new QFile(locationString);
184 bool opened = file->open(QFile::ReadOnly);
185 if (!opened)
186 errorOccurred = true;
187 decoderInput.reset(file);
188 } else {
189#if QT_CONFIG(network)
190 QNetworkReply *reply = threadLocalNetworkAccessManager().get(QNetworkRequest(url));
191
192 if (reply->error() != QNetworkReply::NoError)
193 errorOccurred = true;
194
195 connect(reply, &QNetworkReply::errorOccurred, reply,
196 [&]([[maybe_unused]] QNetworkReply::NetworkError errorCode) {
197 errorOccurred = true;
198 });
199
200 decoderInput.reset(reply);
201#else
202 return std::nullopt;
203#endif
204 }
205
206 if (!decoderInput->isOpen())
207 return std::nullopt;
208
209 QByteArray data = decoderInput->readAll();
210 if (data.isEmpty() || errorOccurred)
211 return std::nullopt;
212
213 return loadSample(data);
214}
215
216#endif
217
218QFuture<QSampleCache::SampleLoadResult> QSampleCache::loadSampleAsync(const QUrl &url)
219{
220 auto promise = std::make_shared<QPromise<QSampleCache::SampleLoadResult>>();
221 auto future = promise->future();
222
223 auto fulfilPromise = [&](auto &&result) mutable {
224 promise->start();
225 promise->addResult(result);
226 promise->finish();
227 };
228
229 using namespace Qt::Literals;
230
231 SampleSourceType realSourceType = (url.scheme() == u"qrc"_s || url.scheme() == u"file"_s)
232 ? SampleSourceType::File
233 : SampleSourceType::NetworkManager;
234 if (realSourceType == SampleSourceType::File) {
235 QString locationString = url.toString(QUrl::RemoveScheme);
236 if (url.scheme() == u"qrc"_s)
237 locationString = u":" + locationString;
238 QFile file{ locationString };
239 bool opened = file.open(QFile::ReadOnly);
240 if (!opened) {
241 fulfilPromise(std::nullopt);
242 return future;
243 }
244
245 QByteArray data = file.readAll();
246 if (data.isEmpty()) {
247 fulfilPromise(std::nullopt);
248 return future;
249 }
250
251 fulfilPromise(loadSample(data));
252 return future;
253 }
254
255#if QT_CONFIG(network)
256
257 QNetworkReply *reply = threadLocalNetworkAccessManager().get(QNetworkRequest(url));
258
259 if (reply->error() != QNetworkReply::NoError) {
260 fulfilPromise(std::nullopt);
261 reply->deleteLater();
262 return future;
263 }
264
265 connect(reply, &QNetworkReply::errorOccurred, reply,
266 [reply, promise]([[maybe_unused]] QNetworkReply::NetworkError errorCode) {
267 promise->start();
268 promise->addResult(std::nullopt);
269 promise->finish();
270 reply->deleteLater(); // we cannot delete immediately
271 });
272
273 connect(reply, &QNetworkReply::finished, reply, [promise, reply] {
274 promise->start();
275 QByteArray data = reply->readAll();
276 if (data.isEmpty())
277 promise->addResult(std::nullopt);
278 else
279 promise->addResult(loadSample(data));
280 promise->finish();
281 reply->deleteLater(); // we cannot delete immediately
282 });
283#else
284 fulfilPromise(std::nullopt);
285#endif
286 return future;
287}
288
289bool QSampleCache::isCached(const QUrl &url) const
290{
291 std::lock_guard guard(m_mutex);
292
293 return m_loadedSamples.find(url) != m_loadedSamples.end()
294 || m_pendingSamples.find(url) != m_pendingSamples.end();
295}
296
297QFuture<SharedSamplePtr> QSampleCache::requestSampleFuture(const QUrl &url)
298{
299 std::lock_guard guard(m_mutex);
300
301 auto promise = std::make_shared<QPromise<SharedSamplePtr>>();
302 auto future = promise->future();
303
304 // found and ready
305 auto found = m_loadedSamples.find(url);
306 if (found != m_loadedSamples.end()) {
307 SharedSamplePtr foundSample = found->second.lock();
308 Q_ASSERT(foundSample);
309 Q_ASSERT(foundSample->state() == QSample::Ready);
310 promise->start();
311 promise->addResult(std::move(foundSample));
312 promise->finish();
313 return future;
314 }
315
316 // already in the process of being loaded
317 auto pending = m_pendingSamples.find(url);
318 if (pending != m_pendingSamples.end()) {
319 pending->second.second.append(promise);
320 return future;
321 }
322
323 // we need to start a new load process
324 SharedSamplePtr sample = std::make_shared<QSample>(url, this);
325 m_pendingSamples.emplace(url, std::pair{ sample, QList<SharedSamplePromise>{ promise } });
326
327 QFuture<SampleLoadResult> futureResult = [&] {
328#if QT_CONFIG(thread)
329 if (threadPool()->maxThreadCount() > 0)
330 return QtConcurrent::run(threadPool(), [url, type = m_sampleSourceType] {
331 return loadSample(url, type);
332 });
333#endif
334 return loadSampleAsync(url);
335 }();
336
337 futureResult.then(this,
338 [this, url, sample = std::move(sample)](SampleLoadResult loadResult) mutable {
339 if (loadResult)
340 sample->setData(loadResult->first, loadResult->second);
341 else
342 sample->setError();
343
344 std::lock_guard guard(m_mutex);
345
346 auto pending = m_pendingSamples.find(url);
347 if (pending != m_pendingSamples.end()) {
348 for (auto &promise : pending->second.second) {
349 promise->start();
350 promise->addResult(loadResult ? sample : nullptr);
351 promise->finish();
352 }
353 }
354
355 if (loadResult)
356 m_loadedSamples.emplace(url, sample);
357
358 if (pending != m_pendingSamples.end())
359 m_pendingSamples.erase(pending);
360 sample = {};
361 });
362
363 return future;
364}
365
366QSample::~QSample()
367{
368 // Remove ourselves from our parent
369 if (m_parent)
370 m_parent->removeUnreferencedSample(m_url);
371
372 qCDebug(qLcSampleCache) << "~QSample" << this << ": deleted [" << m_url << "]" << QThread::currentThread();
373}
374
375void QSampleCache::removeUnreferencedSample(const QUrl &url)
376{
377 std::lock_guard guard(m_mutex);
378 m_loadedSamples.erase(url);
379}
380
381void QSample::setError()
382{
383 m_state = State::Error;
384}
385
386void QSample::setData(QByteArray data, QAudioFormat format)
387{
388 m_state = State::Ready;
389 m_soundData = std::move(data);
390 m_audioFormat = format;
391}
392
393QSample::State QSample::state() const
394{
395 return m_state;
396}
397
398QSample::QSample(QUrl url, QSampleCache *parent) : m_parent(parent), m_url(std::move(url)) { }
399
400void QSample::clearParent()
401{
402 m_parent = nullptr;
403}
404
405QT_END_NAMESPACE
406
407#if !QT_CONFIG(thread)
408# undef thread_local
409#endif
QT_BEGIN_NAMESPACE Q_STATIC_LOGGING_CATEGORY(lcSynthesizedIterableAccess, "qt.iterable.synthesized", QtWarningMsg)