Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
4 changes: 2 additions & 2 deletions examples/stream_last_track.rs
Original file line number Diff line number Diff line change
@@ -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<dyn std::error::Error>> {
let reader = CdReader::open_default()?;
Expand All @@ -14,7 +14,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {

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()? {
Expand Down
4 changes: 2 additions & 2 deletions examples/stream_with_progress.rs
Original file line number Diff line number Diff line change
@@ -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<dyn std::error::Error>> {
let reader = CdReader::open_default()?;
Expand All @@ -12,7 +12,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.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!(
Expand Down
2 changes: 1 addition & 1 deletion src/data_reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 4 additions & 8 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
//! }
Expand Down Expand Up @@ -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;

Expand Down
87 changes: 63 additions & 24 deletions src/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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> {
Expand All @@ -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<Option<Vec<u8>>, CdReaderError> {
self.next_chunk_with(|lba, sectors, mode, retry| {
let options = ReadOptions {
Expand All @@ -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;
Expand Down Expand Up @@ -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<TrackStream<'a>, CdReaderError> {
let (start_lba, sectors) =
utils::get_track_bounds(toc, track_no).map_err(CdReaderError::Io)?;
Expand All @@ -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,
Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand Down
Loading