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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "cd-da-reader"
version = "0.2.1"
version = "0.3.0"
edition = "2024"
description = "CD-DA (audio CD) reading library"
repository = "https://github.com/Bloomca/rust-cd-da-reader"
Expand Down
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,21 @@ This is a library to read audio CDs. This is intended to be a fairly low-level l

It works on Windows, macOS and Linux, although each platform has slightly different behaviour regarding the handle exclusivity. Specifically, on macOS, it will not work if you use the audio CD somewhere -- the library will attempt to unmount it, claim exclusive access and only after read the data from it. After it is done, it will remount the CD back so other apps can use, which will cause the OS to treat as if you just inserted the CD.

There is an example to read TOC and save a track ([ref](./examples/read_track.rs)); the example is cross-platform, but you'll likely need to adjust the drive letter. On Windows, simply look in your File Explorer; on macOS, execute `diskutil list` and find the drive with `Audio CD` name; on Linux, call `cat /proc/sys/dev/cdrom/info`.
For example, if you want to read TOC and save the first track as a WAV file, you can do the following:

```rust
let reader = CDReader::open_default()?;
let toc = reader.read_toc()?;

let first_audio_track = toc
.tracks
.iter()
.find(|track| track.is_audio)
.ok_or_else(|| std::io::Error::other("no audio tracks in TOC"))?;

let data = reader.read_track(&toc, last_audio_track.number)?;
let wav_track = CdReader::create_wav(data);
std::fs::write("myfile.wav", wav_track)?;
```

You can open a specific drive, but often the machine will have only 1 valid audio CD, so the default drive method should work in most scenarios. Reading track data is a pretty slow operation due to size and CD reading speeds, so there is a streaming API available if you want to interact with chunks of data directly.
23 changes: 19 additions & 4 deletions examples/read_track.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use cd_da_reader::CdReader;
use cd_da_reader::{CdReader, RetryConfig, TrackStreamConfig};

fn main() -> Result<(), Box<dyn std::error::Error>> {
let drive_path = default_drive_path();
Expand Down Expand Up @@ -33,9 +33,24 @@ fn read_cd(path: &str) -> Result<(), Box<dyn std::error::Error>> {
.ok_or_else(|| std::io::Error::other("no audio tracks in TOC"))?;

println!("Reading track {}", last_audio_track.number);
let data = reader.read_track(&toc, last_audio_track.number)?;
let wav_track = CdReader::create_wav(data);
std::fs::write("myfile.wav", wav_track)?;
let stream_cfg = TrackStreamConfig {
sectors_per_chunk: 27,
retry: RetryConfig {
max_attempts: 5,
initial_backoff_ms: 30,
max_backoff_ms: 500,
reduce_chunk_on_retry: true,
min_sectors_per_read: 1,
},
};
let mut stream = reader.open_track_stream(&toc, last_audio_track.number, stream_cfg)?;

let mut pcm = Vec::new();
while let Some(chunk) = stream.next_chunk()? {
pcm.extend_from_slice(&chunk);
}
let wav = CdReader::create_wav(pcm);
std::fs::write("myfile.wav", wav)?;

Ok(())
}
35 changes: 34 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,12 @@ mod windows;
mod discovery;
mod errors;
mod retry;
mod stream;
mod utils;
pub use discovery::DriveInfo;
pub use errors::{CdReaderError, ScsiError, ScsiOp};
pub use retry::RetryConfig;
pub use stream::{TrackStream, TrackStreamConfig};

mod parse_toc;

Expand Down Expand Up @@ -93,13 +95,17 @@ pub struct Toc {
/// Please note that you should not read multiple CDs at the same time, and preferably do
/// not use it in multiple threads. CD drives are a physical thing and they really want to
/// have exclusive access, because of that currently only sequential access is supported.
///
/// This is especially true on macOS, where releasing exclusive lock on the audio CD will
/// cause it to remount, and the default application (very likely Apple Music) will get
/// the exclusive access and it will be challenging to implement a reliable waiting strategy.
pub struct CdReader {}

impl CdReader {
/// Opens a CD drive at the specified path in order to read data.
///
/// It is crucial to call this function and not to create the Reader
/// by yourself, as each OS needs its own way of handling the drive acess.
/// by yourself, as each OS needs its own way of handling the drive access.
///
/// You don't need to close the drive, it will be handled automatically
/// when the `CdReader` is dropped. On macOS, that will cause the CD drive
Expand Down Expand Up @@ -213,6 +219,33 @@ impl CdReader {
compile_error!("Unsupported platform")
}
}

pub(crate) fn read_sectors_with_retry(
&self,
start_lba: u32,
sectors: u32,
cfg: &RetryConfig,
) -> Result<Vec<u8>, CdReaderError> {
#[cfg(target_os = "windows")]
{
windows::read_sectors_with_retry(start_lba, sectors, cfg)
}

#[cfg(target_os = "macos")]
{
macos::read_sectors_with_retry(start_lba, sectors, cfg)
}

#[cfg(target_os = "linux")]
{
linux::read_sectors_with_retry(start_lba, sectors, cfg)
}

#[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
{
compile_error!("Unsupported platform")
}
}
}

impl Drop for CdReader {
Expand Down
8 changes: 8 additions & 0 deletions src/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,14 @@ pub fn read_track_with_retry(
cfg: &RetryConfig,
) -> std::result::Result<Vec<u8>, CdReaderError> {
let (start_lba, sectors) = get_track_bounds(toc, track_no).map_err(CdReaderError::Io)?;
read_sectors_with_retry(start_lba, sectors, cfg)
}

pub fn read_sectors_with_retry(
start_lba: u32,
sectors: u32,
cfg: &RetryConfig,
) -> std::result::Result<Vec<u8>, CdReaderError> {
read_cd_audio_range(start_lba, sectors, cfg)
}

Expand Down
10 changes: 9 additions & 1 deletion src/macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,19 @@ pub fn read_track_with_retry(
toc: &Toc,
track_no: u8,
cfg: &RetryConfig,
) -> Result<Vec<u8>, CdReaderError> {
let (start_lba, sectors) = get_track_bounds(toc, track_no).map_err(CdReaderError::Io)?;
read_sectors_with_retry(start_lba, sectors, cfg)
}

pub fn read_sectors_with_retry(
start_lba: u32,
sectors: u32,
cfg: &RetryConfig,
) -> Result<Vec<u8>, CdReaderError> {
const SECTOR_BYTES: usize = 2352;
const MAX_SECTORS_PER_XFER: u32 = 27;

let (start_lba, sectors) = get_track_bounds(toc, track_no).map_err(CdReaderError::Io)?;
let mut out = Vec::<u8>::with_capacity((sectors as usize) * SECTOR_BYTES);
let mut remaining = sectors;
let mut lba = start_lba;
Expand Down
Loading