diff --git a/README.md b/README.md index 546843e..54eb0cb 100644 --- a/README.md +++ b/README.md @@ -104,13 +104,14 @@ let data = reader.read_track(&toc, 1)?; This is a blocking call and takes a lot of time (depends on the track length and CD/drive quality due to retries). If you want to do something with the data as it comes, use streaming API: ```rust -use cd_da_reader::{CdReader, RetryConfig, TrackStreamConfig}; +use cd_da_reader::{CdReader, RetryConfig, SectorReadMode, TrackStreamConfig}; let reader = CdReader::open_default()?; let toc = reader.read_toc()?; let stream_cfg = TrackStreamConfig { - sectors_per_chunk: 27, // ~64 KB per chunk + sectors_per_chunk: 27, // ~64 KB per audio chunk + mode: SectorReadMode::Audio, retry: RetryConfig::default(), }; diff --git a/src/data_reader.rs b/src/data_reader.rs index 42a4b52..ee609db 100644 --- a/src/data_reader.rs +++ b/src/data_reader.rs @@ -1,6 +1,6 @@ use crate::retry::RetryConfig; -/// Options for reading a complete track. +/// Sector format and retry options for track and sector-range reads. #[derive(Debug, Clone)] pub struct ReadOptions { /// Sector format requested from the drive. diff --git a/src/lib.rs b/src/lib.rs index d202eb1..b1d5af2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,13 +85,14 @@ //! use the streaming API instead: //! //! ```no_run -//! use cd_da_reader::{CdReader, RetryConfig, TrackStreamConfig}; +//! use cd_da_reader::{CdReader, RetryConfig, SectorReadMode, TrackStreamConfig}; //! //! let reader = CdReader::open_default()?; //! let toc = reader.read_toc()?; //! //! let cfg = TrackStreamConfig { -//! sectors_per_chunk: 27, // ~64 KB per chunk +//! sectors_per_chunk: 27, // ~64 KB per audio chunk +//! mode: SectorReadMode::Audio, //! retry: RetryConfig::default(), //! }; //! diff --git a/src/stream.rs b/src/stream.rs index 6f6f8a8..3b73583 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -7,8 +7,10 @@ use crate::{CdReader, CdReaderError, ReadOptions, RetryConfig, SectorReadMode, T pub struct TrackStreamConfig { /// Target chunk size in sectors for each `next_chunk` call. /// - /// `27` sectors is approximately 64 KiB of CD-DA payload. + /// The byte size of a chunk also depends on [`SectorReadMode`]. pub sectors_per_chunk: u32, + /// Sector format requested from the drive. + pub mode: SectorReadMode, /// Retry policy applied to each chunk read. pub retry: RetryConfig, } @@ -17,18 +19,16 @@ impl Default for TrackStreamConfig { fn default() -> Self { Self { sectors_per_chunk: 27, + mode: SectorReadMode::Audio, retry: RetryConfig::default(), } } } -/// Track-scoped streaming reader for CD-DA PCM data. -/// You can iterate through the data manually; this -/// allows to receive initial data much faster and -/// also allows you to navigate to specific points. +/// Track-scoped streaming reader for audio or data sectors. /// -/// To create one, use "reader.open_track_stream" method in -/// order to have correct drive's lifecycle management. +/// You can pull sector-aligned chunks incrementally and seek to track-relative +/// sector or time positions. Create a stream with [`CdReader::open_track_stream`]. pub struct TrackStream<'a> { reader: &'a CdReader, start_lba: u32, @@ -41,13 +41,14 @@ pub struct TrackStream<'a> { impl<'a> TrackStream<'a> { const SECTORS_PER_SECOND: f32 = 75.0; - /// Read the next chunk of PCM data. + /// Read the next chunk of sector data. /// - /// Returns `Ok(None)` when end-of-track is reached. + /// Returns `Ok(None)` when end-of-track is reached. The bytes per sector + /// depend on the [`SectorReadMode`] in [`TrackStreamConfig`]. pub fn next_chunk(&mut self) -> Result>, CdReaderError> { - self.next_chunk_with(|lba, sectors, retry| { + self.next_chunk_with(|lba, sectors, mode, retry| { let options = ReadOptions { - mode: SectorReadMode::Audio, + mode, retry: retry.clone(), }; self.reader.read_sector_range(lba, sectors, &options) @@ -56,14 +57,14 @@ impl<'a> TrackStream<'a> { fn next_chunk_with(&mut self, mut read_fn: F) -> Result>, CdReaderError> where - F: FnMut(u32, u32, &RetryConfig) -> Result, CdReaderError>, + F: FnMut(u32, u32, SectorReadMode, &RetryConfig) -> Result, CdReaderError>, { if self.remaining_sectors == 0 { return Ok(None); } let sectors = min(self.remaining_sectors, self.cfg.sectors_per_chunk.max(1)); - let chunk = read_fn(self.next_lba, sectors, &self.cfg.retry)?; + let chunk = read_fn(self.next_lba, sectors, self.cfg.mode, &self.cfg.retry)?; self.next_lba += sectors; self.remaining_sectors -= sectors; @@ -104,7 +105,7 @@ impl<'a> TrackStream<'a> { /// Current stream position in seconds. Functionally equivalent /// to "current_sector", but converted to seconds. /// - /// Audio CD timing uses `75 sectors = 1 second`. + /// CD addresses advance at `75 sectors = 1 second`. pub fn current_seconds(&self) -> f32 { self.current_sector() as f32 / Self::SECTORS_PER_SECOND } @@ -112,7 +113,7 @@ impl<'a> TrackStream<'a> { /// Total stream duration in seconds. Functionally equivalent /// to "total_sectors", but converted to seconds. /// - /// Audio CD timing uses `75 sectors = 1 second`. + /// CD addresses advance at `75 sectors = 1 second`. pub fn total_seconds(&self) -> f32 { self.total_sectors as f32 / Self::SECTORS_PER_SECOND } @@ -138,7 +139,8 @@ impl CdReader { /// It is important to create track streams through this method so the /// drive session is managed through a single CDReader instance. /// - /// Use `TrackStream::next_chunk` to pull sector-aligned PCM chunks. + /// Use [`TrackStream::next_chunk`] to pull sector-aligned chunks in the + /// format selected by [`TrackStreamConfig::mode`]. pub fn open_track_stream<'a>( &'a self, toc: &Toc, @@ -162,7 +164,7 @@ impl CdReader { #[cfg(test)] mod tests { use super::{TrackStream, TrackStreamConfig}; - use crate::{CdReader, CdReaderError, RetryConfig}; + use crate::{CdReader, CdReaderError, SectorReadMode}; fn mk_stream( start_lba: u32, @@ -178,7 +180,7 @@ mod tests { total_sectors, cfg: TrackStreamConfig { sectors_per_chunk, - retry: RetryConfig::default(), + ..TrackStreamConfig::default() }, } } @@ -225,22 +227,24 @@ mod tests { } #[test] - fn next_chunk_reads_expected_size_and_advances() { + fn next_chunk_uses_configured_mode_and_advances() { let mut stream = mk_stream(10_000, 100, 27); + stream.cfg.mode = SectorReadMode::DataCooked; let mut called = false; let chunk = stream - .next_chunk_with(|lba, sectors, _| { + .next_chunk_with(|lba, sectors, mode, _| { called = true; assert_eq!(lba, 10_000); assert_eq!(sectors, 27); - Ok(vec![0u8; (sectors as usize) * 2352]) + assert_eq!(mode, SectorReadMode::DataCooked); + Ok(vec![0u8; (sectors as usize) * mode.sector_size()]) }) .unwrap() .unwrap(); assert!(called); - assert_eq!(chunk.len(), 27 * 2352); + assert_eq!(chunk.len(), 27 * 2048); assert_eq!(stream.current_sector(), 27); assert_eq!(stream.remaining_sectors, 73); } @@ -248,7 +252,9 @@ mod tests { #[test] fn next_chunk_returns_none_when_finished() { let mut stream = mk_stream(10_000, 0, 27); - let result = stream.next_chunk_with(|_, _, _| Ok(vec![1, 2, 3])).unwrap(); + let result = stream + .next_chunk_with(|_, _, _, _| Ok(vec![1, 2, 3])) + .unwrap(); assert!(result.is_none()); } @@ -256,7 +262,7 @@ mod tests { fn next_chunk_error_does_not_advance_position() { let mut stream = mk_stream(10_000, 100, 27); let err = stream - .next_chunk_with(|_, _, _| { + .next_chunk_with(|_, _, _, _| { Err(CdReaderError::Io(std::io::Error::other( "simulated read failure", )))