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