Skip to main content

Crate cd_da_reader

Crate cd_da_reader 

Source
Expand description

§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 it can also be used to build a live audio CD player. The primary API reads physical discs; to read from a file, image, or another custom source, implement AudioSectorReader and provide a Toc.

Physical-disc access uses platform CD-drive APIs on macOS and direct SCSI commands on Windows and Linux. The library abstracts both access to the drive and reading the data, so callers do not interact with the hardware directly. It operates entirely in user space.

A typical drive-backed read happens in this order:

  1. Get a CD drive’s handle
  2. Read the ToC (table of contents) of the audio CD
  3. Read track data using ranges from the ToC

§CD access

The easiest way to open a drive is to use CdReader::open_default, which scans all drives and opens the first one that contains an audio CD:

use cd_da_reader::CdReader;

let reader = CdReader::open_default()?;

If you need to pick a specific drive, use CdReader::list_drives followed by calling CdReader::open with the selected drive:

use cd_da_reader::CdReader;

let drives = CdReader::list_drives()?;
let selected = drives
    .iter()
    .find(|drive| drive.has_audio_cd) // we check for audio by checking ToC
    .ok_or("no drive with an audio CD found")?;

let reader = CdReader::open(selected)?;

If you already know the platform-specific device path, use CdReader::open_path instead.

§Reading ToC

Each audio CD carries a Table of Contents with the block address of every track. You need to read it first before issuing any track read commands:

use cd_da_reader::CdReader;

let reader = CdReader::open_default()?;
let toc = reader.read_toc()?;

The returned Toc contains a Vec<Track>. Each Track reports its disc track number in Track::number and whether it contains audio in Track::is_audio. Track numbers are not zero-based indices into Toc::tracks and are not guaranteed to begin at 1 (but they usually do).

Each track also has two equivalent address fields:

  • start_lba – Logical Block Address, which is a sector index. LBA 0 is the first readable sector after the 2-second lead-in pre-gap. This is the format used internally for read commands.
  • start_msf — Minutes/Seconds/Frames, a time-based address inherited from the physical disc layout. A “frame” is one sector; the spec defines 75 frames per second. MSF includes a fixed 2-second (150-frame) lead-in offset, so (0, 2, 0) corresponds to LBA 0. You can convert between them easily: LBA + 150 = total frames, then divide by 75 and 60 for M/S/F.

§Reading tracks

Pass the Toc and the track’s Track::number to CdReader::read_track. The library calculates the sector boundaries automatically. On CD-Extra discs where the last audio track is followed only by data tracks, the trailing audio/data session gap is excluded from the audio read – this is usually what you want, and you can read custom range by using CdReader::read_sector_range.

use cd_da_reader::CdReader;

let reader = CdReader::open_default()?;
let toc = reader.read_toc()?;

// Track numbers come from the disc; do not assume the first audio track is #1.
let track = toc
    .tracks
    .iter()
    .find(|track| track.is_audio)
    .ok_or("no audio tracks found")?;
let data = reader.read_track(&toc, track.number)?;

CdReader::read_track is a blocking call that buffers the complete track, so it can take some time and use hundreds of megabytes of memory. The streaming API instead returns sector-aligned chunks as they are read, which keeps memory usage low and supports progress reporting or playback before the complete track is available.

Streaming is still synchronous: each TrackStream::next_chunk call waits for the drive to return the next chunk. This is often suitable for a CLI, where the read loop can run on the main thread and report progress. A GUI should run the loop on a worker thread so drive reads do not block its event loop. Open a stream with CdReader::open_track_stream:

use cd_da_reader::CdReader;

let reader = CdReader::open_default()?;
let toc = reader.read_toc()?;

// Select by track metadata rather than assuming track #1 contains audio.
let track = toc
    .tracks
    .iter()
    .find(|track| track.is_audio)
    .ok_or("no audio tracks found")?;
let mut stream = reader.open_track_stream(&toc, track.number)?;
while let Some(chunk) = stream.next_chunk()? {
    // process chunk — raw PCM, 2 352 bytes per sector
}

§Audio track format

Audio track data is raw PCM, the same uncompressed sample representation used by PCM WAV files. Audio CDs use signed 16-bit little-endian stereo PCM sampled at 44,100 Hz:

44,100 sample frames * 2 channels * 2 bytes = 176,400 bytes/second

