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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
```

Expand Down Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions examples/custom_retry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn std::error::Error>> {
let output_dir = common::fresh_output_dir("custom_retry")?;
Expand Down Expand Up @@ -36,7 +36,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
);
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());
Expand Down
4 changes: 2 additions & 2 deletions examples/play_audio_track.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -66,7 +66,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
);

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)
Expand Down
4 changes: 2 additions & 2 deletions examples/read_all_tracks.rs
Original file line number Diff line number Diff line change
@@ -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<dyn std::error::Error>> {
let output_dir = common::fresh_output_dir("read_all_tracks")?;
Expand All @@ -17,7 +17,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
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());
Expand Down
4 changes: 2 additions & 2 deletions examples/read_first_track.rs
Original file line number Diff line number Diff line change
@@ -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<dyn std::error::Error>> {
let output_dir = common::fresh_output_dir("read_first_track")?;
Expand All @@ -17,7 +17,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
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());
Expand Down
4 changes: 2 additions & 2 deletions examples/stream_last_track.rs
Original file line number Diff line number Diff line change
@@ -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<dyn std::error::Error>> {
let output_dir = common::fresh_output_dir("stream_last_track")?;
Expand All @@ -23,7 +23,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
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());
Expand Down
4 changes: 2 additions & 2 deletions examples/stream_with_progress.rs
Original file line number Diff line number Diff line change
@@ -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<dyn std::error::Error>> {
let output_dir = common::fresh_output_dir("stream_with_progress")?;
Expand Down Expand Up @@ -34,7 +34,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
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());
Expand Down
47 changes: 42 additions & 5 deletions src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<u8>, Self::Error>;
}

Expand Down Expand Up @@ -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<R: AudioSectorReader>(
src: &R,
toc: &Toc,
Expand All @@ -140,6 +147,11 @@ pub fn read_track<R: AudioSectorReader>(

/// 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<R: AudioSectorReader>(
src: &R,
toc: &Toc,
Expand Down Expand Up @@ -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<Option<Vec<u8>>, CdReaderError> {
if self.remaining_sectors == 0 {
return Ok(None);
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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,
Expand All @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions src/data_reader/detect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SectorReadFormat, CdReaderError> {
if track.is_audio {
return Ok(SectorReadFormat::Audio);
Expand Down
8 changes: 5 additions & 3 deletions src/data_reader/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
21 changes: 19 additions & 2 deletions src/discovery.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<Vec<DriveInfo>, CdReaderError> {
let mut paths = crate::platform::list_drive_paths()?;
paths.sort();
Expand All @@ -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<Self, CdReaderError> {
let drives = Self::list_drives()?;
let chosen = pick_default_drive(&drives).ok_or(CdReaderError::NoUsableDrive)?;
Expand Down
Loading
Loading