diff --git a/Cargo.lock b/Cargo.lock index 78c2e27..0cd4280 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,7 +13,7 @@ dependencies = [ [[package]] name = "cd-da-reader" -version = "0.4.0" +version = "0.4.1" dependencies = [ "cc", "libc", diff --git a/Cargo.toml b/Cargo.toml index 0d37847..ea04f27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/README.md b/README.md index 50cf031..c21cbf1 100644 --- a/README.md +++ b/README.md @@ -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}; diff --git a/examples/read_toc.rs b/examples/read_toc.rs index cdca7cb..7be63ad 100644 --- a/examples/read_toc.rs +++ b/examples/read_toc.rs @@ -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> { let reader = CdReader::open_default()?; let toc = reader.read_toc()?; @@ -18,7 +20,7 @@ fn main() -> Result<(), Box> { 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; @@ -32,13 +34,25 @@ fn main() -> Result<(), Box> { 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 { diff --git a/src/lib.rs b/src/lib.rs index df63101..3f9ddd3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; @@ -189,7 +191,7 @@ pub struct Toc { pub last_track: u8, /// List of tracks with LBA and MSF offsets pub tracks: Vec, - /// 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, } diff --git a/src/utils.rs b/src/utils.rs index a4b85fe..874016f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -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 @@ -8,19 +10,10 @@ 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; @@ -28,6 +21,31 @@ pub fn get_track_bounds(toc: &Toc, track_no: u8) -> std::io::Result<(u32, u32)> Ok((start_lba, sectors)) } +fn get_track_end_lba(toc: &Toc, idx: usize) -> std::io::Result { + 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 { let mut header = Vec::with_capacity(44); @@ -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(); @@ -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();