Skip to content

Commit

Permalink
Make sure Loop cue Indexes are pereserved in a file metadata roundtrip
Browse files Browse the repository at this point in the history
  • Loading branch information
daschuer committed Apr 30, 2023
1 parent c7f7ec1 commit 383f682
Showing 1 changed file with 67 additions and 86 deletions.
153 changes: 67 additions & 86 deletions src/track/serato/tags.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,36 +40,6 @@ QString getPrimaryDecoderNameForFilePath(const QString& filePath) {
constexpr int kFirstLoopIndex = mixxx::kFirstHotCueIndex + 8;
constexpr int kNumCuesInMarkersTag = 5;

std::optional<int> 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 &&
Expand All @@ -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;
}

Expand Down Expand Up @@ -219,75 +192,83 @@ QList<CueInfo> SeratoTags::getCueInfos() const {
// "Serato Markers_" and "Serato Markers2" contradict each other,
// Serato will use the values from "Serato Markers_").

QMap<int, CueInfo> cueMap;
QMap<int, CueInfo> hotCueMap;
QMap<int, CueInfo> loopCueMap;
const QList<CueInfo> cuesMarkers2 = m_seratoMarkers2.getCues();
for (const CueInfo& cueInfo : cuesMarkers2) {
if (!isCueInfoValid(cueInfo)) {
continue;
}
std::optional<int> 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<int> unsetCuesInMarkersTag;
for (int i = 0; i < kNumCuesInMarkersTag; i++) {
unsetCuesInMarkersTag.insert(i);
}

const QList<CueInfo> cuesMarkers = m_seratoMarkers.getCues();
for (const CueInfo& cueInfo : cuesMarkers) {
if (!isCueInfoValid(cueInfo)) {
continue;
}
std::optional<int> 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<int> 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<CueInfo>& cueInfos, double timingOffsetMillis) {
Expand Down

0 comments on commit 383f682

Please sign in to comment.