1 #include "track/serato/beatgrid.h"
2 
3 #include <QtEndian>
4 
5 #include "util/logger.h"
6 
7 namespace {
8 
9 mixxx::Logger kLogger("SeratoBeatGrid");
10 
11 /// Max difference of two beat distances so that they can still be considered equal
12 constexpr double kEpsilon = 1.0;
13 
14 constexpr quint16 kVersion = 0x0100;
15 constexpr int kMarkerSizeID3 = 8;
16 constexpr char kSeratoBeatGridBase64EncodedPrefixStr[] =
17         "application/octet-stream\0\0Serato BeatGrid";
18 const QByteArray kSeratoBeatGridBase64EncodedPrefix = QByteArray::fromRawData(
19         kSeratoBeatGridBase64EncodedPrefixStr,
20         sizeof(kSeratoBeatGridBase64EncodedPrefixStr));
21 
base64encode(const QByteArray & data,bool chopPadding)22 QByteArray base64encode(const QByteArray& data, bool chopPadding) {
23     QByteArray dataBase64;
24 
25     // Serato inserts a newline char after every 72 bytes of base64-encoded
26     // content.  To mirror that behaviour, we can split the data into blocks of
27     // 72 bytes * 3/4 = 54 bytes and base64-encode them one at a time.
28     int offset = 0;
29     while (offset < data.size()) {
30         if (offset > 0) {
31             // Add newline char after previous block of 54 raw bytes.
32             dataBase64.append('\n');
33         }
34         QByteArray block = data.mid(offset, 54);
35         dataBase64.append(block.toBase64(
36                 QByteArray::Base64Encoding | QByteArray::OmitTrailingEquals));
37         offset += block.size();
38 
39         if (chopPadding) {
40             // In case that the last block would require padding, Serato seems to
41             // chop off the last byte of the base64-encoded data
42             if (block.size() % 3) {
43                 dataBase64.chop(1);
44             }
45         }
46     }
47 
48     return dataBase64;
49 }
50 
51 } // namespace
52 
53 namespace mixxx {
54 
dumpID3() const55 QByteArray SeratoBeatGridNonTerminalMarker::dumpID3() const {
56     QByteArray data;
57     data.reserve(kMarkerSizeID3);
58 
59     QDataStream stream(&data, QIODevice::WriteOnly);
60     stream.setByteOrder(QDataStream::BigEndian);
61     stream.setFloatingPointPrecision(QDataStream::SinglePrecision);
62     stream << m_positionSecs
63            << m_beatsTillNextMarker;
64     return data;
65 }
66 
67 // static
68 SeratoBeatGridNonTerminalMarkerPointer
parseID3(const QByteArray & data)69 SeratoBeatGridNonTerminalMarker::parseID3(const QByteArray& data) {
70     if (data.length() != kMarkerSizeID3) {
71         kLogger.warning() << "Parsing SeratoBeatGridNonTerminalMarker failed:"
72                           << "Length" << data.length()
73                           << "!=" << kMarkerSizeID3;
74         return nullptr;
75     }
76 
77     float positionSecs;
78     quint32 beatsTillNextMarker;
79 
80     QDataStream stream(data);
81     stream.setByteOrder(QDataStream::BigEndian);
82     stream.setFloatingPointPrecision(QDataStream::SinglePrecision);
83     stream >> positionSecs >> beatsTillNextMarker;
84 
85     if (positionSecs < 0) {
86         kLogger.warning() << "Parsing SeratoBeatGridNonTerminalMarker failed:"
87                           << "Position value" << positionSecs
88                           << "is negative";
89         return nullptr;
90     }
91 
92     if (stream.status() != QDataStream::Status::Ok) {
93         kLogger.warning() << "Parsing SeratoBeatGridNonTerminalMarker failed:"
94                           << "Stream read failed with status"
95                           << stream.status();
96         return nullptr;
97     }
98 
99     if (!stream.atEnd()) {
100         kLogger.warning() << "Parsing SeratoBeatGridNonTerminalMarker failed:"
101                           << "Unexpected trailing data";
102         return nullptr;
103     }
104 
105     SeratoBeatGridNonTerminalMarkerPointer pMarker =
106             std::make_shared<SeratoBeatGridNonTerminalMarker>(
107                     positionSecs, beatsTillNextMarker);
108     kLogger.trace() << "SeratoBeatGridNonTerminalMarker" << *pMarker;
109     return pMarker;
110 }
111 
dumpID3() const112 QByteArray SeratoBeatGridTerminalMarker::dumpID3() const {
113     QByteArray data;
114     data.reserve(kMarkerSizeID3);
115 
116     QDataStream stream(&data, QIODevice::WriteOnly);
117     stream.setByteOrder(QDataStream::BigEndian);
118     stream.setFloatingPointPrecision(QDataStream::SinglePrecision);
119     stream << m_positionSecs << m_bpm;
120     return data;
121 }
122 
123 // static
parseID3(const QByteArray & data)124 SeratoBeatGridTerminalMarkerPointer SeratoBeatGridTerminalMarker::parseID3(
125         const QByteArray& data) {
126     if (data.length() != kMarkerSizeID3) {
127         kLogger.warning() << "Parsing SeratoBeatGridTerminalMarker failed:"
128                           << "Length" << data.length()
129                           << "!=" << kMarkerSizeID3;
130         return nullptr;
131     }
132 
133     float positionSecs;
134     float bpm;
135 
136     QDataStream stream(data);
137     stream.setByteOrder(QDataStream::BigEndian);
138     stream.setFloatingPointPrecision(QDataStream::SinglePrecision);
139     stream >> positionSecs >> bpm;
140 
141     if (positionSecs < 0) {
142         kLogger.warning() << "Parsing SeratoBeatGridTerminalMarker failed:"
143                           << "Position value" << positionSecs
144                           << "is negative";
145         return nullptr;
146     }
147 
148     if (bpm < 0) {
149         kLogger.warning() << "Parsing SeratoBeatGridTerminalMarker failed:"
150                           << "BPM value" << bpm << "is negative";
151         return nullptr;
152     }
153 
154     if (stream.status() != QDataStream::Status::Ok) {
155         kLogger.warning() << "Parsing SeratoBeatGridTerminalMarker failed:"
156                           << "Stream read failed with status"
157                           << stream.status();
158         return nullptr;
159     }
160 
161     if (!stream.atEnd()) {
162         kLogger.warning() << "Parsing SeratoBeatGridTerminalMarker failed:"
163                           << "Unexpected trailing data";
164         return nullptr;
165     }
166 
167     SeratoBeatGridTerminalMarkerPointer pMarker =
168             std::make_shared<SeratoBeatGridTerminalMarker>(positionSecs, bpm);
169     kLogger.trace() << "SeratoBeatGridTerminalMarker" << *pMarker;
170     return pMarker;
171 }
172 
173 // static
parse(SeratoBeatGrid * seratoBeatGrid,const QByteArray & data,taglib::FileType fileType)174 bool SeratoBeatGrid::parse(SeratoBeatGrid* seratoBeatGrid,
175         const QByteArray& data,
176         taglib::FileType fileType) {
177     VERIFY_OR_DEBUG_ASSERT(seratoBeatGrid) {
178         return false;
179     }
180 
181     switch (fileType) {
182     case taglib::FileType::MP3:
183     case taglib::FileType::AIFF:
184         return parseID3(seratoBeatGrid, data);
185     case taglib::FileType::MP4:
186     case taglib::FileType::FLAC:
187         return parseBase64Encoded(seratoBeatGrid, data);
188     default:
189         return false;
190     }
191 }
192 
193 // static
parseID3(SeratoBeatGrid * seratoBeatGrid,const QByteArray & data)194 bool SeratoBeatGrid::parseID3(
195         SeratoBeatGrid* seratoBeatGrid, const QByteArray& data) {
196     QDataStream stream(data);
197     stream.setByteOrder(QDataStream::BigEndian);
198     stream.setFloatingPointPrecision(QDataStream::SinglePrecision);
199 
200     quint16 version;
201     stream >> version;
202     if (version != kVersion) {
203         kLogger.warning() << "Parsing SeratoBeatGrid failed:"
204                           << "Unknown Serato BeatGrid tag version";
205         return false;
206     }
207 
208     quint32 numMarkers;
209     stream >> numMarkers;
210 
211     if (numMarkers <= 0) {
212         kLogger.warning() << "Parsing SeratoBeatGrid failed:"
213                           << "Expected at least one marker, but found"
214                           << numMarkers;
215         return false;
216     }
217 
218     char buffer[kMarkerSizeID3];
219     double previousBeatPositionSecs = -1;
220 
221     // Read non-terminal beatgrid markers
222     QList<SeratoBeatGridNonTerminalMarkerPointer> nonTerminalMarkers;
223     for (quint32 i = 0; i < numMarkers - 1; i++) {
224         if (stream.readRawData(buffer, sizeof(buffer)) != sizeof(buffer)) {
225             kLogger.warning() << "Parsing SeratoBeatGrid failed:"
226                               << "unable to read non-terminal marker data";
227             return false;
228         }
229 
230         QByteArray markerData = QByteArray::fromRawData(buffer, kMarkerSizeID3);
231         SeratoBeatGridNonTerminalMarkerPointer pNonTerminalMarker =
232                 SeratoBeatGridNonTerminalMarker::parseID3(markerData);
233         if (!pNonTerminalMarker) {
234             kLogger.warning() << "Parsing SeratoBeatGrid failed:"
235                               << "Unable to parse non-terminal marker!";
236             return false;
237         }
238 
239         if (pNonTerminalMarker->beatsTillNextMarker() <= 0) {
240             kLogger.warning() << "Parsing SeratoBeatGrid failed:"
241                               << "Non-terminal marker's beatsTillNextMarker"
242                               << pNonTerminalMarker->beatsTillNextMarker()
243                               << "must be greater than 0";
244             return false;
245         }
246 
247         if (pNonTerminalMarker->positionSecs() < 0) {
248             kLogger.warning() << "Parsing SeratoBeatGrid failed:"
249                               << "Non-terminal marker has invalid position"
250                               << pNonTerminalMarker->positionSecs()
251                               << "< 0";
252             return false;
253         }
254 
255         if (pNonTerminalMarker->positionSecs() <= previousBeatPositionSecs) {
256             kLogger.warning() << "Parsing SeratoBeatGrid failed:"
257                               << "Non-terminal marker's position"
258                               << pNonTerminalMarker->positionSecs()
259                               << "must be greater than the previous marker's position"
260                               << previousBeatPositionSecs;
261             return false;
262         }
263         previousBeatPositionSecs = pNonTerminalMarker->positionSecs();
264 
265         nonTerminalMarkers.append(pNonTerminalMarker);
266     }
267 
268     // Read last (terminal) beatgrid marker
269     if (stream.readRawData(buffer, sizeof(buffer)) != sizeof(buffer)) {
270         kLogger.warning() << "Parsing SeratoBeatGrid failed:"
271                           << "unable to read terminal marker data";
272         return false;
273     }
274 
275     QByteArray markerData = QByteArray::fromRawData(buffer, kMarkerSizeID3);
276     SeratoBeatGridTerminalMarkerPointer pTerminalMarker =
277             SeratoBeatGridTerminalMarker::parseID3(markerData);
278     if (!pTerminalMarker) {
279         kLogger.warning() << "Parsing SeratoBeatGrid failed:"
280                           << "Unable to parse terminal marker!";
281         return false;
282     }
283 
284     if (pTerminalMarker->bpm() <= 0) {
285         kLogger.warning() << "Parsing SeratoBeatGrid failed:"
286                           << "Terminal marker's BPM"
287                           << pTerminalMarker->bpm()
288                           << "must be greater than 0";
289         return false;
290     }
291 
292     if (pTerminalMarker->positionSecs() < 0) {
293         kLogger.warning() << "Parsing SeratoBeatGrid failed:"
294                           << "Non-terminal marker has invalid position"
295                           << pTerminalMarker->positionSecs()
296                           << "< 0";
297         return false;
298     }
299 
300     if (pTerminalMarker->positionSecs() <= previousBeatPositionSecs) {
301         kLogger.warning() << "Parsing SeratoBeatGrid failed:"
302                           << "Terminal marker's position"
303                           << pTerminalMarker->positionSecs()
304                           << "must be greater than the previous marker's position"
305                           << previousBeatPositionSecs;
306         return false;
307     }
308 
309     // Read footer
310     //
311     // FIXME: This byte has caused some headache because I have not the
312     // slightest idea what this value could be. Apparently it's random, because
313     // it changes even when entering Serato's "Edit Grid" mode and then leaving
314     // it immediately without making any changes.
315     // For now, we only read it to be able to dump the exact same byte sequence
316     // later on.
317     quint8 footer;
318     stream >> footer;
319 
320     if (stream.status() != QDataStream::Status::Ok) {
321         kLogger.warning() << "Parsing SeratoBeatGrid failed:"
322                           << "Stream read failed with status" << stream.status();
323         return false;
324     }
325 
326     if (!stream.atEnd()) {
327         kLogger.warning() << "Parsing SeratoBeatGrid failed:"
328                           << "Unexpected trailing data";
329         return false;
330     }
331     seratoBeatGrid->setNonTerminalMarkers(std::move(nonTerminalMarkers));
332     seratoBeatGrid->setTerminalMarker(pTerminalMarker);
333     seratoBeatGrid->setFooter(footer);
334 
335     return true;
336 }
337 
parseBase64Encoded(SeratoBeatGrid * seratoBeatGrid,const QByteArray & base64EncodedData)338 bool SeratoBeatGrid::parseBase64Encoded(
339         SeratoBeatGrid* seratoBeatGrid, const QByteArray& base64EncodedData) {
340     if (base64EncodedData.isEmpty()) {
341         kLogger.warning() << "Decoding SeratoBeatGrid from base64 failed:"
342                           << "No data";
343         return true;
344     }
345     char extraBase64Byte = base64EncodedData.at(base64EncodedData.size() - 1);
346     const auto decodedData = QByteArray::fromBase64(
347             base64EncodedData.left(base64EncodedData.size() - 1));
348     if (!decodedData.startsWith(kSeratoBeatGridBase64EncodedPrefix)) {
349         kLogger.warning() << "Decoding SeratoBeatGrid from base64 failed:"
350                           << "Unexpected prefix"
351                           << decodedData.left(kSeratoBeatGridBase64EncodedPrefix.size())
352                           << "!="
353                           << kSeratoBeatGridBase64EncodedPrefix;
354         return false;
355     }
356     DEBUG_ASSERT(decodedData.size() >= kSeratoBeatGridBase64EncodedPrefix.size());
357     if (!parseID3(
358                 seratoBeatGrid,
359                 decodedData.mid(kSeratoBeatGridBase64EncodedPrefix.size()))) {
360         kLogger.warning() << "Parsing base64encoded SeratoBeatGrid failed!";
361         return false;
362     }
363 
364     seratoBeatGrid->setExtraBase64Byte(extraBase64Byte);
365 
366     return true;
367 }
368 
dump(taglib::FileType fileType) const369 QByteArray SeratoBeatGrid::dump(taglib::FileType fileType) const {
370     switch (fileType) {
371     case taglib::FileType::MP3:
372     case taglib::FileType::AIFF:
373         return dumpID3();
374     case taglib::FileType::MP4:
375     case taglib::FileType::FLAC:
376         return dumpBase64Encoded();
377     default:
378         DEBUG_ASSERT(false);
379         return {};
380     }
381 }
382 
dumpID3() const383 QByteArray SeratoBeatGrid::dumpID3() const {
384     QByteArray data;
385     if (isEmpty() || !m_pTerminalMarker) {
386         // Return empty QByteArray
387         return data;
388     }
389 
390     quint32 numMarkers = m_nonTerminalMarkers.size() + 1;
391     data.reserve(
392             sizeof(quint16) + // Version
393             sizeof(quint32) + // Number of Markers
394             (kMarkerSizeID3 * numMarkers) +
395             sizeof(quint8) // Footer
396     );
397 
398     QDataStream stream(&data, QIODevice::WriteOnly);
399     stream.setByteOrder(QDataStream::BigEndian);
400     stream.setFloatingPointPrecision(QDataStream::SinglePrecision);
401     stream << kVersion << numMarkers;
402     for (const SeratoBeatGridNonTerminalMarkerPointer& pMarker : m_nonTerminalMarkers) {
403         stream.writeRawData(pMarker->dumpID3(), kMarkerSizeID3);
404     }
405     stream.writeRawData(m_pTerminalMarker->dumpID3(), kMarkerSizeID3);
406     stream << m_footer;
407     return data;
408 }
409 
dumpBase64Encoded() const410 QByteArray SeratoBeatGrid::dumpBase64Encoded() const {
411     if (isEmpty()) {
412         // Return empty QByteArray
413         return {};
414     }
415 
416     QByteArray data = kSeratoBeatGridBase64EncodedPrefix;
417     data.append(dumpID3());
418 
419     QByteArray base64EncodedData = base64encode(data, false);
420     base64EncodedData.append(extraBase64Byte());
421     return base64EncodedData;
422 }
423 
setBeats(BeatsPointer pBeats,const audio::SignalInfo & signalInfo,const Duration & duration,double timingOffsetMillis)424 void SeratoBeatGrid::setBeats(BeatsPointer pBeats,
425         const audio::SignalInfo& signalInfo,
426         const Duration& duration,
427         double timingOffsetMillis) {
428     if (!pBeats) {
429         setTerminalMarker(nullptr);
430         setNonTerminalMarkers({});
431         return;
432     }
433 
434     const double timingOffsetSecs = timingOffsetMillis / 1000;
435 
436     // Beats::findBeats expects a sample range. We want to retrieve all beats
437     // in the track, therefore we calculate the track duration in samples and
438     // round up. This value might be longer than the actual track, but that's
439     // okay because we want to make sure we get all beats.
440     const SINT trackDurationSamples = signalInfo.frames2samples(
441             static_cast<SINT>(signalInfo.secs2frames(
442                     std::ceil(duration.toDoubleSeconds()))));
443     auto pBeatsIterator = pBeats->findBeats(0, trackDurationSamples);
444 
445     // This might be null if the track doesn't contain any beats
446     if (!pBeatsIterator || !pBeatsIterator->hasNext()) {
447         qWarning() << "Serato Beatgrid: No beats available, exporting empty beatgrid!";
448         setTerminalMarker(nullptr);
449         setNonTerminalMarkers({});
450         return;
451     }
452 
453     const double beatgridFrameOffset = signalInfo.samples2framesFractional(pBeatsIterator->next());
454     double currentBeatPositionFrames = 0;
455     double previousBeatPositionFrames = 0;
456     double previousDeltaFrames = -1;
457     QList<SeratoBeatGridNonTerminalMarkerPointer> nonTerminalMarkers;
458     {
459         const double positionSecs = signalInfo.frames2secsFractional(
460                 currentBeatPositionFrames + beatgridFrameOffset);
461         nonTerminalMarkers.append(
462                 std::make_shared<SeratoBeatGridNonTerminalMarker>(
463                         positionSecs - timingOffsetSecs, 0));
464     }
465     while (pBeatsIterator->hasNext()) {
466         previousBeatPositionFrames = currentBeatPositionFrames;
467         currentBeatPositionFrames =
468                 signalInfo.samples2framesFractional(pBeatsIterator->next()) -
469                 beatgridFrameOffset;
470 
471         // Calculate the delta between the current beat and the previous beat.
472         // If the distance is the same as the distance between the previous
473         // beat and the beat before that, we can just increment
474         // `beatsSinceLastMarker`. If not, we need to add a new marker.
475         const double currentDeltaFrames = currentBeatPositionFrames - previousBeatPositionFrames;
476         if (previousDeltaFrames < 0) {
477             previousDeltaFrames = currentDeltaFrames;
478         }
479 
480         const double differenceBetweenCurrentAndPreviousDelta =
481                 abs(currentDeltaFrames - previousDeltaFrames);
482         if (differenceBetweenCurrentAndPreviousDelta >= kEpsilon) {
483             const double positionSecs = signalInfo.frames2secsFractional(
484                     previousBeatPositionFrames + beatgridFrameOffset);
485             nonTerminalMarkers.append(
486                     std::make_shared<SeratoBeatGridNonTerminalMarker>(
487                             positionSecs - timingOffsetSecs, 1));
488         } else {
489             // We are adding a new beat marker, therefore we need to update the
490             // `beatsSinceLastMarker` variable of the last marker we added.
491             const auto pNonTerminalMarker = nonTerminalMarkers.last();
492             pNonTerminalMarker->setBeatsTillNextMarker(
493                     pNonTerminalMarker->beatsTillNextMarker() + 1);
494         }
495 
496         previousDeltaFrames = currentDeltaFrames;
497     }
498 
499     // If the beatgrid is constant, i.e. if there is only a single non-terminal
500     // beatgrid marker at the start of the beatgrid and a terminal marker at
501     // the end, then Serato discards the non-terminal marker and just saves the
502     // terminal marker instead.
503     if (nonTerminalMarkers.size() == 1) {
504         nonTerminalMarkers.clear();
505     }
506 
507     // Finally, create the terminal marker.
508     const double currentBeatPositionFramesWithOffset =
509             currentBeatPositionFrames + beatgridFrameOffset;
510     const double positionSecs = signalInfo.frames2secsFractional(
511             currentBeatPositionFramesWithOffset);
512     const double bpm = pBeats->getBpmAroundPosition(
513             signalInfo.getChannelCount() * currentBeatPositionFramesWithOffset,
514             1);
515 
516     setTerminalMarker(std::make_shared<SeratoBeatGridTerminalMarker>(
517             positionSecs - timingOffsetSecs, bpm));
518     setNonTerminalMarkers(nonTerminalMarkers);
519 }
520 
operator <<(QDebug dbg,const SeratoBeatGridTerminalMarker & arg)521 QDebug operator<<(QDebug dbg, const SeratoBeatGridTerminalMarker& arg) {
522     return dbg << "SeratoBeatGridTerminalMarker"
523                << "PositionSecs =" << arg.positionSecs()
524                << "BPM =" << arg.bpm();
525 }
526 
operator <<(QDebug dbg,const SeratoBeatGridNonTerminalMarker & arg)527 QDebug operator<<(QDebug dbg, const SeratoBeatGridNonTerminalMarker& arg) {
528     return dbg << "SeratoBeatGridNonTerminalMarker"
529                << "PositionSecs =" << arg.positionSecs()
530                << "BeatTillNextMarker = " << arg.beatsTillNextMarker();
531 }
532 
operator <<(QDebug dbg,const SeratoBeatGrid & arg)533 QDebug operator<<(QDebug dbg, const SeratoBeatGrid& arg) {
534     // TODO: Improve debug output
535     return dbg << "number of markers ="
536                << (arg.nonTerminalMarkers().length() +
537                           (arg.terminalMarker() ? 1 : 0));
538 }
539 
540 } // namespace mixxx
541