Each audio sector holds exactly 2,352 bytes (176,400 ÷ 75 = 2,352), which gives 75 sectors per second. A typical 3-minute track is about 31.8 MB (30.3 MiB). A 74-minute disc contains about 783 MB (747 MiB) of raw PCM; common 80-minute media contains about 847 MB (808 MiB).

Converting raw PCM to a playable WAV file only requires prepending a 44-byte RIFF header — create_wav does exactly that:

use cd_da_reader::{CdReader, create_wav};

let reader = CdReader::open_default()?;
let toc = reader.read_toc()?;
let track = toc
    .tracks
    .iter()
    .find(|track| track.is_audio)
    .ok_or("no audio tracks found")?;
let data = reader.read_track(&toc, track.number)?;
let wav = create_wav(data);
let output = format!("track{:02}.wav", track.number);
std::fs::write(output, wav)?;

§Read options

CdReader::read_track and CdReader::open_track_stream use the ReadOptions defaults: CD-DA audio sectors, the default retry policy, and no read-speed change. These settings are sufficient for most audio reads.

For more control, start with ReadOptions::default() and pass the configured options to CdReader::read_track_with_options or CdReader::open_track_stream_with_options. The configurable options are:

  • Sector format: SectorReadFormat controls the type and layout of sectors returned by the drive. SectorReadFormat::Audio is the default. For a data track, CdReader::detect_track_format can select an appropriate default format to pass to ReadOptions::with_format.
  • Retry policy: RetryConfig controls the number of attempts, retry delays, and adaptive reduction of the number of sectors requested after a failed read. Its defaults are suitable for most drives.
  • Read speed: ReadSpeed requests an automatic or custom drive speed. The default, ReadSpeed::Unchanged, issues no speed-change request. Requested speeds are not guaranteed, and this crate does not restore the previous drive setting afterward. Speed behavior depends on the OS and drive firmware.
use cd_da_reader::{CdReader, ReadOptions, ReadSpeed, RetryConfig, SectorReadFormat};

let reader = CdReader::open_default()?;
let toc = reader.read_toc()?;
let track = toc
    .tracks
    .iter()
    .find(|track| track.is_audio)
    .ok_or("no audio tracks found")?;
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, track.number, &options)?;

§Metadata

Audio CDs carry almost no semantic metadata. CD-TEXT exists but is 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.

Structs§

AudioTrackStream
A pull-based, sector-aligned stream of raw CD-DA PCM from an AudioSectorReader.
CdReader
Helper struct to interact with the audio CD. Internally it holds a platform-specific handle to the open CD drive to read from it and it is correctly closed when CDReader is dropped.
DriveInfo
Information about an optical drive discovered by CdReader::list_drives.
ReadOptions
Sector format, retry policy, and read speed options for track, streaming, and sector-range reads.
RetryConfig
Retry policy for failed drive reads.
ScsiError
Structured SCSI failure context captured at the call site.
Toc
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.
Track
Representation of the track from ToC, purely in terms of data location on the CD.
TrackStream
Track-scoped streaming reader for audio or data sectors.

Enums§

CdReaderError
Top-level error type returned by cd-da-reader.
ReadSpeed
A platform-independent read-speed request for an optical drive.
ScsiOp
SCSI command groups issued by this library.
SectorReadFormat
Selects the type and layout of sectors returned when reading from an optical drive.
TrackBounds
Policy for deriving a track’s half-open sector range from a Toc.

Traits§

AudioSectorReader
A source of raw CD-DA audio sectors.

Functions§

create_wav
Prepends a standard 44-byte RIFF/WAVE header to raw CD-DA PCM.
lba_to_msf
Convert a Logical Block Address to its Minutes/Seconds/Frames address.
open_track_stream
Open a streaming reader for a track assuming the TOC includes the inter-session gap (TrackBounds::SessionGap). See AudioTrackStream.
open_track_stream_at
Open a streaming reader over an explicit absolute sector range (start_lba .. start_lba + sectors), bypassing TOC bounds resolution.
open_track_stream_with_bounds
Open a streaming reader for a track with an explicit TrackBounds geometry. Use TrackBounds::Gapless for a contiguous, gap-stripped layout.
read_track
Reads one complete audio track from an AudioSectorReader into memory.
read_track_with_bounds
Reads one complete audio track into memory using an explicit TrackBounds policy.