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