Skip to main content

triseratops/tag/
markers.rs

1// Copyright (c) 2023 Jan Holthuis <jan.holthuis@rub.de>
2//
3// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy
4// of the MPL was not distributed with this file, You can obtain one at
5// http://mozilla.org/MPL/2.0/.
6//
7// SPDX-License-Identifier: MPL-2.0
8
9//! The `Serato Markers_` tag stores information about the first 5 Cues, 9 Loops and the track
10//! color.
11//!
12//! This is redundant with some of the information from the `Serato Markers2` tag. Serato will
13//! prefer information from `Serato Markers_` if it's present.
14
15use super::color::Color;
16use super::format::{enveloped, id3, mp4, Tag};
17use super::generic::{Position, Version};
18use super::serato32;
19use super::util::{take_color, take_version, write_color, write_version};
20use crate::error::Error;
21use crate::util::{Res, NULL};
22use nom::error::ParseError;
23use std::io;
24use std::io::Cursor;
25
26/// Represents a single marker in the `Serato Markers_` tag.
27#[derive(Debug, Clone)]
28pub struct Marker {
29    /// The position of the loop or cue.
30    pub start_position: Option<Position>,
31
32    /// If this is a loop, this field stores the end position.
33    pub end_position: Option<Position>,
34
35    /// The color of the cue.
36    ///
37    /// For loop, this field should always be `#27AAE1`.
38    pub color: Color,
39
40    /// The type of this marker.
41    pub marker_type: MarkerType,
42
43    /// Indicates whether the loop is locked.
44    ///
45    /// For cues, this field should always be `false`.
46    pub is_locked: bool,
47}
48
49/// Represents the `Serato Markers_` tag.
50///
51/// It contains the the first 5 cue points, the first 9 loops and the color of the track.
52///
53/// This seems to be a legacy tag, since it lacks some information such as cue labels and all information of the `Serato Markers_` tag is also part of the [`Serato Markers2`](super::markers2] tag.
54/// If the two tags contradict each other, Serato DJ will prefer the data from the `Serato Markers_` tag.
55///
56/// # Example
57///
58/// ```
59/// use triseratops::tag::{Markers, format::id3::ID3Tag};
60///
61/// // First, read the tag data from the ID3 GEOB tag (the tag name can be accessed using the
62/// // Markers::ID3_TAG), then parse the data like this:
63/// fn parse(data: &[u8]) {
64///     let content = Markers::parse_id3(data).expect("Failed to parse data!");
65///     println!("{:?}", content);
66/// }
67/// ```
68#[derive(Debug, Clone)]
69pub struct Markers {
70    /// The tag version.
71    pub version: Version,
72
73    /// The marker entries.
74    pub entries: Vec<Marker>,
75
76    /// The color of the track in Serato's library view.
77    pub track_color: Color,
78}
79
80impl Markers {
81    #[must_use]
82    pub fn cues(&self) -> Vec<(u8, &Marker)> {
83        let mut index: u8 = 0;
84        let mut cues = Vec::new();
85        for marker in &self.entries {
86            if marker.marker_type != MarkerType::Invalid && marker.marker_type != MarkerType::Cue {
87                continue;
88            }
89
90            cues.push((index, marker));
91            index += 1;
92        }
93        cues
94    }
95
96    #[must_use]
97    pub fn loops(&self) -> Vec<(u8, &Marker)> {
98        let mut index: u8 = 0;
99        let mut loops = Vec::new();
100        for marker in &self.entries {
101            if marker.marker_type != MarkerType::Loop {
102                continue;
103            }
104
105            loops.push((index, marker));
106            index += 1;
107        }
108        loops
109    }
110
111    #[must_use]
112    pub fn track_color(&self) -> Color {
113        self.track_color
114    }
115}
116
117impl Tag for Markers {
118    const NAME: &'static str = "Serato Markers_";
119
120    fn parse(input: &[u8]) -> Result<Self, Error> {
121        let (_, autotags) = nom::combinator::all_consuming(take_markers)(input)?;
122        Ok(autotags)
123    }
124
125    fn write(&self, writer: &mut impl io::Write) -> Result<usize, Error> {
126        write_markers(writer, self)
127    }
128}
129
130impl id3::ID3Tag for Markers {}
131impl enveloped::EnvelopedTag for Markers {}
132impl mp4::MP4Tag for Markers {
133    const MP4_ATOM_FREEFORM_NAME: &'static str = "markers";
134
135    fn parse_mp4(input: &[u8]) -> Result<Self, Error> {
136        let (_, encoded) = nom::combinator::all_consuming(
137            super::format::enveloped::take_base64_with_newline,
138        )(input)?;
139        let content = super::format::enveloped::envelope_decode_with_name(encoded, Self::NAME)?;
140        let (_, markers) = nom::combinator::all_consuming(take_markers_mp4)(&content)?;
141        Ok(markers)
142    }
143
144    fn write_mp4(&self, writer: &mut impl io::Write) -> Result<usize, Error> {
145        let mut buffer = Cursor::new(vec![]);
146        write_markers_mp4(&mut buffer, self)?;
147        let plain_data = &buffer.get_ref()[..];
148        enveloped::envelope_encode_with_name(writer, plain_data, Self::NAME)
149    }
150}
151
152/// Type of a Marker.
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub enum MarkerType {
155    /// Used for unset cues.
156    ///
157    /// In the binary format, this is represented by `0x00`.
158    ///
159    /// **Note:** For unset loops, use [`MarkerType::Loop`](MarkerType::Loop) without a position.
160    Invalid,
161    /// Used for set cues.
162    ///
163    /// In the binary format, this is represented by `0x01`.
164    Cue,
165    /// Used for loops (both set and unset ones).
166    ///
167    /// In the binary format, this is represented by `0x03`.
168    Loop,
169}
170
171/// Returns a bool parsed from the next input byte.
172///
173/// This function returns `false` if the byte is `0x00`, else `true`.
174///
175/// # Example
176/// ```
177/// use triseratops::tag::markers::take_bool;
178/// use nom::Err;
179/// use nom::error::{Error, ErrorKind};
180///
181/// assert_eq!(take_bool(&[0x00]), Ok((&[][..], false)));
182/// assert_eq!(take_bool(&[0x01]), Ok((&[][..], true)));
183/// assert!(take_bool(&[0xAB, 0x00, 0x01]).is_err());
184/// assert!(take_bool(&[]).is_err());
185/// ```
186pub fn take_bool(input: &[u8]) -> Res<&[u8], bool> {
187    let (input, position_prefix) = nom::number::complete::u8(input)?;
188    match position_prefix {
189        0x00 => Ok((input, false)),
190        0x01 => Ok((input, true)),
191        _ => Err(nom::Err::Error(nom::error::VerboseError::from_error_kind(
192            input,
193            nom::error::ErrorKind::Tag,
194        ))),
195    }
196}
197
198/// Returns a bool parsed from the next input byte that indicates if the following marker position
199/// is set.
200///
201/// Do not use `take_bool` for this, because the value mapping is different:
202///
203/// | Byte   | `bool`  | Description
204/// | ------ | ------- | ----------------------------------------------------------------
205/// | `0x00` | `true`  | The following 4 bytes contain the position in `serato32` format.
206/// | `0x7F` | `false` | The position is not set and following 4 bytes be `0x7f7f7f7f`.
207/// | Other  | `_`     | Invalid data, throws an error.
208///
209fn take_has_position(input: &[u8]) -> Res<&[u8], bool> {
210    let (input, position_prefix) = nom::number::complete::u8(input)?;
211    match position_prefix {
212        0x00 => Ok((input, true)),
213        0x7F => Ok((input, false)),
214        _ => Err(nom::Err::Error(nom::error::VerboseError::from_error_kind(
215            input,
216            nom::error::ErrorKind::Tag,
217        ))),
218    }
219}
220
221#[test]
222fn test_take_has_position() {
223    assert_eq!(take_has_position(&[0x00]), Ok((&[][..], true)));
224    assert_eq!(take_has_position(&[0x7F]), Ok((&[][..], false)));
225    assert_eq!(take_has_position(&[0x00, 0x05]), Ok((&[0x05][..], true)));
226    assert!(take_has_position(&[0xAB, 0x00, 0x01]).is_err());
227    assert!(take_has_position(&[]).is_err());
228}
229
230/// Returns an `Option<Position>` which contains the position parsed from the next 5 input bytes.
231///
232/// Uses `take_has_position` internally to determine if the position is set, then either returns
233/// the position as `Some` or ensures the that "no position" constant is used and returns `None`.
234pub fn take_position(input: &[u8]) -> Res<&[u8], Option<Position>> {
235    if input.len() < 5 {
236        return Err(nom::Err::Error(nom::error::VerboseError::from_error_kind(
237            input,
238            nom::error::ErrorKind::Digit,
239        )));
240    }
241    let (input, has_position) = nom::error::context("take has_position", take_has_position)(input)?;
242    if has_position {
243        let (input, millis) = serato32::take_u32(input)?;
244        let position = Position { millis };
245        Ok((input, Some(position)))
246    } else {
247        let (input, _) = nom::bytes::complete::tag(b"\x7f\x7f\x7f\x7f")(input)?;
248        Ok((input, None))
249    }
250}
251
252#[test]
253fn test_take_position() {
254    assert_eq!(
255        take_position(&[0x00, 0x00, 0x00, 0x00, 0x00]),
256        Ok((&[][..], Some(Default::default())))
257    );
258    assert_eq!(
259        take_position(&[0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x00]),
260        Ok((&[0x00][..], None))
261    );
262}
263
264/// Returns the [`MarkerType`](MarkerType) for the cue marker parsed from the next input byte.
265fn take_marker_type(input: &[u8]) -> Res<&[u8], MarkerType> {
266    let (next_input, position_prefix) = nom::number::complete::u8(input)?;
267    match position_prefix {
268        0x00 => Ok((next_input, MarkerType::Invalid)),
269        0x01 => Ok((next_input, MarkerType::Cue)),
270        0x03 => Ok((next_input, MarkerType::Loop)),
271        _ => Err(nom::Err::Error(nom::error::VerboseError::from_error_kind(
272            input,
273            nom::error::ErrorKind::Tag,
274        ))),
275    }
276}
277
278#[test]
279fn test_take_marker_type() {
280    assert_eq!(
281        take_marker_type(&[0x00]),
282        Ok((&[][..], MarkerType::Invalid))
283    );
284    assert_eq!(
285        take_marker_type(&[0x03, 0x01]),
286        Ok((&[0x01][..], MarkerType::Loop))
287    );
288    assert!(take_marker_type(&[0xAB]).is_err());
289}
290
291/// Returns a `Marker` parsed from the input slice.
292fn take_marker(input: &[u8]) -> Res<&[u8], Marker> {
293    let (input, start_position) =
294        nom::error::context("marker start position", take_position)(input)?;
295    let (input, end_position) = nom::error::context("marker end position", take_position)(input)?;
296    let (input, _) = nom::error::context(
297        "marker unknown bytes",
298        nom::bytes::complete::tag(b"\x00\x7F\x7F\x7F\x7F\x7F"),
299    )(input)?;
300    let (input, color) = nom::error::context("marker color", serato32::take_color)(input)?;
301    let (input, marker_type) = nom::error::context("marker type", take_marker_type)(input)?;
302    let (input, is_locked) = nom::error::context("marker locked state", take_bool)(input)?;
303    Ok((
304        input,
305        Marker {
306            start_position,
307            end_position,
308            color,
309            marker_type,
310            is_locked,
311        },
312    ))
313}
314
315/// Parses the data into a `Markers` struct, consuming the whole input slice.
316fn take_markers(input: &[u8]) -> Res<&[u8], Markers> {
317    let (input, version) = take_version(input)?;
318    let (input, entries) =
319        nom::multi::length_count(nom::number::complete::be_u32, take_marker)(input)?;
320    let (input, track_color) = nom::combinator::all_consuming(serato32::take_color)(input)?;
321
322    let markers = Markers {
323        version,
324        entries,
325        track_color,
326    };
327    Ok((input, markers))
328}
329
330/// Returns a `Marker` parsed from the input slice (MP4 version).
331fn take_marker_mp4(input: &[u8]) -> Res<&[u8], Marker> {
332    let (input, start_position_raw) =
333        nom::error::context("marker start position", nom::number::complete::be_u32)(input)?;
334    let (input, end_position_raw) =
335        nom::error::context("marker end position", nom::number::complete::be_u32)(input)?;
336    let (input, _) =
337        nom::error::context("marker unknown bytes", nom::bytes::complete::take(6usize))(input)?;
338    let (input, color) = nom::error::context("marker color", take_color)(input)?;
339    let (input, marker_type) = nom::error::context("marker type", take_marker_type)(input)?;
340    let (input, is_locked) = nom::error::context("marker locked state", take_bool)(input)?;
341
342    let start_position = if start_position_raw != 0xFFFFFFFF {
343        Some(Position {
344            millis: start_position_raw,
345        })
346    } else {
347        None
348    };
349    let end_position = if end_position_raw != 0xFFFFFFFF && marker_type == MarkerType::Loop {
350        Some(Position {
351            millis: end_position_raw,
352        })
353    } else {
354        None
355    };
356
357    Ok((
358        input,
359        Marker {
360            start_position,
361            end_position,
362            color,
363            marker_type,
364            is_locked,
365        },
366    ))
367}
368
369/// Parses the data into a `Markers` struct, consuming the whole input slice (MP4 version).
370fn take_markers_mp4(input: &[u8]) -> Res<&[u8], Markers> {
371    let (input, version) = take_version(input)?;
372    let (input, entries) =
373        nom::multi::length_count(nom::number::complete::be_u32, take_marker_mp4)(input)?;
374    let (input, _) = nom::bytes::complete::tag(b"\0")(input)?;
375    let (input, track_color) = nom::combinator::all_consuming(take_color)(input)?;
376
377    let markers = Markers {
378        version,
379        entries,
380        track_color,
381    };
382    Ok((input, markers))
383}
384
385fn write_position(writer: &mut impl io::Write, position: Option<Position>) -> Result<usize, Error> {
386    match position {
387        Some(Position { millis }) => {
388            let mut bytes_written = writer.write(NULL)?;
389            bytes_written += serato32::write_u32(writer, millis)?;
390            Ok(bytes_written)
391        }
392        None => Ok(writer.write(b"\x7F\x7F\x7F\x7F\x7F")?),
393    }
394}
395
396fn write_position_mp4(
397    writer: &mut impl io::Write,
398    position: Option<Position>,
399) -> Result<usize, Error> {
400    // TODO: Implement this
401    let data = match position {
402        Some(Position { millis }) => millis.to_be_bytes(),
403        None => *b"\xFF\xFF\xFF\xFF",
404    };
405    Ok(writer.write(&data)?)
406}
407
408fn write_marker_type(writer: &mut impl io::Write, marker_type: MarkerType) -> Result<usize, Error> {
409    let byte: u8 = match marker_type {
410        MarkerType::Invalid => 0x00,
411        MarkerType::Cue => 0x01,
412        MarkerType::Loop => 0x03,
413    };
414    Ok(writer.write(&[byte])?)
415}
416
417fn write_bool(writer: &mut impl io::Write, value: bool) -> Result<usize, Error> {
418    let byte: u8 = match value {
419        true => 0x01,
420        false => 0x00,
421    };
422    Ok(writer.write(&[byte])?)
423}
424
425fn write_marker(writer: &mut impl io::Write, marker: &Marker) -> Result<usize, Error> {
426    let &Marker {
427        start_position,
428        end_position,
429        color,
430        marker_type,
431        is_locked,
432    } = marker;
433    let mut bytes_written = write_position(writer, start_position)?;
434    bytes_written += write_position(writer, end_position)?;
435    bytes_written += writer.write(b"\x00\x7F\x7F\x7F\x7F\x7F")?;
436    bytes_written += serato32::write_color(writer, color)?;
437    bytes_written += write_marker_type(writer, marker_type)?;
438    bytes_written += write_bool(writer, is_locked)?;
439    Ok(bytes_written)
440}
441
442fn write_marker_mp4(writer: &mut impl io::Write, marker: &Marker) -> Result<usize, Error> {
443    let &Marker {
444        start_position,
445        end_position,
446        color,
447        marker_type,
448        is_locked,
449    } = marker;
450    let mut bytes_written = write_position_mp4(writer, start_position)?;
451    bytes_written += write_position_mp4(writer, end_position)?;
452    bytes_written += writer.write(b"\x00\xFF\xFF\xFF\xFF\x00")?;
453    bytes_written += write_color(writer, color)?;
454    bytes_written += write_marker_type(writer, marker_type)?;
455    bytes_written += write_bool(writer, is_locked)?;
456    Ok(bytes_written)
457}
458
459pub fn write_markers(writer: &mut impl io::Write, markers: &Markers) -> Result<usize, Error> {
460    let Markers {
461        version,
462        entries,
463        track_color,
464    } = markers;
465    let mut bytes_written = write_version(writer, *version)?;
466    let num_markers = markers.entries.len() as u32;
467    bytes_written += writer.write(&num_markers.to_be_bytes())?;
468    for marker in entries {
469        bytes_written += write_marker(writer, marker)?;
470    }
471    bytes_written += serato32::write_color(writer, *track_color)?;
472    Ok(bytes_written)
473}
474
475pub fn write_markers_mp4(writer: &mut impl io::Write, markers: &Markers) -> Result<usize, Error> {
476    let mut bytes_written = write_version(writer, markers.version)?;
477    let num_markers = markers.entries.len() as u32;
478    bytes_written += writer.write(&num_markers.to_be_bytes())?;
479    for marker in &markers.entries {
480        bytes_written += write_marker_mp4(writer, marker)?;
481    }
482    bytes_written += writer.write(NULL)?;
483    bytes_written += write_color(writer, markers.track_color)?;
484    Ok(bytes_written)
485}