From 675626d26f74be69551c5d7a5768aaaf10e96a9f Mon Sep 17 00:00:00 2001 From: Seva Zaikov Date: Sat, 11 Jul 2026 21:41:58 -0700 Subject: [PATCH] change stream options configuration to builder API --- README.md | 10 ++-- examples/stream_last_track.rs | 4 +- examples/stream_with_progress.rs | 4 +- src/data_reader.rs | 2 +- src/lib.rs | 12 ++--- src/stream.rs | 87 +++++++++++++++++++++++--------- 6 files changed, 75 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 54eb0cb..0a2da14 100644 --- a/README.md +++ b/README.md @@ -104,18 +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, SectorReadMode, TrackStreamConfig}; +use cd_da_reader::{CdReader, TrackStreamOptions}; let reader = CdReader::open_default()?; let toc = reader.read_toc()?; -let stream_cfg = TrackStreamConfig { - sectors_per_chunk: 27, // ~64 KB per audio chunk - mode: SectorReadMode::Audio, - retry: RetryConfig::default(), -}; +let options = TrackStreamOptions::default(); -let mut stream = reader.open_track_stream(&toc, 1, stream_cfg)?; +let mut stream = reader.open_track_stream(&toc, 1, options)?; while let Some(chunk) = stream.next_chunk()? { // do something with the chunk directly } diff --git a/examples/stream_last_track.rs b/examples/stream_last_track.rs index 1ed456c..3d94c81 100644 --- a/examples/stream_last_track.rs +++ b/examples/stream_last_track.rs @@ -1,5 +1,5 @@ /// Reads the last audio track using the streaming API and saves it as a WAV file. -use cd_da_reader::{CdReader, TrackStreamConfig}; +use cd_da_reader::{CdReader, TrackStreamOptions}; fn main() -> Result<(), Box> { let reader = CdReader::open_default()?; @@ -14,7 +14,7 @@ fn main() -> Result<(), Box> { println!("Streaming track {}...", last_audio.number); let mut stream = - reader.open_track_stream(&toc, last_audio.number, TrackStreamConfig::default())?; + reader.open_track_stream(&toc, last_audio.number, TrackStreamOptions::default())?; let mut pcm = Vec::new(); while let Some(chunk) = stream.next_chunk()? { diff --git a/examples/stream_with_progress.rs b/examples/stream_with_progress.rs index 95039fc..b86f36b 100644 --- a/examples/stream_with_progress.rs +++ b/examples/stream_with_progress.rs @@ -1,5 +1,5 @@ /// Streams the first audio track while printing a live progress line. -use cd_da_reader::{CdReader, TrackStreamConfig}; +use cd_da_reader::{CdReader, TrackStreamOptions}; fn main() -> Result<(), Box> { let reader = CdReader::open_default()?; @@ -12,7 +12,7 @@ fn main() -> Result<(), Box> { .ok_or("no audio tracks found")?; let mut stream = - reader.open_track_stream(&toc, first_audio.number, TrackStreamConfig::default())?; + reader.open_track_stream(&toc, first_audio.number, TrackStreamOptions::default())?; let total_secs = stream.total_seconds(); println!( diff --git a/src/data_reader.rs b/src/data_reader.rs index ee609db..94bfb1b 100644 --- a/src/data_reader.rs +++ b/src/data_reader.rs @@ -71,7 +71,7 @@ impl SectorReadMode { /// This is the sole chunker for the blocking `read_track` / /// `read_sector_range` paths, which can hand a whole track (tens of thousands /// of sectors) straight to the read loop; the streaming API already limits - /// itself via `TrackStreamConfig::sectors_per_chunk`. The cap is not about + /// itself via its `TrackStreamOptions` chunk size. The cap is not about /// OS pass-through limits (modern SG_IO/SPTI handle far larger transfers) /// but about optical-drive firmware and USB-bridge reliability: large /// multi-sector `READ CD` requests are flaky across the zoo of drives. The diff --git a/src/lib.rs b/src/lib.rs index b1d5af2..9416c7d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,18 +85,14 @@ //! use the streaming API instead: //! //! ```no_run -//! use cd_da_reader::{CdReader, RetryConfig, SectorReadMode, TrackStreamConfig}; +//! use cd_da_reader::{CdReader, TrackStreamOptions}; //! //! let reader = CdReader::open_default()?; //! let toc = reader.read_toc()?; //! -//! let cfg = TrackStreamConfig { -//! sectors_per_chunk: 27, // ~64 KB per audio chunk -//! mode: SectorReadMode::Audio, -//! retry: RetryConfig::default(), -//! }; +//! let options = TrackStreamOptions::default(); //! -//! let mut stream = reader.open_track_stream(&toc, 1, cfg)?; +//! let mut stream = reader.open_track_stream(&toc, 1, options)?; //! while let Some(chunk) = stream.next_chunk()? { //! // process chunk — raw PCM, 2 352 bytes per sector //! } @@ -155,7 +151,7 @@ pub use data_reader::{ReadOptions, SectorReadMode}; pub use discovery::DriveInfo; pub use errors::{CdReaderError, ScsiError, ScsiOp}; pub use retry::RetryConfig; -pub use stream::{TrackStream, TrackStreamConfig}; +pub use stream::{TrackStream, TrackStreamOptions}; mod parse_toc; diff --git a/src/stream.rs b/src/stream.rs index 3b73583..e70b933 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -2,20 +2,41 @@ use std::cmp::min; use crate::{CdReader, CdReaderError, ReadOptions, RetryConfig, SectorReadMode, Toc, utils}; -/// Configuration for streamed track reads. +/// Options for streamed track reads. +/// +/// The defaults read audio sectors in chunks of 27 using the default retry +/// policy. Use the builder methods to override only the options you need. #[derive(Debug, Clone)] -pub struct TrackStreamConfig { - /// Target chunk size in sectors for each `next_chunk` call. +pub struct TrackStreamOptions { + sectors_per_chunk: u32, + mode: SectorReadMode, + retry: RetryConfig, +} + +impl TrackStreamOptions { + /// Select the sector format requested from the drive. + pub fn with_mode(mut self, mode: SectorReadMode) -> Self { + self.mode = mode; + self + } + + /// Set the retry policy applied to each chunk read. + pub fn with_retry(mut self, retry: RetryConfig) -> Self { + self.retry = retry; + self + } + + /// Set the target chunk size in sectors. /// - /// 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, + /// The byte size of a chunk also depends on [`SectorReadMode`]. A value of + /// zero is normalized to one sector. + pub fn with_sectors_per_chunk(mut self, sectors: u32) -> Self { + self.sectors_per_chunk = sectors.max(1); + self + } } -impl Default for TrackStreamConfig { +impl Default for TrackStreamOptions { fn default() -> Self { Self { sectors_per_chunk: 27, @@ -35,7 +56,7 @@ pub struct TrackStream<'a> { next_lba: u32, remaining_sectors: u32, total_sectors: u32, - cfg: TrackStreamConfig, + options: TrackStreamOptions, } impl<'a> TrackStream<'a> { @@ -44,7 +65,7 @@ impl<'a> TrackStream<'a> { /// Read the next chunk of sector data. /// /// Returns `Ok(None)` when end-of-track is reached. The bytes per sector - /// depend on the [`SectorReadMode`] in [`TrackStreamConfig`]. + /// depend on the [`SectorReadMode`] selected in [`TrackStreamOptions`]. pub fn next_chunk(&mut self) -> Result>, CdReaderError> { self.next_chunk_with(|lba, sectors, mode, retry| { let options = ReadOptions { @@ -63,8 +84,13 @@ impl<'a> TrackStream<'a> { 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.mode, &self.cfg.retry)?; + let sectors = min(self.remaining_sectors, self.options.sectors_per_chunk); + let chunk = read_fn( + self.next_lba, + sectors, + self.options.mode, + &self.options.retry, + )?; self.next_lba += sectors; self.remaining_sectors -= sectors; @@ -140,12 +166,12 @@ impl CdReader { /// drive session is managed through a single CDReader instance. /// /// Use [`TrackStream::next_chunk`] to pull sector-aligned chunks in the - /// format selected by [`TrackStreamConfig::mode`]. + /// format selected in [`TrackStreamOptions`]. pub fn open_track_stream<'a>( &'a self, toc: &Toc, track_no: u8, - cfg: TrackStreamConfig, + options: TrackStreamOptions, ) -> Result, CdReaderError> { let (start_lba, sectors) = utils::get_track_bounds(toc, track_no).map_err(CdReaderError::Io)?; @@ -156,15 +182,15 @@ impl CdReader { next_lba: start_lba, remaining_sectors: sectors, total_sectors: sectors, - cfg, + options, }) } } #[cfg(test)] mod tests { - use super::{TrackStream, TrackStreamConfig}; - use crate::{CdReader, CdReaderError, SectorReadMode}; + use super::{TrackStream, TrackStreamOptions}; + use crate::{CdReader, CdReaderError, RetryConfig, SectorReadMode}; fn mk_stream( start_lba: u32, @@ -178,13 +204,26 @@ mod tests { next_lba: start_lba, remaining_sectors: total_sectors, total_sectors, - cfg: TrackStreamConfig { - sectors_per_chunk, - ..TrackStreamConfig::default() - }, + options: TrackStreamOptions::default().with_sectors_per_chunk(sectors_per_chunk), } } + #[test] + fn options_builders_override_individual_defaults() { + let retry = RetryConfig { + max_attempts: 9, + ..RetryConfig::default() + }; + let options = TrackStreamOptions::default() + .with_mode(SectorReadMode::DataRaw) + .with_retry(retry) + .with_sectors_per_chunk(0); + + assert_eq!(options.mode, SectorReadMode::DataRaw); + assert_eq!(options.retry.max_attempts, 9); + assert_eq!(options.sectors_per_chunk, 1); + } + #[test] fn seek_to_sector_updates_position() { let mut stream = mk_stream(10_000, 1_000, 27); @@ -229,7 +268,7 @@ mod tests { #[test] fn next_chunk_uses_configured_mode_and_advances() { let mut stream = mk_stream(10_000, 100, 27); - stream.cfg.mode = SectorReadMode::DataCooked; + stream.options = stream.options.with_mode(SectorReadMode::DataCooked); let mut called = false; let chunk = stream