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    /// lifetime for the drive exclusive access is managed through a single
136    /// CDReader instance.
137    ///
138    /// Use `TrackStream::next_chunk` to pull sector-aligned PCM chunks.
139    pub fn open_track_stream<'a>(
140        &'a self,
141        toc: &Toc,
142        track_no: u8,
143        cfg: TrackStreamConfig,
144    ) -> Result<TrackStream<'a>, CdReaderError> {
145        let (start_lba, sectors) =
146            utils::get_track_bounds(toc, track_no).map_err(CdReaderError::Io)?;
147
148        Ok(TrackStream {
149            reader: self,
150            start_lba,
151            next_lba: start_lba,
152            remaining_sectors: sectors,
153            total_sectors: sectors,
154            cfg,
155        })
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::{TrackStream, TrackStreamConfig};
162    use crate::{CdReader, CdReaderError, RetryConfig};
163
164    fn mk_stream(
165        start_lba: u32,
166        total_sectors: u32,
167        sectors_per_chunk: u32,
168    ) -> TrackStream<'static> {
169        let reader: &'static CdReader = Box::leak(Box::new(CdReader {}));
170        TrackStream {
171            reader,
172            start_lba,
173            next_lba: start_lba,
174            remaining_sectors: total_sectors,
175            total_sectors,
176            cfg: TrackStreamConfig {
177                sectors_per_chunk,
178                retry: RetryConfig::default(),
179            },
180        }
181    }
182
183    #[test]
184    fn seek_to_sector_updates_position() {
185        let mut stream = mk_stream(10_000, 1_000, 27);
186        stream.seek_to_sector(250).unwrap();
187
188        assert_eq!(stream.current_sector(), 250);
189        assert_eq!(stream.next_lba, 10_250);
190        assert_eq!(stream.remaining_sectors, 750);
191    }
192
193    #[test]
194    fn seek_to_sector_returns_error_out_of_bounds() {
195        let mut stream = mk_stream(10_000, 1_000, 27);
196        let err = stream.seek_to_sector(1_001).unwrap_err();
197
198        match err {
199            CdReaderError::Io(io) => assert_eq!(io.kind(), std::io::ErrorKind::InvalidInput),
200            _ => panic!("expected Io(InvalidInput)"),
201        }
202    }
203
204    #[test]
205    fn seek_to_seconds_and_time_helpers_work() {
206        let mut stream = mk_stream(10_000, 750, 27); // 10 seconds
207        assert_eq!(stream.total_seconds(), 10.0);
208
209        stream.seek_to_seconds(2.0).unwrap();
210        assert_eq!(stream.current_sector(), 150);
211        assert!((stream.current_seconds() - 2.0).abs() < f32::EPSILON);
212    }
213
214    #[test]
215    fn seek_to_seconds_rejects_invalid_input() {
216        let mut stream = mk_stream(10_000, 750, 27);
217        let err = stream.seek_to_seconds(f32::NAN).unwrap_err();
218        match err {
219            CdReaderError::Io(io) => assert_eq!(io.kind(), std::io::ErrorKind::InvalidInput),
220            _ => panic!("expected Io(InvalidInput)"),
221        }
222    }
223
224    #[test]
225    fn next_chunk_reads_expected_size_and_advances() {
226        let mut stream = mk_stream(10_000, 100, 27);
227        let mut called = false;
228
229        let chunk = stream
230            .next_chunk_with(|lba, sectors, _| {
231                called = true;
232                assert_eq!(lba, 10_000);
233                assert_eq!(sectors, 27);
234                Ok(vec![0u8; (sectors as usize) * 2352])
235            })
236            .unwrap()
237            .unwrap();
238
239        assert!(called);
240        assert_eq!(chunk.len(), 27 * 2352);
241        assert_eq!(stream.current_sector(), 27);
242        assert_eq!(stream.remaining_sectors, 73);
243    }
244
245    #[test]
246    fn next_chunk_returns_none_when_finished() {
247        let mut stream = mk_stream(10_000, 0, 27);
248        let result = stream.next_chunk_with(|_, _, _| Ok(vec![1, 2, 3])).unwrap();
249        assert!(result.is_none());
250    }
251
252    #[test]
253    fn next_chunk_error_does_not_advance_position() {
254        let mut stream = mk_stream(10_000, 100, 27);
255        let err = stream
256            .next_chunk_with(|_, _, _| {
257                Err(CdReaderError::Io(std::io::Error::other(
258                    "simulated read failure",
259                )))
260            })
261            .unwrap_err();
262
263        match err {
264            CdReaderError::Io(io) => assert_eq!(io.kind(), std::io::ErrorKind::Other),
265            _ => panic!("expected Io(Other)"),
266        }
267        assert_eq!(stream.current_sector(), 0);
268        assert_eq!(stream.next_lba, 10_000);
269        assert_eq!(stream.remaining_sectors, 100);
270    }
271}