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#include "qwavedecoder.h"
6#include "qfile.h"
7#include "qmultimediautils_p.h"
8
9#if QT_CONFIG(network)
10# include <QtNetwork/QNetworkAccessManager>
11# include <QtNetwork/QNetworkReply>
12# include <QtNetwork/QNetworkRequest>
13#endif
14
15#include <QtCore/QDebug>
16#include <QtCore/qloggingcategory.h>
17
18static Q_LOGGING_CATEGORY(qLcSampleCache, "qt.multimedia.samplecache")
19
20#include <mutex>
21
22QT_BEGIN_NAMESPACE
23
24
25/*!
26 \class QSampleCache
27 \internal
28
29 When you want to get a sound sample data, you need to request the QSample reference from QSampleCache.
30
31
32 \code
33 QSample *m_sample; // class member.
34
35 private Q_SLOTS:
36 void decoderError();
37 void sampleReady();
38 \endcode
39
40 \code
41 Q_GLOBAL_STATIC(QSampleCache, sampleCache) //declare a singleton manager
42 \endcode
43
44 \code
45 m_sample = sampleCache()->requestSample(url);
46 switch(m_sample->state()) {
47 case QSample::Ready:
48 sampleReady();
49 break;
50 case QSample::Error:
51 decoderError();
52 break;
53 default:
54 connect(m_sample, SIGNAL(error()), this, SLOT(decoderError()));
55 connect(m_sample, SIGNAL(ready()), this, SLOT(sampleReady()));
56 break;
57 }
58 \endcode
59
60 When you no longer need the sound sample data, you need to release it:
61
62 \code
63 if (m_sample) {
64 m_sample->release();
65 m_sample = 0;
66 }
67 \endcode
68*/
69
70QSampleCache::QSampleCache(QObject *parent)
71 : QObject(parent), m_capacity(0), m_usage(0), m_loadingRefCount(0)
72{
73 m_loadingThread.setObjectName(QLatin1String("QSampleCache::LoadingThread"));
74}
75
76std::unique_ptr<QIODevice> QSampleCache::createStreamForSample(QSample &sample)
77{
78#if QT_CONFIG(network)
79 if (m_sampleSourceType == SampleSourceType::NetworkManager) {
80 if (sample.m_url.scheme().isEmpty()) {
81 // exit early, to avoid QNetworkAccessManager trying to construct a default ssl
82 // configuration, which tends to cause timeouts on CI on macos.
83 // catch this case and exit early.
84 return nullptr;
85 }
86
87 if (!m_networkAccessManager)
88 m_networkAccessManager = std::make_unique<QNetworkAccessManager>();
89 std::unique_ptr<QNetworkReply> reply(
90 m_networkAccessManager->get(request: QNetworkRequest(sample.m_url)));
91 if (reply)
92 connect(sender: reply.get(), signal: &QNetworkReply::errorOccurred, context: &sample,
93 slot: &QSample::handleLoadingError);
94 return reply;
95 }
96#endif
97
98 // The QFile source is needed of the QtLite build, which excludes networking
99 if (m_sampleSourceType == SampleSourceType::File) {
100 auto file = std::make_unique<QFile>(args: sample.m_url.toLocalFile());
101 if (file->open(flags: QFile::ReadOnly))
102 return file;
103 }
104
105 return nullptr;
106}
107
108QSampleCache::~QSampleCache()
109{
110 const std::lock_guard<QRecursiveMutex> locker(m_mutex);
111
112 m_loadingThread.quit();
113 m_loadingThread.wait();
114
115 // Killing the loading thread means that no samples can be
116 // deleted using deleteLater. And some samples that had deleteLater
117 // already called won't have been processed (m_staleSamples)
118 for (auto it = m_samples.cbegin(), end = m_samples.cend(); it != end; ++it)
119 delete it.value();
120
121 const auto copyStaleSamples = m_staleSamples; //deleting a sample does affect the m_staleSamples list, but we create a copy
122 for (QSample* sample : copyStaleSamples)
123 delete sample;
124
125#if QT_CONFIG(network)
126 // Should we delete it under the mutex?
127 m_networkAccessManager.reset();
128#endif
129}
130
131void QSampleCache::loadingRelease()
132{
133 QMutexLocker locker(&m_loadingMutex);
134 m_loadingRefCount--;
135 if (m_loadingRefCount == 0) {
136 if (m_loadingThread.isRunning()) {
137#if QT_CONFIG(network)
138 if (m_networkAccessManager)
139 m_networkAccessManager.release()->deleteLater();
140#endif
141 m_loadingThread.exit();
142 }
143 }
144}
145
146bool QSampleCache::isLoading() const
147{
148 return m_loadingThread.isRunning();
149}
150
151bool QSampleCache::isCached(const QUrl &url) const
152{
153 const std::lock_guard<QRecursiveMutex> locker(m_mutex);
154 return m_samples.contains(key: url);
155}
156
157QSample* QSampleCache::requestSample(const QUrl& url)
158{
159 //lock and add first to make sure live loadingThread will not be killed during this function call
160 m_loadingMutex.lock();
161 const bool needsThreadStart = m_loadingRefCount == 0;
162 m_loadingRefCount++;
163 m_loadingMutex.unlock();
164
165 qCDebug(qLcSampleCache) << "QSampleCache: request sample [" << url << "]";
166 std::unique_lock<QRecursiveMutex> locker(m_mutex);
167 QMap<QUrl, QSample*>::iterator it = m_samples.find(key: url);
168 QSample* sample;
169 if (it == m_samples.end()) {
170 if (needsThreadStart) {
171 // Previous thread might be finishing, need to wait for it. If not, this is a no-op.
172 m_loadingThread.wait();
173 m_loadingThread.start();
174 }
175 sample = new QSample(url, this);
176 m_samples.insert(key: url, value: sample);
177#if QT_CONFIG(thread)
178 sample->moveToThread(thread: &m_loadingThread);
179#endif
180 } else {
181 sample = *it;
182 if (sample->state() == QSample::Error && needsThreadStart) {
183 m_loadingThread.wait();
184 m_loadingThread.start();
185 }
186 }
187
188 sample->addRef();
189 locker.unlock();
190
191 sample->loadIfNecessary();
192 return sample;
193}
194
195void QSampleCache::setCapacity(qint64 capacity)
196{
197 const std::lock_guard<QRecursiveMutex> locker(m_mutex);
198 if (m_capacity == capacity)
199 return;
200 qCDebug(qLcSampleCache) << "QSampleCache: capacity changes from " << m_capacity << "to " << capacity;
201 if (m_capacity > 0 && capacity <= 0) { //memory management strategy changed
202 for (QMap<QUrl, QSample*>::iterator it = m_samples.begin(); it != m_samples.end();) {
203 QSample* sample = *it;
204 if (sample->m_ref == 0) {
205 unloadSample(sample);
206 it = m_samples.erase(it);
207 } else {
208 ++it;
209 }
210 }
211 }
212
213 m_capacity = capacity;
214 refresh(usageChange: 0);
215}
216
217// Called locked
218void QSampleCache::unloadSample(QSample *sample)
219{
220 m_usage -= sample->m_soundData.size();
221 m_staleSamples.insert(value: sample);
222 sample->deleteLater();
223}
224
225// Called in both threads
226void QSampleCache::refresh(qint64 usageChange)
227{
228 const std::lock_guard<QRecursiveMutex> locker(m_mutex);
229 m_usage += usageChange;
230 if (m_capacity <= 0 || m_usage <= m_capacity)
231 return;
232
233 qint64 recoveredSize = 0;
234
235 //free unused samples to keep usage under capacity limit.
236 for (QMap<QUrl, QSample*>::iterator it = m_samples.begin(); it != m_samples.end();) {
237 QSample* sample = *it;
238 if (sample->m_ref > 0) {
239 ++it;
240 continue;
241 }
242 recoveredSize += sample->m_soundData.size();
243 unloadSample(sample);
244 it = m_samples.erase(it);
245 if (m_usage <= m_capacity)
246 return;
247 }
248
249 qCDebug(qLcSampleCache) << "QSampleCache: refresh(" << usageChange
250 << ") recovered size =" << recoveredSize
251 << "new usage =" << m_usage;
252
253 if (m_usage > m_capacity)
254 qWarning() << "QSampleCache: usage" << m_usage << "out of limit" << m_capacity;
255}
256
257// Called in both threads
258void QSampleCache::removeUnreferencedSample(QSample *sample)
259{
260 const std::lock_guard<QRecursiveMutex> locker(m_mutex);
261 m_staleSamples.remove(value: sample);
262}
263
264// Called in loader thread (since this lives in that thread)
265// Also called from application thread after loader thread dies.
266QSample::~QSample()
267{
268 // Remove ourselves from our parent
269 m_parent->removeUnreferencedSample(sample: this);
270
271 QMutexLocker locker(&m_mutex);
272 qCDebug(qLcSampleCache) << "~QSample" << this << ": deleted [" << m_url << "]" << QThread::currentThread();
273 cleanup();
274}
275
276// Called in application thread
277void QSample::loadIfNecessary()
278{
279 QMutexLocker locker(&m_mutex);
280 if (m_state == QSample::Error || m_state == QSample::Creating) {
281 m_state = QSample::Loading;
282 QMetaObject::invokeMethod(object: this, function: &QSample::load, type: Qt::QueuedConnection);
283 } else {
284 m_parent->loadingRelease();
285 }
286}
287
288// Called in application thread
289bool QSampleCache::notifyUnreferencedSample(QSample* sample)
290{
291 if (m_loadingThread.isRunning())
292 m_loadingThread.wait();
293
294 const std::lock_guard<QRecursiveMutex> locker(m_mutex);
295
296 if (m_capacity > 0)
297 return false;
298 m_samples.remove(key: sample->m_url);
299 unloadSample(sample);
300 return true;
301}
302
303// Called in application thread
304void QSample::release()
305{
306 QMutexLocker locker(&m_mutex);
307 qCDebug(qLcSampleCache) << "Sample:: release" << this << QThread::currentThread() << m_ref;
308 if (--m_ref == 0) {
309 locker.unlock();
310 m_parent->notifyUnreferencedSample(sample: this);
311 }
312}
313
314// Called in dtor and when stream is loaded
315// must be called locked.
316void QSample::cleanup()
317{
318 qCDebug(qLcSampleCache) << "QSample: cleanup";
319 if (m_waveDecoder) {
320 m_waveDecoder->disconnect(receiver: this);
321 m_waveDecoder.release()->deleteLater();
322 }
323
324 if (m_stream) {
325 m_stream->disconnect(receiver: this);
326 m_stream.release()->deleteLater();
327 }
328}
329
330// Called in application thread
331void QSample::addRef()
332{
333 m_ref++;
334}
335
336// Called in loading thread
337void QSample::readSample()
338{
339#if QT_CONFIG(thread)
340 Q_ASSERT(QThread::currentThread()->objectName() == QLatin1String("QSampleCache::LoadingThread"));
341#endif
342 QMutexLocker m(&m_mutex);
343 qint64 read = m_waveDecoder->read(data: m_soundData.data() + m_sampleReadLength,
344 maxlen: qMin(a: m_waveDecoder->bytesAvailable(),
345 b: qint64(m_waveDecoder->size() - m_sampleReadLength)));
346 qCDebug(qLcSampleCache) << "QSample: readSample" << read;
347 if (read > 0)
348 m_sampleReadLength += read;
349 if (m_sampleReadLength < m_waveDecoder->size())
350 return;
351 Q_ASSERT(m_sampleReadLength == qint64(m_soundData.size()));
352 onReady();
353}
354
355// Called in loading thread
356void QSample::decoderReady()
357{
358#if QT_CONFIG(thread)
359 Q_ASSERT(QThread::currentThread()->objectName() == QLatin1String("QSampleCache::LoadingThread"));
360#endif
361 QMutexLocker m(&m_mutex);
362 qCDebug(qLcSampleCache) << "QSample: decoder ready";
363 m_parent->refresh(usageChange: m_waveDecoder->size());
364
365 m_soundData.resize(size: m_waveDecoder->size());
366 m_sampleReadLength = 0;
367 qint64 read = m_waveDecoder->read(data: m_soundData.data(), maxlen: m_waveDecoder->size());
368 qCDebug(qLcSampleCache) << " bytes read" << read;
369 if (read > 0)
370 m_sampleReadLength += read;
371 if (m_sampleReadLength >= m_waveDecoder->size())
372 onReady();
373}
374
375// Called in all threads
376QSample::State QSample::state() const
377{
378 QMutexLocker m(&m_mutex);
379 return m_state;
380}
381
382// Called in loading thread
383// Essentially a second ctor, doesn't need locks (?)
384void QSample::load()
385{
386#if QT_CONFIG(thread)
387 Q_ASSERT(QThread::currentThread()->objectName() == QLatin1String("QSampleCache::LoadingThread"));
388#endif
389 qCDebug(qLcSampleCache) << "QSample: load [" << m_url << "]";
390
391 m_stream = m_parent->createStreamForSample(sample&: *this);
392
393 if (!m_stream) {
394 handleLoadingError();
395 return;
396 }
397
398 m_waveDecoder = std::make_unique<QWaveDecoder>(args: m_stream.get());
399 connect(sender: m_waveDecoder.get(), signal: &QWaveDecoder::formatKnown, context: this, slot: &QSample::decoderReady);
400 connect(sender: m_waveDecoder.get(), signal: &QWaveDecoder::parsingError, context: this, slot: &QSample::decoderError);
401 connect(sender: m_waveDecoder.get(), signal: &QIODevice::readyRead, context: this, slot: &QSample::readSample);
402
403 m_waveDecoder->open(mode: QIODevice::ReadOnly);
404}
405
406void QSample::handleLoadingError(int errorCode)
407{
408#if QT_CONFIG(thread)
409 Q_ASSERT(QThread::currentThread()->objectName() == QLatin1String("QSampleCache::LoadingThread"));
410#endif
411 QMutexLocker m(&m_mutex);
412 qCDebug(qLcSampleCache) << "QSample: loading error:" << errorCode
413 << "source type: " << qToUnderlying(e: m_parent->sampleSourceType());
414 cleanup();
415 m_state = QSample::Error;
416 m_parent->loadingRelease();
417 emit error(self: this);
418}
419
420// Called in loading thread
421void QSample::decoderError()
422{
423#if QT_CONFIG(thread)
424 Q_ASSERT(QThread::currentThread()->objectName() == QLatin1String("QSampleCache::LoadingThread"));
425#endif
426 QMutexLocker m(&m_mutex);
427 qCDebug(qLcSampleCache) << "QSample: decoder error";
428 cleanup();
429 m_state = QSample::Error;
430 m_parent->loadingRelease();
431 emit error(self: this);
432}
433
434// Called in loading thread from decoder when sample is done. Locked already.
435void QSample::onReady()
436{
437#if QT_CONFIG(thread)
438 Q_ASSERT(QThread::currentThread()->objectName() == QLatin1String("QSampleCache::LoadingThread"));
439#endif
440 m_audioFormat = m_waveDecoder->audioFormat();
441 qCDebug(qLcSampleCache) << "QSample: load ready format:" << m_audioFormat;
442 cleanup();
443 m_state = QSample::Ready;
444 m_parent->loadingRelease();
445 emit ready(self: this);
446}
447
448// Called in application thread, then moved to loader thread
449QSample::QSample(const QUrl &url, QSampleCache *parent)
450 : m_parent(parent),
451 m_waveDecoder(nullptr),
452 m_url(url),
453 m_sampleReadLength(0),
454 m_state(Creating),
455 m_ref(0)
456{
457}
458
459QT_END_NAMESPACE
460
461#include "moc_qsamplecache_p.cpp"
462

Provided by KDAB

Privacy Policy
Learn Advanced QML with KDAB
Find out more

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