//! Common CD-ROM device abstractions and TOC helpers. //! //! The layout and behaviour intentionally mirror cdparanoia 10.2's //! `cdda_interface` API. Device specific backends live in sibling modules. pub mod discover; pub mod image; pub mod linux; use std::fmt; pub const CD_FRAMESIZE_RAW: usize = 2352; pub const CD_FRAMESIZE: usize = 2048; pub const CD_FRAMEWORDS: i64 = 1176; pub const CD_FRAMESAMPLES: usize = CD_FRAMESIZE_RAW / 4; pub const MAXTRK: usize = 100; /// Error codes used by the original cdda_interface library. pub mod error { pub const DEVICE_NOT_OPEN: i64 = -400; pub const INVALID_TRACK: i64 = -401; pub const TRACK_NOT_AUDIO: i64 = -402; pub const NO_AUDIO_TRACKS: i64 = -403; pub const NO_MEDIUM: i64 = -404; pub const OPTION_NOT_SUPPORTED: i64 = -405; pub const INTERFACE_NOT_SUPPORTED: i64 = -100; pub const PERMISSION_IOCTL: i64 = -102; pub const PERMISSION_DATA: i64 = -103; pub const KERNEL_MEMORY: i64 = -300; pub const CANT_SET_CDDA: i64 = -1; pub const CANT_READ_LEADOUT: i64 = -2; pub const ILLEGAL_TRACKS: i64 = -3; pub const CANT_READ_TOC_HEADER: i64 = -4; pub const CANT_READ_TOC_ENTRY: i64 = -5; pub const CANT_READ_ANY_DATA: i64 = -6; pub const UNKNOWN_READ: i64 = -7; pub const CANT_IDENTIFY: i64 = -8; pub const ILLEGAL_TOC: i64 = -9; pub const UNREADABLE_SECTOR: i64 = -10; } #[derive(Clone, Copy, Default, Debug, PartialEq, Eq)] pub struct TocEntry { pub flags: u8, pub track: u8, pub start_sector: i64, } impl TocEntry { pub fn audio(&self) -> bool { self.flags & 0x04 == 0 } pub fn copyp(&self) -> bool { self.flags & 0x02 != 0 } pub fn preemp(&self) -> bool { self.flags & 0x01 != 0 } pub fn channels(&self) -> i32 { if self.flags & 0x08 != 0 { 4 } else { 2 } } } #[derive(Clone, Debug)] pub struct DiscToc { pub entries: [TocEntry; MAXTRK], pub tracks: i32, pub cd_extra: i32, } impl Default for DiscToc { fn default() -> Self { Self { entries: [TocEntry::default(); MAXTRK], tracks: 0, cd_extra: 0, } } } impl DiscToc { pub fn track_firstsector(&self, track: i32) -> i64 { if self.tracks <= 0 { return error::DEVICE_NOT_OPEN; } if track == 0 { if self.entries[0].start_sector == 0 { return error::INVALID_TRACK; } return 0; } if track < 0 || track as usize > self.tracks as usize { return error::INVALID_TRACK; } self.entries[track as usize - 1].start_sector } pub fn track_lastsector(&self, track: i32) -> i64 { if self.tracks <= 0 { return error::DEVICE_NOT_OPEN; } if track == 0 { if self.entries[0].start_sector == 0 { return error::INVALID_TRACK; } return self.entries[0].start_sector - 1; } if track < 1 || track as usize > self.tracks as usize { return error::INVALID_TRACK; } self.entries[track as usize].start_sector - 1 } pub fn disc_firstsector(&self) -> i64 { if self.tracks <= 0 { return error::DEVICE_NOT_OPEN; } for i in 0..self.tracks as usize { if self.entries[i].audio() { if i == 0 { return 0; } return self.entries[i].start_sector; } } error::NO_AUDIO_TRACKS } pub fn disc_lastsector(&self) -> i64 { if self.tracks <= 0 { return error::DEVICE_NOT_OPEN; } for i in (0..self.tracks as usize).rev() { if self.entries[i].audio() { return self.entries[i + 1].start_sector - 1; } } error::NO_AUDIO_TRACKS } pub fn sector_gettrack(&self, sector: i64) -> i32 { if self.tracks <= 0 { return error::DEVICE_NOT_OPEN as i32; } if sector < self.entries[0].start_sector { return 0; } for i in 0..self.tracks as usize { if self.entries[i].start_sector <= sector && self.entries[i + 1].start_sector > sector { return (i + 1) as i32; } } error::INVALID_TRACK as i32 } pub fn track_bitmap(&self, track: i32, bit: u8, set: i32, clear: i32) -> i32 { let track = if track == 0 { 1 } else { track }; if track < 1 || track as usize > self.tracks as usize { return error::INVALID_TRACK as i32; } if self.entries[track as usize - 1].flags & bit != 0 { set } else { clear } } pub fn track_channels(&self, track: i32) -> i32 { self.track_bitmap(track, 8, 4, 2) } pub fn track_audiop(&self, track: i32) -> i32 { self.track_bitmap(track, 4, 0, 1) } pub fn track_copyp(&self, track: i32) -> i32 { self.track_bitmap(track, 2, 1, 0) } pub fn track_preemp(&self, track: i32) -> i32 { self.track_bitmap(track, 1, 1, 0) } pub fn is_audio_index(&self, idx: usize, tracks: usize) -> bool { if idx >= tracks { return false; } self.entries[idx].audio() } /// Reproduces `FixupTOC`'s non-ioctl repairs. pub fn fixup(&mut self, tracks: usize) { for j in 0..tracks { if self.entries[j].start_sector < 0 { self.entries[j].start_sector = 0; } if j + 1 < tracks && self.entries[j].start_sector > self.entries[j + 1].start_sector { self.entries[j].start_sector = 0; } } if tracks == 0 { return; } let mut last = self.entries[0].start_sector; for j in 1..tracks { if self.entries[j].start_sector < last { self.entries[j].start_sector = last; } last = self.entries[j].start_sector; } } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum InterfaceKind { SgIo, Cooked, Image, } #[derive(Debug, Clone)] pub struct ReadError { pub errno: i32, pub message: String, } impl ReadError { pub fn new(errno: i32, message: impl Into) -> Self { Self { errno, message: message.into(), } } pub fn no_medium() -> Self { Self::new(libc::ENOMEDIUM, "No medium present") } pub fn closed() -> Self { Self::new(libc::EBADF, "Device not open") } pub fn io(message: impl Into) -> Self { Self::new(libc::EIO, message) } } impl fmt::Display for ReadError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{} (errno {})", self.message, self.errno) } } #[derive(Debug, Clone)] pub struct TimedRead { pub result: Result, /// Whole milliseconds, matching the C ABI. pub milliseconds: i32, /// Higher precision duration for internal timing analysis. Backends /// that only know millisecond figures fill this with `ms * 1000`. pub microseconds: i64, } /// Backend interface used by both the command line program and the C ABI. /// /// Positions are absolute CD sectors (not sample positions). Audio data is /// returned in host byte order, exactly like `cdda_read`. pub trait Drive { fn toc(&self) -> &DiscToc; fn toc_mut(&mut self) -> &mut DiscToc; fn opened(&self) -> bool; /// Perform the media-dependent part of opening a drive (TOC read, /// CDDA capability probe, endianness detection). Identify-only /// operations leave this undone, just like cdda_identify/cdda_open. fn open_disc(&mut self) -> Result<(), String> { Ok(()) } fn read_audio( &mut self, buffer: Option<&mut [u8]>, begin: i64, sectors: i64, ) -> Result; /// Read audio directly into a host-endian `i16` buffer. /// /// This is a byte-view over the same memory; it lets the paranoia engine /// read straight into the `CBlock` vector without an intermediate /// `Vec` plus `bytes_to_samples()` copy. Implementations of /// `read_audio()` already promise host endian PCM data. fn read_audio_samples( &mut self, buffer: &mut [i16], begin: i64, sectors: i64, ) -> Result { // SAFETY: `i16` has no padding and the byte view covers exactly the // initialized elements of `buffer`. The view lives only for this // call, and all bytes are written by the backend before it returns. let bytes = unsafe { std::slice::from_raw_parts_mut( buffer.as_mut_ptr().cast::(), std::mem::size_of_val(buffer), ) }; self.read_audio(Some(bytes), begin, sectors) } /// Low-level read without host-endian conversion, matching the original /// `cdrom_drive::read_audio` function pointer. fn read_audio_raw( &mut self, buffer: Option<&mut [u8]>, begin: i64, sectors: i64, ) -> Result { self.read_audio(buffer, begin, sectors) } fn read_timed(&mut self, buffer: Option<&mut [u8]>, begin: i64, sectors: i64) -> TimedRead; fn speed_set(&mut self, speed: i32) -> i32; fn close(&mut self); fn drive_model(&self) -> &str; fn bigendianp(&self) -> i32; fn set_bigendianp(&mut self, value: i32); fn nsectors(&self) -> i64; fn set_nsectors(&mut self, value: i64); /// Enable or disable the low-level read retry loop. Cache analysis /// disables retries so that timing reflects the first access. fn set_error_retry(&mut self, _enabled: bool) {} fn interface(&self) -> InterfaceKind; /// Optional drive attributes exposed through the C ABI. fn is_atapi(&self) -> bool { false } fn is_mmc(&self) -> bool { false } fn density(&self) -> u8 { 0 } fn fua(&self) -> bool { false } fn lun(&self) -> i32 { 0 } fn adjust_ssize(&self) -> i64 { 1 } fn cdda_version(&self) -> &'static str { crate::VERSIONNUM } // Convenience TOC wrappers. fn tracks(&self) -> i32 { self.toc().tracks } fn track_firstsector(&self, track: i32) -> i64 { self.toc().track_firstsector(track) } fn track_lastsector(&self, track: i32) -> i64 { self.toc().track_lastsector(track) } fn disc_firstsector(&self) -> i64 { self.toc().disc_firstsector() } fn disc_lastsector(&self) -> i64 { self.toc().disc_lastsector() } fn sector_gettrack(&self, sector: i64) -> i32 { self.toc().sector_gettrack(sector) } fn track_audiop(&self, track: i32) -> bool { self.toc().track_audiop(track) == 1 } fn track_copyp(&self, track: i32) -> bool { self.toc().track_copyp(track) == 1 } fn track_preemp(&self, track: i32) -> bool { self.toc().track_preemp(track) == 1 } fn track_channels(&self, track: i32) -> i32 { self.toc().track_channels(track) } } pub fn swap16(x: i16) -> i16 { (x as u16).swap_bytes() as i16 } pub fn host_big_endian() -> bool { cfg!(target_endian = "big") } /// Swap a buffer of host endian samples to the requested byte order. pub fn swap_samples(buffer: &mut [i16], target_big_endian: bool) { if host_big_endian() != target_big_endian { for s in buffer { *s = swap16(*s); } } } /// Convert raw CDDA bytes into host endian i16 samples. pub fn bytes_to_samples(bytes: &[u8]) -> Vec { let mut out = Vec::with_capacity(bytes.len() / 2); for chunk in bytes.chunks_exact(2) { out.push(i16::from_ne_bytes([chunk[0], chunk[1]])); } out } pub fn samples_to_bytes(samples: &[i16]) -> Vec { let mut out = Vec::with_capacity(samples.len() * 2); for s in samples { out.extend_from_slice(&s.to_ne_bytes()); } out } /// Swap bytes in a raw buffer between little and big endian 16-bit words. pub fn swap_raw16(buffer: &mut [u8]) { for chunk in buffer.chunks_exact_mut(2) { chunk.swap(0, 1); } } #[cfg(test)] mod tests { use super::*; fn toc() -> DiscToc { let mut t = DiscToc::default(); t.tracks = 2; t.entries[0] = TocEntry { flags: 0, track: 1, start_sector: 0, }; t.entries[1] = TocEntry { flags: 0, track: 2, start_sector: 100, }; t.entries[2] = TocEntry { flags: 4, track: 0xAA, start_sector: 200, }; t } #[test] fn toc_queries() { let t = toc(); assert_eq!(t.track_firstsector(1), 0); assert_eq!(t.track_lastsector(1), 99); assert_eq!(t.track_firstsector(2), 100); assert_eq!(t.track_lastsector(2), 199); assert_eq!(t.disc_firstsector(), 0); assert_eq!(t.disc_lastsector(), 199); assert_eq!(t.sector_gettrack(0), 1); assert_eq!(t.sector_gettrack(99), 1); assert_eq!(t.sector_gettrack(100), 2); assert_eq!(t.track_audiop(1), 1); assert_eq!(t.track_audiop(2), 1); } #[test] fn byte_conversion() { let s = [1i16, -2, 300]; let b = samples_to_bytes(&s); assert_eq!(bytes_to_samples(&b), s); } }