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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
};

Expand Down
2 changes: 1 addition & 1 deletion src/data_reader.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
5 changes: 3 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
//! };
//!
Expand Down
54 changes: 30 additions & 24 deletions src/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand All @@ -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,
Expand All @@ -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<Option<Vec<u8>>, 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)
Expand All @@ -56,14 +57,14 @@ impl<'a> TrackStream<'a> {

fn next_chunk_with<F>(&mut self, mut read_fn: F) -> Result<Option<Vec<u8>>, CdReaderError>
where
F: FnMut(u32, u32, &RetryConfig) -> Result<Vec<u8>, CdReaderError>,
F: FnMut(u32, u32, SectorReadMode, &RetryConfig) -> Result<Vec<u8>, 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;
Expand Down Expand Up @@ -104,15 +105,15 @@ 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
}

/// 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
}
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -178,7 +180,7 @@ mod tests {
total_sectors,
cfg: TrackStreamConfig {
sectors_per_chunk,
retry: RetryConfig::default(),
..TrackStreamConfig::default()
},
}
}
Expand Down Expand Up @@ -225,38 +227,42 @@ 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);
}

#[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());
}

#[test]
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",
)))
Expand Down
Loading