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.4.0"
version = "0.4.1"
edition = "2024"
description = "CD-DA (audio CD) reading library"
repository = "https://github.com/Bloomca/rust-cd-da-reader"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ The two are fully interchangeable: `LBA + 150 = total frames from disc start`, f

## Reading tracks

Finally, after we got ToC, we can read tracks. The boundaries for the track are the starting LBA and the starting LBA for the next track (or leadout LBA value for the last track). This library abstracts these things and simply reads provided track numbers. To read a track, all you need to do is call:
Finally, after we got ToC, we can read tracks. The usual boundaries for the track are the starting LBA and the starting LBA for the next track (or leadout LBA value for the last track). For CD-Extra discs where the last audio track is followed only by data tracks, the library subtracts the standard 11,400-sector audio/data session gap from the first data track start. This library abstracts these things and simply reads provided track numbers. To read a track, all you need to do is call:

```rust
use cd_da_reader::{CdReader};
Expand Down
20 changes: 17 additions & 3 deletions examples/read_toc.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/// Opens the default CD drive and prints the Table of Contents.
use cd_da_reader::CdReader;

const CD_EXTRA_TRAILING_DATA_GAP_SECTORS: u32 = 11_400;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let reader = CdReader::open_default()?;
let toc = reader.read_toc()?;
Expand All @@ -18,7 +20,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
for track in &toc.tracks {
let kind = if track.is_audio { "audio" } else { "data " };
let (m, s, f) = track.start_msf;
let sectors = next_track_lba(&toc, track.number) - track.start_lba;
let sectors = track_end_lba(&toc, track.number) - track.start_lba;
let duration_secs = sectors as f64 / 75.0;
let mins = (duration_secs / 60.0) as u32;
let secs = (duration_secs % 60.0) as u32;
Expand All @@ -32,13 +34,25 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}

/// Returns the start LBA of the next track, or the lead-out LBA for the last track.
fn next_track_lba(toc: &cd_da_reader::Toc, track_no: u8) -> u32 {
/// Returns the exclusive end LBA for a track.
fn track_end_lba(toc: &cd_da_reader::Toc, track_no: u8) -> u32 {
let idx = toc
.tracks
.iter()
.position(|t| t.number == track_no)
.unwrap();

// in case all next tracks are data (but they do exist),
// we need to subtract 11,400 sectors
if toc.tracks[idx].is_audio
&& idx + 1 < toc.tracks.len()
&& toc.tracks[idx + 1..].iter().all(|track| !track.is_audio)
{
return toc.tracks[idx + 1]
.start_lba
.saturating_sub(CD_EXTRA_TRAILING_DATA_GAP_SECTORS);
}

if idx + 1 < toc.tracks.len() {
toc.tracks[idx + 1].start_lba
} else {
Expand Down
6 changes: 4 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@
//! ## Reading tracks
//!
//! Pass the [`Toc`] and a track number to [`CdReader::read_track`]. The
//! library calculates the sector boundaries automatically:
//! 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.
//!
//! ```no_run
//! use cd_da_reader::CdReader;
Expand Down Expand Up @@ -189,7 +191,7 @@ pub struct Toc {
pub last_track: u8,
/// List of tracks with LBA and MSF offsets
pub tracks: Vec<Track>,
/// Used to calculate number of sectors for the last track. You'll also need this
/// Lead-out LBA reported by the drive for the disc TOC. You'll also need this
/// in order to calculate MusicBrainz ID.
pub leadout_lba: u32,
}
Expand Down
96 changes: 85 additions & 11 deletions src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use crate::Toc;

const CD_EXTRA_TRAILING_DATA_GAP_SECTORS: u32 = 11_400;

pub fn get_track_bounds(toc: &Toc, track_no: u8) -> std::io::Result<(u32, u32)> {
let idx = toc
.tracks
Expand All @@ -8,26 +10,42 @@ pub fn get_track_bounds(toc: &Toc, track_no: u8) -> std::io::Result<(u32, u32)>
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "track not in TOC"))?;

let start_lba = toc.tracks[idx].start_lba;

// Determine end LBA (next track start, or lead-out for the last track)
let end_lba: u32 = if (idx + 1) < toc.tracks.len() {
toc.tracks[idx + 1].start_lba
} else {
toc.leadout_lba
};
let end_lba = get_track_end_lba(toc, idx)?;

if end_lba <= start_lba {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"bad TOC bounds",
));
return Err(bad_toc_bounds());
}

let sectors = end_lba - start_lba;

Ok((start_lba, sectors))
}

fn get_track_end_lba(toc: &Toc, idx: usize) -> std::io::Result<u32> {
if is_cd_extra_audio_session_boundary(toc, idx) {
return toc.tracks[idx + 1]
.start_lba
.checked_sub(CD_EXTRA_TRAILING_DATA_GAP_SECTORS)
.ok_or_else(bad_toc_bounds);
}

if (idx + 1) < toc.tracks.len() {
Ok(toc.tracks[idx + 1].start_lba)
} else {
Ok(toc.leadout_lba)
}
}

fn is_cd_extra_audio_session_boundary(toc: &Toc, idx: usize) -> bool {
toc.tracks[idx].is_audio
&& (idx + 1) < toc.tracks.len()
&& toc.tracks[idx + 1..].iter().all(|track| !track.is_audio)
}

fn bad_toc_bounds() -> std::io::Error {
std::io::Error::new(std::io::ErrorKind::InvalidData, "bad TOC bounds")
}

pub fn create_wav_header(pcm_data_size: u32) -> Vec<u8> {
let mut header = Vec::with_capacity(44);

Expand Down Expand Up @@ -134,6 +152,15 @@ mod test {
}
}

fn track(number: u8, start_lba: u32, is_audio: bool) -> Track {
Track {
number,
start_lba,
start_msf: (0, 0, 0),
is_audio,
}
}

#[test]
fn finds_non_last_track_bounds_correctly() {
let toc = get_toc();
Expand All @@ -157,6 +184,53 @@ mod test {
assert_eq!(sectors, 204855 - 179485);
}

#[test]
fn subtracts_cd_extra_gap_for_last_audio_track_before_trailing_data_tracks() {
let toc = Toc {
first_track: 1,
last_track: 4,
tracks: vec![
track(1, 0, true),
track(2, 10_000, true),
track(3, 40_000, false),
track(4, 80_000, false),
],
leadout_lba: 120_000,
};

let result = get_track_bounds(&toc, 2);
assert!(result.is_ok());
let (start_lba, sectors) = result.unwrap();

assert_eq!(start_lba, 10_000);
assert_eq!(
sectors,
(40_000 - CD_EXTRA_TRAILING_DATA_GAP_SECTORS) - 10_000
);
}

#[test]
fn does_not_subtract_cd_extra_gap_when_audio_track_follows_later() {
let toc = Toc {
first_track: 1,
last_track: 4,
tracks: vec![
track(1, 0, true),
track(2, 10_000, true),
track(3, 40_000, false),
track(4, 80_000, true),
],
leadout_lba: 120_000,
};

let result = get_track_bounds(&toc, 2);
assert!(result.is_ok());
let (start_lba, sectors) = result.unwrap();

assert_eq!(start_lba, 10_000);
assert_eq!(sectors, 40_000 - 10_000);
}

#[test]
fn returns_error_for_invalid_track() {
let toc = get_toc();
Expand Down
Loading