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
4#include "qsamplecache_p.h"
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(pWav: &wavParser, data: data.constData(), dataSize: data.size(), pAllocationCallbacks: 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(channelCount: wavParser.channels));
87
88 QByteArray sampleData;
89 sampleData.resizeForOverwrite(size: sizeof(float) * wavParser.channels
90 * wavParser.totalPCMFrameCount);
91 uint64_t framesRead = drwav_read_pcm_frames_f32(pWav: &wavParser, framesToRead: wavParser.totalPCMFrameCount,
92 pBufferOut: 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(thread) && 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(u: 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(options: QUrl::RemoveScheme);
151
152 auto *file = new QFile(locationString);
153 bool opened = file->open(flags: QFile::ReadOnly);
154 if (!opened)
155 errorOccurred = true;
156 decoderInput.reset(p: file);
157 } else {
158#if QT_CONFIG(network)
159 QNetworkReply *reply = threadLocalNetworkAccessManager().get(request: QNetworkRequest(url));
160
161 if (reply->error() != QNetworkReply::NoError)
162 errorOccurred = true;
163
164 connect(sender: reply, signal: &QNetworkReply::errorOccurred, context: reply,
165 slot: [&]([[maybe_unused]] QNetworkReply::NetworkError errorCode) {
166 errorOccurred = true;
167 });
168
169 decoderInput.reset(p: 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(data: 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(options: QUrl::RemoveScheme);
205 if (url.scheme() == u"qrc"_s)
206 locationString = u":" + locationString;
207 QFile file{ locationString };
208 bool opened = file.open(flags: 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(data: std::move(data)));
221 return future;
222 }
223#if QT_CONFIG(network)
224 QNetworkReply *reply = threadLocalNetworkAccessManager().get(request: QNetworkRequest(url));
225
226 if (reply->error() != QNetworkReply::NoError) {
227 fulfilPromise(std::nullopt);
228 delete reply;
229 return future;
230 }
231
232 connect(sender: reply, signal: &QNetworkReply::errorOccurred, context: reply,
233 slot: [reply, promise]([[maybe_unused]] QNetworkReply::NetworkError errorCode) {
234 promise->start();
235 promise->addResult(result: std::nullopt);
236 promise->finish();
237 reply->deleteLater(); // we cannot delete immediately
238 });
239
240 connect(sender: reply, signal: &QNetworkReply::finished, context: reply, slot: [promise, reply] {
241 promise->start();
242 QByteArray data = reply->readAll();
243 if (data.isEmpty())
244 promise->addResult(result: std::nullopt);
245 else
246 promise->addResult(result: loadSample(data: std::move(data)));
247 promise->finish();
248 reply->deleteLater(); // we cannot delete immediately
249 });
250#else
251 fulfilPromise(std::nullopt);
252#endif
253 return future;
254}
255
256bool QSampleCache::isCached(const QUrl &url) const
257{
258 std::lock_guard guard(m_mutex);
259
260 return m_loadedSamples.find(x: url) != m_loadedSamples.end()
261 || m_pendingSamples.find(x: url) != m_pendingSamples.end();
262}
263
264QFuture<SharedSamplePtr> QSampleCache::requestSampleFuture(const QUrl &url)
265{
266 std::lock_guard guard(m_mutex);
267
268 auto promise = std::make_shared<QPromise<SharedSamplePtr>>();
269 auto future = promise->future();
270
271 // found and ready
272 auto found = m_loadedSamples.find(x: url);
273 if (found != m_loadedSamples.end()) {
274 SharedSamplePtr foundSample = found->second.lock();
275 Q_ASSERT(foundSample);
276 Q_ASSERT(foundSample->state() == QSample::Ready);
277 promise->start();
278 promise->addResult(result: std::move(foundSample));
279 promise->finish();
280 return future;
281 }
282
283 // already in the process of being loaded
284 auto pending = m_pendingSamples.find(x: url);
285 if (pending != m_pendingSamples.end()) {
286 pending->second.second.append(t: promise);
287 return future;
288 }
289
290 // we need to start a new load process
291 SharedSamplePtr sample = std::make_shared<QSample>(args: url, args: this);
292 m_pendingSamples.emplace(args: url, args: std::pair{ sample, QList<SharedSamplePromise>{ promise } });
293
294#if QT_CONFIG(thread)
295 QFuture<SampleLoadResult> futureResult =
296 QtConcurrent::run(pool: &m_threadPool, f: [url, type = m_sampleSourceType] {
297 return loadSample(url, forceSourceType: type);
298 });
299#else
300 QFuture<SampleLoadResult> futureResult = loadSampleAsync(url);
301#endif
302
303 futureResult.then(context: this,
304 function: [this, url, sample = std::move(sample)](SampleLoadResult loadResult) mutable {
305 if (loadResult)
306 sample->setData(loadResult->first, loadResult->second);
307 else
308 sample->setError();
309
310 std::lock_guard guard(m_mutex);
311
312 auto pending = m_pendingSamples.find(x: url);
313 if (pending != m_pendingSamples.end()) {
314 for (auto &promise : pending->second.second) {
315 promise->start();
316 promise->addResult(result: loadResult ? sample : nullptr);
317 promise->finish();
318 }
319 }
320
321 if (loadResult)
322 m_loadedSamples.emplace(args: url, args&: sample);
323
324 if (pending != m_pendingSamples.end())
325 m_pendingSamples.erase(position: pending);
326 sample = {};
327 });
328
329 return future;
330}
331
332QSample::~QSample()
333{
334 // Remove ourselves from our parent
335 if (m_parent)
336 m_parent->removeUnreferencedSample(url: m_url);
337
338 qCDebug(qLcSampleCache) << "~QSample" << this << ": deleted [" << m_url << "]" << QThread::currentThread();
339}
340
341void QSampleCache::removeUnreferencedSample(const QUrl &url)
342{
343 std::lock_guard guard(m_mutex);
344 m_loadedSamples.erase(x: url);
345}
346
347void QSample::setError()
348{
349 m_state = State::Error;
350}
351
352void QSample::setData(QByteArray data, QAudioFormat format)
353{
354 m_state = State::Ready;
355 m_soundData = std::move(data);
356 m_audioFormat = format;
357}
358
359QSample::State QSample::state() const
360{
361 return m_state;
362}
363
364QSample::QSample(QUrl url, QSampleCache *parent) : m_parent(parent), m_url(std::move(url)) { }
365
366void QSample::clearParent()
367{
368 m_parent = nullptr;
369}
370
371QT_END_NAMESPACE
372
373#if !QT_CONFIG(thread)
374# undef thread_local
375#endif
376

source code of qtmultimedia/src/multimedia/audio/qsamplecache_p.cpp