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}