1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2016 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:LGPL$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU Lesser General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU Lesser |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
21 | ** packaging of this file. Please review the following information to |
22 | ** ensure the GNU Lesser General Public License version 3 requirements |
23 | ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
24 | ** |
25 | ** GNU General Public License Usage |
26 | ** Alternatively, this file may be used under the terms of the GNU |
27 | ** General Public License version 2.0 or (at your option) the GNU General |
28 | ** Public license version 3 or any later version approved by the KDE Free |
29 | ** Qt Foundation. The licenses are as published by the Free Software |
30 | ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
31 | ** included in the packaging of this file. Please review the following |
32 | ** information to ensure the GNU General Public License requirements will |
33 | ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
34 | ** https://www.gnu.org/licenses/gpl-3.0.html. |
35 | ** |
36 | ** $QT_END_LICENSE$ |
37 | ** |
38 | ****************************************************************************/ |
39 | |
40 | #include "qsamplecache_p.h" |
41 | #include "qwavedecoder_p.h" |
42 | |
43 | #include <QtNetwork/QNetworkAccessManager> |
44 | #include <QtNetwork/QNetworkReply> |
45 | #include <QtNetwork/QNetworkRequest> |
46 | |
47 | #include <QtCore/QDebug> |
48 | //#define QT_SAMPLECACHE_DEBUG |
49 | |
50 | #include <mutex> |
51 | |
52 | QT_BEGIN_NAMESPACE |
53 | |
54 | |
55 | /*! |
56 | \class QSampleCache |
57 | \internal |
58 | |
59 | When you want to get a sound sample data, you need to request the QSample reference from QSampleCache. |
60 | |
61 | |
62 | \code |
63 | QSample *m_sample; // class member. |
64 | |
65 | private Q_SLOTS: |
66 | void decoderError(); |
67 | void sampleReady(); |
68 | \endcode |
69 | |
70 | \code |
71 | Q_GLOBAL_STATIC(QSampleCache, sampleCache) //declare a singleton manager |
72 | \endcode |
73 | |
74 | \code |
75 | m_sample = sampleCache()->requestSample(url); |
76 | switch(m_sample->state()) { |
77 | case QSample::Ready: |
78 | sampleReady(); |
79 | break; |
80 | case QSample::Error: |
81 | decoderError(); |
82 | break; |
83 | default: |
84 | connect(m_sample, SIGNAL(error()), this, SLOT(decoderError())); |
85 | connect(m_sample, SIGNAL(ready()), this, SLOT(sampleReady())); |
86 | break; |
87 | } |
88 | \endcode |
89 | |
90 | When you no longer need the sound sample data, you need to release it: |
91 | |
92 | \code |
93 | if (m_sample) { |
94 | m_sample->release(); |
95 | m_sample = 0; |
96 | } |
97 | \endcode |
98 | */ |
99 | |
100 | QSampleCache::QSampleCache(QObject *parent) |
101 | : QObject(parent) |
102 | , m_networkAccessManager(nullptr) |
103 | , m_capacity(0) |
104 | , m_usage(0) |
105 | , m_loadingRefCount(0) |
106 | { |
107 | m_loadingThread.setObjectName(QLatin1String("QSampleCache::LoadingThread" )); |
108 | connect(sender: &m_loadingThread, SIGNAL(finished()), receiver: this, SIGNAL(isLoadingChanged())); |
109 | connect(sender: &m_loadingThread, SIGNAL(started()), receiver: this, SIGNAL(isLoadingChanged())); |
110 | } |
111 | |
112 | QNetworkAccessManager& QSampleCache::networkAccessManager() |
113 | { |
114 | if (!m_networkAccessManager) |
115 | m_networkAccessManager = new QNetworkAccessManager(); |
116 | return *m_networkAccessManager; |
117 | } |
118 | |
119 | QSampleCache::~QSampleCache() |
120 | { |
121 | const std::lock_guard<QRecursiveMutex> locker(m_mutex); |
122 | |
123 | m_loadingThread.quit(); |
124 | m_loadingThread.wait(); |
125 | |
126 | // Killing the loading thread means that no samples can be |
127 | // deleted using deleteLater. And some samples that had deleteLater |
128 | // already called won't have been processed (m_staleSamples) |
129 | for (auto it = m_samples.cbegin(), end = m_samples.cend(); it != end; ++it) |
130 | delete it.value(); |
131 | |
132 | const auto copyStaleSamples = m_staleSamples; //deleting a sample does affect the m_staleSamples list, but we create a copy |
133 | for (QSample* sample : copyStaleSamples) |
134 | delete sample; |
135 | |
136 | delete m_networkAccessManager; |
137 | } |
138 | |
139 | void QSampleCache::loadingRelease() |
140 | { |
141 | QMutexLocker locker(&m_loadingMutex); |
142 | m_loadingRefCount--; |
143 | if (m_loadingRefCount == 0) { |
144 | if (m_loadingThread.isRunning()) { |
145 | if (m_networkAccessManager) { |
146 | m_networkAccessManager->deleteLater(); |
147 | m_networkAccessManager = nullptr; |
148 | } |
149 | m_loadingThread.exit(); |
150 | } |
151 | } |
152 | } |
153 | |
154 | bool QSampleCache::isLoading() const |
155 | { |
156 | return m_loadingThread.isRunning(); |
157 | } |
158 | |
159 | bool QSampleCache::isCached(const QUrl &url) const |
160 | { |
161 | const std::lock_guard<QRecursiveMutex> locker(m_mutex); |
162 | return m_samples.contains(akey: url); |
163 | } |
164 | |
165 | QSample* QSampleCache::requestSample(const QUrl& url) |
166 | { |
167 | //lock and add first to make sure live loadingThread will not be killed during this function call |
168 | m_loadingMutex.lock(); |
169 | m_loadingRefCount++; |
170 | m_loadingMutex.unlock(); |
171 | |
172 | if (!m_loadingThread.isRunning()) |
173 | m_loadingThread.start(); |
174 | |
175 | #ifdef QT_SAMPLECACHE_DEBUG |
176 | qDebug() << "QSampleCache: request sample [" << url << "]" ; |
177 | #endif |
178 | std::unique_lock<QRecursiveMutex> locker(m_mutex); |
179 | QMap<QUrl, QSample*>::iterator it = m_samples.find(akey: url); |
180 | QSample* sample; |
181 | if (it == m_samples.end()) { |
182 | sample = new QSample(url, this); |
183 | m_samples.insert(akey: url, avalue: sample); |
184 | sample->moveToThread(thread: &m_loadingThread); |
185 | } else { |
186 | sample = *it; |
187 | } |
188 | |
189 | sample->addRef(); |
190 | locker.unlock(); |
191 | |
192 | sample->loadIfNecessary(); |
193 | return sample; |
194 | } |
195 | |
196 | void QSampleCache::setCapacity(qint64 capacity) |
197 | { |
198 | const std::lock_guard<QRecursiveMutex> locker(m_mutex); |
199 | if (m_capacity == capacity) |
200 | return; |
201 | #ifdef QT_SAMPLECACHE_DEBUG |
202 | qDebug() << "QSampleCache: capacity changes from " << m_capacity << "to " << capacity; |
203 | #endif |
204 | if (m_capacity > 0 && capacity <= 0) { //memory management strategy changed |
205 | for (QMap<QUrl, QSample*>::iterator it = m_samples.begin(); it != m_samples.end();) { |
206 | QSample* sample = *it; |
207 | if (sample->m_ref == 0) { |
208 | unloadSample(sample); |
209 | it = m_samples.erase(it); |
210 | } else { |
211 | ++it; |
212 | } |
213 | } |
214 | } |
215 | |
216 | m_capacity = capacity; |
217 | refresh(usageChange: 0); |
218 | } |
219 | |
220 | // Called locked |
221 | void QSampleCache::unloadSample(QSample *sample) |
222 | { |
223 | m_usage -= sample->m_soundData.size(); |
224 | m_staleSamples.insert(value: sample); |
225 | sample->deleteLater(); |
226 | } |
227 | |
228 | // Called in both threads |
229 | void QSampleCache::refresh(qint64 usageChange) |
230 | { |
231 | const std::lock_guard<QRecursiveMutex> locker(m_mutex); |
232 | m_usage += usageChange; |
233 | if (m_capacity <= 0 || m_usage <= m_capacity) |
234 | return; |
235 | |
236 | #ifdef QT_SAMPLECACHE_DEBUG |
237 | qint64 recoveredSize = 0; |
238 | #endif |
239 | |
240 | //free unused samples to keep usage under capacity limit. |
241 | for (QMap<QUrl, QSample*>::iterator it = m_samples.begin(); it != m_samples.end();) { |
242 | QSample* sample = *it; |
243 | if (sample->m_ref > 0) { |
244 | ++it; |
245 | continue; |
246 | } |
247 | #ifdef QT_SAMPLECACHE_DEBUG |
248 | recoveredSize += sample->m_soundData.size(); |
249 | #endif |
250 | unloadSample(sample); |
251 | it = m_samples.erase(it); |
252 | if (m_usage <= m_capacity) |
253 | return; |
254 | } |
255 | |
256 | #ifdef QT_SAMPLECACHE_DEBUG |
257 | qDebug() << "QSampleCache: refresh(" << usageChange |
258 | << ") recovered size =" << recoveredSize |
259 | << "new usage =" << m_usage; |
260 | #endif |
261 | |
262 | if (m_usage > m_capacity) |
263 | qWarning() << "QSampleCache: usage[" << m_usage << " out of limit[" << m_capacity << "]" ; |
264 | } |
265 | |
266 | // Called in both threads |
267 | void QSampleCache::removeUnreferencedSample(QSample *sample) |
268 | { |
269 | const std::lock_guard<QRecursiveMutex> locker(m_mutex); |
270 | m_staleSamples.remove(value: sample); |
271 | } |
272 | |
273 | // Called in loader thread (since this lives in that thread) |
274 | // Also called from application thread after loader thread dies. |
275 | QSample::~QSample() |
276 | { |
277 | // Remove ourselves from our parent |
278 | m_parent->removeUnreferencedSample(sample: this); |
279 | |
280 | QMutexLocker locker(&m_mutex); |
281 | #ifdef QT_SAMPLECACHE_DEBUG |
282 | qDebug() << "~QSample" << this << ": deleted [" << m_url << "]" << QThread::currentThread(); |
283 | #endif |
284 | cleanup(); |
285 | } |
286 | |
287 | // Called in application thread |
288 | void QSample::loadIfNecessary() |
289 | { |
290 | QMutexLocker locker(&m_mutex); |
291 | if (m_state == QSample::Error || m_state == QSample::Creating) { |
292 | m_state = QSample::Loading; |
293 | QMetaObject::invokeMethod(obj: this, member: "load" , type: Qt::QueuedConnection); |
294 | } else { |
295 | qobject_cast<QSampleCache*>(object: m_parent)->loadingRelease(); |
296 | } |
297 | } |
298 | |
299 | // Called in application thread |
300 | bool QSampleCache::notifyUnreferencedSample(QSample* sample) |
301 | { |
302 | if (m_loadingThread.isRunning()) |
303 | m_loadingThread.wait(); |
304 | |
305 | const std::lock_guard<QRecursiveMutex> locker(m_mutex); |
306 | |
307 | if (m_capacity > 0) |
308 | return false; |
309 | m_samples.remove(akey: sample->m_url); |
310 | unloadSample(sample); |
311 | return true; |
312 | } |
313 | |
314 | // Called in application thread |
315 | void QSample::release() |
316 | { |
317 | QMutexLocker locker(&m_mutex); |
318 | #ifdef QT_SAMPLECACHE_DEBUG |
319 | qDebug() << "Sample:: release" << this << QThread::currentThread() << m_ref; |
320 | #endif |
321 | if (--m_ref == 0) { |
322 | locker.unlock(); |
323 | m_parent->notifyUnreferencedSample(sample: this); |
324 | } |
325 | } |
326 | |
327 | // Called in dtor and when stream is loaded |
328 | // must be called locked. |
329 | void QSample::cleanup() |
330 | { |
331 | if (m_waveDecoder) |
332 | m_waveDecoder->deleteLater(); |
333 | if (m_stream) |
334 | m_stream->deleteLater(); |
335 | |
336 | m_waveDecoder = nullptr; |
337 | m_stream = nullptr; |
338 | } |
339 | |
340 | // Called in application thread |
341 | void QSample::addRef() |
342 | { |
343 | m_ref++; |
344 | } |
345 | |
346 | // Called in loading thread |
347 | void QSample::readSample() |
348 | { |
349 | Q_ASSERT(QThread::currentThread()->objectName() == QLatin1String("QSampleCache::LoadingThread" )); |
350 | QMutexLocker m(&m_mutex); |
351 | #ifdef QT_SAMPLECACHE_DEBUG |
352 | qDebug() << "QSample: readSample" ; |
353 | #endif |
354 | qint64 read = m_waveDecoder->read(data: m_soundData.data() + m_sampleReadLength, |
355 | maxlen: qMin(a: m_waveDecoder->bytesAvailable(), |
356 | b: qint64(m_waveDecoder->size() - m_sampleReadLength))); |
357 | if (read > 0) |
358 | m_sampleReadLength += read; |
359 | if (m_sampleReadLength < m_waveDecoder->size()) |
360 | return; |
361 | Q_ASSERT(m_sampleReadLength == qint64(m_soundData.size())); |
362 | onReady(); |
363 | } |
364 | |
365 | // Called in loading thread |
366 | void QSample::decoderReady() |
367 | { |
368 | Q_ASSERT(QThread::currentThread()->objectName() == QLatin1String("QSampleCache::LoadingThread" )); |
369 | QMutexLocker m(&m_mutex); |
370 | #ifdef QT_SAMPLECACHE_DEBUG |
371 | qDebug() << "QSample: decoder ready" ; |
372 | #endif |
373 | m_parent->refresh(usageChange: m_waveDecoder->size()); |
374 | |
375 | m_soundData.resize(size: m_waveDecoder->size()); |
376 | m_sampleReadLength = 0; |
377 | qint64 read = m_waveDecoder->read(data: m_soundData.data(), maxlen: m_waveDecoder->size()); |
378 | if (read > 0) |
379 | m_sampleReadLength += read; |
380 | if (m_sampleReadLength >= m_waveDecoder->size()) |
381 | onReady(); |
382 | } |
383 | |
384 | // Called in all threads |
385 | QSample::State QSample::state() const |
386 | { |
387 | QMutexLocker m(&m_mutex); |
388 | return m_state; |
389 | } |
390 | |
391 | // Called in loading thread |
392 | // Essentially a second ctor, doesn't need locks (?) |
393 | void QSample::load() |
394 | { |
395 | Q_ASSERT(QThread::currentThread()->objectName() == QLatin1String("QSampleCache::LoadingThread" )); |
396 | #ifdef QT_SAMPLECACHE_DEBUG |
397 | qDebug() << "QSample: load [" << m_url << "]" ; |
398 | #endif |
399 | m_stream = m_parent->networkAccessManager().get(request: QNetworkRequest(m_url)); |
400 | connect(asender: m_stream, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), SLOT(decoderError())); |
401 | m_waveDecoder = new QWaveDecoder(m_stream); |
402 | connect(asender: m_waveDecoder, SIGNAL(formatKnown()), SLOT(decoderReady())); |
403 | connect(asender: m_waveDecoder, SIGNAL(parsingError()), SLOT(decoderError())); |
404 | connect(asender: m_waveDecoder, SIGNAL(readyRead()), SLOT(readSample())); |
405 | } |
406 | |
407 | // Called in loading thread |
408 | void QSample::decoderError() |
409 | { |
410 | Q_ASSERT(QThread::currentThread()->objectName() == QLatin1String("QSampleCache::LoadingThread" )); |
411 | QMutexLocker m(&m_mutex); |
412 | #ifdef QT_SAMPLECACHE_DEBUG |
413 | qDebug() << "QSample: decoder error" ; |
414 | #endif |
415 | cleanup(); |
416 | m_state = QSample::Error; |
417 | qobject_cast<QSampleCache*>(object: m_parent)->loadingRelease(); |
418 | emit error(); |
419 | } |
420 | |
421 | // Called in loading thread from decoder when sample is done. Locked already. |
422 | void QSample::onReady() |
423 | { |
424 | Q_ASSERT(QThread::currentThread()->objectName() == QLatin1String("QSampleCache::LoadingThread" )); |
425 | #ifdef QT_SAMPLECACHE_DEBUG |
426 | qDebug() << "QSample: load ready" ; |
427 | #endif |
428 | m_audioFormat = m_waveDecoder->audioFormat(); |
429 | cleanup(); |
430 | m_state = QSample::Ready; |
431 | qobject_cast<QSampleCache*>(object: m_parent)->loadingRelease(); |
432 | emit ready(); |
433 | } |
434 | |
435 | // Called in application thread, then moved to loader thread |
436 | QSample::QSample(const QUrl& url, QSampleCache *parent) |
437 | : m_parent(parent) |
438 | , m_stream(nullptr) |
439 | , m_waveDecoder(nullptr) |
440 | , m_url(url) |
441 | , m_sampleReadLength(0) |
442 | , m_state(Creating) |
443 | , m_ref(0) |
444 | { |
445 | } |
446 | |
447 | QT_END_NAMESPACE |
448 | |
449 | #include "moc_qsamplecache_p.cpp" |
450 | |