Skip to main content

cd_da_reader/
stream.rs

1use std::cmp::min;
2
3use crate::data_reader::validate_track_format;
4use crate::{CdReader, CdReaderError, ReadOptions, ReadSpeed, Toc, utils};
5
6fn apply_stream_read_speed_once(
7    options: &ReadOptions,
8    request_read_speed: impl FnOnce(ReadSpeed) -> Result<(), CdReaderError>,
9) -> Result<ReadOptions, CdReaderError> {
10    request_read_speed(options.read_speed())?;
11
12    // The speed request applies to the stream as a whole. Chunk reads go
13    // through read_sector_range, so to avoid constant speed setting, we
14    // apply it once and clone ReadOptions with unchanged speed
15    Ok(options.clone().with_read_speed(ReadSpeed::Unchanged))
16}
17
18/// Track-scoped streaming reader for audio or data sectors.
19///
20/// You can pull sector-aligned chunks incrementally and seek to track-relative
21/// sector or time positions. Create a stream with [`CdReader::open_track_stream`].
22pub struct TrackStream<'a> {
23    reader: &'a CdReader,
24    start_lba: u32,
25    next_lba: u32,
26    remaining_sectors: u32,
27    total_sectors: u32,
28    sectors_per_chunk: u32,
29    read_options: ReadOptions,
30}
31
32impl<'a> TrackStream<'a> {
33    const DEFAULT_SECTORS_PER_CHUNK: u32 = 27;
34    const SECTORS_PER_SECOND: f32 = 75.0;
35
36    /// Set the target chunk size in sectors (default 27).
37    ///
38    /// The byte size of a chunk also depends on the
39    /// [`SectorReadFormat`](crate::SectorReadFormat) selected in [`ReadOptions`].
40    /// A value of zero is normalized to one sector.
41    pub fn with_sectors_per_chunk(mut self, sectors: u32) -> Self {
42        self.sectors_per_chunk = sectors.max(1);
43        self
44    }
45
46    /// Read the next chunk of sector data.
47    ///
48    /// Returns `Ok(None)` when end-of-track is reached. The bytes per sector
49    /// depend on the [`SectorReadFormat`](crate::SectorReadFormat) selected in
50    /// [`ReadOptions`].
51    ///
52    /// # Errors
53    ///
54    /// Returns [`CdReaderError::Io`] or [`CdReaderError::Scsi`] if the drive
55    /// read fails. The stream position does not advance on error.
56    pub fn next_chunk(&mut self) -> Result<Option<Vec<u8>>, CdReaderError> {
57        self.next_chunk_with(|lba, sectors, options| {
58            self.reader.read_sector_range(lba, sectors, options)
59        })
60    }
61
62    fn next_chunk_with<F>(&mut self, mut read_fn: F) -> Result<Option<Vec<u8>>, CdReaderError>
63    where
64        F: FnMut(u32, u32, &ReadOptions) -> Result<Vec<u8>, CdReaderError>,
65    {
66        if self.remaining_sectors == 0 {
67            return Ok(None);
68        }
69
70        let sectors = min(self.remaining_sectors, self.sectors_per_chunk);
71        let chunk = read_fn(self.next_lba, sectors, &self.read_options)?;
72
73        self.next_lba += sectors;
74        self.remaining_sectors -= sectors;
75
76        Ok(Some(chunk))
77    }
78
79    /// Total number of sectors in this track stream.
80    pub fn total_sectors(&self) -> u32 {
81        self.total_sectors
82    }
83
84    /// Current stream position as a track-relative sector index.
85    /// Keep in mind that if you are playing the sound directly, this
86    /// is likely not the track's current position because you probably
87    /// keep some of the data in your buffer.
88    pub fn current_sector(&self) -> u32 {
89        self.total_sectors - self.remaining_sectors
90    }
91
92    /// Seek to a sector position relative to the start of the track.
93    ///
94    /// Valid range is `0..=total_sectors()`.
95    ///
96    /// # Errors
97    ///
98    /// Returns [`CdReaderError::Io`] containing
99    /// [`std::io::ErrorKind::InvalidInput`] if `sector` exceeds the track
100    /// length.
101    pub fn seek_to_sector(&mut self, sector: u32) -> Result<(), CdReaderError> {
102        if sector > self.total_sectors {
103            return Err(CdReaderError::Io(std::io::Error::new(
104                std::io::ErrorKind::InvalidInput,
105                "seek sector is out of track bounds",
106            )));
107        }
108
109        self.next_lba = self.start_lba + sector;
110        self.remaining_sectors = self.total_sectors - sector;
111        Ok(())
112    }
113
114    /// Current stream position in seconds. Functionally equivalent
115    /// to "current_sector", but converted to seconds.
116    ///
117    /// CD addresses advance at `75 sectors = 1 second`.
118    pub fn current_seconds(&self) -> f32 {
119        self.current_sector() as f32 / Self::SECTORS_PER_SECOND
120    }
121
122    /// Total stream duration in seconds. Functionally equivalent
123    /// to "total_sectors", but converted to seconds.
124    ///
125    /// CD addresses advance at `75 sectors = 1 second`.
126    pub fn total_seconds(&self) -> f32 {
127        self.total_sectors as f32 / Self::SECTORS_PER_SECOND
128    }
129
130    /// Seek to a time position relative to the start of the track in seconds.
131    ///
132    /// Input is converted to sector offset and clamped to track bounds.
133    ///
134    /// # Errors
135    ///
136    /// Returns [`CdReaderError::Io`] containing
137    /// [`std::io::ErrorKind::InvalidInput`] if `seconds` is negative or not
138    /// finite.
139    pub fn seek_to_seconds(&mut self, seconds: f32) -> Result<(), CdReaderError> {
140        if !seconds.is_finite() || seconds < 0.0 {
141            return Err(CdReaderError::Io(std::io::Error::new(
142                std::io::ErrorKind::InvalidInput,
143                "seek seconds must be a finite non-negative number",
144            )));
145        }
146
147        let target_sector = (seconds * Self::SECTORS_PER_SECOND).round() as u32;
148        self.seek_to_sector(target_sector.min(self.total_sectors))
149    }
150}
151
152impl CdReader {
153    /// Open a streaming reader for an audio track using the default options.
154    ///
155    /// # Errors
156    ///
157    /// Returns the same errors as [`CdReader::open_track_stream_with_options`].
158    pub fn open_track_stream<'a>(
159        &'a self,
160        toc: &Toc,
161        track_no: u8,
162    ) -> Result<TrackStream<'a>, CdReaderError> {
163        self.open_track_stream_with_options(toc, track_no, &ReadOptions::default())
164    }
165
166    /// Open a streaming reader using explicit read options.
167    ///
168    /// Use [`TrackStream::next_chunk`] to pull sector-aligned chunks in the
169    /// selected format. The requested read speed is applied once before the
170    /// stream is returned. To override the default
171    /// chunk size, call [`TrackStream::with_sectors_per_chunk`] on the returned
172    /// stream.
173    ///
174    /// # Errors
175    ///
176    /// - Returns [`CdReaderError::TrackFormatMismatch`] if the selected format
177    ///   is incompatible with the track.
178    /// - Returns [`CdReaderError::Io`] if the track is absent, its bounds are
179    ///   invalid, or the read-speed request fails.
180    pub fn open_track_stream_with_options<'a>(
181        &'a self,
182        toc: &Toc,
183        track_no: u8,
184        options: &ReadOptions,
185    ) -> Result<TrackStream<'a>, CdReaderError> {
186        if let Some(track) = toc.tracks.iter().find(|track| track.number == track_no) {
187            validate_track_format(track, options.format())?;
188        }
189
190        let (start_lba, sectors) =
191            utils::get_track_bounds(toc, track_no).map_err(CdReaderError::Io)?;
192        let read_options = apply_stream_read_speed_once(options, |read_speed| {
193            self.drive.request_read_speed(read_speed)
194        })?;
195
196        Ok(TrackStream {
197            reader: self,
198            start_lba,
199            next_lba: start_lba,
200            remaining_sectors: sectors,
201            total_sectors: sectors,
202            sectors_per_chunk: TrackStream::DEFAULT_SECTORS_PER_CHUNK,
203            read_options,
204        })
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::{TrackStream, apply_stream_read_speed_once};
211    use crate::{
212        CdReader, CdReaderError, ReadOptions, ReadSpeed, RetryConfig, SectorReadFormat, Toc, Track,
213    };
214
215    fn mk_stream(
216        start_lba: u32,
217        total_sectors: u32,
218        sectors_per_chunk: u32,
219    ) -> TrackStream<'static> {
220        let reader: &'static CdReader = Box::leak(Box::new(CdReader::test_reader()));
221        TrackStream {
222            reader,
223            start_lba,
224            next_lba: start_lba,
225            remaining_sectors: total_sectors,
226            total_sectors,
227            sectors_per_chunk: TrackStream::DEFAULT_SECTORS_PER_CHUNK,
228            read_options: ReadOptions::default(),
229        }
230        .with_sectors_per_chunk(sectors_per_chunk)
231    }
232
233    #[test]
234    fn sectors_per_chunk_normalizes_zero() {
235        let stream = mk_stream(10_000, 100, 0);
236        assert_eq!(stream.sectors_per_chunk, 1);
237    }
238
239    #[test]
240    fn stream_speed_is_requested_once_before_chunk_reads() {
241        let options = ReadOptions::default().with_read_speed(ReadSpeed::CustomMultiplier(4));
242        let mut speed_requests = 0;
243        let chunk_options = apply_stream_read_speed_once(&options, |read_speed| {
244            speed_requests += 1;
245            assert!(matches!(read_speed, ReadSpeed::CustomMultiplier(4)));
246            Ok(())
247        })
248        .unwrap();
249
250        assert_eq!(speed_requests, 1);
251        assert!(matches!(chunk_options.read_speed(), ReadSpeed::Unchanged));
252
253        let mut stream = mk_stream(10_000, 100, 27);
254        stream.read_options = chunk_options;
255        for _ in 0..2 {
256            stream
257                .next_chunk_with(|_, _, options| {
258                    assert!(matches!(options.read_speed(), ReadSpeed::Unchanged));
259                    Ok(Vec::new())
260                })
261                .unwrap();
262        }
263
264        assert_eq!(speed_requests, 1);
265    }
266
267    #[test]
268    fn open_stream_preserves_chunk_read_options() {
269        let reader = CdReader::test_reader();
270        let toc = Toc {
271            first_track: 1,
272            last_track: 1,
273            tracks: vec![Track {
274                number: 1,
275                start_lba: 10_000,
276                start_msf: (2, 15, 25),
277                is_audio: false,
278            }],
279            leadout_lba: 10_100,
280        };
281        let options = ReadOptions::default()
282            .with_format(SectorReadFormat::Mode1Cooked)
283            .with_retry(RetryConfig::default().with_max_attempts(9));
284
285        let stream = reader
286            .open_track_stream_with_options(&toc, 1, &options)
287            .unwrap();
288
289        assert_eq!(stream.read_options.format(), SectorReadFormat::Mode1Cooked);
290        assert_eq!(stream.read_options.retry().max_attempts, 9);
291        assert!(matches!(
292            stream.read_options.read_speed(),
293            ReadSpeed::Unchanged
294        ));
295        assert_eq!(
296            stream.sectors_per_chunk,
297            TrackStream::DEFAULT_SECTORS_PER_CHUNK
298        );
299    }
300
301    #[test]
302    fn seek_to_sector_updates_position() {
303        let mut stream = mk_stream(10_000, 1_000, 27);
304        stream.seek_to_sector(250).unwrap();
305
306        assert_eq!(stream.current_sector(), 250);
307        assert_eq!(stream.next_lba, 10_250);
308        assert_eq!(stream.remaining_sectors, 750);
309    }
310
311    #[test]
312    fn seek_to_sector_returns_error_out_of_bounds() {
313        let mut stream = mk_stream(10_000, 1_000, 27);
314        let err = stream.seek_to_sector(1_001).unwrap_err();
315
316        match err {
317            CdReaderError::Io(io) => assert_eq!(io.kind(), std::io::ErrorKind::InvalidInput),
318            _ => panic!("expected Io(InvalidInput)"),
319        }
320    }
321
322    #[test]
323    fn seek_to_seconds_and_time_helpers_work() {
324        let mut stream = mk_stream(10_000, 750, 27); // 10 seconds
325        assert_eq!(stream.total_seconds(), 10.0);
326
327        stream.seek_to_seconds(2.0).unwrap();
328        assert_eq!(stream.current_sector(), 150);
329        assert!((stream.current_seconds() - 2.0).abs() < f32::EPSILON);
330    }
331
332    #[test]
333    fn seek_to_seconds_rejects_invalid_input() {
334        let mut stream = mk_stream(10_000, 750, 27);
335        let err = stream.seek_to_seconds(f32::NAN).unwrap_err();
336        match err {
337            CdReaderError::Io(io) => assert_eq!(io.kind(), std::io::ErrorKind::InvalidInput),
338            _ => panic!("expected Io(InvalidInput)"),
339        }
340    }
341
342    #[test]
343    fn next_chunk_uses_configured_read_options_and_advances() {
344        let mut stream = mk_stream(10_000, 100, 27);
345        stream.read_options = ReadOptions::default()
346            .with_format(SectorReadFormat::Mode1Cooked)
347            .with_retry(RetryConfig::default().with_max_attempts(9));
348        let mut called = false;
349
350        let chunk = stream
351            .next_chunk_with(|lba, sectors, options| {
352                called = true;
353                assert_eq!(lba, 10_000);
354                assert_eq!(sectors, 27);
355                assert_eq!(options.format(), SectorReadFormat::Mode1Cooked);
356                assert_eq!(options.retry().max_attempts, 9);
357                assert!(matches!(options.read_speed(), ReadSpeed::Unchanged));
358                Ok(vec![
359                    0u8;
360                    (sectors as usize) * options.format().sector_size()
361                ])
362            })
363            .unwrap()
364            .unwrap();
365
366        assert!(called);
367        assert_eq!(chunk.len(), 27 * 2048);
368        assert_eq!(stream.current_sector(), 27);
369        assert_eq!(stream.remaining_sectors, 73);
370    }
371
372    #[test]
373    fn next_chunk_returns_none_when_finished() {
374        let mut stream = mk_stream(10_000, 0, 27);
375        let result = stream.next_chunk_with(|_, _, _| Ok(vec![1, 2, 3])).unwrap();
376        assert!(result.is_none());
377    }
378
379    #[test]
380    fn next_chunk_error_does_not_advance_position() {
381        let mut stream = mk_stream(10_000, 100, 27);
382        let err = stream
383            .next_chunk_with(|_, _, _| {
384                Err(CdReaderError::Io(std::io::Error::other(
385                    "simulated read failure",
386                )))
387            })
388            .unwrap_err();
389
390        match err {
391            CdReaderError::Io(io) => assert_eq!(io.kind(), std::io::ErrorKind::Other),
392            _ => panic!("expected Io(Other)"),
393        }
394        assert_eq!(stream.current_sector(), 0);
395        assert_eq!(stream.next_lba, 10_000);
396        assert_eq!(stream.remaining_sectors, 100);
397    }
398}