From 300fd5ec6376073315adcbbc49d143fb7866689a Mon Sep 17 00:00:00 2001 From: Seva Zaikov Date: Tue, 25 Aug 2026 07:51:49 -0700 Subject: [PATCH 1/2] use read options in streaming API --- examples/save_data_track.rs | 6 +- src/data_reader/mod.rs | 4 +- src/lib.rs | 9 +- src/stream.rs | 180 ++++++++++++++++++------------------ 4 files changed, 102 insertions(+), 97 deletions(-) diff --git a/examples/save_data_track.rs b/examples/save_data_track.rs index 1936683..89926af 100644 --- a/examples/save_data_track.rs +++ b/examples/save_data_track.rs @@ -21,7 +21,7 @@ use std::fs::File; use std::io::{BufWriter, Write}; use std::path::Path; -use cd_da_reader::{CdReader, SectorReadFormat, Toc, TrackStreamOptions}; +use cd_da_reader::{CdReader, ReadOptions, SectorReadFormat, Toc}; fn main() -> Result<(), Box> { let output_dir = common::fresh_output_dir("save_data_track")?; @@ -96,8 +96,8 @@ fn stream_track_to_file( format: SectorReadFormat, path: &Path, ) -> Result> { - let options = TrackStreamOptions::default().with_format(format); - let mut stream = reader.open_track_stream_with_options(toc, track_no, options)?; + let options = ReadOptions::default().with_format(format); + let mut stream = reader.open_track_stream_with_options(toc, track_no, &options)?; let total_sectors = stream.total_sectors(); let mut writer = BufWriter::new(File::create(path)?); diff --git a/src/data_reader/mod.rs b/src/data_reader/mod.rs index 553532e..0b1a9e3 100644 --- a/src/data_reader/mod.rs +++ b/src/data_reader/mod.rs @@ -10,8 +10,8 @@ pub use sector_read_format::SectorReadFormat; use crate::retry::RetryConfig; use crate::{CdReaderError, Track}; -/// Sector format, retry policy, and read speed options for track and -/// sector-range reads. +/// Sector format, retry policy, and read speed options for track, streaming, +/// and sector-range reads. /// /// The defaults read audio sectors using the default retry policy and leave the /// drive's current read speed unchanged. Use the builder methods to override diff --git a/src/lib.rs b/src/lib.rs index 6c30875..aac4184 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -129,9 +129,10 @@ //! //! ## Read options //! -//! [`CdReader::read_track`] uses sensible defaults for audio CDs and should be enough to get -//! started. For more control, build [`ReadOptions`] from its defaults and pass it to -//! [`CdReader::read_track_with_options`]. The configurable options are: +//! [`CdReader::read_track`] and [`CdReader::open_track_stream`] use sensible defaults for audio +//! CDs and should be enough to get started. For more control, build [`ReadOptions`] from its +//! defaults and pass it to [`CdReader::read_track_with_options`] or +//! [`CdReader::open_track_stream_with_options`]. The configurable options are: //! //! - **Sector format:** [`SectorReadFormat::Audio`] returns 2,352 bytes of PCM per sector and is //! the default. Data tracks can be read as the 2,048-byte [`SectorReadFormat::Mode1Cooked`] @@ -189,7 +190,7 @@ pub use data_reader::{ReadOptions, ReadSpeed, SectorReadFormat}; pub use discovery::DriveInfo; pub use errors::{CdReaderError, ScsiError, ScsiOp}; pub use retry::RetryConfig; -pub use stream::{TrackStream, TrackStreamOptions}; +pub use stream::TrackStream; mod parse_toc; pub use parse_toc::lba_to_msf; diff --git a/src/stream.rs b/src/stream.rs index 3b8f4ac..22d11cf 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -1,51 +1,7 @@ use std::cmp::min; use crate::data_reader::validate_track_format; -use crate::{CdReader, CdReaderError, ReadOptions, RetryConfig, SectorReadFormat, Toc, utils}; - -/// 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 TrackStreamOptions { - sectors_per_chunk: u32, - format: SectorReadFormat, - retry: RetryConfig, -} - -impl TrackStreamOptions { - /// Select the sector format requested from the drive. - pub fn with_format(mut self, format: SectorReadFormat) -> Self { - self.format = format; - 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 [`SectorReadFormat`]. 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 TrackStreamOptions { - fn default() -> Self { - Self { - sectors_per_chunk: 27, - format: SectorReadFormat::Audio, - retry: RetryConfig::default(), - } - } -} +use crate::{CdReader, CdReaderError, ReadOptions, Toc, utils}; /// Track-scoped streaming reader for audio or data sectors. /// @@ -57,45 +13,50 @@ pub struct TrackStream<'a> { next_lba: u32, remaining_sectors: u32, total_sectors: u32, - options: TrackStreamOptions, + sectors_per_chunk: u32, + read_options: ReadOptions, } impl<'a> TrackStream<'a> { + const DEFAULT_SECTORS_PER_CHUNK: u32 = 27; const SECTORS_PER_SECOND: f32 = 75.0; + /// Set the target chunk size in sectors (default 27). + /// + /// The byte size of a chunk also depends on the + /// [`SectorReadFormat`](crate::SectorReadFormat) selected in [`ReadOptions`]. + /// 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 + } + /// Read the next chunk of sector data. /// /// Returns `Ok(None)` when end-of-track is reached. The bytes per sector - /// depend on the [`SectorReadFormat`] selected in [`TrackStreamOptions`]. + /// depend on the [`SectorReadFormat`](crate::SectorReadFormat) selected in + /// [`ReadOptions`]. /// /// # Errors /// /// Returns [`CdReaderError::Io`] or [`CdReaderError::Scsi`] if the drive /// read fails. The stream position does not advance on error. pub fn next_chunk(&mut self) -> Result>, CdReaderError> { - self.next_chunk_with(|lba, sectors, format, retry| { - let options = ReadOptions::default() - .with_format(format) - .with_retry(retry.clone()); - self.reader.read_sector_range(lba, sectors, &options) + self.next_chunk_with(|lba, sectors, options| { + self.reader.read_sector_range(lba, sectors, options) }) } fn next_chunk_with(&mut self, mut read_fn: F) -> Result>, CdReaderError> where - F: FnMut(u32, u32, SectorReadFormat, &RetryConfig) -> Result, CdReaderError>, + F: FnMut(u32, u32, &ReadOptions) -> Result, CdReaderError>, { if self.remaining_sectors == 0 { return Ok(None); } - let sectors = min(self.remaining_sectors, self.options.sectors_per_chunk); - let chunk = read_fn( - self.next_lba, - sectors, - self.options.format, - &self.options.retry, - )?; + let sectors = min(self.remaining_sectors, self.sectors_per_chunk); + let chunk = read_fn(self.next_lba, sectors, &self.read_options)?; self.next_lba += sectors; self.remaining_sectors -= sectors; @@ -187,13 +148,14 @@ impl CdReader { toc: &Toc, track_no: u8, ) -> Result, CdReaderError> { - self.open_track_stream_with_options(toc, track_no, TrackStreamOptions::default()) + self.open_track_stream_with_options(toc, track_no, &ReadOptions::default()) } - /// Open a streaming reader using explicit sector-format, retry, and chunk options. + /// Open a streaming reader using explicit read options. /// /// Use [`TrackStream::next_chunk`] to pull sector-aligned chunks in the - /// format selected in [`TrackStreamOptions`]. + /// selected format. To override the default chunk size, call + /// [`TrackStream::with_sectors_per_chunk`] on the returned stream. /// /// # Errors /// @@ -205,10 +167,10 @@ impl CdReader { &'a self, toc: &Toc, track_no: u8, - options: TrackStreamOptions, + options: &ReadOptions, ) -> Result, CdReaderError> { if let Some(track) = toc.tracks.iter().find(|track| track.number == track_no) { - validate_track_format(track, options.format)?; + validate_track_format(track, options.format())?; } let (start_lba, sectors) = @@ -220,15 +182,18 @@ impl CdReader { next_lba: start_lba, remaining_sectors: sectors, total_sectors: sectors, - options, + sectors_per_chunk: TrackStream::DEFAULT_SECTORS_PER_CHUNK, + read_options: options.clone(), }) } } #[cfg(test)] mod tests { - use super::{TrackStream, TrackStreamOptions}; - use crate::{CdReader, CdReaderError, RetryConfig, SectorReadFormat}; + use super::TrackStream; + use crate::{ + CdReader, CdReaderError, ReadOptions, ReadSpeed, RetryConfig, SectorReadFormat, Toc, Track, + }; fn mk_stream( start_lba: u32, @@ -242,21 +207,51 @@ mod tests { next_lba: start_lba, remaining_sectors: total_sectors, total_sectors, - options: TrackStreamOptions::default().with_sectors_per_chunk(sectors_per_chunk), + sectors_per_chunk: TrackStream::DEFAULT_SECTORS_PER_CHUNK, + read_options: ReadOptions::default(), } + .with_sectors_per_chunk(sectors_per_chunk) + } + + #[test] + fn sectors_per_chunk_normalizes_zero() { + let stream = mk_stream(10_000, 100, 0); + assert_eq!(stream.sectors_per_chunk, 1); } #[test] - fn options_builders_override_individual_defaults() { - let retry = RetryConfig::default().with_max_attempts(9); - let options = TrackStreamOptions::default() - .with_format(SectorReadFormat::Mode1Raw) - .with_retry(retry) - .with_sectors_per_chunk(0); - - assert_eq!(options.format, SectorReadFormat::Mode1Raw); - assert_eq!(options.retry.max_attempts, 9); - assert_eq!(options.sectors_per_chunk, 1); + fn open_stream_preserves_all_read_options() { + let reader = CdReader::test_reader(); + let toc = Toc { + first_track: 1, + last_track: 1, + tracks: vec![Track { + number: 1, + start_lba: 10_000, + start_msf: (2, 15, 25), + is_audio: false, + }], + leadout_lba: 10_100, + }; + let options = ReadOptions::default() + .with_format(SectorReadFormat::Mode1Cooked) + .with_retry(RetryConfig::default().with_max_attempts(9)) + .with_read_speed(ReadSpeed::CustomMultiplier(4)); + + let stream = reader + .open_track_stream_with_options(&toc, 1, &options) + .unwrap(); + + assert_eq!(stream.read_options.format(), SectorReadFormat::Mode1Cooked); + assert_eq!(stream.read_options.retry().max_attempts, 9); + assert!(matches!( + stream.read_options.read_speed(), + ReadSpeed::CustomMultiplier(4) + )); + assert_eq!( + stream.sectors_per_chunk, + TrackStream::DEFAULT_SECTORS_PER_CHUNK + ); } #[test] @@ -301,18 +296,29 @@ mod tests { } #[test] - fn next_chunk_uses_configured_format_and_advances() { + fn next_chunk_uses_configured_read_options_and_advances() { let mut stream = mk_stream(10_000, 100, 27); - stream.options = stream.options.with_format(SectorReadFormat::Mode1Cooked); + stream.read_options = ReadOptions::default() + .with_format(SectorReadFormat::Mode1Cooked) + .with_retry(RetryConfig::default().with_max_attempts(9)) + .with_read_speed(ReadSpeed::CustomMultiplier(4)); let mut called = false; let chunk = stream - .next_chunk_with(|lba, sectors, format, _| { + .next_chunk_with(|lba, sectors, options| { called = true; assert_eq!(lba, 10_000); assert_eq!(sectors, 27); - assert_eq!(format, SectorReadFormat::Mode1Cooked); - Ok(vec![0u8; (sectors as usize) * format.sector_size()]) + assert_eq!(options.format(), SectorReadFormat::Mode1Cooked); + assert_eq!(options.retry().max_attempts, 9); + assert!(matches!( + options.read_speed(), + ReadSpeed::CustomMultiplier(4) + )); + Ok(vec![ + 0u8; + (sectors as usize) * options.format().sector_size() + ]) }) .unwrap() .unwrap(); @@ -326,9 +332,7 @@ 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()); } @@ -336,7 +340,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", ))) From 1ee233fcd02494fe2e061f178581cfb9d26d8f95 Mon Sep 17 00:00:00 2001 From: Seva Zaikov Date: Tue, 25 Aug 2026 08:05:37 -0700 Subject: [PATCH 2/2] set speed only once when stream is opened --- src/data_reader/mod.rs | 2 +- src/stream.rs | 74 ++++++++++++++++++++++++++++++++---------- 2 files changed, 58 insertions(+), 18 deletions(-) diff --git a/src/data_reader/mod.rs b/src/data_reader/mod.rs index 0b1a9e3..fa46eae 100644 --- a/src/data_reader/mod.rs +++ b/src/data_reader/mod.rs @@ -37,7 +37,7 @@ impl ReadOptions { } /// Set the read speed to request from the drive. See [`ReadSpeed`] for - /// details. + /// details. Streaming reads apply this request once when the stream is opened. /// /// # Note /// For simplicity, this crate doesn't restore the previous speed setting. diff --git a/src/stream.rs b/src/stream.rs index 22d11cf..76484ba 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -1,7 +1,19 @@ use std::cmp::min; use crate::data_reader::validate_track_format; -use crate::{CdReader, CdReaderError, ReadOptions, Toc, utils}; +use crate::{CdReader, CdReaderError, ReadOptions, ReadSpeed, Toc, utils}; + +fn apply_stream_read_speed_once( + options: &ReadOptions, + request_read_speed: impl FnOnce(ReadSpeed) -> Result<(), CdReaderError>, +) -> Result { + request_read_speed(options.read_speed())?; + + // The speed request applies to the stream as a whole. Chunk reads go + // through read_sector_range, so to avoid constant speed setting, we + // apply it once and clone ReadOptions with unchanged speed + Ok(options.clone().with_read_speed(ReadSpeed::Unchanged)) +} /// Track-scoped streaming reader for audio or data sectors. /// @@ -154,15 +166,17 @@ impl CdReader { /// Open a streaming reader using explicit read options. /// /// Use [`TrackStream::next_chunk`] to pull sector-aligned chunks in the - /// selected format. To override the default chunk size, call - /// [`TrackStream::with_sectors_per_chunk`] on the returned stream. + /// selected format. The requested read speed is applied once before the + /// stream is returned. To override the default + /// chunk size, call [`TrackStream::with_sectors_per_chunk`] on the returned + /// stream. /// /// # Errors /// /// - Returns [`CdReaderError::TrackFormatMismatch`] if the selected format /// is incompatible with the track. - /// - Returns [`CdReaderError::Io`] if the track is absent or its bounds are - /// invalid. + /// - Returns [`CdReaderError::Io`] if the track is absent, its bounds are + /// invalid, or the read-speed request fails. pub fn open_track_stream_with_options<'a>( &'a self, toc: &Toc, @@ -175,6 +189,9 @@ impl CdReader { let (start_lba, sectors) = utils::get_track_bounds(toc, track_no).map_err(CdReaderError::Io)?; + let read_options = apply_stream_read_speed_once(options, |read_speed| { + self.drive.request_read_speed(read_speed) + })?; Ok(TrackStream { reader: self, @@ -183,14 +200,14 @@ impl CdReader { remaining_sectors: sectors, total_sectors: sectors, sectors_per_chunk: TrackStream::DEFAULT_SECTORS_PER_CHUNK, - read_options: options.clone(), + read_options, }) } } #[cfg(test)] mod tests { - use super::TrackStream; + use super::{TrackStream, apply_stream_read_speed_once}; use crate::{ CdReader, CdReaderError, ReadOptions, ReadSpeed, RetryConfig, SectorReadFormat, Toc, Track, }; @@ -220,7 +237,35 @@ mod tests { } #[test] - fn open_stream_preserves_all_read_options() { + fn stream_speed_is_requested_once_before_chunk_reads() { + let options = ReadOptions::default().with_read_speed(ReadSpeed::CustomMultiplier(4)); + let mut speed_requests = 0; + let chunk_options = apply_stream_read_speed_once(&options, |read_speed| { + speed_requests += 1; + assert!(matches!(read_speed, ReadSpeed::CustomMultiplier(4))); + Ok(()) + }) + .unwrap(); + + assert_eq!(speed_requests, 1); + assert!(matches!(chunk_options.read_speed(), ReadSpeed::Unchanged)); + + let mut stream = mk_stream(10_000, 100, 27); + stream.read_options = chunk_options; + for _ in 0..2 { + stream + .next_chunk_with(|_, _, options| { + assert!(matches!(options.read_speed(), ReadSpeed::Unchanged)); + Ok(Vec::new()) + }) + .unwrap(); + } + + assert_eq!(speed_requests, 1); + } + + #[test] + fn open_stream_preserves_chunk_read_options() { let reader = CdReader::test_reader(); let toc = Toc { first_track: 1, @@ -235,8 +280,7 @@ mod tests { }; let options = ReadOptions::default() .with_format(SectorReadFormat::Mode1Cooked) - .with_retry(RetryConfig::default().with_max_attempts(9)) - .with_read_speed(ReadSpeed::CustomMultiplier(4)); + .with_retry(RetryConfig::default().with_max_attempts(9)); let stream = reader .open_track_stream_with_options(&toc, 1, &options) @@ -246,7 +290,7 @@ mod tests { assert_eq!(stream.read_options.retry().max_attempts, 9); assert!(matches!( stream.read_options.read_speed(), - ReadSpeed::CustomMultiplier(4) + ReadSpeed::Unchanged )); assert_eq!( stream.sectors_per_chunk, @@ -300,8 +344,7 @@ mod tests { let mut stream = mk_stream(10_000, 100, 27); stream.read_options = ReadOptions::default() .with_format(SectorReadFormat::Mode1Cooked) - .with_retry(RetryConfig::default().with_max_attempts(9)) - .with_read_speed(ReadSpeed::CustomMultiplier(4)); + .with_retry(RetryConfig::default().with_max_attempts(9)); let mut called = false; let chunk = stream @@ -311,10 +354,7 @@ mod tests { assert_eq!(sectors, 27); assert_eq!(options.format(), SectorReadFormat::Mode1Cooked); assert_eq!(options.retry().max_attempts, 9); - assert!(matches!( - options.read_speed(), - ReadSpeed::CustomMultiplier(4) - )); + assert!(matches!(options.read_speed(), ReadSpeed::Unchanged)); Ok(vec![ 0u8; (sectors as usize) * options.format().sector_size()