diff --git a/README.md b/README.md index 4133300..6c02558 100644 --- a/README.md +++ b/README.md @@ -128,13 +128,13 @@ Each CD sector holds exactly 2,352 bytes of audio payload (176,400 / 75 = 2,352) Converting PCM data to a playable WAV file only requires prepending a 44-byte RIFF header. In fact, there is a helper for that in this library: ```rust -use cd_da_reader::{CdReader}; +use cd_da_reader::{CdReader, create_wav}; let reader = CdReader::open_default()?; let toc = reader.read_toc()?; // we assume that track #1 exists for simplicity let data = reader.read_track(&toc, 1)?; -let wav = CdReader::create_wav(data); +let wav = create_wav(data); std::fs::write("myfile.wav", wav)?; ``` @@ -180,7 +180,7 @@ impl AudioSectorReader for MyImage { } let pcm = read_track(&image, &toc, 1)?; // build `toc` from the image's metadata -let wav = create_wav(pcm); // free fn; also CdReader::create_wav +let wav = create_wav(pcm); ``` `CdReader` itself implements `AudioSectorReader`, so drive-backed and file-backed code share the generic `read_track` path. diff --git a/examples/custom_retry.rs b/examples/custom_retry.rs index d218d78..60284b7 100644 --- a/examples/custom_retry.rs +++ b/examples/custom_retry.rs @@ -7,7 +7,7 @@ mod common; use std::time::Duration; -use cd_da_reader::{CdReader, ReadOptions, RetryConfig}; +use cd_da_reader::{CdReader, ReadOptions, RetryConfig, create_wav}; fn main() -> Result<(), Box> { let output_dir = common::fresh_output_dir("custom_retry")?; @@ -36,7 +36,7 @@ fn main() -> Result<(), Box> { ); let data = reader.read_track_with_options(&toc, first_audio.number, &options)?; - let wav = CdReader::create_wav(data); + let wav = create_wav(data); let output_path = output_dir.join(format!("track{:02}.wav", first_audio.number)); std::fs::write(&output_path, wav)?; println!("Saved {}", output_path.display()); diff --git a/examples/play_audio_track.rs b/examples/play_audio_track.rs index 3bbfa4f..9979112 100644 --- a/examples/play_audio_track.rs +++ b/examples/play_audio_track.rs @@ -20,7 +20,7 @@ mod common; use std::path::Path; -use cd_da_reader::{CdReader, ReadOptions}; +use cd_da_reader::{CdReader, ReadOptions, create_wav}; /// CD-DA plays 75 sectors (each 2352 bytes) per second. const SECTORS_PER_SECOND: u32 = 75; @@ -66,7 +66,7 @@ fn main() -> Result<(), Box> { ); let output_path = output_dir.join(format!("track{:02}_preview.wav", track.number)); - std::fs::write(&output_path, CdReader::create_wav(pcm))?; + std::fs::write(&output_path, create_wav(pcm))?; println!("Saved {}", output_path.display()); play(&output_path) diff --git a/examples/read_all_tracks.rs b/examples/read_all_tracks.rs index a6c7480..25d1b39 100644 --- a/examples/read_all_tracks.rs +++ b/examples/read_all_tracks.rs @@ -1,7 +1,7 @@ /// Reads every audio track from the default CD drive and saves each as a WAV file. mod common; -use cd_da_reader::CdReader; +use cd_da_reader::{CdReader, create_wav}; fn main() -> Result<(), Box> { let output_dir = common::fresh_output_dir("read_all_tracks")?; @@ -17,7 +17,7 @@ fn main() -> Result<(), Box> { print!("Reading track {:>2}... ", track.number); match reader.read_track(&toc, track.number) { Ok(data) => { - let wav = CdReader::create_wav(data); + let wav = create_wav(data); let output_path = output_dir.join(format!("track{:02}.wav", track.number)); std::fs::write(&output_path, wav)?; println!("saved {}", output_path.display()); diff --git a/examples/read_first_track.rs b/examples/read_first_track.rs index caa8330..2df96b4 100644 --- a/examples/read_first_track.rs +++ b/examples/read_first_track.rs @@ -1,7 +1,7 @@ /// Reads the first audio track from the default CD drive and saves it as a WAV file. mod common; -use cd_da_reader::CdReader; +use cd_da_reader::{CdReader, create_wav}; fn main() -> Result<(), Box> { let output_dir = common::fresh_output_dir("read_first_track")?; @@ -17,7 +17,7 @@ fn main() -> Result<(), Box> { println!("Reading track {}...", first_audio.number); let data = reader.read_track(&toc, first_audio.number)?; - let wav = CdReader::create_wav(data); + let wav = create_wav(data); let output_path = output_dir.join(format!("track{:02}.wav", first_audio.number)); std::fs::write(&output_path, wav)?; println!("Saved {}", output_path.display()); diff --git a/examples/stream_last_track.rs b/examples/stream_last_track.rs index 8995649..6563874 100644 --- a/examples/stream_last_track.rs +++ b/examples/stream_last_track.rs @@ -1,7 +1,7 @@ /// Reads the last audio track using the streaming API and saves it as a WAV file. mod common; -use cd_da_reader::CdReader; +use cd_da_reader::{CdReader, create_wav}; fn main() -> Result<(), Box> { let output_dir = common::fresh_output_dir("stream_last_track")?; @@ -23,7 +23,7 @@ fn main() -> Result<(), Box> { pcm.extend_from_slice(&chunk); } - let wav = CdReader::create_wav(pcm); + let wav = create_wav(pcm); let output_path = output_dir.join(format!("track{:02}.wav", last_audio.number)); std::fs::write(&output_path, wav)?; println!("Saved {}", output_path.display()); diff --git a/examples/stream_with_progress.rs b/examples/stream_with_progress.rs index 4c9ca96..bcfef36 100644 --- a/examples/stream_with_progress.rs +++ b/examples/stream_with_progress.rs @@ -1,7 +1,7 @@ /// Streams the first audio track while printing a live progress line. mod common; -use cd_da_reader::CdReader; +use cd_da_reader::{CdReader, create_wav}; fn main() -> Result<(), Box> { let output_dir = common::fresh_output_dir("stream_with_progress")?; @@ -34,7 +34,7 @@ fn main() -> Result<(), Box> { } eprintln!("\r [{:.1}s / {:.1}s] 100.0%", total_secs, total_secs); - let wav = CdReader::create_wav(pcm); + let wav = create_wav(pcm); let output_path = output_dir.join(format!("track{:02}.wav", first_audio.number)); std::fs::write(&output_path, wav)?; println!("\nSaved {}", output_path.display()); diff --git a/src/backend.rs b/src/backend.rs index ea9c73d..b279409 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -77,6 +77,11 @@ pub trait AudioSectorReader { /// Read `count` sectors starting at absolute `start_lba`, returning exactly /// `count * 2352` bytes of little-endian PCM. + /// + /// # Errors + /// + /// Implementations must return an error if they cannot provide the complete + /// requested sector range in the required format. fn read_audio_sectors(&self, start_lba: u32, count: u32) -> Result, Self::Error>; } @@ -123,13 +128,15 @@ impl TrackBounds { /// 2352-B/sector PCM, so [`create_wav`](crate::create_wav) wraps them into a /// playable file unchanged. /// +/// If the backing addresses tracks contiguously without gaps, use +/// [`read_track_with_bounds`] with [`TrackBounds::Gapless`]. +/// +/// # Errors +/// /// A bad track request (not in the TOC, or invalid bounds) is /// [`CdReaderError::Io`]; a failure inside the backing is /// [`CdReaderError::Backend`], which preserves the backing's own error as the /// boxed [`source`](std::error::Error::source). -/// -/// If the backing addresses tracks contiguously (a gap-stripped extract), use -/// [`read_track_with_bounds`] with [`TrackBounds::Gapless`]. pub fn read_track( src: &R, toc: &Toc, @@ -140,6 +147,11 @@ pub fn read_track( /// Read one track like [`read_track`], but with an explicit [`TrackBounds`] /// geometry — pass [`TrackBounds::Gapless`] for a contiguous, gap-stripped layout. +/// +/// # Errors +/// +/// Returns [`CdReaderError::Io`] if the track is absent or its bounds are +/// invalid, and [`CdReaderError::Backend`] if the backing read fails. pub fn read_track_with_bounds( src: &R, toc: &Toc, @@ -195,8 +207,11 @@ impl<'a, R: AudioSectorReader> AudioTrackStream<'a, R> { /// Read the next chunk of PCM, or `Ok(None)` at end-of-track. /// /// Each chunk is `sectors_per_chunk * 2352` bytes except possibly the last. - /// A backing failure is [`CdReaderError::Backend`]; the position does not - /// advance on error, so a retry re-reads the same chunk. + /// + /// # Errors + /// + /// Returns [`CdReaderError::Backend`] if the backing read fails. The stream + /// position does not advance on error, so a retry re-reads the same chunk. pub fn next_chunk(&mut self) -> Result>, CdReaderError> { if self.remaining_sectors == 0 { return Ok(None); @@ -225,6 +240,12 @@ impl<'a, R: AudioSectorReader> AudioTrackStream<'a, R> { } /// Seek to a track-relative sector position (valid range `0..=total_sectors()`). + /// + /// # Errors + /// + /// Returns [`CdReaderError::Io`] containing + /// [`std::io::ErrorKind::InvalidInput`] if `sector` exceeds the track + /// length. pub fn seek_to_sector(&mut self, sector: u32) -> Result<(), CdReaderError> { if sector > self.total_sectors { return Err(CdReaderError::Io(std::io::Error::new( @@ -249,6 +270,12 @@ impl<'a, R: AudioSectorReader> AudioTrackStream<'a, R> { } /// Seek to a track-relative time in seconds, clamped to the track length. + /// + /// # Errors + /// + /// Returns [`CdReaderError::Io`] containing + /// [`std::io::ErrorKind::InvalidInput`] if `seconds` is negative or not + /// finite. pub fn seek_to_seconds(&mut self, seconds: f32) -> Result<(), CdReaderError> { if !seconds.is_finite() || seconds < 0.0 { return Err(CdReaderError::Io(std::io::Error::new( @@ -264,6 +291,11 @@ impl<'a, R: AudioSectorReader> AudioTrackStream<'a, R> { /// Open a streaming reader for a track assuming the TOC includes the inter-session /// gap ([`TrackBounds::SessionGap`]). See [`AudioTrackStream`]. +/// +/// # Errors +/// +/// Returns [`CdReaderError::Io`] if the track is absent or its bounds are +/// invalid. pub fn open_track_stream<'a, R: AudioSectorReader>( src: &'a R, toc: &Toc, @@ -274,6 +306,11 @@ pub fn open_track_stream<'a, R: AudioSectorReader>( /// Open a streaming reader for a track with an explicit [`TrackBounds`] geometry. /// Use [`TrackBounds::Gapless`] for a contiguous, gap-stripped layout. +/// +/// # Errors +/// +/// Returns [`CdReaderError::Io`] if the track is absent or its bounds are +/// invalid. pub fn open_track_stream_with_bounds<'a, R: AudioSectorReader>( src: &'a R, toc: &Toc, diff --git a/src/data_reader/detect.rs b/src/data_reader/detect.rs index 3a02610..b2b52b4 100644 --- a/src/data_reader/detect.rs +++ b/src/data_reader/detect.rs @@ -7,6 +7,13 @@ impl CdReader { /// Audio tracks are identified directly from the TOC. Data tracks are /// queried with MMC READ TRACK INFORMATION. If its Data Mode is /// inconclusive, one raw sector is inspected as a fallback. + /// + /// # Errors + /// + /// - Returns [`CdReaderError::CannotDetectTrackFormat`] if neither the + /// track metadata nor a raw sector identifies the data format. + /// - Returns [`CdReaderError::Io`], [`CdReaderError::Scsi`], or + /// [`CdReaderError::Parse`] if querying the drive fails. pub fn detect_track_format(&self, track: &Track) -> Result { if track.is_audio { return Ok(SectorReadFormat::Audio); diff --git a/src/data_reader/mod.rs b/src/data_reader/mod.rs index a9bcaba..553532e 100644 --- a/src/data_reader/mod.rs +++ b/src/data_reader/mod.rs @@ -10,10 +10,12 @@ pub use sector_read_format::SectorReadFormat; use crate::retry::RetryConfig; use crate::{CdReaderError, Track}; -/// Sector format and retry options for track and sector-range reads. +/// Sector format, retry policy, and read speed options for track and +/// sector-range reads. /// -/// The defaults read audio sectors using the default retry policy. Use the -/// builder methods to override only the options you need. +/// 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 +/// only the options you need. #[derive(Debug, Clone)] pub struct ReadOptions { format: SectorReadFormat, diff --git a/src/discovery.rs b/src/discovery.rs index 6ff72e0..8e77124 100644 --- a/src/discovery.rs +++ b/src/discovery.rs @@ -1,7 +1,11 @@ use crate::{CdReader, CdReaderError}; -/// Information about all found drives. This info is not tested extensively, and in -/// general it is encouraged to provide a disk drive directly. +/// Information about an optical drive discovered by [`CdReader::list_drives`]. +/// +/// Audio-CD detection is best-effort. If a drive cannot be opened or its TOC +/// cannot be read, it is still returned with [`DriveInfo::has_audio_cd`] set to +/// `false`. If you already know the platform-specific device path, you can +/// bypass discovery with [`CdReader::open_path`]. #[derive(Debug, Clone)] pub struct DriveInfo { /// Path to the drive, which can be something like 'disk6' on macOS, @@ -13,6 +17,12 @@ pub struct DriveInfo { impl CdReader { /// Enumerate candidate optical drives and probe whether they currently have an audio CD. + /// + /// # Errors + /// + /// Returns [`CdReaderError::Io`] if platform drive enumeration fails. + /// Errors while probing an individual drive are represented by + /// [`DriveInfo::has_audio_cd`] being `false` instead. pub fn list_drives() -> Result, CdReaderError> { let mut paths = crate::platform::list_drive_paths()?; paths.sort(); @@ -35,6 +45,13 @@ impl CdReader { } /// Open the first discovered drive that currently has an audio CD. + /// + /// # Errors + /// + /// - Returns [`CdReaderError::NoUsableDrive`] if no discovered drive has a + /// readable audio CD. + /// - Returns [`CdReaderError::Io`] if drive enumeration or opening the + /// selected drive fails. pub fn open_default() -> Result { let drives = Self::list_drives()?; let chosen = pick_default_drive(&drives).ok_or(CdReaderError::NoUsableDrive)?; diff --git a/src/lib.rs b/src/lib.rs index bd7883a..6c30875 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,13 +1,15 @@ //! # CD-DA (audio CD) reading library //! -//! This library provides cross-platform audio CD reading capabilities -//! (tested on Windows, macOS and Linux). It was written to enable CD ripping, -//! but you can also implement a live audio CD player with its help. -//! The library works through platform CD-drive APIs on macOS and issuing direct -//! SCSI commands on Windows and Linux and abstracts both access to the CD drive -//! and reading the actual data from it, so you don't deal with the hardware directly. +//! This library provides cross-platform audio CD reading capabilities (tested +//! on Windows, macOS and Linux). It was written to enable CD ripping, but you +//! can also implement a live audio CD player with its help, and implement +//! reading using your own files if you can provide ToC (table of contents) by +//! yourself. The library works through platform CD-drive APIs on macOS and +//! issuing direct SCSI commands on Windows and Linux and abstracts both access +//! to the CD drive and reading the actual data from it, so you don't deal with +//! the hardware directly. The library works in the userland. //! -//! All operations happen in this order: +//! All CD reading operations happen in this order: //! //! 1. Get a CD drive's handle //! 2. Read the ToC (table of contents) of the audio CD @@ -112,23 +114,55 @@ //! ~31 MB; a full 74-minute CD is ~650 MB. //! //! Converting raw PCM to a playable WAV file only requires prepending a 44-byte -//! RIFF header — [`CdReader::create_wav`] does exactly that: +//! RIFF header — [`create_wav`] does exactly that: //! //! ```no_run -//! use cd_da_reader::CdReader; +//! use cd_da_reader::{CdReader, create_wav}; //! //! let reader = CdReader::open_default()?; //! let toc = reader.read_toc()?; //! let data = reader.read_track(&toc, 1)?; -//! let wav = CdReader::create_wav(data); +//! let wav = create_wav(data); //! std::fs::write("track01.wav", wav)?; //! # Ok::<(), Box>(()) //! ``` //! +//! ## 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: +//! +//! - **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`] +//! payload or as complete 2,352-byte [`SectorReadFormat::Mode1Raw`] or +//! [`SectorReadFormat::Mode2Raw`] sectors. Mode 2 is available only as raw sectors; inspect +//! each sector's XA subheader to locate its payload. Use [`CdReader::detect_track_format`] when +//! the data-track format is not already known. +//! - **Retry policy:** [`RetryConfig`] controls the number of attempts, retry delays, and adaptive +//! reduction of the number of sectors read at once. Its defaults are suitable for most drives. +//! - **Read speed:** [`ReadSpeed`] requests an optimal or custom drive speed. The default, +//! [`ReadSpeed::Unchanged`], leaves the current setting alone. Requested speeds are not +//! guaranteed, and the previous drive setting is not restored after the read. Speed settings +//! are drive and OS dependent. +//! +//! ```no_run +//! use cd_da_reader::{CdReader, ReadOptions, ReadSpeed, RetryConfig, SectorReadFormat}; +//! +//! let reader = CdReader::open_default()?; +//! let toc = reader.read_toc()?; +//! let options = ReadOptions::default() +//! .with_format(SectorReadFormat::Audio) +//! .with_retry(RetryConfig::default().with_max_attempts(6)) +//! .with_read_speed(ReadSpeed::CustomMultiplier(4)); +//! let data = reader.read_track_with_options(&toc, 1, &options)?; +//! # Ok::<(), Box>(()) +//! ``` +//! //! ## Metadata //! //! Audio CDs carry almost no semantic metadata. [CD-TEXT] exists but is -//! unreliable and because of that is not provided by this lbirary. The practical approach is to +//! unreliable and because of that is not provided by this library. The practical approach is to //! calculate a Disc ID from the ToC and look it up on a service such as //! [MusicBrainz]. The [`Toc`] struct exposes everything required for the //! [MusicBrainz disc ID algorithm]. @@ -168,35 +202,49 @@ pub struct Track { /// reading raw track data. There might be gaps, and also in the future /// there might be hidden track support, which will be located at number 0. pub number: u8, - /// starting offset, unnecessary to use directly + /// starting offset pub start_lba: u32, - /// starting offset, but in (minute, second, frame) format + /// Track start address in `(minutes, seconds, frames)` (MSF) form. + /// + /// MSF uses 75 frames per second and includes the standard 150-frame + /// lead-in offset, so LBA 0 corresponds to `(0, 2, 0)`. See [`lba_to_msf`]. pub start_msf: (u8, u8, u8), + /// Whether the TOC identifies this as an audio track. + /// A value of `false` indicates a data track. pub is_audio: bool, } /// Table of Contents, read directly from the Audio CD. The most important part /// is the `tracks` vector, which allows you to read raw track data. +/// +/// If you read from file/image directly, you need to construct it manually. #[derive(Debug)] pub struct Toc { - /// Helper value with the first track number + /// First track number reported in the TOC header. + /// + /// This is a disc track number, not a zero-based index into [`Toc::tracks`]. + /// It does not have to start with 1 and can be up to 99. pub first_track: u8, /// Helper value with the last track number. You should not use it directly to /// iterate over all available tracks, as there might be gaps. pub last_track: u8, /// List of tracks with LBA and MSF offsets pub tracks: Vec, - /// Lead-out LBA reported by the drive for the disc TOC. You'll also need this - /// in order to calculate MusicBrainz ID. + /// LBA at which the lead-out area begins, as reported by the disc TOC. + /// + /// Track-bound calculations use this as the upper bound only for the last + /// entry in [`Toc::tracks`]. If another track follows, its start and any + /// applicable CD-Extra session-gap handling determine the preceding track's + /// bound instead. The lead-out LBA is also required to calculate a + /// MusicBrainz Disc ID. pub leadout_lba: u32, } /// Wrap raw CD-DA PCM in a 44-byte WAV/RIFF header (44100 Hz, 2 channels, /// 16-bit) so the bytes become a playable file. /// -/// This is the free-function form of [`CdReader::create_wav`], usable without -/// naming the physical-drive type — for example on PCM obtained from a file or -/// image backing via [`read_track`]. +/// Use this with PCM returned by [`CdReader::read_track`] or obtained from a +/// file or image backing via [`read_track`]. pub fn create_wav(data: Vec) -> Vec { let mut header = utils::create_wav_header(data.len() as u32); header.extend_from_slice(&data); @@ -214,6 +262,11 @@ impl CdReader { /// Opens a drive returned by [`CdReader::list_drives`]. /// /// The reader owns the opened drive until it is dropped. + /// + /// # Errors + /// + /// Returns [`CdReaderError::Io`] if the discovered drive path cannot be + /// opened with the access required for raw drive commands. pub fn open(drive: &DriveInfo) -> Result { Self::open_path(&drive.path) } @@ -222,6 +275,11 @@ impl CdReader { /// /// Example paths are `/dev/sr0` on Linux, `disk6` on macOS, and /// `\\.\E:` on Windows. The reader owns the opened drive until it is dropped. + /// + /// # Errors + /// + /// Returns [`CdReaderError::Io`] if `path` is invalid or the operating + /// system cannot open it with the required access. pub fn open_path(path: &str) -> Result { Ok(Self { drive: platform::Drive::open(path)?, @@ -261,22 +319,17 @@ impl CdReader { } } - /// While this is a low-level library and does not include any codecs to compress the audio, - /// it includes a helper function to convert raw PCM data into a wav file, which is done by - /// prepending a 44 RIFF bytes header - /// - /// # Arguments - /// - /// * `data` - vector of bytes received from `read_track` function - pub fn create_wav(data: Vec) -> Vec { - crate::create_wav(data) - } - /// Read Table of Contents for the opened drive. You'll likely only need to access /// `tracks` from the returned value in order to iterate and read each track's raw data. /// Please note that each track in the vector has `number` property, which you should use /// when calling `read_track`, as it doesn't start with 0. It is important to do so, /// because in the future it might include 0 for the hidden track. + /// + /// # Errors + /// + /// Returns [`CdReaderError::Io`] or [`CdReaderError::Scsi`] if the drive + /// command fails, and [`CdReaderError::Parse`] if the returned TOC is + /// malformed. pub fn read_toc(&self) -> Result { self.drive.read_toc() } @@ -284,12 +337,24 @@ impl CdReader { /// Read an audio track using the default options. /// /// It returns raw PCM data, but if you want to save it directly and make it playable, - /// wrap the result with [`CdReader::create_wav`]. + /// wrap the result with [`create_wav`]. + /// + /// # Errors + /// + /// Returns the same errors as [`CdReader::read_track_with_options`]. pub fn read_track(&self, toc: &Toc, track_no: u8) -> Result, CdReaderError> { self.read_track_with_options(toc, track_no, &ReadOptions::default()) } - /// Read a complete track using explicit sector-format and retry options. + /// Read a complete track using explicit read options. + /// + /// # Errors + /// + /// - Returns [`CdReaderError::TrackFormatMismatch`] if the selected sector + /// format is incompatible with the track. + /// - Returns [`CdReaderError::Io`] if the track is absent, its bounds are + /// invalid, or an operating-system drive operation fails. + /// - Returns [`CdReaderError::Scsi`] if the drive rejects a read command. pub fn read_track_with_options( &self, toc: &Toc, @@ -305,13 +370,19 @@ impl CdReader { self.read_sector_range(start_lba, sectors, options) } - /// Read an arbitrary range of sectors using explicit format and retry options. + /// Read an arbitrary range of sectors using explicit read options. /// /// # Low-level API /// /// Callers are responsible for providing valid sector boundaries and selecting /// a format compatible with the sectors on the disc. Prefer [`CdReader::read_track`] /// or [`CdReader::read_track_with_options`] when reading a complete TOC track. + /// + /// # Errors + /// + /// - Returns [`CdReaderError::Io`] if the range is invalid, the read-speed + /// request fails, or an operating-system read fails. + /// - Returns [`CdReaderError::Scsi`] if the drive rejects a read command. pub fn read_sector_range( &self, start_lba: u32, diff --git a/src/retry.rs b/src/retry.rs index 541c5b7..e5d9730 100644 --- a/src/retry.rs +++ b/src/retry.rs @@ -1,12 +1,16 @@ use std::time::Duration; -/// Retry policy for read operations. +/// Retry policy for failed drive reads. /// -/// The policy is applied when we fail to read a chunk, and it will both -/// wait a bit before attempting the next read and will decrease the number -/// of chunks to read. The default values are aimed to be universally good -/// and unless you have specific requirements using RetryConfig::default() -/// is recommended. +/// Track and sector-range reads are split into chunks, and this policy is +/// applied independently to each chunk. If a chunk read fails, the next +/// attempt starts at the same LBA. Retry delays use capped exponential backoff. +/// When adaptive chunk reduction is enabled, retries request fewer sectors from +/// that LBA, down to the configured minimum. Chunks that were already read +/// successfully are not repeated. +/// +/// The default values are suitable for most drives. Unless you have specific +/// requirements, using [`RetryConfig::default`] is recommended. /// /// The default policy uses: /// @@ -25,7 +29,7 @@ pub struct RetryConfig { } impl RetryConfig { - /// Set the maximum attempts per operation, including the initial attempt. + /// Set the maximum attempts per chunk, including the initial read. /// /// A value of zero is normalized to one attempt. pub fn with_max_attempts(mut self, attempts: u8) -> Self { @@ -49,7 +53,7 @@ impl RetryConfig { self } - /// Enable or disable adaptive sector-count reduction after failed reads. + /// Enable or disable requesting fewer sectors after a failed chunk read. pub fn with_chunk_reduction(mut self, enabled: bool) -> Self { self.reduce_chunk_on_retry = enabled; self diff --git a/src/stream.rs b/src/stream.rs index 9449962..3b8f4ac 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -67,6 +67,11 @@ impl<'a> TrackStream<'a> { /// /// Returns `Ok(None)` when end-of-track is reached. The bytes per sector /// depend on the [`SectorReadFormat`] selected in [`TrackStreamOptions`]. + /// + /// # 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() @@ -111,10 +116,15 @@ impl<'a> TrackStream<'a> { self.total_sectors - self.remaining_sectors } - /// Seek to an absolute track-relative sector position. + /// Seek to a sector position relative to the start of the track. /// /// Valid range is `0..=total_sectors()`. - /// If the sector value is higher than the total, it will throw an error. + /// + /// # Errors + /// + /// Returns [`CdReaderError::Io`] containing + /// [`std::io::ErrorKind::InvalidInput`] if `sector` exceeds the track + /// length. pub fn seek_to_sector(&mut self, sector: u32) -> Result<(), CdReaderError> { if sector > self.total_sectors { return Err(CdReaderError::Io(std::io::Error::new( @@ -144,9 +154,15 @@ impl<'a> TrackStream<'a> { self.total_sectors as f32 / Self::SECTORS_PER_SECOND } - /// Seek to an absolute track-relative time position in seconds. + /// Seek to a time position relative to the start of the track in seconds. /// /// Input is converted to sector offset and clamped to track bounds. + /// + /// # Errors + /// + /// Returns [`CdReaderError::Io`] containing + /// [`std::io::ErrorKind::InvalidInput`] if `seconds` is negative or not + /// finite. pub fn seek_to_seconds(&mut self, seconds: f32) -> Result<(), CdReaderError> { if !seconds.is_finite() || seconds < 0.0 { return Err(CdReaderError::Io(std::io::Error::new( @@ -162,6 +178,10 @@ impl<'a> TrackStream<'a> { impl CdReader { /// Open a streaming reader for an audio track using the default options. + /// + /// # Errors + /// + /// Returns the same errors as [`CdReader::open_track_stream_with_options`]. pub fn open_track_stream<'a>( &'a self, toc: &Toc, @@ -174,6 +194,13 @@ impl CdReader { /// /// Use [`TrackStream::next_chunk`] to pull sector-aligned chunks in the /// format selected in [`TrackStreamOptions`]. + /// + /// # 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. pub fn open_track_stream_with_options<'a>( &'a self, toc: &Toc,