diff --git a/src/track/serato/tags.cpp b/src/track/serato/tags.cpp index a6fc2870f71..968abc87568 100644 --- a/src/track/serato/tags.cpp +++ b/src/track/serato/tags.cpp @@ -40,36 +40,6 @@ QString getPrimaryDecoderNameForFilePath(const QString& filePath) { constexpr int kFirstLoopIndex = mixxx::kFirstHotCueIndex + 8; constexpr int kNumCuesInMarkersTag = 5; -std::optional findIndexForCueInfo(const mixxx::CueInfo& cueInfo) { - VERIFY_OR_DEBUG_ASSERT(cueInfo.getHotCueIndex()) { - qWarning() << "SeratoTags::getCues: Cue without number found!"; - return std::nullopt; - } - - int index = *cueInfo.getHotCueIndex(); - VERIFY_OR_DEBUG_ASSERT(index >= mixxx::kFirstHotCueIndex) { - qWarning() << "SeratoTags::getCues: Cue with number < 0 found!"; - return std::nullopt; - } - - switch (cueInfo.getType()) { - case mixxx::CueType::HotCue: - if (index >= kFirstLoopIndex) { - qWarning() - << "SeratoTags::getCues: Non-loop Cue with number >=" - << kFirstLoopIndex << "found!"; - return std::nullopt; - } - break; - case mixxx::CueType::Loop: - index += kFirstLoopIndex; - break; - default: - return std::nullopt; - } - - return index; -} bool isCueInfoValid(const mixxx::CueInfo& cueInfo) { if (cueInfo.getType() == mixxx::CueType::Loop && @@ -87,6 +57,9 @@ bool isCueInfoValid(const mixxx::CueInfo& cueInfo) { << "at position 0"; return false; } + VERIFY_OR_DEBUG_ASSERT(cueInfo.getHotCueIndex()) { + return false; + } return true; } @@ -219,75 +192,83 @@ QList SeratoTags::getCueInfos() const { // "Serato Markers_" and "Serato Markers2" contradict each other, // Serato will use the values from "Serato Markers_"). - QMap cueMap; + QMap hotCueMap; + QMap loopCueMap; const QList cuesMarkers2 = m_seratoMarkers2.getCues(); for (const CueInfo& cueInfo : cuesMarkers2) { if (!isCueInfoValid(cueInfo)) { continue; } - std::optional index = findIndexForCueInfo(cueInfo); - if (!index) { - continue; + if (cueInfo.getType() == CueType::Loop) { + loopCueMap.insert(*cueInfo.getHotCueIndex(), cueInfo); + } else { + hotCueMap.insert(*cueInfo.getHotCueIndex(), cueInfo); } - CueInfo newCueInfo(cueInfo); - newCueInfo.setHotCueIndex(*index); - cueMap.insert(*index, newCueInfo); }; - // If the "Serato Markers_" tag does not exist at all, Serato DJ Pro just - // takes data from the "Serato Markers2" tag, so we can exit early - // here. If the "Serato Markers_" exists, its data will take precedence. - if (m_seratoMarkers.isEmpty()) { - return cueMap.values(); - } - - // The "Serato Markers_" tag always contains entries for the first five - // cues. If a cue is not set, that entry is present but empty. - // If a cue is set in "Serato Markers2" but not in "Serato Markers_", - // Serato DJ Pro considers it as "not set" and ignores it. - // To mirror the behaviour of Serato, we need to remove from the output of - // this function. - QSet unsetCuesInMarkersTag; - for (int i = 0; i < kNumCuesInMarkersTag; i++) { - unsetCuesInMarkersTag.insert(i); - } - const QList cuesMarkers = m_seratoMarkers.getCues(); - for (const CueInfo& cueInfo : cuesMarkers) { - if (!isCueInfoValid(cueInfo)) { - continue; - } - std::optional index = findIndexForCueInfo(cueInfo); - if (!index) { - continue; + if (cuesMarkers.size()) { + // The "Serato Markers_" tag always contains entries for the first five + // cues. If a cue is not set, that entry is present but empty. + // If a cue is set in "Serato Markers2" but not in "Serato Markers_", + // Serato DJ Pro considers it as "not set" and ignores it. + // To mirror the behaviour of Serato, we need to remove from the output of + // this function. + QSet unsetCuesInMarkersTag; + unsetCuesInMarkersTag.reserve(kNumCuesInMarkersTag); + for (int i = 0; i < kNumCuesInMarkersTag; i++) { + unsetCuesInMarkersTag.insert(i); } - // Take a pre-existing CueInfo object that was read from - // "SeratoMarkers2" from the CueMap (or a default constructed CueInfo - // object if none exists) and use it as template for the new CueInfo - // object. Then overwrite all object values that are present in the - // "SeratoMarkers_"tag. - CueInfo newCueInfo = cueMap.value(*index); - newCueInfo.setType(cueInfo.getType()); - newCueInfo.setStartPositionMillis(cueInfo.getStartPositionMillis()); - newCueInfo.setEndPositionMillis(cueInfo.getEndPositionMillis()); - newCueInfo.setHotCueIndex(*index); - newCueInfo.setFlags(cueInfo.flags()); - newCueInfo.setColor(cueInfo.getColor()); - cueMap.insert(*index, newCueInfo); - - // This cue is set in the "Serato Markers_" tag, so remove it from the - // set of unset cues - unsetCuesInMarkersTag.remove(*index); - }; - - // Now that we know which cues should be present in the "Serato Markers_" - // tag but aren't, remove them from the set. - for (const int index : unsetCuesInMarkersTag) { - cueMap.remove(index); + for (const CueInfo& cueInfo : cuesMarkers) { + if (!isCueInfoValid(cueInfo)) { + continue; + } + // Take a pre-existing CueInfo object that was read from + // "SeratoMarkers2" from the CueMap (or a default constructed CueInfo + // object if none exists) and use it as template for the new CueInfo + // object. Then overwrite all object values that are present in the + // "SeratoMarkers_"tag. + CueInfo& newCueInfo = cueInfo.getType() == CueType::Loop + ? loopCueMap[*cueInfo.getHotCueIndex()] + : hotCueMap[*cueInfo.getHotCueIndex()]; + newCueInfo.setType(cueInfo.getType()); + newCueInfo.setStartPositionMillis(cueInfo.getStartPositionMillis()); + newCueInfo.setEndPositionMillis(cueInfo.getEndPositionMillis()); + newCueInfo.setHotCueIndex(*cueInfo.getHotCueIndex()); + newCueInfo.setFlags(cueInfo.flags()); + newCueInfo.setColor(cueInfo.getColor()); + + // This cue is set in the "Serato Markers_" tag, so remove it from the + // set of unset cues + unsetCuesInMarkersTag.remove(*cueInfo.getHotCueIndex()); + }; + + // Now that we know which cues should be present in the "Serato Markers_" + // tag but aren't, remove them from the set. + for (const int index : unsetCuesInMarkersTag) { + hotCueMap.remove(index); + } } - return cueMap.values(); + // Serato has a separate indexing for loop and hot cues. + // We sort the Loops Cues into the HotCue map if the given index is free, + // add an offset of 8 otherwise or loop until we find a free index. + // Cues above index 8 are not accessible in Mixxx, only visible in waveforms + // During export the Mixxx indexes are kept to allow a perfect round-trip. + for (const CueInfo& loopCueInfo : qAsConst(loopCueMap)) { + if (hotCueMap.contains(*loopCueInfo.getHotCueIndex())) { + CueInfo cueInfo = loopCueInfo; + cueInfo.setHotCueIndex(*loopCueInfo.getHotCueIndex() + kFirstLoopIndex); + while (hotCueMap.contains(*cueInfo.getHotCueIndex())) { + cueInfo.setHotCueIndex(*cueInfo.getHotCueIndex() + 1); + } + hotCueMap.insert(*cueInfo.getHotCueIndex(), cueInfo); + } else { + hotCueMap.insert(*loopCueInfo.getHotCueIndex(), loopCueInfo); + } + } + return hotCueMap.values(); } void SeratoTags::setCueInfos(const QList& cueInfos, double timingOffsetMillis) {