Skip to main content

cd_da_reader/
stream.rs

1use std::cmp::min;
2
3use crate::{CdReader, CdReaderError, RetryConfig, Toc, utils};
4
5/// Configuration for streamed track reads.
6#[derive(Debug, Clone)]
7pub struct TrackStreamConfig {
8    /// Target chunk size in sectors for each `next_chunk` call.
9    ///
10    /// `27` sectors is approximately 64 KiB of CD-DA payload.
11    pub sectors_per_chunk: u32,
12    /// Retry policy applied to each chunk read.
13    pub retry: RetryConfig,
14}
15
16impl Default for TrackStreamConfig {
17    fn default() -> Self {
18        Self {
19            sectors_per_chunk: 27,
20            retry: RetryConfig::default(),
21        }
22    }
23}
24
25/// Track-scoped streaming reader for CD-DA PCM data.
26/// You can iterate through the data manually; this
27/// allows to receive initial data much faster and
28/// also allows you to navigate to specific points.
29///
30/// To create one, use "reader.open_track_stream" method in
31/// order to have correct drive's lifecycle management.
32pub struct TrackStream<'a> {
33    reader: &'a CdReader,
34    start_lba: u32,
35    next_lba: u32,
36    remaining_sectors: u32,
37    total_sectors: u32,
38    cfg: TrackStreamConfig,
39}
40
41impl<'a> TrackStream<'a> {
42    const SECTORS_PER_SECOND: f32 = 75.0;
43
44    /// Read the next chunk of PCM data.
45    ///
46    /// Returns `Ok(None)` when end-of-track is reached.
47    pub fn next_chunk(&mut self) -> Result<Option<Vec<u8>>, CdReaderError> {
48        self.next_chunk_with(|lba, sectors, retry| {
49            self.reader.read_sectors_with_retry(lba, sectors, retry)
50        })
51    }
52
53    fn next_chunk_with<F>(&mut self, mut read_fn: F) -> Result<Option<Vec<u8>>, CdReaderError>
54    where
55        F: FnMut(u32, u32, &RetryConfig) -> Result<Vec<u8>, CdReaderError>,
56    {
57        if self.remaining_sectors == 0 {
58            return Ok(None);
59        }
60
61        let sectors = min(self.remaining_sectors, self.cfg.sectors_per_chunk.max(1));
62        let chunk = read_fn(self.next_lba, sectors, &self.cfg.retry)?;
63
64        self.next_lba += sectors;
65        self.remaining_sectors -= sectors;
66
67        Ok(Some(chunk))
68    }
69
70    /// Total number of sectors in this track stream.
71    pub fn total_sectors(&self) -> u32 {
72        self.total_sectors
73    }
74
75    /// Current stream position as a track-relative sector index.
76    /// Keep in mind that if you are playing the sound directly, this
77    /// is likely not the track's current position because you probably
78    /// keep some of the data in your buffer.
79    pub fn current_sector(&self) -> u32 {
80        self.total_sectors - self.remaining_sectors
81    }
82
83    /// Seek to an absolute track-relative sector position.
84    ///
85    /// Valid range is `0..=total_sectors()`.
86    /// If the sector value is higher than the total, it will throw an error.
87    pub fn seek_to_sector(&mut self, sector: u32) -> Result<(), CdReaderError> {
88        if sector > self.total_sectors {
89            return Err(CdReaderError::Io(std::io::Error::new(
90                std::io::ErrorKind::InvalidInput,
91                "seek sector is out of track bounds",
92            )));
93        }
94
95        self.next_lba = self.start_lba + sector;
96        self.remaining_sectors = self.total_sectors - sector;
97        Ok(())
98    }
99
100    /// Current stream position in seconds. Functionally equivalent
101    /// to "current_sector", but converted to seconds.
102    ///
103    /// Audio CD timing uses `75 sectors = 1 second`.
104    pub fn current_seconds(&self) -> f32 {
105        self.current_sector() as f32 / Self::SECTORS_PER_SECOND
106    }
107
108    /// Total stream duration in seconds. Functionally equivalent
109    /// to "total_sectors", but converted to seconds.
110    ///
111    /// Audio CD timing uses `75 sectors = 1 second`.
112    pub fn total_seconds(&self) -> f32 {
113        self.total_sectors as f32 / Self::SECTORS_PER_SECOND
114    }
115
116    /// Seek to an absolute track-relative time position in seconds.
117    ///
118    /// Input is converted to sector offset and clamped to track bounds.
119    pub fn seek_to_seconds(&mut self, seconds: f32) -> Result<(), CdReaderError> {
120        if !seconds.is_finite() || seconds < 0.0 {
121            return Err(CdReaderError::Io(std::io::Error::new(
122                std::io::ErrorKind::InvalidInput,
123                "seek seconds must be a finite non-negative number",
124            )));
125        }
126
127        let target_sector = (seconds * Self::SECTORS_PER_SECOND).round() as u32;
128        self.seek_to_sector(target_sector.min(self.total_sectors))
129    }
130}
131
132impl CdReader {
133    /// Open a streaming reader for a specific track in the provided TOC.
134    /// It is important to create track streams through this method so the
135    /// drive session is managed through a single CDReader instance.
136    ///
137    /// Use `TrackStream::next_chunk` to pull sector-aligned PCM chunks.
138    pub fn open_track_stream<'a>(
139        &'a self,
140        toc: &Toc,
141        track_no: u8,
142        cfg: TrackStreamConfig,
143    ) -> Result<TrackStream<'a>, CdReaderError> {
144        let (start_lba, sectors) =
145            utils::get_track_bounds(toc, track_no).map_err(CdReaderError::Io)?;
146
147        Ok(TrackStream {
148            reader: self,
149            start_lba,
150            next_lba: start_lba,
151            remaining_sectors: sectors,
152            total_sectors: sectors,
153            cfg,
154        })
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::{TrackStream, TrackStreamConfig};
161    use crate::{CdReader, CdReaderError, RetryConfig};
162
163    fn mk_stream(
164        start_lba: u32,
165        total_sectors: u32,
166        sectors_per_chunk: u32,
167    ) -> TrackStream<'static> {
168        let reader: &'static CdReader = Box::leak(Box::new(CdReader {}));
169        TrackStream {
170            reader,
171            start_lba,
172            next_lba: start_lba,
173            remaining_sectors: total_sectors,
174            total_sectors,
175            cfg: TrackStreamConfig {
176                sectors_per_chunk,
177                retry: RetryConfig::default(),
178            },
179        }
180    }
181
182    #[test]
183    fn seek_to_sector_updates_position() {
184        let mut stream = mk_stream(10_000, 1_000, 27);
185        stream.seek_to_sector(250).unwrap();
186
187        assert_eq!(stream.current_sector(), 250);
188        assert_eq!(stream.next_lba, 10_250);
189        assert_eq!(stream.remaining_sectors, 750);
190    }
191
192    #[test]
193    fn seek_to_sector_returns_error_out_of_bounds() {
194        let mut stream = mk_stream(10_000, 1_000, 27);
195        let err = stream.seek_to_sector(1_001).unwrap_err();
196
197        match err {
198            CdReaderError::Io(io) => assert_eq!(io.kind(), std::io::ErrorKind::InvalidInput),
199            _ => panic!("expected Io(InvalidInput)"),
200        }
201    }
202
203    #[test]
204    fn seek_to_seconds_and_time_helpers_work() {
205        let mut stream = mk_stream(10_000, 750, 27); // 10 seconds
206        assert_eq!(stream.total_seconds(), 10.0);
207
208        stream.seek_to_seconds(2.0).unwrap();
209        assert_eq!(stream.current_sector(), 150);
210        assert!((stream.current_seconds() - 2.0).abs() < f32::EPSILON);
211    }
212
213    #[test]
214    fn seek_to_seconds_rejects_invalid_input() {
215        let mut stream = mk_stream(10_000, 750, 27);
216        let err = stream.seek_to_seconds(f32::NAN).unwrap_err();
217        match err {
218            CdReaderError::Io(io) => assert_eq!(io.kind(), std::io::ErrorKind::InvalidInput),
219            _ => panic!("expected Io(InvalidInput)"),
220        }
221    }
222
223    #[test]
224    fn next_chunk_reads_expected_size_and_advances() {
225        let mut stream = mk_stream(10_000, 100, 27);
226        let mut called = false;
227
228        let chunk = stream
229            .next_chunk_with(|lba, sectors, _| {
230                called = true;
231                assert_eq!(lba, 10_000);
232                assert_eq!(sectors, 27);
233                Ok(vec![0u8; (sectors as usize) * 2352])
234            })
235            .unwrap()
236            .unwrap();
237
238        assert!(called);
239        assert_eq!(chunk.len(), 27 * 2352);
240        assert_eq!(stream.current_sector(), 27);
241        assert_eq!(stream.remaining_sectors, 73);
242    }
243
244    #[test]
245    fn next_chunk_returns_none_when_finished() {
246        let mut stream = mk_stream(10_000, 0, 27);
247        let result = stream.next_chunk_with(|_, _, _| Ok(vec![1, 2, 3])).unwrap();
248        assert!(result.is_none());
249    }
250
251    #[test]
252    fn next_chunk_error_does_not_advance_position() {
253        let mut stream = mk_stream(10_000, 100, 27);
254        let err = stream
255            .next_chunk_with(|_, _, _| {
256                Err(CdReaderError::Io(std::io::Error::other(
257                    "simulated read failure",
258                )))
259            })
260            .unwrap_err();
261
262        match err {
263            CdReaderError::Io(io) => assert_eq!(io.kind(), std::io::ErrorKind::Other),
264            _ => panic!("expected Io(Other)"),
265        }
266        assert_eq!(stream.current_sector(), 0);
267        assert_eq!(stream.next_lba, 10_000);
268        assert_eq!(stream.remaining_sectors, 100);
269    }
270}