cue.rsannotatedcue.rssource147 lines · 3.7 KB · raw
1//! Code for representing CUE files / TOC info
2
3use crate::cdtime::CdTime;
4use bincode::{Decode, Encode};
5use std::str::FromStr;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
8pub enum TrackType {
9    Data,
10    Audio,
11}
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
14pub enum TrackMode {
15    Mode1,
16    Mode1DataOnly,
17    Mode2,
18    Audio,
19}
20
21impl TrackMode {
22    #[must_use]
23    pub fn to_type(self) -> TrackType {
24        match self {
25            Self::Mode1 | Self::Mode1DataOnly | Self::Mode2 => TrackType::Data,
26            Self::Audio => TrackType::Audio,
27        }
28    }
29
30    #[must_use]
31    pub fn bytes_per_sector(self) -> u64 {
32        match self {
33            Self::Mode1DataOnly => 2048,
34            Self::Mode1 | Self::Mode2 | Self::Audio => 2352,
35        }
36    }
37}
38
39impl FromStr for TrackMode {
40    type Err = String;
41
42    fn from_str(s: &str) -> Result<Self, Self::Err> {
43        match s {
44            "MODE1/2352" => Ok(Self::Mode1),
45            "MODE1/2048" => Ok(Self::Mode1DataOnly),
46            "MODE2/2352" => Ok(Self::Mode2),
47            "AUDIO" => Ok(Self::Audio),
48            _ => Err(format!("unsupported CD track type: {s}")),
49        }
50    }
51}
52
53#[derive(Debug, Clone, Encode, Decode)]
54pub struct Track {
55    pub number: u8,
56    pub mode: TrackMode,
57    pub track_type: TrackType,
58    pub start_time: CdTime,
59    pub end_time: CdTime,
60    pub pregap_len: CdTime,
61    pub pause_len: CdTime,
62    pub postgap_len: CdTime,
63}
64
65impl Track {
66    #[must_use]
67    pub fn effective_start_time(&self) -> CdTime {
68        self.start_time + self.pregap_len + self.pause_len
69    }
70}
71
72#[derive(Debug, Clone, Encode, Decode)]
73pub struct CueSheet {
74    tracks: Vec<Track>,
75    track_start_times: Vec<CdTime>,
76}
77
78impl CueSheet {
79    /// Create a new `CueSheet` from the given track list.
80    ///
81    /// # Panics
82    ///
83    /// This function will panic if the track list is empty.
84    #[must_use]
85    pub(crate) fn new(tracks: Vec<Track>) -> Self {
86        assert!(!tracks.is_empty(), "track list must not be empty");
87
88        let track_start_times = tracks.iter().map(|track| track.start_time).collect();
89
90        Self { tracks, track_start_times }
91    }
92
93    #[must_use]
94    pub fn track(&self, track_number: u8) -> &Track {
95        &self.tracks[(track_number - 1) as usize]
96    }
97
98    #[must_use]
99    #[allow(clippy::missing_panics_doc)]
100    pub fn last_track(&self) -> &Track {
101        self.tracks.last().unwrap()
102    }
103
104    /// Find the track containing the specified time. Returns `None` if the time is past the end of
105    /// the disc.
106    #[must_use]
107    pub fn find_track_by_time(&self, time: CdTime) -> Option<&Track> {
108        match self.track_start_times.binary_search(&time) {
109            Ok(i) => Some(&self.tracks[i]),
110            Err(i) => {
111                if i < self.tracks.len() {
112                    Some(&self.tracks[i - 1])
113                } else {
114                    let last_track = self.last_track();
115                    (time <= last_track.end_time).then_some(last_track)
116                }
117            }
118        }
119    }
120}
121
122#[must_use]
123pub(crate) fn tracks_are_continuous(tracks: &[Track]) -> bool {
124    if tracks[0].start_time != CdTime::ZERO {
125        return false;
126    }
127
128    for window in tracks.windows(2) {
129        let [track, next] = window else { unreachable!("windows(2)") };
130        if next.start_time != track.end_time {
131            return false;
132        }
133    }
134
135    true
136}
137
138pub(crate) fn finalize_track_list(tracks: &mut [Track]) {
139    tracks.sort_by_key(|track| track.number);
140
141    // The final track always has a 2-second postgap
142    let last_track = tracks.last_mut().unwrap();
143    if last_track.postgap_len == CdTime::ZERO {
144        last_track.postgap_len = CdTime::new(0, 2, 0);
145        last_track.end_time += CdTime::new(0, 2, 0);
146    }
147}