diff --git a/CMakeLists.txt b/CMakeLists.txt index f11ce8c997e..84e8f6a2654 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -514,7 +514,8 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/track/keyutils.cpp src/track/playcounter.cpp src/track/replaygain.cpp - src/track/seratomarkers2.cpp + src/track/serato/markers.cpp + src/track/serato/markers2.cpp src/track/track.cpp src/track/trackfile.cpp src/track/trackinfo.cpp @@ -966,6 +967,7 @@ add_executable(mixxx-test src/test/sampleutiltest.cpp src/test/schemamanager_test.cpp src/test/searchqueryparsertest.cpp + src/test/seratomarkerstest.cpp src/test/seratomarkers2test.cpp src/test/signalpathtest.cpp src/test/skincontext_test.cpp diff --git a/build/depends.py b/build/depends.py index f558598ac96..226037eac39 100644 --- a/build/depends.py +++ b/build/depends.py @@ -1205,7 +1205,8 @@ def sources(self, build): "src/track/keyutils.cpp", "src/track/playcounter.cpp", "src/track/replaygain.cpp", - "src/track/seratomarkers2.cpp", + "src/track/serato/markers.cpp", + "src/track/serato/markers2.cpp", "src/track/track.cpp", "src/track/globaltrackcache.cpp", "src/track/trackfile.cpp", diff --git a/src/test/seratomarkers2-data/bpmlock-enabled.octet-stream b/src/test/serato/data/markers2/bpmlock-enabled.octet-stream similarity index 100% rename from src/test/seratomarkers2-data/bpmlock-enabled.octet-stream rename to src/test/serato/data/markers2/bpmlock-enabled.octet-stream diff --git a/src/test/seratomarkers2-data/flips.octet-stream b/src/test/serato/data/markers2/flips.octet-stream similarity index 100% rename from src/test/seratomarkers2-data/flips.octet-stream rename to src/test/serato/data/markers2/flips.octet-stream diff --git a/src/test/seratomarkers2-data/hotcue-00m00s-red.octet-stream b/src/test/serato/data/markers2/hotcue-00m00s-red.octet-stream similarity index 100% rename from src/test/seratomarkers2-data/hotcue-00m00s-red.octet-stream rename to src/test/serato/data/markers2/hotcue-00m00s-red.octet-stream diff --git a/src/test/seratomarkers2-data/hotcue-00m00s0-blue.octet-stream b/src/test/serato/data/markers2/hotcue-00m00s0-blue.octet-stream similarity index 100% rename from src/test/seratomarkers2-data/hotcue-00m00s0-blue.octet-stream rename to src/test/serato/data/markers2/hotcue-00m00s0-blue.octet-stream diff --git a/src/test/seratomarkers2-data/hotcue-colors.octet-stream b/src/test/serato/data/markers2/hotcue-colors.octet-stream similarity index 100% rename from src/test/seratomarkers2-data/hotcue-colors.octet-stream rename to src/test/serato/data/markers2/hotcue-colors.octet-stream diff --git a/src/test/seratomarkers2-data/hotcue-positions-00m00s0-03m38s4-01m00s0-00m00s1-00m01s0.octet-stream b/src/test/serato/data/markers2/hotcue-positions-00m00s0-03m38s4-01m00s0-00m00s1-00m01s0.octet-stream similarity index 100% rename from src/test/seratomarkers2-data/hotcue-positions-00m00s0-03m38s4-01m00s0-00m00s1-00m01s0.octet-stream rename to src/test/serato/data/markers2/hotcue-positions-00m00s0-03m38s4-01m00s0-00m00s1-00m01s0.octet-stream diff --git a/src/test/seratomarkers2-data/hotcues-with-names.octet-stream b/src/test/serato/data/markers2/hotcues-with-names.octet-stream similarity index 100% rename from src/test/seratomarkers2-data/hotcues-with-names.octet-stream rename to src/test/serato/data/markers2/hotcues-with-names.octet-stream diff --git a/src/test/seratomarkers2-data/saved-loops.octet-stream b/src/test/serato/data/markers2/saved-loops.octet-stream similarity index 100% rename from src/test/seratomarkers2-data/saved-loops.octet-stream rename to src/test/serato/data/markers2/saved-loops.octet-stream diff --git a/src/test/seratomarkers2-data/tracklist-color.octet-stream b/src/test/serato/data/markers2/tracklist-color.octet-stream similarity index 100% rename from src/test/seratomarkers2-data/tracklist-color.octet-stream rename to src/test/serato/data/markers2/tracklist-color.octet-stream diff --git a/src/test/seratomarkers2-data/very-long-names.octet-stream b/src/test/serato/data/markers2/very-long-names.octet-stream similarity index 100% rename from src/test/seratomarkers2-data/very-long-names.octet-stream rename to src/test/serato/data/markers2/very-long-names.octet-stream diff --git a/src/test/serato/data/markers_/analyzed.octet-stream b/src/test/serato/data/markers_/analyzed.octet-stream new file mode 100644 index 00000000000..c50e827d9a6 Binary files /dev/null and b/src/test/serato/data/markers_/analyzed.octet-stream differ diff --git a/src/test/serato/data/markers_/bpmlock-enabled.octet-stream b/src/test/serato/data/markers_/bpmlock-enabled.octet-stream new file mode 100644 index 00000000000..c50e827d9a6 Binary files /dev/null and b/src/test/serato/data/markers_/bpmlock-enabled.octet-stream differ diff --git a/src/test/serato/data/markers_/flips.octet-stream b/src/test/serato/data/markers_/flips.octet-stream new file mode 100644 index 00000000000..222c3677fa3 Binary files /dev/null and b/src/test/serato/data/markers_/flips.octet-stream differ diff --git a/src/test/serato/data/markers_/hotcue-00m00s-red.octet-stream b/src/test/serato/data/markers_/hotcue-00m00s-red.octet-stream new file mode 100644 index 00000000000..510a62891db Binary files /dev/null and b/src/test/serato/data/markers_/hotcue-00m00s-red.octet-stream differ diff --git a/src/test/serato/data/markers_/hotcue-00m00s0-blue.octet-stream b/src/test/serato/data/markers_/hotcue-00m00s0-blue.octet-stream new file mode 100644 index 00000000000..4e8468b065d Binary files /dev/null and b/src/test/serato/data/markers_/hotcue-00m00s0-blue.octet-stream differ diff --git a/src/test/serato/data/markers_/hotcue-colors.octet-stream b/src/test/serato/data/markers_/hotcue-colors.octet-stream new file mode 100644 index 00000000000..38d25788389 Binary files /dev/null and b/src/test/serato/data/markers_/hotcue-colors.octet-stream differ diff --git a/src/test/serato/data/markers_/hotcue-positions-00m00s0-03m38s4-01m00s0-00m00s1-00m01s0.octet-stream b/src/test/serato/data/markers_/hotcue-positions-00m00s0-03m38s4-01m00s0-00m00s1-00m01s0.octet-stream new file mode 100644 index 00000000000..efc3d8c895a Binary files /dev/null and b/src/test/serato/data/markers_/hotcue-positions-00m00s0-03m38s4-01m00s0-00m00s1-00m01s0.octet-stream differ diff --git a/src/test/serato/data/markers_/hotcues-with-names.octet-stream b/src/test/serato/data/markers_/hotcues-with-names.octet-stream new file mode 100644 index 00000000000..0e1b2530ef0 Binary files /dev/null and b/src/test/serato/data/markers_/hotcues-with-names.octet-stream differ diff --git a/src/test/serato/data/markers_/saved-loops.octet-stream b/src/test/serato/data/markers_/saved-loops.octet-stream new file mode 100644 index 00000000000..06ef374a3b1 Binary files /dev/null and b/src/test/serato/data/markers_/saved-loops.octet-stream differ diff --git a/src/test/serato/data/markers_/tracklist-color.octet-stream b/src/test/serato/data/markers_/tracklist-color.octet-stream new file mode 100644 index 00000000000..9b3e6208500 Binary files /dev/null and b/src/test/serato/data/markers_/tracklist-color.octet-stream differ diff --git a/src/test/serato/data/markers_/very-long-names.octet-stream b/src/test/serato/data/markers_/very-long-names.octet-stream new file mode 100644 index 00000000000..92e7638c89e Binary files /dev/null and b/src/test/serato/data/markers_/very-long-names.octet-stream differ diff --git a/src/test/seratomarkers2test.cpp b/src/test/seratomarkers2test.cpp index fc53f11e655..f38c908245e 100644 --- a/src/test/seratomarkers2test.cpp +++ b/src/test/seratomarkers2test.cpp @@ -1,6 +1,6 @@ #include -#include "track/seratomarkers2.h" +#include "track/serato/markers2.h" #include "util/memory.h" #include @@ -23,10 +23,10 @@ class SeratoMarkers2Test : public testing::Test { EXPECT_EQ(locked, bpmlockEntry->isLocked()); - EXPECT_EQ(inputValue, bpmlockEntry->data()); + EXPECT_EQ(inputValue, bpmlockEntry->dump()); } - void parseColorEntry(const QByteArray inputValue, bool valid, QColor color) { + void parseColorEntry(const QByteArray inputValue, bool valid, mixxx::RgbColor color) { const mixxx::SeratoMarkers2EntryPointer parsedEntry = mixxx::SeratoMarkers2ColorEntry::parse(inputValue); if (!parsedEntry) { EXPECT_FALSE(valid); @@ -37,10 +37,16 @@ class SeratoMarkers2Test : public testing::Test { EXPECT_EQ(color, colorEntry->getColor()); - EXPECT_EQ(inputValue, colorEntry->data()); + EXPECT_EQ(inputValue, colorEntry->dump()); } - void parseCueEntry(const QByteArray inputValue, bool valid, quint8 index, quint32 position, QColor color, QString label) { + void parseCueEntry( + const QByteArray inputValue, + bool valid, + quint8 index, + quint32 position, + mixxx::RgbColor color, + QString label) { const mixxx::SeratoMarkers2EntryPointer parsedEntry = mixxx::SeratoMarkers2CueEntry::parse(inputValue); if (!parsedEntry) { EXPECT_FALSE(valid); @@ -54,10 +60,16 @@ class SeratoMarkers2Test : public testing::Test { EXPECT_EQ(color, cueEntry->getColor()); EXPECT_EQ(label, cueEntry->getLabel()); - EXPECT_EQ(inputValue, cueEntry->data()); + EXPECT_EQ(inputValue, cueEntry->dump()); } - void parseLoopEntry(const QByteArray inputValue, bool valid, quint8 index, quint32 startposition, quint32 endposition, bool locked, QString label) { + void parseLoopEntry(const QByteArray inputValue, + bool valid, + quint8 index, + quint32 startposition, + quint32 endposition, + bool locked, + QString label) { const mixxx::SeratoMarkers2EntryPointer parsedEntry = mixxx::SeratoMarkers2LoopEntry::parse(inputValue); if (!parsedEntry) { EXPECT_FALSE(valid); @@ -72,8 +84,8 @@ class SeratoMarkers2Test : public testing::Test { EXPECT_EQ(locked, loopEntry->isLocked()); EXPECT_EQ(label, loopEntry->getLabel()); - EXPECT_EQ(inputValue, loopEntry->data()); - } + EXPECT_EQ(inputValue, loopEntry->dump()); + } void parseMarkers2Data(const QByteArray inputValue, bool valid) { mixxx::SeratoMarkers2 seratoMarkers2; @@ -82,7 +94,7 @@ class SeratoMarkers2Test : public testing::Test { if (!parseOk) { return; } - EXPECT_EQ(inputValue, seratoMarkers2.data()); + EXPECT_EQ(inputValue, seratoMarkers2.dump()); } }; @@ -93,70 +105,103 @@ TEST_F(SeratoMarkers2Test, ParseBpmlockEntry) { } TEST_F(SeratoMarkers2Test, ParseColorEntry) { - parseColorEntry(QByteArray("\x00\xcc\x00\x00", 4), true, QColor(0xcc, 0, 0)); - parseColorEntry(QByteArray("\x00\x00\xcc\x00", 4), true, QColor(0, 0xcc, 0)); - parseColorEntry(QByteArray("\x00\x00\x00\xcc", 4), true, QColor(0, 0, 0xcc)); - parseColorEntry(QByteArray("\x00\x89\xab\xcd", 4), true, QColor(0x89, 0xab, 0xcd)); + parseColorEntry(QByteArray("\x00\xcc\x00\x00", 4), true, mixxx::RgbColor(qRgb(0xcc, 0, 0))); + parseColorEntry(QByteArray("\x00\x00\xcc\x00", 4), true, mixxx::RgbColor(qRgb(0, 0xcc, 0))); + parseColorEntry(QByteArray("\x00\x00\x00\xcc", 4), true, mixxx::RgbColor(qRgb(0, 0, 0xcc))); + parseColorEntry(QByteArray("\x00\x89\xab\xcd", 4), true, mixxx::RgbColor(qRgb(0x89, 0xab, 0xcd))); // Invalid value - parseColorEntry(QByteArray("\x01\xff\x00\x00", 1), false, QColor()); + parseColorEntry(QByteArray("\x01\xff\x00\x00", 1), false, mixxx::RgbColor(qRgb(0, 0, 0))); // Invalid size - parseColorEntry(QByteArray("\x00", 1), false, QColor()); - parseColorEntry(QByteArray("\x00\xff\x00\x00\x00", 5), false, QColor()); + parseColorEntry(QByteArray("\x00", 1), false, mixxx::RgbColor(qRgb(0, 0, 0))); + parseColorEntry(QByteArray("\x00\xff\x00\x00\x00", 5), false, mixxx::RgbColor(qRgb(0, 0, 0))); } TEST_F(SeratoMarkers2Test, ParseCueEntry) { parseCueEntry( - QByteArray("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 13), - true, - 0, 0, QColor(0, 0, 0), QString("")); + QByteArray("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 13), + true, + 0, + 0, + mixxx::RgbColor(qRgb(0, 0, 0)), + QString("")); parseCueEntry( - QByteArray("\x00\x01\x00\x00\x10\x00\x00\xcc\x00\x00\x00\x00Test\x00", 17), - true, - 1, 4096, QColor(0xcc, 0, 0), QString("Test")); + QByteArray("\x00\x01\x00\x00\x10\x00\x00\xcc\x00\x00\x00\x00Test\x00", 17), + true, + 1, + 4096, + mixxx::RgbColor(qRgb(0xcc, 0, 0)), + QString("Test")); parseCueEntry( - QByteArray("\x00\x02\x00\x00\x00\xff\x00\x00\xcc\x00\x00\x00\xc3\xa4\xc3\xbc\xc3\xb6\xc3\x9f\xc3\xa9\xc3\xa8!\x00", 26), - true, - 2, 255, QColor(0, 0xcc, 0), QString("äüößéè!")); + QByteArray("\x00\x02\x00\x00\x00\xff\x00\x00\xcc\x00\x00\x00\xc3\xa4\xc3\xbc\xc3\xb6\xc3\x9f\xc3\xa9\xc3\xa8!\x00", 26), + true, + 2, + 255, + mixxx::RgbColor(qRgb(0, 0xcc, 0)), + QString("äüößéè!")); parseCueEntry( - QByteArray("\x00\x03\x02\x03\x04\x05\x00\x06\x07\x08\x00\x00Hello World\x00", 24), - true, - 3, 33752069, QColor(0x06, 0x07, 0x08), QString("Hello World")); + QByteArray("\x00\x03\x02\x03\x04\x05\x00\x06\x07\x08\x00\x00Hello World\x00", 24), + true, + 3, + 33752069, + mixxx::RgbColor(qRgb(0x06, 0x07, 0x08)), + QString("Hello World")); // Invalid value parseCueEntry( - QByteArray("\x01\x04\x00\x00\x10\x00\x00\xcc\x00\x00\x00\x00Test\x00", 17), - false, - 0, 0, QColor(), QString()); + QByteArray("\x01\x04\x00\x00\x10\x00\x00\xcc\x00\x00\x00\x00Test\x00", 17), + false, + 0, + 0, + mixxx::RgbColor(qRgb(0, 0, 0)), + QString()); parseCueEntry( - QByteArray("\x00\x05\x00\x00\x10\x00\x01\xcc\x00\x00\x00\x00Test\x00", 17), - false, - 0, 0, QColor(), QString()); + QByteArray("\x00\x05\x00\x00\x10\x00\x01\xcc\x00\x00\x00\x00Test\x00", 17), + false, + 0, + 0, + mixxx::RgbColor(qRgb(0, 0, 0)), + QString()); parseCueEntry( - QByteArray("\x00\x06\x00\x00\x10\x00\x00\xcc\x00\x00\x01\x00Test\x00", 17), - false, - 0, 0, QColor(), QString()); + QByteArray("\x00\x06\x00\x00\x10\x00\x00\xcc\x00\x00\x01\x00Test\x00", 17), + false, + 0, + 0, + mixxx::RgbColor(qRgb(0, 0, 0)), + QString()); parseCueEntry( - QByteArray("\x00\x07\x00\x00\x10\x00\x00\xcc\x00\x00\x00\x01Test\x00", 17), - false, - 0, 0, QColor(), QString()); + QByteArray("\x00\x07\x00\x00\x10\x00\x00\xcc\x00\x00\x00\x01Test\x00", 17), + false, + 0, + 0, + mixxx::RgbColor(qRgb(0, 0, 0)), + QString()); // Missing null terminator parseCueEntry( - QByteArray("\x00\x08\x00\x00\x10\x00\x00\xcc\x00\x00\x00\x00Test", 16), - false, - 0, 0, QColor(), QString()); + QByteArray("\x00\x08\x00\x00\x10\x00\x00\xcc\x00\x00\x00\x00Test", 16), + false, + 0, + 0, + mixxx::RgbColor(qRgb(0, 0, 0)), + QString()); //Invalid size parseCueEntry( - QByteArray("\x00\x09\x00\x00\x10\x00\x00\xcc\x00\x00\x00\x00", 12), - false, - 0, 0, QColor(), QString()); + QByteArray("\x00\x09\x00\x00\x10\x00\x00\xcc\x00\x00\x00\x00", 12), + false, + 0, + 0, + mixxx::RgbColor(qRgb(0, 0, 0)), + QString()); parseCueEntry( - QByteArray("\x00\x0a\x00\x00\x10\x00\x00\xcc\x00\x00\x00\x00\x00\x00", 14), - false, - 0, 0, QColor(), QString()); + QByteArray("\x00\x0a\x00\x00\x10\x00\x00\xcc\x00\x00\x00\x00\x00\x00", 14), + false, + 0, + 0, + mixxx::RgbColor(qRgb(0, 0, 0)), + QString()); } TEST_F(SeratoMarkers2Test, ParseLoopEntry) { @@ -213,7 +258,7 @@ TEST_F(SeratoMarkers2Test, ParseLoopEntry) { } TEST_F(SeratoMarkers2Test, ParseMarkers2Data) { - QDir dir("src/test/seratomarkers2-data"); + QDir dir("src/test/serato/data/markers2"); dir.setFilter(QDir::Files); dir.setNameFilters(QStringList() << "*.octet-stream"); diff --git a/src/test/seratomarkerstest.cpp b/src/test/seratomarkerstest.cpp new file mode 100644 index 00000000000..2c5bf19fd1c --- /dev/null +++ b/src/test/seratomarkerstest.cpp @@ -0,0 +1,158 @@ +#include +#include +#include + +#include +#include + +#include "track/serato/markers.h" +#include "util/memory.h" + +namespace { + +class SeratoMarkersTest : public testing::Test { + protected: + void parseEntry( + const QByteArray& inputValue, + bool valid, + bool hasStartPosition, + quint32 startPosition, + bool hasEndPosition, + quint32 endPosition, + mixxx::RgbColor color, + mixxx::SeratoMarkersEntry::TypeId typeId, + bool isLocked) { + const mixxx::SeratoMarkersEntryPointer pEntry = mixxx::SeratoMarkersEntry::parse(inputValue); + if (!pEntry) { + EXPECT_FALSE(valid); + return; + } + EXPECT_TRUE(valid); + + EXPECT_EQ(hasStartPosition, pEntry->hasStartPosition()); + EXPECT_EQ(startPosition, pEntry->getStartPosition()); + EXPECT_EQ(hasEndPosition, pEntry->hasEndPosition()); + EXPECT_EQ(endPosition, pEntry->getEndPosition()); + EXPECT_EQ(color, pEntry->getColor()); + EXPECT_EQ(typeId, pEntry->typeId()); + EXPECT_EQ(isLocked, pEntry->isLocked()); + + EXPECT_EQ(inputValue, pEntry->dump()); + } + + void parseMarkersData(const QByteArray& inputValue, bool valid) { + mixxx::SeratoMarkers seratoMarkers; + bool parseOk = mixxx::SeratoMarkers::parse(&seratoMarkers, inputValue); + EXPECT_EQ(valid, parseOk); + if (!parseOk) { + return; + } + + EXPECT_EQ(inputValue, seratoMarkers.dump()); + } +}; + +TEST_F(SeratoMarkersTest, ParseEntry) { + parseEntry( + QByteArray("\x00\x00\x00\x00\x00\x7f\x7f\x7f\x7f\x7f\x00\x7f\x7f\x7f\x7f\x7f\x06\x30\x00\x00\x01", 21), + false, + false, + 0x7f7f7f7f, + false, + 0x7f7f7f7f, + mixxx::RgbColor(0x000000), + mixxx::SeratoMarkersEntry::TypeId::Unknown, + false); + parseEntry( + QByteArray("\x00\x00\x00\x00\x00\x7f\x7f\x7f\x7f\x7f\x00\x7f\x7f\x7f\x7f\x7f\x06\x30\x00\x00\x01\x00", 22), + true, + true, + 0, + false, + 0x7f7f7f7f, + mixxx::RgbColor(0xcc0000), + mixxx::SeratoMarkersEntry::TypeId::Cue, + false); + parseEntry( + QByteArray("\x00\x00\x0d\x2a\x58\x7f\x7f\x7f\x7f\x7f\x00\x7f\x7f\x7f\x7f\x7f\x06\x32\x10\x00\x01\x00", 22), + true, + true, + 862808, + false, + 0x7f7f7f7f, + mixxx::RgbColor(0xcc8800), + mixxx::SeratoMarkersEntry::TypeId::Cue, + false); + parseEntry( + QByteArray("\x00\x00\x03\x54\x64\x7f\x7f\x7f\x7f\x7f\x00\x7f\x7f\x7f\x7f\x7f\x00\x00\x01\x4c\x01\x00", 22), + true, + true, + 218212, + false, + 0x7f7f7f7f, + mixxx::RgbColor(0x0000cc), + mixxx::SeratoMarkersEntry::TypeId::Cue, + false); + parseEntry( + QByteArray("\x00\x00\x00\x00\x6c\x7f\x7f\x7f\x7f\x7f\x00\x7f\x7f\x7f\x7f\x7f\x06\x33\x18\x00\x01\x00", 22), + true, + true, + 108, + false, + 0x7f7f7f7f, + mixxx::RgbColor(0xcccc00), + mixxx::SeratoMarkersEntry::TypeId::Cue, + false); + parseEntry( + QByteArray("\x00\x00\x00\x07\x77\x7f\x7f\x7f\x7f\x7f\x00\x7f\x7f\x7f\x7f\x7f\x00\x03\x18\x00\x01\x00", 22), + true, + true, + 1911, + false, + 0x7f7f7f7f, + mixxx::RgbColor(0x00cc00), + mixxx::SeratoMarkersEntry::TypeId::Cue, + false); + parseEntry( + QByteArray("\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x00\x7f\x7f\x7f\x7f\x7f\x00\x00\x00\x00\x01\x00", 22), + true, + false, + 0x7f7f7f7f, + false, + 0x7f7f7f7f, + mixxx::RgbColor(0x000000), + mixxx::SeratoMarkersEntry::TypeId::Cue, + false); + parseEntry( + QByteArray("\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x00\x7f\x7f\x7f\x7f\x7f\x00\x00\x00\x00\x03\x00", 22), + true, + false, + 0x7f7f7f7f, + false, + 0x7f7f7f7f, + mixxx::RgbColor(0x000000), + mixxx::SeratoMarkersEntry::TypeId::Loop, + false); +} + +TEST_F(SeratoMarkersTest, ParseMarkersData) { + QDir dir("src/test/serato/data/markers_"); + dir.setFilter(QDir::Files); + dir.setNameFilters(QStringList() << "*.octet-stream"); + + QFileInfoList list = dir.entryInfoList(); + for (int i = 0; i < list.size(); i++) { + QFileInfo fileInfo = list.at(i); + qDebug() << "--- File:" << fileInfo.fileName(); + QFile file(dir.filePath(fileInfo.fileName())); + bool openOk = file.open(QIODevice::ReadOnly); + EXPECT_TRUE(openOk); + if (!openOk) { + continue; + } + QByteArray data = file.readAll(); + parseMarkersData(data, true); + } +} + +} // namespace diff --git a/src/track/serato/markers.cpp b/src/track/serato/markers.cpp new file mode 100644 index 00000000000..d3675b7a4cf --- /dev/null +++ b/src/track/serato/markers.cpp @@ -0,0 +1,269 @@ +#include "track/serato/markers.h" + +#include + +#include "util/color/rgbcolor.h" + +namespace { + +const int kNumEntries = 14; +const int kLoopEntryStartIndex = 5; +const int kEntrySize = 22; +const quint16 kVersion = 0x0205; +constexpr mixxx::RgbColor kDefaultTrackColor = mixxx::RgbColor(0xFF9999); + +// These functions conversion between the 4-byte "Serato Markers_" color format +// and RgbColor (3-Byte RGB, transparency disabled). +// +// Serato's custom color format that is used here also represents RGB colors, +// but inserts a single null bit after every 7 payload bits, starting from the +// rightmost bit. +// +// Here's an example: +// +// | Hex Binary +// ------------- | ----------- -------------------------------- +// 3-byte RGB | 00 00 cc 000 0000000 0000001 1001100 +// Serato format | 00 00 01 4c 00000000000000000000000101001100 +// | +// 3-byte RGB | cc 88 00 110 0110010 0010000 0000000 +// Serato format | 06 32 10 00 00000110001100100001000000000000 +// +// See this for details: +// https://github.com/Holzhaus/serato-tags/blob/master/docs/serato_markers_.md#color-format + +mixxx::RgbColor seratoColorToRgb(quint8 w, quint8 x, quint8 y, quint8 z) { + quint8 b = (z & 0x7F) | ((y & 0x01) << 7); + quint8 g = ((y & 0x7F) >> 1) | ((x & 0x03) << 6); + quint8 r = ((x & 0x7F) >> 2) | ((w & 0x07) << 5); + return mixxx::RgbColor((r << 16) | (g << 8) | b); +} + +mixxx::RgbColor seratoColorToRgb(quint32 color) { + return seratoColorToRgb( + (color >> 24) & 0xFF, + (color >> 16) & 0xFF, + (color >> 8) & 0xFF, + color & 0xFF); +} + +quint32 seratoColorFromRgb(quint8 r, quint8 g, quint8 b) { + quint8 z = b & 0x7F; + quint8 y = ((b >> 7) | (g << 1)) & 0x7F; + quint8 x = ((g >> 6) | (r << 2)) & 0x7F; + quint8 w = (r >> 5); + return (static_cast(w) << 24) | + (static_cast(x) << 16) | + (static_cast(y) << 8) | + static_cast(z); +} + +quint32 seratoColorFromRgb(mixxx::RgbColor rgb) { + return seratoColorFromRgb( + (rgb >> 16) & 0xFF, + (rgb >> 8) & 0xFF, + rgb & 0xFF); +} +} + +namespace mixxx { + +QByteArray SeratoMarkersEntry::dump() const { + QByteArray data; + data.resize(kEntrySize); + + QDataStream stream(&data, QIODevice::WriteOnly); + stream.setVersion(QDataStream::Qt_5_0); + stream.setByteOrder(QDataStream::BigEndian); + stream << static_cast((m_hasStartPosition ? 0x00 : 0x7F)) + << static_cast((m_hasStartPosition ? m_startPosition : 0x7F7F7F7F)) + << static_cast((m_hasEndPosition ? 0x00 : 0x7F)) + << static_cast((m_hasEndPosition ? m_endPosition : 0x7F7F7F7F)); + stream.writeRawData("\x00\x7F\x7F\x7F\x7F\x7F", 6); + stream << static_cast(seratoColorFromRgb(m_color)) + << static_cast(m_type) + << static_cast(m_isLocked); + return data; +} + +SeratoMarkersEntryPointer SeratoMarkersEntry::parse(const QByteArray& data) { + if (data.length() != kEntrySize) { + qWarning() << "Parsing SeratoMarkersEntry failed:" + << "Length" << data.length() << "!=" << kEntrySize; + return nullptr; + } + + quint8 type; + quint8 startPositionStatus; + quint8 endPositionStatus; + quint32 startPosition; + quint32 endPosition; + quint32 colorRaw; + bool isLocked; + char buffer[6]; + + QDataStream stream(data); + stream.setVersion(QDataStream::Qt_5_0); + stream.setByteOrder(QDataStream::BigEndian); + stream >> startPositionStatus >> startPosition >> endPositionStatus >> endPosition; + + if (stream.readRawData(buffer, sizeof(buffer)) != sizeof(buffer)) { + qWarning() << "Parsing SeratoMarkersEntry failed:" + << "unable to read bytes 10..16"; + return nullptr; + } + + stream >> colorRaw >> type >> isLocked; + + const RgbColor color = seratoColorToRgb(colorRaw); + + // Parse Start Position + bool hasStartPosition = (startPositionStatus != 0x7F); + if (!hasStartPosition) { + // Start position not set + if (startPosition != 0x7F7F7F7F) { + qWarning() << "Parsing SeratoMarkersEntry failed:" + << "startPosition != 0x7F7F7F7F"; + + return nullptr; + } + } + + // Parse End Position + bool hasEndPosition = (endPositionStatus != 0x7F); + if (!hasEndPosition) { + // End position not set + if (endPosition != 0x7F7F7F7F) { + qWarning() << "Parsing SeratoMarkersEntry failed:" + << "endPosition != 0x7F7F7F7F"; + + return nullptr; + } + } + + // Make sure that the unknown (and probably unused) bytes have the expected value + if (strncmp(buffer, "\x00\x7F\x7F\x7F\x7F\x7F", sizeof(buffer)) != 0) { + qWarning() << "Parsing SeratoMarkersEntry failed:" + << "Unexpected value at offset 10"; + return nullptr; + } + + if (stream.status() != QDataStream::Status::Ok) { + qWarning() << "Parsing SeratoMarkersEntry failed:" + << "Stream read failed with status" << stream.status(); + return nullptr; + } + + if (!stream.atEnd()) { + qWarning() << "Parsing SeratoMarkersEntry failed:" + << "Unexpected trailing data"; + return nullptr; + } + + SeratoMarkersEntryPointer pEntry = SeratoMarkersEntryPointer(new SeratoMarkersEntry( + hasStartPosition, + startPosition, + hasEndPosition, + endPosition, + color, + type, + isLocked)); + qDebug() << "SeratoMarkersEntry" << *pEntry; + return pEntry; +} + +bool SeratoMarkers::parse(SeratoMarkers* seratoMarkers, const QByteArray& data) { + QDataStream stream(data); + stream.setVersion(QDataStream::Qt_5_0); + stream.setByteOrder(QDataStream::BigEndian); + + quint16 version; + stream >> version; + if (version != kVersion) { + qWarning() << "Parsing SeratoMarkers_ failed:" + << "Unknown Serato Markers_ tag version"; + return false; + } + + quint32 numEntries; + stream >> numEntries; + + if (numEntries != kNumEntries) { + qWarning() << "Parsing SeratoMarkers_ failed:" + << "Expected" << kNumEntries << "entries but found" << numEntries; + return false; + } + + char buffer[kEntrySize]; + QList entries; + for (quint32 i = 0; i < numEntries; i++) { + if (stream.readRawData(buffer, sizeof(buffer)) != sizeof(buffer)) { + qWarning() << "Parsing SeratoMarkersEntry failed:" + << "unable to read entry data"; + return false; + } + + QByteArray entryData = QByteArray(buffer, kEntrySize); + SeratoMarkersEntryPointer pEntry = SeratoMarkersEntryPointer( + SeratoMarkersEntry::parse(entryData)); + if (!pEntry) { + qWarning() << "Parsing SeratoMarkers_ failed:" + << "Unable to parse entry!"; + return false; + } + + if (i < kLoopEntryStartIndex && + pEntry->typeId() != SeratoMarkersEntry::TypeId::Cue) { + qWarning() << "Parsing SeratoMarkers_ failed:" + << "Expected cue entry but found type" << pEntry->type(); + return false; + } + + if (i >= kLoopEntryStartIndex && + pEntry->typeId() != SeratoMarkersEntry::TypeId::Loop) { + qWarning() << "Parsing SeratoMarkers_ failed:" + << "Expected loop entry but found type" << pEntry->type(); + return false; + } + + entries.append(pEntry); + } + + quint32 trackColorRaw; + stream >> trackColorRaw; + RgbColor trackColor = seratoColorToRgb(trackColorRaw); + + if (stream.status() != QDataStream::Status::Ok) { + qWarning() << "Parsing SeratoMarkers_ failed:" + << "Stream read failed with status" << stream.status(); + return false; + } + + if (!stream.atEnd()) { + qWarning() << "Parsing SeratoMarkers_ failed:" + << "Unexpected trailing data"; + return false; + } + seratoMarkers->setEntries(std::move(entries)); + seratoMarkers->setTrackColor(trackColor); + + return true; +} + +QByteArray SeratoMarkers::dump() const { + QByteArray data; + data.resize(sizeof(quint16) + 2 * sizeof(quint32) + kEntrySize * m_entries.size()); + + QDataStream stream(&data, QIODevice::WriteOnly); + stream.setVersion(QDataStream::Qt_5_0); + stream.setByteOrder(QDataStream::BigEndian); + stream << kVersion << m_entries.size(); + for (int i = 0; i < m_entries.size(); i++) { + SeratoMarkersEntryPointer pEntry = m_entries.at(i); + stream.writeRawData(pEntry->dump(), kEntrySize); + } + stream << seratoColorFromRgb(m_trackColor.value_or(kDefaultTrackColor)); + return data; +} + +} //namespace mixxx diff --git a/src/track/serato/markers.h b/src/track/serato/markers.h new file mode 100644 index 00000000000..b81f7b55634 --- /dev/null +++ b/src/track/serato/markers.h @@ -0,0 +1,179 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "util/color/rgbcolor.h" +#include "util/types.h" + +namespace mixxx { + +// Forward declaration +class SeratoMarkersEntry; +typedef std::shared_ptr SeratoMarkersEntryPointer; + +class SeratoMarkersEntry { + public: + enum class TypeId { + Unknown, + Cue, + Loop, + }; + + SeratoMarkersEntry( + bool hasStartPosition, + int startPosition, + bool hasEndPosition, + int endPosition, + RgbColor color, + int type, + bool isLocked) + : m_color(color), + m_hasStartPosition(hasStartPosition), + m_hasEndPosition(hasEndPosition), + m_isLocked(isLocked), + m_startPosition(startPosition), + m_endPosition(endPosition), + m_type(type) { + } + ~SeratoMarkersEntry() = default; + + QByteArray dump() const; + static SeratoMarkersEntryPointer parse(const QByteArray& data); + + int type() const { + return m_type; + } + + SeratoMarkersEntry::TypeId typeId() const { + SeratoMarkersEntry::TypeId typeId = SeratoMarkersEntry::TypeId::Unknown; + switch (type()) { + case 0: // This seems to be an unset Hotcue (i.e. without a position) + case 1: // Hotcue + typeId = SeratoMarkersEntry::TypeId::Cue; + break; + case 3: // Saved Loop + typeId = SeratoMarkersEntry::TypeId::Loop; + break; + } + return typeId; + } + + RgbColor getColor() const { + return m_color; + } + + bool isLocked() const { + return m_isLocked; + } + + bool hasStartPosition() const { + return m_hasStartPosition; + } + + quint32 getStartPosition() const { + return m_startPosition; + } + + bool hasEndPosition() const { + return m_hasEndPosition; + } + + quint32 getEndPosition() const { + return m_endPosition; + } + + private: + RgbColor m_color; + bool m_hasStartPosition; + bool m_hasEndPosition; + ; + bool m_isLocked; + bool m_isSet; + quint32 m_startPosition; + quint32 m_endPosition; + int m_type; +}; + +inline bool operator==(const SeratoMarkersEntry& lhs, const SeratoMarkersEntry& rhs) { + return (lhs.dump() == rhs.dump()); +} + +inline bool operator!=(const SeratoMarkersEntry& lhs, const SeratoMarkersEntry& rhs) { + return !(lhs == rhs); +} + +inline QDebug operator<<(QDebug dbg, const SeratoMarkersEntry& arg) { + dbg << "type =" << arg.type(); + if (arg.hasStartPosition()) { + dbg << "startPosition =" << arg.getStartPosition(); + } + if (arg.hasEndPosition()) { + dbg << "endPosition =" << arg.getEndPosition(); + } + return dbg << "color =" << arg.getColor() + << "isLocked =" << arg.isLocked(); +} + +// DTO for storing information from the SeratoMarkers_ tags used by the Serato +// DJ Pro software. +// +// Parsing & Formatting +// -------------------- +// This class includes functions for formatting and parsing SeratoMarkers_ +// metadata according to the specification: +// https://github.com/Holzhaus/serato-tags/blob/master/docs/serato_markers_.md +// +class SeratoMarkers final { + public: + SeratoMarkers() = default; + explicit SeratoMarkers(QList entries) + : m_entries(std::move(entries)) { + } + + static bool parse(SeratoMarkers* seratoMarkers, const QByteArray& data); + + QByteArray dump() const; + + bool isEmpty() const { + return m_entries.isEmpty() && !m_trackColor; + } + + const QList& getEntries() const { + return m_entries; + } + void setEntries(QList entries) { + m_entries = entries; + } + + RgbColor::optional_t getTrackColor() const { + return m_trackColor; + } + void setTrackColor(RgbColor::optional_t color) { + m_trackColor = color; + } + + private: + QList m_entries; + RgbColor::optional_t m_trackColor; +}; + +inline bool operator==(const SeratoMarkers& lhs, const SeratoMarkers& rhs) { + return (lhs.getEntries() == rhs.getEntries()); +} + +inline bool operator!=(const SeratoMarkers& lhs, const SeratoMarkers& rhs) { + return !(lhs == rhs); +} + +inline QDebug operator<<(QDebug dbg, const SeratoMarkers& arg) { + return dbg << "entries =" << arg.getEntries().length(); +} + +} // namespace mixxx + +Q_DECLARE_TYPEINFO(mixxx::SeratoMarkers, Q_MOVABLE_TYPE); +Q_DECLARE_METATYPE(mixxx::SeratoMarkers) diff --git a/src/track/seratomarkers2.cpp b/src/track/serato/markers2.cpp similarity index 61% rename from src/track/seratomarkers2.cpp rename to src/track/serato/markers2.cpp index d08da56c71e..e9e93a75ff2 100644 --- a/src/track/seratomarkers2.cpp +++ b/src/track/serato/markers2.cpp @@ -1,11 +1,27 @@ -#include "track/seratomarkers2.h" +#include "track/serato/markers2.h" #include +namespace { +QString zeroTerminatedUtf8StringtoQString(QDataStream* stream) { + DEBUG_ASSERT(stream); + + QByteArray data; + quint8 byte = '\xFF'; + while (byte != '\x00') { + *stream >> byte; + data.append(byte); + if (stream->status() != QDataStream::Status::Ok) { + return QString(); + } + } + return QString::fromUtf8(data); +} +} // namespace + namespace mixxx { -SeratoMarkers2EntryPointer SeratoMarkers2BpmlockEntry::parse(const QByteArray &data) -{ +SeratoMarkers2EntryPointer SeratoMarkers2BpmlockEntry::parse(const QByteArray& data) { if (data.length() != 1) { qWarning() << "Parsing SeratoMarkers2BpmlockEntry failed:" << "Length" << data.length() << "!= 1"; @@ -18,14 +34,14 @@ SeratoMarkers2EntryPointer SeratoMarkers2BpmlockEntry::parse(const QByteArray &d return SeratoMarkers2EntryPointer(pEntry); } -QByteArray SeratoMarkers2BpmlockEntry::data() const { +QByteArray SeratoMarkers2BpmlockEntry::dump() const { QByteArray data; data.resize(length()); QDataStream stream(&data, QIODevice::WriteOnly); stream.setVersion(QDataStream::Qt_5_0); stream.setByteOrder(QDataStream::BigEndian); - stream << (quint8)m_locked; + stream << static_cast(m_locked); return data; } @@ -34,11 +50,10 @@ quint32 SeratoMarkers2BpmlockEntry::length() const { return 1; } -SeratoMarkers2EntryPointer SeratoMarkers2ColorEntry::parse(const QByteArray &data) -{ +SeratoMarkers2EntryPointer SeratoMarkers2ColorEntry::parse(const QByteArray& data) { if (data.length() != 4) { qWarning() << "Parsing SeratoMarkers2ColorEntry failed:" - << "Length" << data.length() << "!= 4"; + << "Length" << data.length() << "!= 4"; return nullptr; } @@ -50,26 +65,27 @@ SeratoMarkers2EntryPointer SeratoMarkers2ColorEntry::parse(const QByteArray &dat return nullptr; } - QColor color(static_cast(data.at(1)), - static_cast(data.at(2)), - static_cast(data.at(3))); + RgbColor color = RgbColor(qRgb( + static_cast(data.at(1)), + static_cast(data.at(2)), + static_cast(data.at(3)))); SeratoMarkers2ColorEntry* pEntry = new SeratoMarkers2ColorEntry(color); qDebug() << "SeratoMarkers2ColorEntry" << *pEntry; return SeratoMarkers2EntryPointer(pEntry); } -QByteArray SeratoMarkers2ColorEntry::data() const { +QByteArray SeratoMarkers2ColorEntry::dump() const { QByteArray data; data.resize(length()); QDataStream stream(&data, QIODevice::WriteOnly); stream.setVersion(QDataStream::Qt_5_0); stream.setByteOrder(QDataStream::BigEndian); - stream << (quint8)0 - << (quint8)m_color.red() - << (quint8)m_color.green() - << (quint8)m_color.blue(); + stream << static_cast('\x00') + << static_cast(qRed(m_color)) + << static_cast(qGreen(m_color)) + << static_cast(qBlue(m_color)); return data; } @@ -78,61 +94,69 @@ quint32 SeratoMarkers2ColorEntry::length() const { return 4; } -SeratoMarkers2EntryPointer SeratoMarkers2CueEntry::parse(const QByteArray &data) -{ +SeratoMarkers2EntryPointer SeratoMarkers2CueEntry::parse(const QByteArray& data) { if (data.length() < 13) { qWarning() << "Parsing SeratoMarkers2CueEntry failed:" << "Length" << data.length() << "< 13"; return nullptr; } + // CUE entry fields in order of appearance + quint8 unknownField1; + quint8 index; + quint32 position; + quint8 unknownField2; + quint8 rawRgbRed; + quint8 rawRgbGreen; + quint8 rawRgbBlue; + quint16 unknownField3; + + QDataStream stream(data); + stream.setVersion(QDataStream::Qt_5_0); + stream.setByteOrder(QDataStream::BigEndian); + + stream >> unknownField1; + // Unknown field, make sure it's 0 in case it's a // null-terminated string - if (data.at(0) != '\x00') { + if (unknownField1 != '\x00') { qWarning() << "Parsing SeratoMarkers2CueEntry failed:" << "Byte 0: " << data.at(0) << "!= '\\0'"; return nullptr; } - const quint8 index = data.at(1); -#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0) - const auto position = qFromBigEndian(data.mid(2, 6)); -#else - const auto position = qFromBigEndian( - reinterpret_cast(data.mid(2, 6).constData())); -#endif + stream >> index >> position >> unknownField2; // Unknown field, make sure it's 0 in case it's a // null-terminated string - if (data.at(6) != '\x00') { + if (unknownField2 != '\x00') { qWarning() << "Parsing SeratoMarkers2CueEntry failed:" << "Byte 6: " << data.at(6) << "!= '\\0'"; return nullptr; } - QColor color(static_cast(data.at(7)), - static_cast(data.at(8)), - static_cast(data.at(9))); + stream >> rawRgbRed >> rawRgbGreen >> rawRgbBlue >> unknownField3; + RgbColor color = RgbColor(qRgb(rawRgbRed, rawRgbGreen, rawRgbBlue)); // Unknown field(s), make sure it's 0 in case it's a // null-terminated string - if (data.at(10) != '\x00' || data.at(11) != '\x00') { + if (unknownField3 != 0x0000) { qWarning() << "Parsing SeratoMarkers2CueEntry failed:" - << "Bytes 10-11:" << data.mid(10, 2) << "!= \"\\0\\0\""; + << "Bytes 10-11:" << unknownField3 << "!= \"\\0\\0\""; return nullptr; } - int labelEndPos = data.indexOf('\x00', 12); - if (labelEndPos < 0) { - qWarning() << "Parsing SeratoMarkers2CueEntry failed:" - << "Label end position not found"; + QString label = zeroTerminatedUtf8StringtoQString(&stream); + + if (stream.status() != QDataStream::Status::Ok) { + qWarning() << "Parsing SeratoMarkersEntry failed:" + << "Stream read failed with status" << stream.status(); return nullptr; } - QString label(data.mid(12, labelEndPos - 12)); - if (data.length() > labelEndPos + 1) { - qWarning() << "Parsing SeratoMarkers2CueEntry failed:" - << "Trailing content" << data.mid(labelEndPos + 1); + if (!stream.atEnd()) { + qWarning() << "Parsing SeratoMarkersEntry failed:" + << "Unexpected trailing data"; return nullptr; } @@ -141,26 +165,26 @@ SeratoMarkers2EntryPointer SeratoMarkers2CueEntry::parse(const QByteArray &data) return SeratoMarkers2EntryPointer(pEntry); } -QByteArray SeratoMarkers2CueEntry::data() const { +QByteArray SeratoMarkers2CueEntry::dump() const { QByteArray data; data.resize(length()); QDataStream stream(&data, QIODevice::WriteOnly); stream.setVersion(QDataStream::Qt_5_0); stream.setByteOrder(QDataStream::BigEndian); - stream << (quint8)0 + stream << static_cast('\x00') << m_index << m_position - << (quint8)0 - << (quint8)m_color.red() - << (quint8)m_color.green() - << (quint8)m_color.blue() - << (quint8)0 - << (quint8)0; + << static_cast('\x00') + << static_cast(qRed(m_color)) + << static_cast(qGreen(m_color)) + << static_cast(qBlue(m_color)) + << static_cast('\x00') + << static_cast('\x00'); QByteArray labelData = m_label.toUtf8(); stream.writeRawData(labelData.constData(), labelData.length()); - stream << (qint8)'\0'; // terminating null-byte + stream << static_cast('\x00'); // terminating null-byte return data; } @@ -169,95 +193,101 @@ quint32 SeratoMarkers2CueEntry::length() const { return 13 + m_label.toUtf8().length(); } -SeratoMarkers2EntryPointer SeratoMarkers2LoopEntry::parse(const QByteArray &data) -{ +SeratoMarkers2EntryPointer SeratoMarkers2LoopEntry::parse(const QByteArray& data) { if (data.length() < 21) { qWarning() << "Parsing SeratoMarkers2LoopEntry failed:" << "Length" << data.length() << "< 21"; return nullptr; } + // LOOP entry fields in order of appearance + quint8 unknownField1; + quint8 index; + quint32 startPosition; + quint32 endPosition; + quint32 unknownField2; + quint32 unknownField3; + quint8 unknownField4; + bool locked; + + QDataStream stream(data); + stream.setVersion(QDataStream::Qt_5_0); + stream.setByteOrder(QDataStream::BigEndian); + + stream >> unknownField1; // Unknown field, make sure it's 0 in case it's a // null-terminated string - if (data.at(0) != '\x00') { + if (unknownField1 != '\x00') { qWarning() << "Parsing SeratoMarkers2LoopEntry failed:" - << "Byte 0: " << data.at(0) << "!= '\\0'"; + << "Byte 0: " << unknownField1 << "!= '\\0'"; return nullptr; } - const quint8 index = data.at(1); -#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0) - const auto startposition = qFromBigEndian(data.mid(2, 6)); - const auto endposition = qFromBigEndian(data.mid(6, 10)); -#else - const auto startposition = qFromBigEndian( - reinterpret_cast(data.mid(2, 6).constData())); - const auto endposition = qFromBigEndian( - reinterpret_cast(data.mid(6, 10).constData())); -#endif + stream >> index >> startPosition >> endPosition >> unknownField2; + // Unknown field, make sure it contains the expected "default" value + if (unknownField2 != 0xffffffff) { + qWarning() << "Parsing SeratoMarkers2LoopEntry failed:" + << "Invalid magic value" << unknownField2 << "at offset 10"; + return nullptr; + } + + stream >> unknownField3; // Unknown field, make sure it contains the expected "default" value - if (data.at(10) != '\xff' || - data.at(11) != '\xff' || - data.at(12) != '\xff' || - data.at(13) != '\xff' || - data.at(14) != '\x00' || - data.at(15) != '\x27' || - data.at(16) != '\xaa' || - data.at(17) != '\xe1') { + if (unknownField3 != 0x0027aae1) { qWarning() << "Parsing SeratoMarkers2LoopEntry failed:" - << "Invalid magic value " << data.mid(10, 16); + << "Invalid magic value" << unknownField3 << "at offset 14"; return nullptr; } + stream >> unknownField4; // Unknown field, make sure it's 0 in case it's a // null-terminated string - if (data.at(18) != '\x00') { + if (unknownField4 != '\x00') { qWarning() << "Parsing SeratoMarkers2LoopEntry failed:" - << "Byte 18:" << data.at(18) << "!= '\\0'"; + << "Byte 18:" << unknownField4 << "!= '\\0'"; return nullptr; } - const bool locked = data.at(19); + stream >> locked; + QString label = zeroTerminatedUtf8StringtoQString(&stream); - int labelEndPos = data.indexOf('\x00', 20); - if (labelEndPos < 0) { - qWarning() << "Parsing SeratoMarkers2LoopEntry failed:" - << "Label end position not found"; + if (stream.status() != QDataStream::Status::Ok) { + qWarning() << "Parsing SeratoMarkersEntry failed:" + << "Stream read failed with status" << stream.status(); return nullptr; } - QString label(data.mid(20, labelEndPos - 20)); - if (data.length() > labelEndPos + 1) { - qWarning() << "Parsing SeratoMarkers2LoopEntry failed:" - << "Trailing content" << data.mid(labelEndPos + 1); + if (!stream.atEnd()) { + qWarning() << "Parsing SeratoMarkersEntry failed:" + << "Unexpected trailing data"; return nullptr; } - SeratoMarkers2LoopEntry* pEntry = new SeratoMarkers2LoopEntry(index, startposition, endposition, locked, label); + SeratoMarkers2LoopEntry* pEntry = new SeratoMarkers2LoopEntry(index, startPosition, endPosition, locked, label); qDebug() << "SeratoMarkers2LoopEntry" << *pEntry; return SeratoMarkers2EntryPointer(pEntry); } -QByteArray SeratoMarkers2LoopEntry::data() const { +QByteArray SeratoMarkers2LoopEntry::dump() const { QByteArray data; data.resize(length()); QDataStream stream(&data, QIODevice::WriteOnly); stream.setVersion(QDataStream::Qt_5_0); stream.setByteOrder(QDataStream::BigEndian); - stream << (quint8)0 + stream << static_cast('\x00') << m_index << m_startposition << m_endposition; stream.writeRawData("\xff\xff\xff\xff\x00\x27\xaa\xe1", 8); - stream << (quint8)0 - << (quint8)m_locked; + stream << static_cast('\x00') + << static_cast(m_locked); QByteArray labelData = m_label.toUtf8(); stream.writeRawData(labelData.constData(), labelData.length()); - stream << (qint8)'\0'; // terminating null-byte + stream << static_cast('\x00'); // terminating null-byte return data; } @@ -285,7 +315,7 @@ bool SeratoMarkers2::parse(SeratoMarkers2* seratoMarkers2, const QByteArray& out int offset = 2; int entryTypeEndPos; - while((entryTypeEndPos = data.indexOf('\x00', offset)) >= 0) { + while ((entryTypeEndPos = data.indexOf('\x00', offset)) >= 0) { // Entry Name QString entryType(data.mid(offset, entryTypeEndPos - offset)); offset = entryTypeEndPos + 1; @@ -314,20 +344,20 @@ bool SeratoMarkers2::parse(SeratoMarkers2* seratoMarkers2, const QByteArray& out // Entry Content SeratoMarkers2EntryPointer pEntry; - if(entryType.compare("BPMLOCK") == 0) { + if (entryType.compare("BPMLOCK") == 0) { pEntry = SeratoMarkers2BpmlockEntry::parse(entryData); - } else if(entryType.compare("COLOR") == 0) { + } else if (entryType.compare("COLOR") == 0) { pEntry = SeratoMarkers2ColorEntry::parse(entryData); - } else if(entryType.compare("CUE") == 0) { + } else if (entryType.compare("CUE") == 0) { pEntry = SeratoMarkers2CueEntry::parse(entryData); - } else if(entryType.compare("LOOP") == 0) { + } else if (entryType.compare("LOOP") == 0) { pEntry = SeratoMarkers2LoopEntry::parse(entryData); } else { pEntry = SeratoMarkers2EntryPointer(new SeratoMarkers2UnknownEntry(entryType, entryData)); qDebug() << "SeratoMarkers2UnknownEntry" << *pEntry; } - if(!pEntry) { + if (!pEntry) { qWarning() << "Parsing SeratoMarkers2 failed:" << "Unable to parse entry of type " << entryType; return false; @@ -340,16 +370,21 @@ bool SeratoMarkers2::parse(SeratoMarkers2* seratoMarkers2, const QByteArray& out return true; } -QByteArray SeratoMarkers2::data() const { - QByteArray data("\x01\x01", 2); +QByteArray SeratoMarkers2::dump() const { + QByteArray data; + QDataStream stream(&data, QIODevice::WriteOnly); + stream.setVersion(QDataStream::Qt_5_0); + stream.setByteOrder(QDataStream::BigEndian); + stream << static_cast(0x0101); for (int i = 0; i < m_entries.size(); i++) { SeratoMarkers2EntryPointer entry = m_entries.at(i); - quint32 lengthBE = qToBigEndian(entry->length()); - data.append(entry->type().toUtf8()); - data.append('\0'); - data.append((const char*)&lengthBE, 4); - data.append(entry->data()); + QByteArray entryName = entry->type().toUtf8(); + QByteArray entryData = entry->dump(); + stream.writeRawData(entryName.constData(), entryName.length()); + stream << static_cast('\x00') // terminating null-byte + << entryData.length(); + stream.writeRawData(entryData.constData(), entryData.length()); } data.append('\0'); @@ -359,7 +394,7 @@ QByteArray SeratoMarkers2::data() const { // Hence, we can split the data into blocks of 72 bytes * 3/4 = 54 bytes // and base64-encode them one at a time: int offset = 0; - while(offset < data.size()) { + while (offset < data.size()) { if (offset > 0) { outerData.append('\n'); } @@ -391,5 +426,4 @@ QByteArray SeratoMarkers2::data() const { return outerData.leftJustified(size, '\0'); } - } //namespace mixxx diff --git a/src/track/seratomarkers2.h b/src/track/serato/markers2.h similarity index 50% rename from src/track/seratomarkers2.h rename to src/track/serato/markers2.h index 4081785827c..3b8fa99e00a 100644 --- a/src/track/seratomarkers2.h +++ b/src/track/serato/markers2.h @@ -1,53 +1,65 @@ #pragma once -#include - -#include #include +#include #include #include +#include +#include "util/color/rgbcolor.h" #include "util/types.h" +namespace { +constexpr mixxx::RgbColor kDefaultTrackColor = mixxx::RgbColor(0xFF9999); +constexpr mixxx::RgbColor kDefaultCueColor = mixxx::RgbColor(0xCC0000); +} // namespace + namespace mixxx { +// Enum values need to appear in the same order as the corresponding entries +// are written to the tag by Serato. class SeratoMarkers2Entry { -public: + public: + enum class TypeId { + Unknown, + Color, + Cue, + Loop, + Bpmlock, + }; + virtual ~SeratoMarkers2Entry() = default; virtual QString type() const = 0; + virtual SeratoMarkers2Entry::TypeId typeId() const = 0; - virtual QByteArray data() const = 0; + virtual QByteArray dump() const = 0; - virtual quint32 length() const { - return data().length(); - } + virtual quint32 length() const = 0; }; typedef std::shared_ptr SeratoMarkers2EntryPointer; -inline -bool operator==(const SeratoMarkers2Entry& lhs, const SeratoMarkers2Entry& rhs) { - return (lhs.type() == rhs.type()) && (lhs.data() == rhs.data()); +inline bool operator==(const SeratoMarkers2Entry& lhs, const SeratoMarkers2Entry& rhs) { + return (lhs.type() == rhs.type()) && (lhs.dump() == rhs.dump()); } -inline -bool operator!=(const SeratoMarkers2Entry& lhs, const SeratoMarkers2Entry& rhs) { +inline bool operator!=(const SeratoMarkers2Entry& lhs, const SeratoMarkers2Entry& rhs) { return !(lhs == rhs); } -inline -QDebug operator<<(QDebug dbg, const SeratoMarkers2Entry& arg) { +inline QDebug operator<<(QDebug dbg, const SeratoMarkers2Entry& arg) { return dbg << "type =" << arg.type() - << "data =" << arg.data() - << "(" << "length =" << arg.length() << ")"; + << "data =" << arg.dump() + << "(" + << "length =" << arg.length() << ")"; } class SeratoMarkers2UnknownEntry : public SeratoMarkers2Entry { -public: + public: SeratoMarkers2UnknownEntry(QString type, QByteArray data) - : m_type(std::move(type)) - , m_data(std::move(data)) { + : m_type(std::move(type)), + m_data(std::move(data)) { } ~SeratoMarkers2UnknownEntry() override = default; @@ -55,34 +67,46 @@ class SeratoMarkers2UnknownEntry : public SeratoMarkers2Entry { return m_type; } - QByteArray data() const override { + SeratoMarkers2Entry::TypeId typeId() const override { + return SeratoMarkers2Entry::TypeId::Unknown; + } + + QByteArray dump() const override { return m_data; } -private: + quint32 length() const override { + return dump().length(); + } + + private: QString m_type; QByteArray m_data; }; class SeratoMarkers2BpmlockEntry : public SeratoMarkers2Entry { -public: + public: SeratoMarkers2BpmlockEntry(bool locked) - : m_locked(locked) { + : m_locked(locked) { } SeratoMarkers2BpmlockEntry() - : m_locked(false) { + : m_locked(false) { } - static SeratoMarkers2EntryPointer parse(const QByteArray &data); + static SeratoMarkers2EntryPointer parse(const QByteArray& data); QString type() const override { return "BPMLOCK"; } - QByteArray data() const override; + SeratoMarkers2Entry::TypeId typeId() const override { + return SeratoMarkers2Entry::TypeId::Bpmlock; + } + + QByteArray dump() const override; - bool isLocked() const { + bool isLocked() const { return m_locked; } @@ -92,102 +116,103 @@ class SeratoMarkers2BpmlockEntry : public SeratoMarkers2Entry { quint32 length() const override; -private: + private: bool m_locked; }; -inline -bool operator==(const SeratoMarkers2BpmlockEntry& lhs, - const SeratoMarkers2BpmlockEntry& rhs) { +inline bool operator==(const SeratoMarkers2BpmlockEntry& lhs, + const SeratoMarkers2BpmlockEntry& rhs) { return (lhs.isLocked() == rhs.isLocked()); } -inline -bool operator!=(const SeratoMarkers2BpmlockEntry& lhs, - const SeratoMarkers2BpmlockEntry& rhs) { +inline bool operator!=(const SeratoMarkers2BpmlockEntry& lhs, + const SeratoMarkers2BpmlockEntry& rhs) { return !(lhs == rhs); } -inline -QDebug operator<<(QDebug dbg, const SeratoMarkers2BpmlockEntry& arg) { +inline QDebug operator<<(QDebug dbg, const SeratoMarkers2BpmlockEntry& arg) { return dbg << "locked =" << arg.isLocked(); } class SeratoMarkers2ColorEntry : public SeratoMarkers2Entry { -public: - SeratoMarkers2ColorEntry(QColor color) - : m_color(color) { + public: + SeratoMarkers2ColorEntry(RgbColor color) + : m_color(color) { } SeratoMarkers2ColorEntry() - : m_color(QColor()) { + : m_color(kDefaultTrackColor) { } - static SeratoMarkers2EntryPointer parse(const QByteArray &data); + static SeratoMarkers2EntryPointer parse(const QByteArray& data); QString type() const override { return "COLOR"; } - QByteArray data() const override; + SeratoMarkers2Entry::TypeId typeId() const override { + return SeratoMarkers2Entry::TypeId::Color; + } + + QByteArray dump() const override; - QColor getColor() const { + RgbColor getColor() const { return m_color; } - void setColor(QColor color) { + void setColor(RgbColor color) { m_color = color; } quint32 length() const override; -private: - QColor m_color; + private: + RgbColor m_color; }; -inline -bool operator==(const SeratoMarkers2ColorEntry& lhs, - const SeratoMarkers2ColorEntry& rhs) { +inline bool operator==(const SeratoMarkers2ColorEntry& lhs, + const SeratoMarkers2ColorEntry& rhs) { return (lhs.getColor() == rhs.getColor()); } -inline -bool operator!=(const SeratoMarkers2ColorEntry& lhs, - const SeratoMarkers2ColorEntry& rhs) { +inline bool operator!=(const SeratoMarkers2ColorEntry& lhs, + const SeratoMarkers2ColorEntry& rhs) { return !(lhs == rhs); } -inline -QDebug operator<<(QDebug dbg, const SeratoMarkers2ColorEntry& arg) { +inline QDebug operator<<(QDebug dbg, const SeratoMarkers2ColorEntry& arg) { return dbg << "color =" << arg.getColor(); } class SeratoMarkers2CueEntry : public SeratoMarkers2Entry { -public: - SeratoMarkers2CueEntry(quint8 index, quint32 position, QColor color, - QString label) - : m_index(index) - , m_position(position) - , m_color(color) - , m_label(label) { + public: + SeratoMarkers2CueEntry(quint8 index, quint32 position, RgbColor color, QString label) + : m_index(index), + m_position(position), + m_color(color), + m_label(label) { } SeratoMarkers2CueEntry() - : m_index(0) - , m_position(0) - , m_color(QColor()) - , m_label(QString("")) { + : m_index(0), + m_position(0), + m_color(kDefaultCueColor), + m_label(QString("")) { } - static SeratoMarkers2EntryPointer parse(const QByteArray &data); + static SeratoMarkers2EntryPointer parse(const QByteArray& data); QString type() const override { return "CUE"; } - QByteArray data() const override; + SeratoMarkers2Entry::TypeId typeId() const override { + return SeratoMarkers2Entry::TypeId::Cue; + } + + QByteArray dump() const override; - quint8 getIndex() const { + quint8 getIndex() const { return m_index; } @@ -203,15 +228,15 @@ class SeratoMarkers2CueEntry : public SeratoMarkers2Entry { m_position = position; } - QColor getColor() const { + RgbColor getColor() const { return m_color; } - void setColor(QColor color) { + void setColor(RgbColor color) { m_color = color; } - QString getLabel() const { + QString getLabel() const { return m_label; } @@ -221,30 +246,27 @@ class SeratoMarkers2CueEntry : public SeratoMarkers2Entry { quint32 length() const override; -private: + private: quint8 m_index; quint32 m_position; - QColor m_color; + RgbColor m_color; QString m_label; }; -inline -bool operator==(const SeratoMarkers2CueEntry& lhs, - const SeratoMarkers2CueEntry& rhs) { +inline bool operator==(const SeratoMarkers2CueEntry& lhs, + const SeratoMarkers2CueEntry& rhs) { return (lhs.getIndex() == rhs.getIndex()) && - (lhs.getPosition() == rhs.getPosition()) && - (lhs.getColor() == rhs.getColor()) && - (lhs.getLabel() == rhs.getLabel()); + (lhs.getPosition() == rhs.getPosition()) && + (lhs.getColor() == rhs.getColor()) && + (lhs.getLabel() == rhs.getLabel()); } -inline -bool operator!=(const SeratoMarkers2CueEntry& lhs, - const SeratoMarkers2CueEntry& rhs) { +inline bool operator!=(const SeratoMarkers2CueEntry& lhs, + const SeratoMarkers2CueEntry& rhs) { return !(lhs == rhs); } -inline -QDebug operator<<(QDebug dbg, const SeratoMarkers2CueEntry& arg) { +inline QDebug operator<<(QDebug dbg, const SeratoMarkers2CueEntry& arg) { return dbg << "index =" << arg.getIndex() << "/" << "position =" << arg.getPosition() << "/" << "color =" << arg.getColor() << "/" @@ -252,34 +274,36 @@ QDebug operator<<(QDebug dbg, const SeratoMarkers2CueEntry& arg) { } class SeratoMarkers2LoopEntry : public SeratoMarkers2Entry { -public: - SeratoMarkers2LoopEntry(quint8 index, quint32 startposition, - quint32 endposition, bool locked, - QString label) - : m_index(index) - , m_startposition(startposition) - , m_endposition(endposition) - , m_locked(locked) - , m_label(label) { + public: + SeratoMarkers2LoopEntry(quint8 index, quint32 startposition, quint32 endposition, bool locked, QString label) + : m_index(index), + m_startposition(startposition), + m_endposition(endposition), + m_locked(locked), + m_label(label) { } SeratoMarkers2LoopEntry() - : m_index(0) - , m_startposition(0) - , m_endposition(0) - , m_locked(false) - , m_label(QString("")) { + : m_index(0), + m_startposition(0), + m_endposition(0), + m_locked(false), + m_label(QString("")) { } - static SeratoMarkers2EntryPointer parse(const QByteArray &data); + static SeratoMarkers2EntryPointer parse(const QByteArray& data); QString type() const override { return "LOOP"; } - QByteArray data() const override; + SeratoMarkers2Entry::TypeId typeId() const override { + return SeratoMarkers2Entry::TypeId::Loop; + } - quint8 getIndex() const { + QByteArray dump() const override; + + quint8 getIndex() const { return m_index; } @@ -311,7 +335,7 @@ class SeratoMarkers2LoopEntry : public SeratoMarkers2Entry { m_locked = locked; } - QString getLabel() const { + QString getLabel() const { return m_label; } @@ -321,7 +345,7 @@ class SeratoMarkers2LoopEntry : public SeratoMarkers2Entry { quint32 length() const override; -private: + private: quint8 m_index; quint32 m_startposition; quint32 m_endposition; @@ -329,24 +353,21 @@ class SeratoMarkers2LoopEntry : public SeratoMarkers2Entry { QString m_label; }; -inline -bool operator==(const SeratoMarkers2LoopEntry& lhs, - const SeratoMarkers2LoopEntry& rhs) { +inline bool operator==(const SeratoMarkers2LoopEntry& lhs, + const SeratoMarkers2LoopEntry& rhs) { return (lhs.getIndex() == rhs.getIndex()) && - (lhs.getStartPosition() == rhs.getStartPosition()) && - (lhs.getEndPosition() == rhs.getEndPosition()) && - (lhs.isLocked() == rhs.isLocked()) && - (lhs.getLabel() == rhs.getLabel()); + (lhs.getStartPosition() == rhs.getStartPosition()) && + (lhs.getEndPosition() == rhs.getEndPosition()) && + (lhs.isLocked() == rhs.isLocked()) && + (lhs.getLabel() == rhs.getLabel()); } -inline -bool operator!=(const SeratoMarkers2LoopEntry& lhs, - const SeratoMarkers2LoopEntry& rhs) { +inline bool operator!=(const SeratoMarkers2LoopEntry& lhs, + const SeratoMarkers2LoopEntry& rhs) { return !(lhs == rhs); } -inline -QDebug operator<<(QDebug dbg, const SeratoMarkers2LoopEntry& arg) { +inline QDebug operator<<(QDebug dbg, const SeratoMarkers2LoopEntry& arg) { return dbg << "index =" << arg.getIndex() << "/" << "startposition =" << arg.getStartPosition() << "/" << "endposition =" << arg.getEndPosition() << "/" @@ -364,19 +385,19 @@ QDebug operator<<(QDebug dbg, const SeratoMarkers2LoopEntry& arg) { // https://github.com/Holzhaus/serato-tags/blob/master/docs/serato_markers2.md // class SeratoMarkers2 final { -public: + public: SeratoMarkers2() = default; explicit SeratoMarkers2( QList> entries) - : m_allocatedSize(0) - , m_entries(std::move(entries)) { + : m_allocatedSize(0), + m_entries(std::move(entries)) { } // Parsing and formatting of gain values according to the // SeratoMarkers2 1.0/2.0 specification. static bool parse(SeratoMarkers2* seratoMarkers2, const QByteArray& outerData); - QByteArray data() const; + QByteArray dump() const; int getAllocatedSize() const { return m_allocatedSize; @@ -398,27 +419,24 @@ class SeratoMarkers2 final { m_entries = std::move(entries); } -private: + private: int m_allocatedSize; QList> m_entries; }; -inline -bool operator==(const SeratoMarkers2& lhs, const SeratoMarkers2& rhs) { +inline bool operator==(const SeratoMarkers2& lhs, const SeratoMarkers2& rhs) { return (lhs.getEntries() == rhs.getEntries()); } -inline -bool operator!=(const SeratoMarkers2& lhs, const SeratoMarkers2& rhs) { +inline bool operator!=(const SeratoMarkers2& lhs, const SeratoMarkers2& rhs) { return !(lhs == rhs); } -inline -QDebug operator<<(QDebug dbg, const SeratoMarkers2& arg) { +inline QDebug operator<<(QDebug dbg, const SeratoMarkers2& arg) { return dbg << "entries =" << arg.getEntries().length(); } -} +} // namespace mixxx Q_DECLARE_TYPEINFO(mixxx::SeratoMarkers2, Q_MOVABLE_TYPE); Q_DECLARE_METATYPE(mixxx::SeratoMarkers2) diff --git a/src/track/trackinfo.cpp b/src/track/trackinfo.cpp index 0cd1da6781b..72ed440a818 100644 --- a/src/track/trackinfo.cpp +++ b/src/track/trackinfo.cpp @@ -75,6 +75,7 @@ bool TrackInfo::compareEq( #endif // __EXTRA_METADATA__ (getReplayGain() == trackInfo.getReplayGain()) && #if defined(__EXTRA_METADATA__) + (getSeratoMarkers() == trackInfo.getSeratoMarkers()) && (getSeratoMarkers2() == trackInfo.getSeratoMarkers2()) && (getSubtitle() == trackInfo.getSubtitle()) && #endif // __EXTRA_METADATA__ diff --git a/src/track/trackinfo.h b/src/track/trackinfo.h index e322587bd69..478e87001c8 100644 --- a/src/track/trackinfo.h +++ b/src/track/trackinfo.h @@ -4,15 +4,13 @@ #include #include "sources/audiosource.h" - #include "track/bpm.h" #include "track/replaygain.h" -#include "track/seratomarkers2.h" - +#include "track/serato/markers.h" +#include "track/serato/markers2.h" #include "util/duration.h" #include "util/macros.h" - namespace mixxx { class TrackInfo final { @@ -47,6 +45,7 @@ class TrackInfo final { #endif // __EXTRA_METADATA__ PROPERTY_SET_BYVAL_GET_BYREF(ReplayGain, replayGain, ReplayGain) #if defined(__EXTRA_METADATA__) + PROPERTY_SET_BYVAL_GET_BYREF(SeratoMarkers, seratoMarkers, SeratoMarkers) PROPERTY_SET_BYVAL_GET_BYREF(SeratoMarkers2, seratoMarkers2, SeratoMarkers2) PROPERTY_SET_BYVAL_GET_BYREF(QString, subtitle, Subtitle) #endif // __EXTRA_METADATA__ diff --git a/src/track/trackmetadatataglib.cpp b/src/track/trackmetadatataglib.cpp index 2925d14693d..efdc96a58d3 100644 --- a/src/track/trackmetadatataglib.cpp +++ b/src/track/trackmetadatataglib.cpp @@ -427,6 +427,19 @@ bool parseAlbumPeak( return isPeakValid; } +bool parseSeratoMarkers( + TrackMetadata* pTrackMetadata, + const QByteArray& data) { + DEBUG_ASSERT(pTrackMetadata); + + SeratoMarkers seratoMarkers(pTrackMetadata->getTrackInfo().getSeratoMarkers()); + bool isValid = SeratoMarkers::parse(&seratoMarkers, data); + if (isValid) { + pTrackMetadata->refTrackInfo().setSeratoMarkers(seratoMarkers); + } + return isValid; +} + bool parseSeratoMarkers2( TrackMetadata* pTrackMetadata, const QByteArray& data) { @@ -1645,6 +1658,11 @@ void importTrackMetadataFromID3v2Tag( pTrackMetadata->refTrackInfo().setEncoderSettings(toQStringFirstNotEmpty(encoderSettingsFrames)); } // Serato tags + QByteArray seratoMarkers = readFirstGeneralEncapsulatedObjectFrame(tag, "Serato Markers_"); + if (!seratoMarkers.isEmpty()) { + parseSeratoMarkers(pTrackMetadata, seratoMarkers); + } + QByteArray seratoMarkers2 = readFirstGeneralEncapsulatedObjectFrame(tag, "Serato Markers2"); if (!seratoMarkers2.isEmpty()) { parseSeratoMarkers2(pTrackMetadata, seratoMarkers2); @@ -2508,10 +2526,14 @@ bool exportTrackMetadataIntoID3v2Tag(TagLib::ID3v2::Tag* pTag, pTag, "TSSE", trackMetadata.getTrackInfo().getEncoderSettings()); + writeID3v2GeneralEncapsulatedObjectFrame( + pTag, + "Serato Markers_", + trackMetadata.getTrackInfo().getSeratoMarkers().dump()); writeID3v2GeneralEncapsulatedObjectFrame( pTag, "Serato Markers2", - trackMetadata.getTrackInfo().getSeratoMarkers2().data()); + trackMetadata.getTrackInfo().getSeratoMarkers2().dump()); #endif // __EXTRA_METADATA__ return true;