1 #include "track/track.h"
2 
3 #include <QDirIterator>
4 #include <atomic>
5 
6 #include "engine/engine.h"
7 #include "moc_track.cpp"
8 #include "track/beatfactory.h"
9 #include "track/beatmap.h"
10 #include "track/trackref.h"
11 #include "util/assert.h"
12 #include "util/color/color.h"
13 #include "util/logger.h"
14 
15 namespace {
16 
17 const mixxx::Logger kLogger("Track");
18 
19 constexpr bool kLogStats = false;
20 const ConfigKey kConfigKeySeratoMetadataExport("[Library]", "SeratoMetadataExport");
21 
22 // Count the number of currently existing instances for detecting
23 // memory leaks.
24 std::atomic<int> s_numberOfInstances;
25 
openSecurityToken(const TrackFile & trackFile,SecurityTokenPointer pSecurityToken=SecurityTokenPointer ())26 SecurityTokenPointer openSecurityToken(
27         const TrackFile& trackFile,
28         SecurityTokenPointer pSecurityToken = SecurityTokenPointer()) {
29     if (pSecurityToken.isNull()) {
30         return Sandbox::openSecurityToken(trackFile.asFileInfo(), true);
31     } else {
32         return pSecurityToken;
33     }
34 }
35 
36 template<typename T>
compareAndSet(T * pField,const T & value)37 inline bool compareAndSet(T* pField, const T& value) {
38     if (*pField != value) {
39         *pField = value;
40         return true;
41     } else {
42         return false;
43     }
44 }
45 
getBeatsPointerBpm(const mixxx::BeatsPointer & pBeats)46 inline mixxx::Bpm getBeatsPointerBpm(
47         const mixxx::BeatsPointer& pBeats) {
48     return pBeats ? mixxx::Bpm{pBeats->getBpm()} : mixxx::Bpm{};
49 }
50 
51 } // anonymous namespace
52 
53 // Don't change this string without an entry in the CHANGELOG!
54 // Otherwise 3rd party software that picks up the currently
55 // playing track from the main window and relies on this
56 // formatting would stop working.
57 //static
58 const QString Track::kArtistTitleSeparator = QStringLiteral(" - ");
59 
Track(TrackFile fileInfo,SecurityTokenPointer pSecurityToken,TrackId trackId)60 Track::Track(
61         TrackFile fileInfo,
62         SecurityTokenPointer pSecurityToken,
63         TrackId trackId)
64         :
65 #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
66           m_qMutex(QMutex::Recursive),
67 #endif
68           m_fileInfo(std::move(fileInfo)),
69           m_pSecurityToken(openSecurityToken(m_fileInfo, std::move(pSecurityToken))),
70           m_record(trackId),
71           m_bDirty(false),
72           m_bMarkedForMetadataExport(false) {
73     if (kLogStats && kLogger.debugEnabled()) {
74         long numberOfInstancesBefore = s_numberOfInstances.fetch_add(1);
75         kLogger.debug()
76                 << "Creating instance:"
77                 << this
78                 << numberOfInstancesBefore
79                 << "->"
80                 << numberOfInstancesBefore + 1;
81     }
82 }
83 
~Track()84 Track::~Track() {
85     if (m_pBeatsImporterPending && !m_pBeatsImporterPending->isEmpty()) {
86         kLogger.warning()
87                 << "Import of beats is still pending and discarded";
88     }
89     if (m_pCueInfoImporterPending && !m_pCueInfoImporterPending->isEmpty()) {
90         kLogger.warning()
91                 << "Import of"
92                 << m_pCueInfoImporterPending->size()
93                 << "cue(s) is still pending and discarded";
94     }
95     if (kLogStats && kLogger.debugEnabled()) {
96         long numberOfInstancesBefore = s_numberOfInstances.fetch_sub(1);
97         kLogger.debug()
98                 << "Destroying instance:"
99                 << this
100                 << numberOfInstancesBefore
101                 << "->"
102                 << numberOfInstancesBefore - 1;
103     }
104 }
105 
106 //static
newTemporary(TrackFile fileInfo,SecurityTokenPointer pSecurityToken)107 TrackPointer Track::newTemporary(
108         TrackFile fileInfo,
109         SecurityTokenPointer pSecurityToken) {
110     return std::make_shared<Track>(
111             std::move(fileInfo),
112             std::move(pSecurityToken));
113 }
114 
115 //static
newDummy(TrackFile fileInfo,TrackId trackId)116 TrackPointer Track::newDummy(
117         TrackFile fileInfo,
118         TrackId trackId) {
119     return std::make_shared<Track>(
120             std::move(fileInfo),
121             SecurityTokenPointer(),
122             trackId);
123 }
124 
relocate(TrackFile fileInfo,SecurityTokenPointer pSecurityToken)125 void Track::relocate(
126         TrackFile fileInfo,
127         SecurityTokenPointer pSecurityToken) {
128     QMutexLocker lock(&m_qMutex);
129     m_pSecurityToken = openSecurityToken(fileInfo, std::move(pSecurityToken));
130     m_fileInfo = std::move(fileInfo);
131     // The track does not need to be marked as dirty,
132     // because this function will always be called with
133     // the updated location from the database.
134 }
135 
importMetadata(mixxx::TrackMetadata importedMetadata,const QDateTime & metadataSynchronized)136 void Track::importMetadata(
137         mixxx::TrackMetadata importedMetadata,
138         const QDateTime& metadataSynchronized) {
139     // Information stored in Serato tags is imported separately after
140     // importing the metadata (see below). The Serato tags BLOB itself
141     // is updated together with the metadata.
142     auto pSeratoBeatsImporter = importedMetadata.getTrackInfo().getSeratoTags().importBeats();
143     const bool seratoBpmLocked = importedMetadata.getTrackInfo().getSeratoTags().isBpmLocked();
144     auto pSeratoCuesImporter = importedMetadata.getTrackInfo().getSeratoTags().importCueInfos();
145 
146     {
147         // enter locking scope
148         QMutexLocker lock(&m_qMutex);
149 
150         // Preserve the both current bpm and key temporarily to avoid
151         // overwriting with an inconsistent value. The bpm must always be
152         // set together with the beat grid and the key text must be parsed
153         // and validated.
154         const auto importedBpm = importedMetadata.getTrackInfo().getBpm();
155         importedMetadata.refTrackInfo().setBpm(getBpmWhileLocked());
156         const auto importedKeyText = importedMetadata.getTrackInfo().getKey();
157         importedMetadata.refTrackInfo().setKey(m_record.getMetadata().getTrackInfo().getKey());
158 
159         bool modified = false;
160         // Only set the metadata synchronized flag (column `header_parsed`
161         // in the database) from false to true, but never reset it back to
162         // false. Otherwise file tags would be re-imported and overwrite
163         // the metadata stored in the database, e.g. after retrieving metadata
164         // from MusicBrainz!
165         // TODO: In the future this flag should become a time stamp
166         // to detect updates of files and then decide based on time
167         // stamps if file tags need to be re-imported.
168         if (!metadataSynchronized.isNull()) {
169             modified |= compareAndSet(
170                     m_record.ptrMetadataSynchronized(),
171                     true);
172         }
173 
174         const auto oldReplayGain =
175                 m_record.getMetadata().getTrackInfo().getReplayGain();
176         if (m_record.getMetadata() != importedMetadata) {
177             m_record.setMetadata(std::move(importedMetadata));
178             // Don't use importedMetadata after move assignment!!
179             modified = true;
180         }
181         const auto newReplayGain =
182                 m_record.getMetadata().getTrackInfo().getReplayGain();
183 
184         // Need to set BPM after sample rate since beat grid creation depends on
185         // knowing the sample rate. Bug #1020438.
186         auto beatsAndBpmModified = false;
187         if (!m_pBeats || !mixxx::Bpm::isValidValue(m_pBeats->getBpm())) {
188             // Only use the imported BPM if the current beat grid is either
189             // missing or not valid! The BPM value in the metadata might be
190             // imprecise (normalized or rounded), e.g. ID3v2 only supports
191             // integer values.
192             beatsAndBpmModified = trySetBpmWhileLocked(importedBpm.getValue());
193         }
194         modified |= beatsAndBpmModified;
195         const auto newBpm = getBpmWhileLocked();
196 
197         auto keysModified = false;
198         if (KeyUtils::guessKeyFromText(importedKeyText) != mixxx::track::io::key::INVALID) {
199             // Only update the current key with a valid value. Otherwise preserve
200             // the existing value.
201             keysModified = m_record.updateGlobalKeyText(
202                     importedKeyText,
203                     mixxx::track::io::key::FILE_METADATA);
204         }
205         modified |= keysModified;
206         const auto newKey = m_record.getGlobalKey();
207 
208         // Import track color from Serato tags if available
209         const auto newColor = m_record.getMetadata().getTrackInfo().getSeratoTags().getTrackColor();
210         const bool colorModified = compareAndSet(m_record.ptrColor(), newColor);
211         modified |= colorModified;
212         DEBUG_ASSERT(!colorModified || m_record.getColor() == newColor);
213 
214         if (!modified) {
215             // Unmodified, nothing todo
216             return;
217         }
218         // Explicitly unlock before emitting signals
219         markDirtyAndUnlock(&lock);
220 
221         if (beatsAndBpmModified) {
222             emitBeatsAndBpmUpdated(newBpm);
223         }
224         if (keysModified) {
225             emitKeysUpdated(newKey);
226         }
227         if (oldReplayGain != newReplayGain) {
228             emit replayGainUpdated(newReplayGain);
229         }
230         if (colorModified) {
231             emit colorUpdated(newColor);
232         }
233     }
234 
235     // TODO: Import Serato metadata within the locking scope and not
236     // as a post-processing step.
237     if (pSeratoBeatsImporter) {
238         kLogger.debug() << "Importing Serato beats";
239         tryImportBeats(std::move(pSeratoBeatsImporter), seratoBpmLocked);
240     }
241     if (pSeratoCuesImporter) {
242         kLogger.debug() << "Importing Serato cues";
243         importCueInfos(std::move(pSeratoCuesImporter));
244     }
245 }
246 
mergeImportedMetadata(const mixxx::TrackMetadata & importedMetadata)247 void Track::mergeImportedMetadata(
248         const mixxx::TrackMetadata& importedMetadata) {
249     QMutexLocker lock(&m_qMutex);
250     if (m_record.mergeImportedMetadata(importedMetadata)) {
251         markDirtyAndUnlock(&lock);
252     }
253 }
254 
readTrackMetadata(mixxx::TrackMetadata * pTrackMetadata,bool * pMetadataSynchronized) const255 void Track::readTrackMetadata(
256         mixxx::TrackMetadata* pTrackMetadata,
257         bool* pMetadataSynchronized) const {
258     DEBUG_ASSERT(pTrackMetadata);
259     QMutexLocker lock(&m_qMutex);
260     *pTrackMetadata = m_record.getMetadata();
261     if (pMetadataSynchronized) {
262         *pMetadataSynchronized = m_record.getMetadataSynchronized();
263     }
264 }
265 
readTrackRecord(mixxx::TrackRecord * pTrackRecord,bool * pDirty) const266 void Track::readTrackRecord(
267         mixxx::TrackRecord* pTrackRecord,
268         bool* pDirty) const {
269     DEBUG_ASSERT(pTrackRecord);
270     QMutexLocker lock(&m_qMutex);
271     *pTrackRecord = m_record;
272     if (pDirty) {
273         *pDirty = m_bDirty;
274     }
275 }
276 
getCanonicalLocation() const277 QString Track::getCanonicalLocation() const {
278     QMutexLocker lock(&m_qMutex);
279     return /*mutable*/ m_fileInfo.freshCanonicalLocation(); // non-const
280 }
281 
getReplayGain() const282 mixxx::ReplayGain Track::getReplayGain() const {
283     QMutexLocker lock(&m_qMutex);
284     return m_record.getMetadata().getTrackInfo().getReplayGain();
285 }
286 
setReplayGain(const mixxx::ReplayGain & replayGain)287 void Track::setReplayGain(const mixxx::ReplayGain& replayGain) {
288     QMutexLocker lock(&m_qMutex);
289     if (compareAndSet(m_record.refMetadata().refTrackInfo().ptrReplayGain(), replayGain)) {
290         markDirtyAndUnlock(&lock);
291         emit replayGainUpdated(replayGain);
292     }
293 }
294 
getBpmWhileLocked() const295 mixxx::Bpm Track::getBpmWhileLocked() const {
296     // BPM values must be synchronized at all times!
297     DEBUG_ASSERT(m_record.getMetadata().getTrackInfo().getBpm() == getBeatsPointerBpm(m_pBeats));
298     return m_record.getMetadata().getTrackInfo().getBpm();
299 }
300 
trySetBpmWhileLocked(double bpmValue)301 bool Track::trySetBpmWhileLocked(double bpmValue) {
302     if (!mixxx::Bpm::isValidValue(bpmValue)) {
303         // If the user sets the BPM to an invalid value, we assume
304         // they want to clear the beatgrid.
305         return trySetBeatsWhileLocked(nullptr);
306     } else if (!m_pBeats) {
307         // No beat grid available -> create and initialize
308         double cue = m_record.getCuePoint().getPosition();
309         auto pBeats = BeatFactory::makeBeatGrid(getSampleRate(), bpmValue, cue);
310         return trySetBeatsWhileLocked(std::move(pBeats));
311     } else if ((m_pBeats->getCapabilities() & mixxx::Beats::BEATSCAP_SETBPM) &&
312             m_pBeats->getBpm() != bpmValue) {
313         // Continue with the regular cases
314         if (kLogger.debugEnabled()) {
315             kLogger.debug() << "Updating BPM:" << getLocation();
316         }
317         return trySetBeatsWhileLocked(m_pBeats->setBpm(bpmValue));
318     }
319     return false;
320 }
321 
getBpmText() const322 QString Track::getBpmText() const {
323     return QString("%1").arg(getBpm(), 3,'f',1);
324 }
325 
trySetBpm(double bpmValue)326 bool Track::trySetBpm(double bpmValue) {
327     QMutexLocker lock(&m_qMutex);
328     if (!trySetBpmWhileLocked(bpmValue)) {
329         return false;
330     }
331     afterBeatsAndBpmUpdated(&lock);
332     return true;
333 }
334 
trySetBeats(mixxx::BeatsPointer pBeats)335 bool Track::trySetBeats(mixxx::BeatsPointer pBeats) {
336     QMutexLocker lock(&m_qMutex);
337     return trySetBeatsMarkDirtyAndUnlock(&lock, pBeats, false);
338 }
339 
trySetAndLockBeats(mixxx::BeatsPointer pBeats)340 bool Track::trySetAndLockBeats(mixxx::BeatsPointer pBeats) {
341     QMutexLocker lock(&m_qMutex);
342     return trySetBeatsMarkDirtyAndUnlock(&lock, pBeats, true);
343 }
344 
setBeatsWhileLocked(mixxx::BeatsPointer pBeats)345 bool Track::setBeatsWhileLocked(mixxx::BeatsPointer pBeats) {
346     if (m_pBeats == pBeats) {
347         return false;
348     }
349     m_pBeats = std::move(pBeats);
350     m_record.refMetadata().refTrackInfo().setBpm(getBeatsPointerBpm(m_pBeats));
351     return true;
352 }
353 
trySetBeatsWhileLocked(mixxx::BeatsPointer pBeats,bool lockBpmAfterSet)354 bool Track::trySetBeatsWhileLocked(
355         mixxx::BeatsPointer pBeats,
356         bool lockBpmAfterSet) {
357     if (m_pBeats && m_record.getBpmLocked()) {
358         // Track has already a valid and locked beats object, abbort.
359         qDebug() << "Track beats is already set and BPM-locked. Discard the new beats";
360         return false;
361     }
362 
363     bool dirty = false;
364     if (setBeatsWhileLocked(pBeats)) {
365         dirty = true;
366     }
367     if (compareAndSet(m_record.ptrBpmLocked(), lockBpmAfterSet)) {
368         dirty = true;
369     }
370     return dirty;
371 }
372 
trySetBeatsMarkDirtyAndUnlock(QMutexLocker * pLock,mixxx::BeatsPointer pBeats,bool lockBpmAfterSet)373 bool Track::trySetBeatsMarkDirtyAndUnlock(
374         QMutexLocker* pLock,
375         mixxx::BeatsPointer pBeats,
376         bool lockBpmAfterSet) {
377     DEBUG_ASSERT(pLock);
378 
379     if (!trySetBeatsWhileLocked(pBeats, lockBpmAfterSet)) {
380         return false;
381     }
382 
383     afterBeatsAndBpmUpdated(pLock);
384     return true;
385 }
386 
getBeats() const387 mixxx::BeatsPointer Track::getBeats() const {
388     QMutexLocker lock(&m_qMutex);
389     return m_pBeats;
390 }
391 
afterBeatsAndBpmUpdated(QMutexLocker * pLock)392 void Track::afterBeatsAndBpmUpdated(
393         QMutexLocker* pLock) {
394     DEBUG_ASSERT(pLock);
395 
396     const auto bpm = getBpmWhileLocked();
397     markDirtyAndUnlock(pLock);
398     emitBeatsAndBpmUpdated(bpm);
399 }
400 
emitBeatsAndBpmUpdated(mixxx::Bpm newBpm)401 void Track::emitBeatsAndBpmUpdated(
402         mixxx::Bpm newBpm) {
403     emit bpmUpdated(newBpm.getValue());
404     emit beatsUpdated();
405 }
406 
setMetadataSynchronized(bool metadataSynchronized)407 void Track::setMetadataSynchronized(bool metadataSynchronized) {
408     QMutexLocker lock(&m_qMutex);
409     if (compareAndSet(m_record.ptrMetadataSynchronized(), metadataSynchronized)) {
410         markDirtyAndUnlock(&lock);
411     }
412 }
413 
isMetadataSynchronized() const414 bool Track::isMetadataSynchronized() const {
415     QMutexLocker lock(&m_qMutex);
416     return m_record.getMetadataSynchronized();
417 }
418 
getInfo() const419 QString Track::getInfo() const {
420     QMutexLocker lock(&m_qMutex);
421     if (m_record.getMetadata().getTrackInfo().getArtist().trimmed().isEmpty()) {
422         if (m_record.getMetadata().getTrackInfo().getTitle().trimmed().isEmpty()) {
423             return m_fileInfo.fileName();
424         } else {
425             return m_record.getMetadata().getTrackInfo().getTitle();
426         }
427     } else {
428         return m_record.getMetadata().getTrackInfo().getArtist() +
429                 kArtistTitleSeparator +
430                 m_record.getMetadata().getTrackInfo().getTitle();
431     }
432 }
433 
getTitleInfo() const434 QString Track::getTitleInfo() const {
435     QMutexLocker lock(&m_qMutex);
436     if (m_record.getMetadata().getTrackInfo().getArtist().trimmed().isEmpty() &&
437             m_record.getMetadata().getTrackInfo().getTitle().trimmed().isEmpty()) {
438         return m_fileInfo.fileName();
439     } else {
440         return m_record.getMetadata().getTrackInfo().getTitle();
441     }
442 }
443 
getDateAdded() const444 QDateTime Track::getDateAdded() const {
445     QMutexLocker lock(&m_qMutex);
446     return m_record.getDateAdded();
447 }
448 
setDateAdded(const QDateTime & dateAdded)449 void Track::setDateAdded(const QDateTime& dateAdded) {
450     QMutexLocker lock(&m_qMutex);
451     m_record.setDateAdded(dateAdded);
452 }
453 
setDuration(mixxx::Duration duration)454 void Track::setDuration(mixxx::Duration duration) {
455     QMutexLocker lock(&m_qMutex);
456     // TODO: Move checks into TrackRecord
457     VERIFY_OR_DEBUG_ASSERT(!m_record.getStreamInfoFromSource() ||
458             m_record.getStreamInfoFromSource()->getDuration() <= mixxx::Duration::empty() ||
459             m_record.getStreamInfoFromSource()->getDuration() == duration) {
460         kLogger.warning()
461                 << "Cannot override stream duration:"
462                 << m_record.getStreamInfoFromSource()->getDuration()
463                 << "->"
464                 << duration;
465         return;
466     }
467     if (compareAndSet(
468                 m_record.refMetadata().refStreamInfo().ptrDuration(),
469                 duration)) {
470         markDirtyAndUnlock(&lock);
471     }
472 }
473 
setDuration(double duration)474 void Track::setDuration(double duration) {
475     setDuration(mixxx::Duration::fromSeconds(duration));
476 }
477 
getDuration(DurationRounding rounding) const478 double Track::getDuration(DurationRounding rounding) const {
479     QMutexLocker lock(&m_qMutex);
480     const auto durationSeconds =
481             m_record.getMetadata().getStreamInfo().getDuration().toDoubleSeconds();
482     switch (rounding) {
483     case DurationRounding::SECONDS:
484         return std::round(durationSeconds);
485     default:
486         return durationSeconds;
487     }
488 }
489 
getDurationText(mixxx::Duration::Precision precision) const490 QString Track::getDurationText(mixxx::Duration::Precision precision) const {
491     QMutexLocker lock(&m_qMutex);
492     return m_record.getMetadata().getDurationText(precision);
493 }
494 
getTitle() const495 QString Track::getTitle() const {
496     QMutexLocker lock(&m_qMutex);
497     return m_record.getMetadata().getTrackInfo().getTitle();
498 }
499 
setTitle(const QString & s)500 void Track::setTitle(const QString& s) {
501     QMutexLocker lock(&m_qMutex);
502     QString trimmed(s.trimmed());
503     if (compareAndSet(m_record.refMetadata().refTrackInfo().ptrTitle(), trimmed)) {
504         markDirtyAndUnlock(&lock);
505     }
506 }
507 
getArtist() const508 QString Track::getArtist() const {
509     QMutexLocker lock(&m_qMutex);
510     return m_record.getMetadata().getTrackInfo().getArtist();
511 }
512 
setArtist(const QString & s)513 void Track::setArtist(const QString& s) {
514     QMutexLocker lock(&m_qMutex);
515     QString trimmed(s.trimmed());
516     if (compareAndSet(m_record.refMetadata().refTrackInfo().ptrArtist(), trimmed)) {
517         markDirtyAndUnlock(&lock);
518     }
519 }
520 
getAlbum() const521 QString Track::getAlbum() const {
522     QMutexLocker lock(&m_qMutex);
523     return m_record.getMetadata().getAlbumInfo().getTitle();
524 }
525 
setAlbum(const QString & s)526 void Track::setAlbum(const QString& s) {
527     QMutexLocker lock(&m_qMutex);
528     QString trimmed(s.trimmed());
529     if (compareAndSet(m_record.refMetadata().refAlbumInfo().ptrTitle(), trimmed)) {
530         markDirtyAndUnlock(&lock);
531     }
532 }
533 
getAlbumArtist() const534 QString Track::getAlbumArtist()  const {
535     QMutexLocker lock(&m_qMutex);
536     return m_record.getMetadata().getAlbumInfo().getArtist();
537 }
538 
setAlbumArtist(const QString & s)539 void Track::setAlbumArtist(const QString& s) {
540     QMutexLocker lock(&m_qMutex);
541     QString trimmed(s.trimmed());
542     if (compareAndSet(m_record.refMetadata().refAlbumInfo().ptrArtist(), trimmed)) {
543         markDirtyAndUnlock(&lock);
544     }
545 }
546 
getYear() const547 QString Track::getYear()  const {
548     QMutexLocker lock(&m_qMutex);
549     return m_record.getMetadata().getTrackInfo().getYear();
550 }
551 
setYear(const QString & s)552 void Track::setYear(const QString& s) {
553     QMutexLocker lock(&m_qMutex);
554     QString trimmed(s.trimmed());
555     if (compareAndSet(m_record.refMetadata().refTrackInfo().ptrYear(), trimmed)) {
556         markDirtyAndUnlock(&lock);
557     }
558 }
559 
getGenre() const560 QString Track::getGenre() const {
561     QMutexLocker lock(&m_qMutex);
562     return m_record.getMetadata().getTrackInfo().getGenre();
563 }
564 
setGenre(const QString & s)565 void Track::setGenre(const QString& s) {
566     QMutexLocker lock(&m_qMutex);
567     QString trimmed(s.trimmed());
568     if (compareAndSet(m_record.refMetadata().refTrackInfo().ptrGenre(), trimmed)) {
569         markDirtyAndUnlock(&lock);
570     }
571 }
572 
getComposer() const573 QString Track::getComposer() const {
574     QMutexLocker lock(&m_qMutex);
575     return m_record.getMetadata().getTrackInfo().getComposer();
576 }
577 
setComposer(const QString & s)578 void Track::setComposer(const QString& s) {
579     QMutexLocker lock(&m_qMutex);
580     QString trimmed(s.trimmed());
581     if (compareAndSet(m_record.refMetadata().refTrackInfo().ptrComposer(), trimmed)) {
582         markDirtyAndUnlock(&lock);
583     }
584 }
585 
getGrouping() const586 QString Track::getGrouping()  const {
587     QMutexLocker lock(&m_qMutex);
588     return m_record.getMetadata().getTrackInfo().getGrouping();
589 }
590 
setGrouping(const QString & s)591 void Track::setGrouping(const QString& s) {
592     QMutexLocker lock(&m_qMutex);
593     QString trimmed(s.trimmed());
594     if (compareAndSet(m_record.refMetadata().refTrackInfo().ptrGrouping(), trimmed)) {
595         markDirtyAndUnlock(&lock);
596     }
597 }
598 
getTrackNumber() const599 QString Track::getTrackNumber()  const {
600     QMutexLocker lock(&m_qMutex);
601     return m_record.getMetadata().getTrackInfo().getTrackNumber();
602 }
603 
getTrackTotal() const604 QString Track::getTrackTotal()  const {
605     QMutexLocker lock(&m_qMutex);
606     return m_record.getMetadata().getTrackInfo().getTrackTotal();
607 }
608 
setTrackNumber(const QString & s)609 void Track::setTrackNumber(const QString& s) {
610     QMutexLocker lock(&m_qMutex);
611     QString trimmed(s.trimmed());
612     if (compareAndSet(m_record.refMetadata().refTrackInfo().ptrTrackNumber(), trimmed)) {
613         markDirtyAndUnlock(&lock);
614     }
615 }
616 
setTrackTotal(const QString & s)617 void Track::setTrackTotal(const QString& s) {
618     QMutexLocker lock(&m_qMutex);
619     QString trimmed(s.trimmed());
620     if (compareAndSet(m_record.refMetadata().refTrackInfo().ptrTrackTotal(), trimmed)) {
621         markDirtyAndUnlock(&lock);
622     }
623 }
624 
getPlayCounter() const625 PlayCounter Track::getPlayCounter() const {
626     QMutexLocker lock(&m_qMutex);
627     return m_record.getPlayCounter();
628 }
629 
setPlayCounter(const PlayCounter & playCounter)630 void Track::setPlayCounter(const PlayCounter& playCounter) {
631     QMutexLocker lock(&m_qMutex);
632     if (compareAndSet(m_record.ptrPlayCounter(), playCounter)) {
633         markDirtyAndUnlock(&lock);
634     }
635 }
636 
updatePlayCounter(bool bPlayed)637 void Track::updatePlayCounter(bool bPlayed) {
638     QMutexLocker lock(&m_qMutex);
639     PlayCounter playCounter(m_record.getPlayCounter());
640     playCounter.setPlayedAndUpdateTimesPlayed(bPlayed);
641     if (compareAndSet(m_record.ptrPlayCounter(), playCounter)) {
642         markDirtyAndUnlock(&lock);
643     }
644 }
645 
getColor() const646 mixxx::RgbColor::optional_t Track::getColor() const {
647     QMutexLocker lock(&m_qMutex);
648     return m_record.getColor();
649 }
650 
setColor(mixxx::RgbColor::optional_t color)651 void Track::setColor(mixxx::RgbColor::optional_t color) {
652     QMutexLocker lock(&m_qMutex);
653     if (compareAndSet(m_record.ptrColor(), color)) {
654         markDirtyAndUnlock(&lock);
655         emit colorUpdated(color);
656     }
657 }
658 
getComment() const659 QString Track::getComment() const {
660     QMutexLocker lock(&m_qMutex);
661     return m_record.getMetadata().getTrackInfo().getComment();
662 }
663 
setComment(const QString & s)664 void Track::setComment(const QString& s) {
665     QMutexLocker lock(&m_qMutex);
666     if (compareAndSet(m_record.refMetadata().refTrackInfo().ptrComment(), s)) {
667         markDirtyAndUnlock(&lock);
668     }
669 }
670 
getType() const671 QString Track::getType() const {
672     QMutexLocker lock(&m_qMutex);
673     return m_record.getFileType();
674 }
675 
setType(const QString & sType)676 void Track::setType(const QString& sType) {
677     QMutexLocker lock(&m_qMutex);
678     if (compareAndSet(m_record.ptrFileType(), sType)) {
679         markDirtyAndUnlock(&lock);
680     }
681 }
682 
getSampleRate() const683 mixxx::audio::SampleRate Track::getSampleRate() const {
684     QMutexLocker lock(&m_qMutex);
685     return m_record.getMetadata().getStreamInfo().getSignalInfo().getSampleRate();
686 }
687 
getChannels() const688 int Track::getChannels() const {
689     QMutexLocker lock(&m_qMutex);
690     return m_record.getMetadata().getStreamInfo().getSignalInfo().getChannelCount();
691 }
692 
getBitrate() const693 int Track::getBitrate() const {
694     QMutexLocker lock(&m_qMutex);
695     return m_record.getMetadata().getStreamInfo().getBitrate();
696 }
697 
getBitrateText() const698 QString Track::getBitrateText() const {
699     QMutexLocker lock(&m_qMutex);
700     return m_record.getMetadata().getBitrateText();
701 }
702 
setBitrate(int iBitrate)703 void Track::setBitrate(int iBitrate) {
704     QMutexLocker lock(&m_qMutex);
705     const mixxx::audio::Bitrate bitrate(iBitrate);
706     // TODO: Move checks into TrackRecord
707     VERIFY_OR_DEBUG_ASSERT(!m_record.getStreamInfoFromSource() ||
708             !m_record.getStreamInfoFromSource()->getBitrate().isValid() ||
709             m_record.getStreamInfoFromSource()->getBitrate() == bitrate) {
710         kLogger.warning()
711                 << "Cannot override stream bitrate:"
712                 << m_record.getStreamInfoFromSource()->getBitrate()
713                 << "->"
714                 << bitrate;
715         return;
716     }
717     if (compareAndSet(
718                 m_record.refMetadata().refStreamInfo().ptrBitrate(),
719                 bitrate)) {
720         markDirtyAndUnlock(&lock);
721     }
722 }
723 
getId() const724 TrackId Track::getId() const {
725     QMutexLocker lock(&m_qMutex);
726     return m_record.getId();
727 }
728 
initId(TrackId id)729 void Track::initId(TrackId id) {
730     QMutexLocker lock(&m_qMutex);
731     DEBUG_ASSERT(id.isValid());
732     if (m_record.getId() == id) {
733         return;
734     }
735     // The track's id must be set only once and immediately after
736     // the object has been created.
737     VERIFY_OR_DEBUG_ASSERT(!m_record.getId().isValid()) {
738         kLogger.warning() << "Cannot change id from"
739                 << m_record.getId() << "to" << id;
740         return; // abort
741     }
742     m_record.setId(id);
743     for (const CuePointer& pCue : qAsConst(m_cuePoints)) {
744         pCue->setTrackId(id);
745     }
746     // Changing the Id does not make the track dirty because the Id is always
747     // generated by the database itself.
748 }
749 
resetId()750 void Track::resetId() {
751     QMutexLocker lock(&m_qMutex);
752     m_record.setId(TrackId());
753     for (const CuePointer& pCue : qAsConst(m_cuePoints)) {
754         pCue->setTrackId(TrackId());
755     }
756 }
757 
setURL(const QString & url)758 void Track::setURL(const QString& url) {
759     QMutexLocker lock(&m_qMutex);
760     if (compareAndSet(m_record.ptrUrl(), url)) {
761         markDirtyAndUnlock(&lock);
762     }
763 }
764 
getURL() const765 QString Track::getURL() const {
766     QMutexLocker lock(&m_qMutex);
767     return m_record.getUrl();
768 }
769 
getWaveform() const770 ConstWaveformPointer Track::getWaveform() const {
771     return m_waveform;
772 }
773 
setWaveform(ConstWaveformPointer pWaveform)774 void Track::setWaveform(ConstWaveformPointer pWaveform) {
775     m_waveform = pWaveform;
776     emit waveformUpdated();
777 }
778 
getWaveformSummary() const779 ConstWaveformPointer Track::getWaveformSummary() const {
780     return m_waveformSummary;
781 }
782 
setWaveformSummary(ConstWaveformPointer pWaveform)783 void Track::setWaveformSummary(ConstWaveformPointer pWaveform) {
784     m_waveformSummary = pWaveform;
785     emit waveformSummaryUpdated();
786 }
787 
setCuePoint(CuePosition cue)788 void Track::setCuePoint(CuePosition cue) {
789     QMutexLocker lock(&m_qMutex);
790 
791     if (!compareAndSet(m_record.ptrCuePoint(), cue)) {
792         // Nothing changed.
793         return;
794     }
795 
796     // Store the cue point in a load cue
797     CuePointer pLoadCue = findCueByType(mixxx::CueType::MainCue);
798     double position = cue.getPosition();
799     if (position != -1.0) {
800         if (pLoadCue) {
801             pLoadCue->setStartPosition(position);
802         } else {
803             pLoadCue = CuePointer(new Cue());
804             // While this method could be called from any thread,
805             // associated Cue objects should always live on the
806             // same thread as their host, namely this->thread().
807             pLoadCue->moveToThread(thread());
808             pLoadCue->setTrackId(m_record.getId());
809             pLoadCue->setType(mixxx::CueType::MainCue);
810             pLoadCue->setStartPosition(position);
811             connect(pLoadCue.get(),
812                     &Cue::updated,
813                     this,
814                     &Track::slotCueUpdated);
815             m_cuePoints.push_back(pLoadCue);
816         }
817     } else if (pLoadCue) {
818         disconnect(pLoadCue.get(), nullptr, this, nullptr);
819         m_cuePoints.removeOne(pLoadCue);
820     }
821 
822     markDirtyAndUnlock(&lock);
823     emit cuesUpdated();
824 }
825 
shiftCuePositionsMillis(double milliseconds)826 void Track::shiftCuePositionsMillis(double milliseconds) {
827     QMutexLocker lock(&m_qMutex);
828 
829     VERIFY_OR_DEBUG_ASSERT(m_record.getStreamInfoFromSource()) {
830         return;
831     }
832     double frames = m_record.getStreamInfoFromSource()->getSignalInfo().millis2frames(milliseconds);
833     for (const CuePointer& pCue : qAsConst(m_cuePoints)) {
834         pCue->shiftPositionFrames(frames);
835     }
836 
837     markDirtyAndUnlock(&lock);
838 }
839 
analysisFinished()840 void Track::analysisFinished() {
841     emit analyzed();
842 }
843 
getCuePoint() const844 CuePosition Track::getCuePoint() const {
845     QMutexLocker lock(&m_qMutex);
846     return m_record.getCuePoint();
847 }
848 
slotCueUpdated()849 void Track::slotCueUpdated() {
850     markDirty();
851     emit cuesUpdated();
852 }
853 
createAndAddCue()854 CuePointer Track::createAndAddCue() {
855     QMutexLocker lock(&m_qMutex);
856     CuePointer pCue(new Cue());
857     // While this method could be called from any thread,
858     // associated Cue objects should always live on the
859     // same thread as their host, namely this->thread().
860     pCue->moveToThread(thread());
861     pCue->setTrackId(m_record.getId());
862     connect(pCue.get(),
863             &Cue::updated,
864             this,
865             &Track::slotCueUpdated);
866     m_cuePoints.push_back(pCue);
867     markDirtyAndUnlock(&lock);
868     emit cuesUpdated();
869     return pCue;
870 }
871 
findCueByType(mixxx::CueType type) const872 CuePointer Track::findCueByType(mixxx::CueType type) const {
873     // This method cannot be used for hotcues because there can be
874     // multiple hotcues and this function returns only a single CuePointer.
875     VERIFY_OR_DEBUG_ASSERT(type != mixxx::CueType::HotCue) {
876         return CuePointer();
877     }
878     QMutexLocker lock(&m_qMutex);
879     for (const CuePointer& pCue: m_cuePoints) {
880         if (pCue->getType() == type) {
881             return pCue;
882         }
883     }
884     return CuePointer();
885 }
886 
findCueById(DbId id) const887 CuePointer Track::findCueById(DbId id) const {
888     QMutexLocker lock(&m_qMutex);
889     for (const CuePointer& pCue : m_cuePoints) {
890         if (pCue->getId() == id) {
891             return pCue;
892         }
893     }
894     return CuePointer();
895 }
896 
removeCue(const CuePointer & pCue)897 void Track::removeCue(const CuePointer& pCue) {
898     if (!pCue) {
899         return;
900     }
901 
902     QMutexLocker lock(&m_qMutex);
903     DEBUG_ASSERT(pCue->getTrackId() == m_record.getId());
904     disconnect(pCue.get(), nullptr, this, nullptr);
905     m_cuePoints.removeOne(pCue);
906     if (pCue->getType() == mixxx::CueType::MainCue) {
907         m_record.setCuePoint(CuePosition());
908     }
909     pCue->setTrackId(TrackId());
910     markDirtyAndUnlock(&lock);
911     emit cuesUpdated();
912 }
913 
removeCuesOfType(mixxx::CueType type)914 void Track::removeCuesOfType(mixxx::CueType type) {
915     QMutexLocker lock(&m_qMutex);
916     bool dirty = false;
917     QMutableListIterator<CuePointer> it(m_cuePoints);
918     while (it.hasNext()) {
919         CuePointer pCue = it.next();
920         // FIXME: Why does this only work for the Hotcue Type?
921         if (pCue->getType() == type) {
922             disconnect(pCue.get(), nullptr, this, nullptr);
923             pCue->setTrackId(TrackId());
924             it.remove();
925             dirty = true;
926         }
927     }
928     if (compareAndSet(m_record.ptrCuePoint(), CuePosition())) {
929         dirty = true;
930     }
931     if (dirty) {
932         markDirtyAndUnlock(&lock);
933         emit cuesUpdated();
934     }
935 }
936 
setCuePoints(const QList<CuePointer> & cuePoints)937 void Track::setCuePoints(const QList<CuePointer>& cuePoints) {
938     // While this method could be called from any thread,
939     // associated Cue objects should always live on the
940     // same thread as their host, namely this->thread().
941     for (const auto& pCue : cuePoints) {
942         pCue->moveToThread(thread());
943     }
944     QMutexLocker lock(&m_qMutex);
945     setCuePointsMarkDirtyAndUnlock(
946             &lock,
947             cuePoints);
948 }
949 
tryImportBeats(mixxx::BeatsImporterPointer pBeatsImporter,bool lockBpmAfterSet)950 Track::ImportStatus Track::tryImportBeats(
951         mixxx::BeatsImporterPointer pBeatsImporter,
952         bool lockBpmAfterSet) {
953     QMutexLocker lock(&m_qMutex);
954     VERIFY_OR_DEBUG_ASSERT(pBeatsImporter) {
955         return ImportStatus::Complete;
956     }
957     DEBUG_ASSERT(!m_pBeatsImporterPending);
958     m_pBeatsImporterPending = pBeatsImporter;
959     if (m_pBeatsImporterPending->isEmpty()) {
960         m_pBeatsImporterPending.reset();
961         return ImportStatus::Complete;
962     } else if (m_record.hasStreamInfoFromSource()) {
963         // Replace existing beats with imported beats immediately
964         tryImportPendingBeatsMarkDirtyAndUnlock(&lock, lockBpmAfterSet);
965         return ImportStatus::Complete;
966     } else {
967         kLogger.debug()
968                 << "Import of beats is pending until the actual sample rate becomes available";
969         // Clear all existing beats, that are supposed
970         // to be replaced with the imported beats soon.
971         if (trySetBeatsMarkDirtyAndUnlock(&lock,
972                     nullptr,
973                     lockBpmAfterSet)) {
974             return ImportStatus::Pending;
975         } else {
976             return ImportStatus::Complete;
977         }
978     }
979 }
980 
getBeatsImportStatus() const981 Track::ImportStatus Track::getBeatsImportStatus() const {
982     QMutexLocker lock(&m_qMutex);
983     return (!m_pBeatsImporterPending || m_pBeatsImporterPending->isEmpty())
984             ? ImportStatus::Complete
985             : ImportStatus::Pending;
986 }
987 
importPendingBeatsWhileLocked()988 bool Track::importPendingBeatsWhileLocked() {
989     if (!m_pBeatsImporterPending) {
990         // Nothing to do here
991         return false;
992     }
993 
994     VERIFY_OR_DEBUG_ASSERT(!m_pBeatsImporterPending->isEmpty()) {
995         m_pBeatsImporterPending.reset();
996         return false;
997     }
998     // The sample rate can only be trusted after the audio
999     // stream has been opened.
1000     DEBUG_ASSERT(m_record.getStreamInfoFromSource());
1001     // The sample rate is supposed to be consistent
1002     DEBUG_ASSERT(m_record.getStreamInfoFromSource()->getSignalInfo().getSampleRate() ==
1003             m_record.getMetadata().getStreamInfo().getSignalInfo().getSampleRate());
1004     const auto pBeats = mixxx::BeatMap::makeBeatMap(
1005             m_record.getStreamInfoFromSource()->getSignalInfo().getSampleRate(),
1006             QString(),
1007             m_pBeatsImporterPending->importBeatsAndApplyTimingOffset(
1008                     getLocation(), *m_record.getStreamInfoFromSource()));
1009     DEBUG_ASSERT(m_pBeatsImporterPending->isEmpty());
1010     m_pBeatsImporterPending.reset();
1011     return setBeatsWhileLocked(pBeats);
1012 }
1013 
tryImportPendingBeatsMarkDirtyAndUnlock(QMutexLocker * pLock,bool lockBpmAfterSet)1014 bool Track::tryImportPendingBeatsMarkDirtyAndUnlock(
1015         QMutexLocker* pLock,
1016         bool lockBpmAfterSet) {
1017     DEBUG_ASSERT(pLock);
1018 
1019     if (m_record.getBpmLocked()) {
1020         return false;
1021     }
1022 
1023     bool modified = false;
1024     // Both functions must be invoked even if one of them
1025     // returns false!
1026     if (importPendingBeatsWhileLocked()) {
1027         modified = true;
1028     }
1029     if (compareAndSet(m_record.ptrBpmLocked(), lockBpmAfterSet)) {
1030         modified = true;
1031     }
1032     if (!modified) {
1033         // Unmodified, nothing todo
1034         return true;
1035     }
1036 
1037     afterBeatsAndBpmUpdated(pLock);
1038     return true;
1039 }
1040 
importCueInfos(mixxx::CueInfoImporterPointer pCueInfoImporter)1041 Track::ImportStatus Track::importCueInfos(
1042         mixxx::CueInfoImporterPointer pCueInfoImporter) {
1043     QMutexLocker lock(&m_qMutex);
1044     VERIFY_OR_DEBUG_ASSERT(pCueInfoImporter) {
1045         return ImportStatus::Complete;
1046     }
1047     DEBUG_ASSERT(!m_pCueInfoImporterPending);
1048     m_pCueInfoImporterPending = pCueInfoImporter;
1049     if (m_pCueInfoImporterPending->isEmpty()) {
1050         // Just return the current import status without clearing any
1051         // existing cue points.
1052         m_pCueInfoImporterPending.reset();
1053         return ImportStatus::Complete;
1054     } else if (m_record.hasStreamInfoFromSource()) {
1055         // Replace existing cue points with imported cue
1056         // points immediately
1057         importPendingCueInfosMarkDirtyAndUnlock(&lock);
1058         return ImportStatus::Complete;
1059     } else {
1060         kLogger.debug()
1061                 << "Import of"
1062                 << m_pCueInfoImporterPending->size()
1063                 << "cue(s) is pending until the actual sample rate becomes available";
1064         // Clear all existing cue points, that are supposed
1065         // to be replaced with the imported cue points soon.
1066         setCuePointsMarkDirtyAndUnlock(
1067                 &lock,
1068                 QList<CuePointer>{});
1069         return ImportStatus::Pending;
1070     }
1071 }
1072 
getCueImportStatus() const1073 Track::ImportStatus Track::getCueImportStatus() const {
1074     QMutexLocker lock(&m_qMutex);
1075     return (!m_pCueInfoImporterPending || m_pCueInfoImporterPending->isEmpty())
1076             ? ImportStatus::Complete
1077             : ImportStatus::Pending;
1078 }
1079 
setCuePointsWhileLocked(const QList<CuePointer> & cuePoints)1080 bool Track::setCuePointsWhileLocked(const QList<CuePointer>& cuePoints) {
1081     if (m_cuePoints.isEmpty() && cuePoints.isEmpty()) {
1082         // Nothing to do
1083         return false;
1084     }
1085     // Prevent inconsistencies between cue infos that have been queued
1086     // and are waiting to be imported and new cue points. At least one
1087     // of these two collections must be empty.
1088     DEBUG_ASSERT(cuePoints.isEmpty() || !m_pCueInfoImporterPending ||
1089             m_pCueInfoImporterPending->isEmpty());
1090     // disconnect existing cue points
1091     for (const auto& pCue : qAsConst(m_cuePoints)) {
1092         disconnect(pCue.get(), nullptr, this, nullptr);
1093         pCue->setTrackId(TrackId());
1094     }
1095     m_cuePoints = cuePoints;
1096     // connect new cue points
1097     for (const auto& pCue : qAsConst(m_cuePoints)) {
1098         DEBUG_ASSERT(pCue->thread() == thread());
1099         // Ensure that the track IDs are correct
1100         pCue->setTrackId(m_record.getId());
1101         // Start listening to cue point updatess AFTER setting
1102         // the track id. Otherwise we would receive unwanted
1103         // signals about changed cue points that may cause all
1104         // sorts of issues, e.g. when adding new tracks during
1105         // the library scan!
1106         connect(pCue.get(),
1107                 &Cue::updated,
1108                 this,
1109                 &Track::slotCueUpdated);
1110         if (pCue->getType() == mixxx::CueType::MainCue) {
1111             m_record.setCuePoint(CuePosition(pCue->getPosition()));
1112         }
1113     }
1114     return true;
1115 }
1116 
setCuePointsMarkDirtyAndUnlock(QMutexLocker * pLock,const QList<CuePointer> & cuePoints)1117 void Track::setCuePointsMarkDirtyAndUnlock(
1118         QMutexLocker* pLock,
1119         const QList<CuePointer>& cuePoints) {
1120     DEBUG_ASSERT(pLock);
1121 
1122     if (!setCuePointsWhileLocked(cuePoints)) {
1123         pLock->unlock();
1124         return;
1125     }
1126 
1127     markDirtyAndUnlock(pLock);
1128     emit cuesUpdated();
1129 }
1130 
importPendingCueInfosWhileLocked()1131 bool Track::importPendingCueInfosWhileLocked() {
1132     if (!m_pCueInfoImporterPending) {
1133         // Nothing to do here
1134         return false;
1135     }
1136 
1137     VERIFY_OR_DEBUG_ASSERT(!m_pCueInfoImporterPending->isEmpty()) {
1138         m_pCueInfoImporterPending.reset();
1139         return false;
1140     }
1141     // The sample rate can only be trusted after the audio
1142     // stream has been opened.
1143     DEBUG_ASSERT(m_record.getStreamInfoFromSource());
1144     const auto sampleRate =
1145             m_record.getStreamInfoFromSource()->getSignalInfo().getSampleRate();
1146     // The sample rate is supposed to be consistent
1147     DEBUG_ASSERT(sampleRate ==
1148             m_record.getMetadata().getStreamInfo().getSignalInfo().getSampleRate());
1149     const auto trackId = m_record.getId();
1150     QList<CuePointer> cuePoints;
1151     cuePoints.reserve(m_pCueInfoImporterPending->size() + m_cuePoints.size());
1152 
1153     // Preserve all existing cues with types that are not available for
1154     // importing.
1155     for (const CuePointer& pCue : qAsConst(m_cuePoints)) {
1156         if (!m_pCueInfoImporterPending->hasCueOfType(pCue->getType())) {
1157             cuePoints.append(pCue);
1158         }
1159     }
1160 
1161     const auto cueInfos =
1162             m_pCueInfoImporterPending->importCueInfosAndApplyTimingOffset(
1163                     getLocation(), m_record.getStreamInfoFromSource()->getSignalInfo());
1164     for (const auto& cueInfo : cueInfos) {
1165         CuePointer pCue(new Cue(cueInfo, sampleRate, true));
1166         // While this method could be called from any thread,
1167         // associated Cue objects should always live on the
1168         // same thread as their host, namely this->thread().
1169         pCue->moveToThread(thread());
1170         pCue->setTrackId(trackId);
1171         cuePoints.append(pCue);
1172     }
1173     DEBUG_ASSERT(m_pCueInfoImporterPending->isEmpty());
1174     m_pCueInfoImporterPending.reset();
1175     return setCuePointsWhileLocked(cuePoints);
1176 }
1177 
importPendingCueInfosMarkDirtyAndUnlock(QMutexLocker * pLock)1178 void Track::importPendingCueInfosMarkDirtyAndUnlock(
1179         QMutexLocker* pLock) {
1180     DEBUG_ASSERT(pLock);
1181 
1182     if (!importPendingCueInfosWhileLocked()) {
1183         pLock->unlock();
1184         return;
1185     }
1186 
1187     markDirtyAndUnlock(pLock);
1188     emit cuesUpdated();
1189 }
1190 
markDirty()1191 void Track::markDirty() {
1192     QMutexLocker lock(&m_qMutex);
1193     setDirtyAndUnlock(&lock, true);
1194 }
1195 
markClean()1196 void Track::markClean() {
1197     QMutexLocker lock(&m_qMutex);
1198     setDirtyAndUnlock(&lock, false);
1199 }
1200 
setDirtyAndUnlock(QMutexLocker * pLock,bool bDirty)1201 void Track::setDirtyAndUnlock(QMutexLocker* pLock, bool bDirty) {
1202     const bool dirtyChanged = m_bDirty != bDirty;
1203     m_bDirty = bDirty;
1204 
1205     const auto trackId = m_record.getId();
1206 
1207     // Unlock before emitting any signals!
1208     pLock->unlock();
1209 
1210     if (trackId.isValid()) {
1211         if (dirtyChanged) {
1212             if (bDirty) {
1213                 emit dirty(trackId);
1214             } else {
1215                 emit clean(trackId);
1216             }
1217         }
1218         if (bDirty) {
1219             // Emit a changed signal regardless if this attempted to set us dirty.
1220             emit changed(trackId);
1221         }
1222     }
1223 }
1224 
isDirty()1225 bool Track::isDirty() {
1226     QMutexLocker lock(&m_qMutex);
1227     return m_bDirty;
1228 }
1229 
1230 
markForMetadataExport()1231 void Track::markForMetadataExport() {
1232     QMutexLocker lock(&m_qMutex);
1233     m_bMarkedForMetadataExport = true;
1234     // No need to mark the track as dirty, because this flag
1235     // is transient and not stored in the database.
1236 }
1237 
isMarkedForMetadataExport() const1238 bool Track::isMarkedForMetadataExport() const {
1239     QMutexLocker lock(&m_qMutex);
1240     return m_bMarkedForMetadataExport;
1241 }
1242 
getRating() const1243 int Track::getRating() const {
1244     QMutexLocker lock(&m_qMutex);
1245     return m_record.getRating();
1246 }
1247 
setRating(int rating)1248 void Track::setRating (int rating) {
1249     QMutexLocker lock(&m_qMutex);
1250     if (compareAndSet(m_record.ptrRating(), rating)) {
1251         markDirtyAndUnlock(&lock);
1252     }
1253 }
1254 
afterKeysUpdated(QMutexLocker * pLock)1255 void Track::afterKeysUpdated(QMutexLocker* pLock) {
1256     const auto newKey = m_record.getGlobalKey();
1257     markDirtyAndUnlock(pLock);
1258     emitKeysUpdated(newKey);
1259 }
1260 
emitKeysUpdated(mixxx::track::io::key::ChromaticKey newKey)1261 void Track::emitKeysUpdated(mixxx::track::io::key::ChromaticKey newKey) {
1262     // New key might be INVALID. We don't care.
1263     emit keyUpdated(KeyUtils::keyToNumericValue(newKey));
1264     emit keysUpdated();
1265 }
1266 
setKeys(const Keys & keys)1267 void Track::setKeys(const Keys& keys) {
1268     QMutexLocker lock(&m_qMutex);
1269     m_record.setKeys(keys);
1270     afterKeysUpdated(&lock);
1271 }
1272 
resetKeys()1273 void Track::resetKeys() {
1274     QMutexLocker lock(&m_qMutex);
1275     m_record.resetKeys();
1276     afterKeysUpdated(&lock);
1277 }
1278 
getKeys() const1279 Keys Track::getKeys() const {
1280     QMutexLocker lock(&m_qMutex);
1281     return m_record.getKeys();
1282 }
1283 
setKey(mixxx::track::io::key::ChromaticKey key,mixxx::track::io::key::Source keySource)1284 void Track::setKey(mixxx::track::io::key::ChromaticKey key,
1285                    mixxx::track::io::key::Source keySource) {
1286     QMutexLocker lock(&m_qMutex);
1287     if (m_record.updateGlobalKey(key, keySource)) {
1288         afterKeysUpdated(&lock);
1289     }
1290 }
1291 
getKey() const1292 mixxx::track::io::key::ChromaticKey Track::getKey() const {
1293     QMutexLocker lock(&m_qMutex);
1294     return m_record.getGlobalKey();
1295 }
1296 
getKeyText() const1297 QString Track::getKeyText() const {
1298     QMutexLocker lock(&m_qMutex);
1299     return m_record.getGlobalKeyText();
1300 }
1301 
setKeyText(const QString & keyText,mixxx::track::io::key::Source keySource)1302 void Track::setKeyText(const QString& keyText,
1303                        mixxx::track::io::key::Source keySource) {
1304     QMutexLocker lock(&m_qMutex);
1305     if (m_record.updateGlobalKeyText(keyText, keySource)) {
1306         afterKeysUpdated(&lock);
1307     }
1308 }
1309 
setBpmLocked(bool bpmLocked)1310 void Track::setBpmLocked(bool bpmLocked) {
1311     QMutexLocker lock(&m_qMutex);
1312     if (compareAndSet(m_record.ptrBpmLocked(), bpmLocked)) {
1313         markDirtyAndUnlock(&lock);
1314     }
1315 }
1316 
isBpmLocked() const1317 bool Track::isBpmLocked() const {
1318     QMutexLocker lock(&m_qMutex);
1319     return m_record.getBpmLocked();
1320 }
1321 
setCoverInfo(const CoverInfoRelative & coverInfo)1322 void Track::setCoverInfo(const CoverInfoRelative& coverInfo) {
1323     DEBUG_ASSERT((coverInfo.type != CoverInfo::METADATA) || coverInfo.coverLocation.isEmpty());
1324     DEBUG_ASSERT((coverInfo.source != CoverInfo::UNKNOWN) || (coverInfo.type == CoverInfo::NONE));
1325     QMutexLocker lock(&m_qMutex);
1326     if (compareAndSet(m_record.ptrCoverInfo(), coverInfo)) {
1327         markDirtyAndUnlock(&lock);
1328         emit coverArtUpdated();
1329     }
1330 }
1331 
refreshCoverImageHash(const QImage & loadedImage)1332 bool Track::refreshCoverImageHash(
1333         const QImage& loadedImage) {
1334     QMutexLocker lock(&m_qMutex);
1335     auto coverInfo = CoverInfo(
1336             m_record.getCoverInfo(),
1337             m_fileInfo.location());
1338     if (!coverInfo.refreshImageHash(
1339             loadedImage,
1340             m_pSecurityToken)) {
1341         return false;
1342     }
1343     if (!compareAndSet(
1344                 m_record.ptrCoverInfo(),
1345                 static_cast<const CoverInfoRelative&>(coverInfo))) {
1346         return false;
1347     }
1348     kLogger.info()
1349             << "Refreshed cover image hash"
1350             << m_fileInfo.location();
1351     markDirtyAndUnlock(&lock);
1352     emit coverArtUpdated();
1353     return true;
1354 }
1355 
getCoverInfo() const1356 CoverInfoRelative Track::getCoverInfo() const {
1357     QMutexLocker lock(&m_qMutex);
1358     return m_record.getCoverInfo();
1359 }
1360 
getCoverInfoWithLocation() const1361 CoverInfo Track::getCoverInfoWithLocation() const {
1362     QMutexLocker lock(&m_qMutex);
1363     return CoverInfo(m_record.getCoverInfo(), m_fileInfo.location());
1364 }
1365 
getCoverHash() const1366 quint16 Track::getCoverHash() const {
1367     QMutexLocker lock(&m_qMutex);
1368     return m_record.getCoverInfo().hash;
1369 }
1370 
exportMetadata(mixxx::MetadataSourcePointer pMetadataSource,UserSettingsPointer pConfig)1371 ExportTrackMetadataResult Track::exportMetadata(
1372         mixxx::MetadataSourcePointer pMetadataSource,
1373         UserSettingsPointer pConfig) {
1374     VERIFY_OR_DEBUG_ASSERT(pMetadataSource) {
1375         kLogger.warning()
1376                 << "Cannot export track metadata:"
1377                 << getLocation();
1378         return ExportTrackMetadataResult::Failed;
1379     }
1380     // Locking shouldn't be necessary here, because this function will
1381     // be called after all references to the object have been dropped.
1382     // But it doesn't hurt much, so let's play it safe ;)
1383     QMutexLocker lock(&m_qMutex);
1384     // TODO(XXX): m_record.getMetadataSynchronized() currently is a
1385     // boolean flag, but it should become a time stamp in the future.
1386     // We could take this time stamp and the file's last modification
1387     // time stamp into account and might decide to skip importing
1388     // the metadata again.
1389     if (!m_bMarkedForMetadataExport && !m_record.getMetadataSynchronized()) {
1390         // If the metadata has never been imported from file tags it
1391         // must be exported explicitly once. This ensures that we don't
1392         // overwrite existing file tags with completely different
1393         // information.
1394         kLogger.info()
1395                 << "Skip exporting of unsynchronized track metadata:"
1396                 << getLocation();
1397         // abort
1398         return ExportTrackMetadataResult::Skipped;
1399     }
1400 
1401     if (pConfig->getValue<bool>(kConfigKeySeratoMetadataExport)) {
1402         const auto streamInfo = m_record.getStreamInfoFromSource();
1403         VERIFY_OR_DEBUG_ASSERT(streamInfo &&
1404                 streamInfo->getSignalInfo().isValid() &&
1405                 streamInfo->getDuration() > mixxx::Duration::empty()) {
1406             kLogger.warning() << "Cannot write Serato metadata because signal "
1407                                  "info and/or duration is not available:"
1408                               << getLocation();
1409             return ExportTrackMetadataResult::Skipped;
1410         }
1411 
1412         const mixxx::audio::SampleRate sampleRate =
1413                 streamInfo->getSignalInfo().getSampleRate();
1414 
1415         mixxx::SeratoTags* seratoTags = m_record.refMetadata().refTrackInfo().ptrSeratoTags();
1416         DEBUG_ASSERT(seratoTags);
1417 
1418         if (seratoTags->status() == mixxx::SeratoTags::ParserStatus::Failed) {
1419             kLogger.warning()
1420                     << "Refusing to overwrite Serato metadata that failed to parse:"
1421                     << getLocation();
1422         } else {
1423             seratoTags->setTrackColor(getColor());
1424             seratoTags->setBpmLocked(isBpmLocked());
1425 
1426             QList<mixxx::CueInfo> cueInfos;
1427             for (const CuePointer& pCue : qAsConst(m_cuePoints)) {
1428                 cueInfos.append(pCue->getCueInfo(sampleRate));
1429             }
1430 
1431             const double timingOffset = mixxx::SeratoTags::guessTimingOffsetMillis(
1432                     getLocation(), streamInfo->getSignalInfo());
1433             seratoTags->setCueInfos(cueInfos, timingOffset);
1434 
1435             seratoTags->setBeats(m_pBeats,
1436                     streamInfo->getSignalInfo(),
1437                     streamInfo->getDuration(),
1438                     timingOffset);
1439         }
1440     }
1441 
1442     // Check if the metadata has actually been modified. Otherwise
1443     // we don't need to write it back. Exporting unmodified metadata
1444     // would needlessly update the file's time stamp and should be
1445     // avoided. Since we don't know in which state the file's metadata
1446     // is we import it again into a temporary variable.
1447     mixxx::TrackMetadata importedFromFile;
1448     // Normalize metadata before exporting to adjust the precision of
1449     // floating values, ... Otherwise the following comparisons may
1450     // repeatedly indicate that values have changed only due to
1451     // rounding errors.
1452     // The normalization has to be performed on a copy of the metadata.
1453     // Otherwise floating-point values like the bpm value might become
1454     // inconsistent with the actual value stored by the beat grid!
1455     mixxx::TrackMetadata normalizedFromRecord;
1456     if ((pMetadataSource->importTrackMetadataAndCoverImage(&importedFromFile, nullptr).first ==
1457             mixxx::MetadataSource::ImportResult::Succeeded)) {
1458         // Prevent overwriting any file tags that are not yet stored in the
1459         // library database! This will in turn update the current metadata
1460         // that is stored in the database. New columns that need to be populated
1461         // from file tags cannot be filled during a database migration.
1462         m_record.mergeImportedMetadata(importedFromFile);
1463 
1464         // Prepare export by cloning and normalizing the metadata
1465         normalizedFromRecord = m_record.getMetadata();
1466         normalizedFromRecord.normalizeBeforeExport();
1467 
1468         // Finally the track's current metadata and the imported/adjusted metadata
1469         // can be compared for differences to decide whether the tags in the file
1470         // would change if we perform the write operation. This function will also
1471         // copy all extra properties that are not (yet) stored in the library before
1472         // checking for differences! If an export has been requested explicitly then
1473         // we will continue even if no differences are detected.
1474         // NOTE(uklotzde, 2020-01-05): Detection of modified bpm values is restricted
1475         // to integer precision to avoid re-exporting of unmodified ID3 tags in case
1476         // of fractional bpm values. As a consequence small changes in bpm values
1477         // cannot be detected and file tags with fractional values might not be
1478         // updated as expected! In these edge cases users need to explicitly
1479         // trigger the re-export of file tags or they could modify other metadata
1480         // properties.
1481         if (!m_bMarkedForMetadataExport &&
1482                 !normalizedFromRecord.anyFileTagsModified(
1483                         importedFromFile,
1484                         mixxx::Bpm::Comparison::Integer)) {
1485             // The file tags are in-sync with the track's metadata and don't need
1486             // to be updated.
1487             if (kLogger.debugEnabled()) {
1488                 kLogger.debug()
1489                             << "Skip exporting of unmodified track metadata into file:"
1490                             << getLocation();
1491             }
1492             // abort
1493             return ExportTrackMetadataResult::Skipped;
1494         }
1495     } else {
1496         // The file doesn't contain any tags yet or it might be missing, unreadable,
1497         // or corrupt.
1498         if (m_bMarkedForMetadataExport) {
1499             kLogger.info()
1500                     << "Adding or overwriting tags after failure to import tags from file:"
1501                     << getLocation();
1502             // Prepare export by cloning and normalizing the metadata
1503             normalizedFromRecord = m_record.getMetadata();
1504             normalizedFromRecord.normalizeBeforeExport();
1505         } else {
1506             kLogger.warning()
1507                     << "Skip exporting of track metadata after failure to import tags from file:"
1508                     << getLocation();
1509             // abort
1510             return ExportTrackMetadataResult::Skipped;
1511         }
1512     }
1513     // The track's metadata will be exported instantly. The export should
1514     // only be tried once so we reset the marker flag.
1515     m_bMarkedForMetadataExport = false;
1516     kLogger.debug()
1517             << "Old metadata (imported)"
1518             << importedFromFile;
1519     kLogger.debug()
1520             << "New metadata (modified)"
1521             << normalizedFromRecord;
1522     const auto trackMetadataExported =
1523             pMetadataSource->exportTrackMetadata(normalizedFromRecord);
1524     switch (trackMetadataExported.first) {
1525     case mixxx::MetadataSource::ExportResult::Succeeded:
1526         // After successfully exporting the metadata we record the fact
1527         // that now the file tags and the track's metadata are in sync.
1528         // This information (flag or time stamp) is stored in the database.
1529         // The database update will follow immediately after returning from
1530         // this operation!
1531         // TODO(XXX): Replace bool with QDateTime
1532         DEBUG_ASSERT(!trackMetadataExported.second.isNull());
1533         //pTrack->setMetadataSynchronized(trackMetadataExported.second);
1534         m_record.setMetadataSynchronized(!trackMetadataExported.second.isNull());
1535         if (kLogger.debugEnabled()) {
1536             kLogger.debug()
1537                     << "Exported track metadata:"
1538                     << getLocation();
1539         }
1540         return ExportTrackMetadataResult::Succeeded;
1541     case mixxx::MetadataSource::ExportResult::Unsupported:
1542         return ExportTrackMetadataResult::Skipped;
1543     case mixxx::MetadataSource::ExportResult::Failed:
1544         kLogger.warning()
1545                 << "Failed to export track metadata:"
1546                 << getLocation();
1547         return ExportTrackMetadataResult::Failed;
1548     }
1549     DEBUG_ASSERT(!"unhandled case in switch statement");
1550     return ExportTrackMetadataResult::Skipped;
1551 }
1552 
setAudioProperties(mixxx::audio::ChannelCount channelCount,mixxx::audio::SampleRate sampleRate,mixxx::audio::Bitrate bitrate,mixxx::Duration duration)1553 void Track::setAudioProperties(
1554         mixxx::audio::ChannelCount channelCount,
1555         mixxx::audio::SampleRate sampleRate,
1556         mixxx::audio::Bitrate bitrate,
1557         mixxx::Duration duration) {
1558     setAudioProperties(mixxx::audio::StreamInfo{
1559             mixxx::audio::SignalInfo{
1560                     channelCount,
1561                     sampleRate,
1562             },
1563             bitrate,
1564             duration,
1565     });
1566 }
1567 
setAudioProperties(const mixxx::audio::StreamInfo & streamInfo)1568 void Track::setAudioProperties(
1569         const mixxx::audio::StreamInfo& streamInfo) {
1570     QMutexLocker lock(&m_qMutex);
1571     // These properties are stored separately in the database
1572     // and are also imported from file tags. They will be
1573     // overriden by the actual properties from the audio
1574     // source later.
1575     DEBUG_ASSERT(!m_record.hasStreamInfoFromSource());
1576     if (compareAndSet(
1577                 m_record.refMetadata().ptrStreamInfo(),
1578                 streamInfo)) {
1579         markDirtyAndUnlock(&lock);
1580     }
1581 }
1582 
updateStreamInfoFromSource(mixxx::audio::StreamInfo && streamInfo)1583 void Track::updateStreamInfoFromSource(
1584         mixxx::audio::StreamInfo&& streamInfo) {
1585     QMutexLocker lock(&m_qMutex);
1586     bool updated = m_record.updateStreamInfoFromSource(streamInfo);
1587 
1588     const bool importBeats = m_pBeatsImporterPending && !m_pBeatsImporterPending->isEmpty();
1589     const bool importCueInfos = m_pCueInfoImporterPending && !m_pCueInfoImporterPending->isEmpty();
1590 
1591     if (!importBeats && !importCueInfos) {
1592         // Nothing more to do
1593         if (updated) {
1594             markDirtyAndUnlock(&lock);
1595         }
1596         return;
1597     }
1598 
1599     auto beatsImported = false;
1600     if (importBeats) {
1601         kLogger.debug() << "Finishing deferred import of beats because stream "
1602                            "audio properties are available now";
1603         beatsImported = importPendingBeatsWhileLocked();
1604     }
1605 
1606     auto cuesImported = false;
1607     if (importCueInfos) {
1608         DEBUG_ASSERT(m_cuePoints.isEmpty());
1609         kLogger.debug()
1610                 << "Finishing deferred import of"
1611                 << m_pCueInfoImporterPending->size()
1612                 << "cue(s) because stream audio properties are available now";
1613         cuesImported = importPendingCueInfosWhileLocked();
1614     }
1615 
1616     if (!beatsImported && !cuesImported) {
1617         return;
1618     }
1619 
1620     if (beatsImported) {
1621         afterBeatsAndBpmUpdated(&lock);
1622     } else {
1623         markDirtyAndUnlock(&lock);
1624     }
1625     if (cuesImported) {
1626         emit cuesUpdated();
1627     }
1628 }
1629