474 lines
13 KiB
Rust
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);
|
|
}
|
|
}
|