summaryrefslogtreecommitdiffstats
path: root/src/track/serato/markers.cpp
blob: bdb540001c9d1f5b245eb8927e823c18f1383437 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
#include "track/serato/markers.h"

#include <QtEndian>

#include "track/serato/tags.h"

namespace {

const int kNumEntries = 14;
const int kLoopEntryStartIndex = 5;
const int kEntrySize = 22;
const quint16 kVersion = 0x0205;

// These functions convert between a custom 4-byte format (that we'll call
// "serato32" for brevity) and 3-byte plaintext (both quint32).
// Serato's custom format inserts a single null bit after every 7 payload
// bits, starting from the rightmost bit.
//
// Here's an example:
//
//                      | Hex          Binary
//     ---------------- | -----------  --------------------------------
//     3-byte plaintext |    00 00 cc       000 0000000 0000001 1001100
//     serato32 value   | 00 00 01 4c  00000000000000000000000101001100
//                      |
//     3-byte plaintext |    cc 88 00       110 0110010 0010000 0000000
//     serato32 value   | 06 32 10 00  00000110001100100001000000000000
//
// See this for details:
// https://github.com/Holzhaus/serato-tags/blob/master/docs/serato_markers_.md#custom-serato32-binary-format

/// Decode value from Serato's 32-bit custom format to 24-bit plaintext.
quint32 serato32toUint24(quint8 w, quint8 x, quint8 y, quint8 z) {
    quint8 c = (z & 0x7F) | ((y & 0x01) << 7);
    quint8 b = ((y & 0x7F) >> 1) | ((x & 0x03) << 6);
    quint8 a = ((x & 0x7F) >> 2) | ((w & 0x07) << 5);
    return ((static_cast<quint32>(a) << 16) | (static_cast<quint32>(b) << 8) |
            static_cast<quint32>(c));
}

/// Decode value from Serato's 32-bit custom format to 24-bit plaintext.
quint32 serato32toUint24(quint32 value) {
    return serato32toUint24((value >> 24) & 0xFF,
            (value >> 16) & 0xFF,
            (value >> 8) & 0xFF,
            value & 0xFF);
}

/// Encode a 24-bit plaintext value into Serato's 32-bit custom format.
quint32 serato32fromUint24(quint8 a, quint8 b, quint8 c) {
    quint8 z = c & 0x7F;
    quint8 y = ((c >> 7) | (b << 1)) & 0x7F;
    quint8 x = ((b >> 6) | (a << 2)) & 0x7F;
    quint8 w = (a >> 5);
    return (static_cast<quint32>(w) << 24) | (static_cast<quint32>(x) << 16) |
            (static_cast<quint32>(y) << 8) | static_cast<quint32>(z);
}

/// Encode a 24-bit plaintext value into Serato's 32-bit custom format. The 8
/// most significant bits of the quint32 will be ignored.
quint32 serato32fromUint24(quint32 value) {
    return serato32fromUint24(
            (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF);
}
} // namespace

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<quint8>((m_hasStartPosition ? 0x00 : 0x7F))
           << static_cast<quint32>(
                      (m_hasStartPosition ? serato32fromUint24(m_startPosition)
                                          : 0x7F7F7F7F))
           << static_cast<quint8>((m_hasEndPosition ? 0x00 : 0x7F))
           << static_cast<quint32>(
                      (m_hasEndPosition ? serato32fromUint24(m_endPosition)
                                        : 0x7F7F7F7F));
    stream.writeRawData("\x00\x7F\x7F\x7F\x7F\x7F", 6);
    stream << serato32fromUint24(static_cast<quint32>(m_color))
           << static_cast<quint8>(m_type) << static_cast<quint8>(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 startPositionSerato32;
    quint32 endPositionSerato32;
    quint32 colorSerato32;
    bool isLocked;
    char buffer[6];

    QDataStream stream(data);
    stream.setVersion(QDataStream::Qt_5_0);
    stream.setByteOrder(QDataStream::BigEndian);
    stream >> startPositionStatus >> startPositionSerato32 >>
            endPositionStatus >> endPositionSerato32;

    if (stream.readRawData(buffer, sizeof(buffer)) != sizeof(buffer)) {
        qWarning() << "Parsing SeratoMarkersEntry failed:"
                   << "unable to read bytes 10..16";
        return nullptr;
    }

    stream >> colorSerato32 >> type >> isLocked;

    const RgbColor color = RgbColor(serato32toUint24(colorSerato32));

    // Parse Start Position
    bool hasStartPosition = (startPositionStatus != 0x7F);
    quint32 startPosition = 0x7F7F7F7F;
    if (!hasStartPosition) {
        // Start position not set
        if (startPositionSerato32 != 0x7F7F7F7F) {
            qWarning() << "Parsing SeratoMarkersEntry failed:"
                       << "startPosition != 0x7F7F7F7F";

            return nullptr;
        }
    } else {
        startPosition = serato32toUint24(startPositionSerato32);
    }

    // Parse End Position
    bool hasEndPosition = (endPositionStatus != 0x7F);
    quint32 endPosition = 0x7F7F7F7F;
    if (!hasEndPosition) {
        // End position not set
        if (endPositionSerato32 != 0x7F7F7F7F) {
            qWarning() << "Parsing SeratoMarkersEntry failed:"
                       << "endPosition != 0x7F7F7F7F";

            return nullptr;
        }
    } else {
        endPosition = serato32toUint24(endPositionSerato32);
    }

    // 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<SeratoMarkersEntryPointer> 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) {
            qWarn