chd.rsannotatedchd.rssource238 lines · 8.4 KB · raw
1//! Code for loading and reading CD-ROM images in CHD format
2
3#[cfg(test)]
4mod tests;
5
6use crate::cdtime::CdTime;
7use crate::cue::{CueSheet, Track, TrackMode, TrackType};
8use crate::reader::{SECTOR_HEADER_LEN, synthesize_data_header};
9use crate::{CdRomError, CdRomResult, cue};
10use chd::Chd;
11use chd::iter::LendingIterator;
12use std::fmt::{Debug, Formatter};
13use std::io::{Read, Seek};
14
15#[derive(Debug, Clone, Copy)]
16#[cfg_attr(test, derive(PartialEq, Eq))]
17struct CdMetadata {
18    track_number: u8,
19    mode: TrackMode,
20    frames: u32,
21    pregap_frames: u32,
22}
23
24impl CdMetadata {
25    fn parse_from(ascii_bytes: Vec<u8>) -> Option<Self> {
26        let text = String::from_utf8(ascii_bytes).ok()?;
27
28        log::debug!("CHD metadata line: {text}");
29
30        let mut track_number: Option<u8> = None;
31        let mut track_mode: Option<TrackMode> = None;
32        let mut frames: Option<u32> = None;
33        let mut pregap_frames: u32 = 0;
34        for token in text.split(' ') {
35            let Some((key, value)) = token.split_once(':') else { continue };
36
37            match key {
38                "TRACK" => track_number = Some(value.parse().ok()?),
39                "TYPE" => match value {
40                    "MODE1/2352" | "MODE1_RAW" => track_mode = Some(TrackMode::Mode1),
41                    "MODE1/2048" | "MODE1" => track_mode = Some(TrackMode::Mode1DataOnly),
42                    "MODE2/2352" | "MODE2_RAW" => track_mode = Some(TrackMode::Mode2),
43                    "AUDIO" => track_mode = Some(TrackMode::Audio),
44                    _ => return None,
45                },
46                "FRAMES" => frames = Some(value.parse().ok()?),
47                "PREGAP" => pregap_frames = value.parse().ok()?,
48                _ => {}
49            }
50        }
51
52        Some(Self {
53            track_number: track_number?,
54            mode: track_mode?,
55            frames: frames?,
56            pregap_frames,
57        })
58    }
59}
60
61pub struct ChdFile<F: Read + Seek> {
62    chd: Chd<F>,
63    cue: CueSheet,
64    track_start_frames: Vec<u32>,
65    compressed_buffer: Vec<u8>,
66    decompressed_buffer: Vec<u8>,
67    current_hunk_number: u32,
68}
69
70impl<F: Read + Seek> ChdFile<F> {
71    pub fn open(f: F) -> CdRomResult<(Self, CueSheet)> {
72        let mut chd = Chd::open(f, None)?;
73
74        // Parse TOC info from CHD metadata
75        let mut metadata_iter = chd.metadata();
76        let mut cd_metadata_list = Vec::new();
77        while let Some(mut metadata_entry) = metadata_iter.next() {
78            let metadata = metadata_entry.read()?;
79
80            let Some(cd_metadata) = CdMetadata::parse_from(metadata.value.clone()) else {
81                let value_str = String::from_utf8_lossy(&metadata.value).to_string();
82                return Err(CdRomError::ChdHeaderParseError { metadata_value: value_str });
83            };
84
85            cd_metadata_list.push(cd_metadata);
86        }
87
88        cd_metadata_list.sort_by_key(|metadata| metadata.track_number);
89        validate_track_numbers(&cd_metadata_list)?;
90
91        // Use parsed info to build the TOC
92        let mut tracks = Vec::new();
93        let mut track_start_frames = Vec::with_capacity(cd_metadata_list.len());
94        let mut current_start_time = CdTime::ZERO;
95        let mut current_frame = 0;
96        for cd_metadata in cd_metadata_list {
97            let track_type = cd_metadata.mode.to_type();
98
99            let pregap_len = match cd_metadata.track_number {
100                1 => {
101                    // Track 1 always has a 2-second pregap; 00:02:00 should be first user data sector of track 1
102                    CdTime::new(0, 2, 0)
103                }
104                _ => CdTime::ZERO,
105            };
106
107            // CHD pregaps exist as data within the image, so count these frames as "pause" time
108            let pause_len = CdTime::from_frames(cd_metadata.pregap_frames);
109
110            let track_len = CdTime::from_frames(cd_metadata.frames);
111            let padded_track_len = pregap_len + track_len;
112
113            tracks.push(Track {
114                number: cd_metadata.track_number,
115                mode: cd_metadata.mode,
116                track_type,
117                start_time: current_start_time,
118                end_time: current_start_time + padded_track_len,
119                pregap_len,
120                pause_len,
121                postgap_len: CdTime::ZERO,
122            });
123            track_start_frames.push(current_frame);
124
125            current_start_time += padded_track_len;
126
127            // CHD pads all tracks to a multiple of 4 frames
128            current_frame += cd_metadata.frames;
129            if current_frame % 4 != 0 {
130                current_frame = (current_frame & !3) + 4;
131            }
132        }
133
134        cue::finalize_track_list(&mut tracks);
135
136        assert!(
137            cue::tracks_are_continuous(&tracks),
138            "CHD tracks are not continuous; this is a bug"
139        );
140
141        log::debug!("CHD track start frames: {track_start_frames:?}");
142
143        let compressed_buffer = Vec::with_capacity(chd.header().hunk_size() as usize);
144        let decompressed_buffer = chd.get_hunksized_buffer();
145
146        let cue_sheet = CueSheet::new(tracks);
147        let chd_file = Self {
148            chd,
149            cue: cue_sheet.clone(),
150            track_start_frames,
151            compressed_buffer,
152            decompressed_buffer,
153            current_hunk_number: u32::MAX,
154        };
155
156        Ok((chd_file, cue_sheet))
157    }
158
159    pub fn read_sector(
160        &mut self,
161        track_number: u8,
162        absolute_time: CdTime,
163        relative_sector_number: u32,
164        out: &mut [u8],
165    ) -> CdRomResult<()> {
166        let track_start_frame = self.track_start_frames[(track_number - 1) as usize];
167        let sector_number = track_start_frame + relative_sector_number;
168
169        let hunk_size = self.chd.header().hunk_size();
170        let unit_bytes = self.chd.header().unit_bytes();
171        let hunk_size_sectors = hunk_size / unit_bytes;
172
173        let hunk_number = sector_number / hunk_size_sectors;
174        let hunk_offset_sectors = sector_number % hunk_size_sectors;
175        let hunk_offset_bytes = (hunk_offset_sectors * unit_bytes) as usize;
176
177        // Only load hunk if necessary
178        if hunk_number != self.current_hunk_number {
179            let mut hunk = self.chd.hunk(hunk_number)?;
180
181            self.compressed_buffer.clear();
182            hunk.read_hunk_in(&mut self.compressed_buffer, &mut self.decompressed_buffer)?;
183
184            self.current_hunk_number = hunk_number;
185        }
186
187        let track = self.cue.track(track_number);
188        let track_mode = track.mode;
189        let track_type = track.track_type;
190
191        match track_mode {
192            TrackMode::Mode1DataOnly => {
193                // 2048-byte sectors
194                out[..SECTOR_HEADER_LEN as usize]
195                    .copy_from_slice(&synthesize_data_header(track_mode, absolute_time));
196                out[SECTOR_HEADER_LEN as usize..(SECTOR_HEADER_LEN + 2048) as usize]
197                    .copy_from_slice(
198                        &self.decompressed_buffer[hunk_offset_bytes..hunk_offset_bytes + 2048],
199                    );
200                out[(SECTOR_HEADER_LEN + 2048) as usize..crate::BYTES_PER_SECTOR as usize].fill(0);
201            }
202            TrackMode::Mode1 | TrackMode::Mode2 | TrackMode::Audio => {
203                out[..crate::BYTES_PER_SECTOR as usize].copy_from_slice(
204                    &self.decompressed_buffer
205                        [hunk_offset_bytes..hunk_offset_bytes + crate::BYTES_PER_SECTOR as usize],
206                );
207            }
208        }
209
210        if track_type == TrackType::Audio {
211            // CHD audio tracks decompress into big-endian audio samples for some reason. Swap all
212            // the bytes to make them little-endian to match the CD-DA format
213            for chunk in out[..crate::BYTES_PER_SECTOR as usize].as_chunks_mut::<2>().0 {
214                chunk.swap(0, 1);
215            }
216        }
217
218        Ok(())
219    }
220}
221
222impl<F: Read + Seek> Debug for ChdFile<F> {
223    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
224        write!(f, "ChdFile {{ current_hunk_number: {} }}", self.current_hunk_number)
225    }
226}
227
228fn validate_track_numbers(cd_metadata_list: &[CdMetadata]) -> CdRomResult<()> {
229    for (i, metadata) in cd_metadata_list.iter().enumerate() {
230        if metadata.track_number != (i + 1) as u8 {
231            let track_numbers =
232                cd_metadata_list.iter().map(|metadata| metadata.track_number).collect();
233            return Err(CdRomError::ChdInvalidTrackList { track_numbers });
234        }
235    }
236
237    Ok(())
238}