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