Files
cdrustanoia/crates/core/src/device/mod.rs
T
2026-11-04 08:32:00 +01:00

474 lines
13 KiB
Rust

//! 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<String>) -> 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<String>) -> 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<usize, ReadError>,
/// 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<usize, ReadError>;
/// 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<u8>` 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<usize, ReadError> {
// 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::<u8>(),
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<usize, ReadError> {
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<i16> {
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<u8> {
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);
}
}