Skip to main content

phemios_cd/
stream.rs

1use super::{FRAMES_PER_SECTOR, Track};
2use cd_da_reader::{AudioSectorReader, CdReader, CdReaderError, ReadOptions, RetryConfig};
3pub(crate) struct NativeReader<'a>(pub &'a CdReader);
4impl AudioSectorReader for NativeReader<'_> {
5    type Error = CdReaderError;
6    fn read_audio_sectors(&self, start: u32, count: u32) -> Result<Vec<u8>, Self::Error> {
7        self.0.read_sector_range(
8            start,
9            count,
10            &ReadOptions::default().with_retry(RetryConfig::default().with_max_attempts(2)),
11        )
12    }
13}
14/// One bounded source chunk, with exact sub-sector seeking and no whole-track allocation.
15pub struct Chunk {
16    bytes: Vec<u8>,
17    skip: usize,
18}
19impl Chunk {
20    /// Convert signed little-endian stereo samples; normalized endpoints stay finite in [-1,1).
21    pub fn frames(&self) -> impl ExactSizeIterator<Item = [f32; 2]> + '_ {
22        self.bytes
23            .as_chunks::<4>()
24            .0
25            .iter()
26            .skip(self.skip)
27            .map(|b| {
28                [
29                    f32::from(i16::from_le_bytes([b[0], b[1]])) / 32768.,
30                    f32::from(i16::from_le_bytes([b[2], b[3]])) / 32768.,
31                ]
32            })
33    }
34}
35/// Blocking sector reader used only by the dedicated CD I/O worker.
36pub struct TrackReader<'a> {
37    reader: &'a dyn AudioSectorReader<Error = CdReaderError>,
38    track: &'a Track,
39    frame: u64,
40}
41impl<'a> TrackReader<'a> {
42    pub(crate) fn new(
43        reader: &'a dyn AudioSectorReader<Error = CdReaderError>,
44        track: &'a Track,
45    ) -> Self {
46        Self {
47            reader,
48            track,
49            frame: 0,
50        }
51    }
52    /// Move to an exact stereo frame, including EOF; out-of-range requests fail explicitly.
53    pub fn seek(&mut self, frame: u64) -> Result<(), String> {
54        if frame > self.track.frames {
55            return Err("Position hors du CD.".into());
56        }
57        self.frame = frame;
58        Ok(())
59    }
60    /// Read at most 27 sectors (~64 KiB); a short/corrupt read never produces partial audio.
61    pub fn next_chunk(&mut self) -> Result<Option<Chunk>, String> {
62        if self.frame == self.track.frames {
63            return Ok(None);
64        }
65        let sector = self.frame / FRAMES_PER_SECTOR;
66        let count = (self.track.frames / FRAMES_PER_SECTOR - sector).min(27) as u32;
67        let bytes = self
68            .reader
69            .read_audio_sectors(self.track.start + sector as u32, count)
70            .map_err(
71                |_| "Lecture du CD impossible : disque retiré, illisible ou erreur du lecteur.",
72            )?;
73        if bytes.len() != count as usize * 2352 {
74            return Err("Le lecteur CD a renvoyé des données incomplètes.".into());
75        }
76        let skip = (self.frame % FRAMES_PER_SECTOR) as usize;
77        self.frame = (sector + u64::from(count)) * FRAMES_PER_SECTOR;
78        Ok(Some(Chunk { bytes, skip }))
79    }
80}
81#[cfg(test)]
82#[path = "stream_tests.rs"]
83mod tests;