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>
16# include <QtNetwork/qnetworkaccessmanager.h>
17# include <QtNetwork/qnetworkreply.h>
18# include <QtNetwork/qnetworkrequest.h>
33Q_APPLICATION_STATIC(QSampleCache, sampleCache)
35QSampleCache *QSampleCache::instance()
41QThreadPool *QSampleCache::threadPool()
44 return QThreadPool::globalInstance();
51QSampleCache::QSampleCache(QObject *parent) : QObject(parent)
54 if (!thread()->isMainThread())
55 moveToThread(qApp->thread());
57# if !defined(Q_OS_WASM)
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);
69 Q_ASSERT(qApp &&
"QApplication is still valid");
71 QSampleCache *instance = sampleCache();
73 instance->m_threadPool.clear();
74 instance->m_threadPool.waitForDone();
81QSampleCache::~QSampleCache()
83#if QT_CONFIG(thread) && !defined(Q_OS_WASM)
85 m_threadPool.waitForDone();
88 for (
auto &entry : m_loadedSamples) {
89 auto samplePtr = entry.second.lock();
91 samplePtr->clearParent();
94 for (
auto &entry : m_pendingSamples) {
95 auto samplePtr = entry.second.first;
97 samplePtr->clearParent();
101QSampleCache::SampleLoadResult QSampleCache::loadSample(QSpan<
const char> data)
103 using namespace QtPrivate;
106 bool success = drwav_init_memory(&wavParser, data.data(), data.size(),
nullptr);
110 auto cleanup = qScopeGuard([&] {
111 drwav_uninit(&wavParser);
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));
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()));
129 if (framesRead != wavParser.totalPCMFrameCount)
133 std::move(sampleData),
138#if QT_CONFIG(network)
142Q_CONSTINIT
thread_local std::optional<QNetworkAccessManager> g_networkAccessManager;
143QNetworkAccessManager &threadLocalNetworkAccessManager()
145 if (!g_networkAccessManager.has_value()) {
146 g_networkAccessManager.emplace();
148 if (QThread::isMainThread()) {
151 g_networkAccessManager.reset();
156 return *g_networkAccessManager;
165QSampleCache::SampleLoadResult
166QSampleCache::loadSample(
const QUrl &url, std::optional<SampleSourceType> forceSourceType)
168 using namespace Qt::Literals;
170 bool errorOccurred =
false;
172 if (url.scheme().isEmpty())
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);
187 auto *file =
new QFile(locationString);
188 bool opened = file->open(QFile::ReadOnly);
190 errorOccurred =
true;
191 decoderInput.reset(file);
193#if QT_CONFIG(network)
194 QNetworkReply *reply = threadLocalNetworkAccessManager().get(QNetworkRequest(url));
196 if (reply->error() != QNetworkReply::NoError)
197 errorOccurred =
true;
199 connect(reply, &QNetworkReply::errorOccurred, reply,
200 [&]([[maybe_unused]] QNetworkReply::NetworkError errorCode) {
201 errorOccurred =
true;
204 decoderInput.reset(reply);
210 if (!decoderInput->isOpen())
213 QByteArray data = decoderInput->readAll();
214 if (data.isEmpty() || errorOccurred)
217 return loadSample(data);
222QFuture<QSampleCache::SampleLoadResult> QSampleCache::loadSampleAsync(
const QUrl &url)
224 auto promise = std::make_shared<QPromise<QSampleCache::SampleLoadResult>>();
225 auto future = promise->future();
227 auto fulfilPromise = [&](
auto &&result)
mutable {
229 promise->addResult(result);
233 using namespace Qt::Literals;
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);
245 fulfilPromise(std::nullopt);
249 QByteArray data = file.readAll();
250 if (data.isEmpty()) {
251 fulfilPromise(std::nullopt);
255 fulfilPromise(loadSample(data));
259#if QT_CONFIG(network)
261 QNetworkReply *reply = threadLocalNetworkAccessManager().get(QNetworkRequest(url));
263 if (reply->error() != QNetworkReply::NoError) {
264 fulfilPromise(std::nullopt);
265 reply->deleteLater();
269 connect(reply, &QNetworkReply::errorOccurred, reply,
270 [reply, promise]([[maybe_unused]] QNetworkReply::NetworkError errorCode) {
272 promise->addResult(std::nullopt);
274 reply->deleteLater();
277 connect(reply, &QNetworkReply::finished, reply, [promise, reply] {
279 QByteArray data = reply->readAll();
281 promise->addResult(std::nullopt);
283 promise->addResult(loadSample(data));
285 reply->deleteLater();
288 fulfilPromise(std::nullopt);
293bool QSampleCache::isCached(
const QUrl &url)
const
295 std::lock_guard guard(m_mutex);
297 return m_loadedSamples.find(url) != m_loadedSamples.end()
298 || m_pendingSamples.find(url) != m_pendingSamples.end();
301QFuture<SharedSamplePtr> QSampleCache::requestSampleFuture(
const QUrl &url)
303 std::lock_guard guard(m_mutex);
305 auto promise = std::make_shared<QPromise<SharedSamplePtr>>();
306 auto future = promise->future();
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);
315 promise->addResult(std::move(foundSample));
321 auto pending = m_pendingSamples.find(url);
322 if (pending != m_pendingSamples.end()) {
323 pending->second.second.append(promise);
328 SharedSamplePtr sample = std::make_shared<QSample>(url,
this);
329 m_pendingSamples.emplace(url, std::pair{ sample, QList<SharedSamplePromise>{ promise } });
331 QFuture<SampleLoadResult> futureResult = [&] {
333 if (threadPool()->maxThreadCount() > 0)
334 return QtConcurrent::run(threadPool(), [url, type = m_sampleSourceType] {
335 return loadSample(url, type);
338 return loadSampleAsync(url);
341 futureResult.then(
this,
342 [
this, url, sample = std::move(sample)](SampleLoadResult loadResult)
mutable {
344 sample->setData(loadResult->first, loadResult->second);
348 std::lock_guard guard(m_mutex);
350 auto pending = m_pendingSamples.find(url);
351 if (pending != m_pendingSamples.end()) {
352 for (
auto &promise : pending->second.second) {
354 promise->addResult(loadResult ? sample :
nullptr);
360 m_loadedSamples.emplace(url, sample);
362 if (pending != m_pendingSamples.end())
363 m_pendingSamples.erase(pending);
374 m_parent->removeUnreferencedSample(m_url);
376 qCDebug(qLcSampleCache) <<
"~QSample" <<
this <<
": deleted [" << m_url <<
"]" << QThread::currentThread();
379void QSampleCache::removeUnreferencedSample(
const QUrl &url)
381 std::lock_guard guard(m_mutex);
382 m_loadedSamples.erase(url);
385void QSample::setError()
387 m_state = State::Error;
390void QSample::setData(QByteArray data, QAudioFormat format)
392 m_state = State::Ready;
393 m_soundData = std::move(data);
394 m_audioFormat = format;
397QSample::State QSample::state()
const
402QSample::QSample(QUrl url, QSampleCache *parent) : m_parent(parent), m_url(std::move(url)) { }
404void QSample::clearParent()
411#if !QT_CONFIG(thread)
QT_BEGIN_NAMESPACE Q_STATIC_LOGGING_CATEGORY(lcSynthesizedIterableAccess, "qt.iterable.synthesized", QtWarningMsg)