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