first draft
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
//! Paranoia callback event codes, from the LGPL cdda_paranoia.h API.
|
||||
|
||||
pub const PARANOIA_CB_READ: i32 = 0;
|
||||
pub const PARANOIA_CB_VERIFY: i32 = 1;
|
||||
pub const PARANOIA_CB_FIXUP_EDGE: i32 = 2;
|
||||
pub const PARANOIA_CB_FIXUP_ATOM: i32 = 3;
|
||||
pub const PARANOIA_CB_SCRATCH: i32 = 4;
|
||||
pub const PARANOIA_CB_REPAIR: i32 = 5;
|
||||
pub const PARANOIA_CB_SKIP: i32 = 6;
|
||||
pub const PARANOIA_CB_DRIFT: i32 = 7;
|
||||
pub const PARANOIA_CB_BACKOFF: i32 = 8;
|
||||
pub const PARANOIA_CB_OVERLAP: i32 = 9;
|
||||
pub const PARANOIA_CB_FIXUP_DROPPED: i32 = 10;
|
||||
pub const PARANOIA_CB_FIXUP_DUPED: i32 = 11;
|
||||
pub const PARANOIA_CB_READERR: i32 = 12;
|
||||
pub const PARANOIA_CB_CACHEERR: i32 = 13;
|
||||
@@ -0,0 +1,138 @@
|
||||
//! CD-ROM device discovery, mirroring scan_devices.c's search order.
|
||||
|
||||
use crate::device::image::ImageDrive;
|
||||
use crate::device::linux::LinuxDrive;
|
||||
use crate::device::{Drive, InterfaceKind};
|
||||
use std::fs;
|
||||
use std::os::unix::fs::FileTypeExt;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn image_override() -> Option<ImageDrive> {
|
||||
let path = std::env::var_os("CDRUSTANOIA_TEST_IMAGE")?;
|
||||
ImageDrive::open_path(Path::new(&path)).ok()
|
||||
}
|
||||
|
||||
pub fn identify_sgio(device: &str, verbose: bool) -> Result<Box<dyn Drive>, String> {
|
||||
if verbose {
|
||||
eprintln!("\tTesting {} for SCSI/MMC interface", device);
|
||||
}
|
||||
let drive = LinuxDrive::sg_io(device)?;
|
||||
if verbose {
|
||||
eprintln!("\t\tSG_IO device: {}", device);
|
||||
eprintln!("\nCDROM model sensed sensed: {}", drive.drive_model());
|
||||
}
|
||||
Ok(Box::new(drive))
|
||||
}
|
||||
|
||||
pub fn identify_cooked(device: &str, verbose: bool) -> Result<Box<dyn Drive>, String> {
|
||||
if verbose {
|
||||
eprintln!("\tTesting {} for cooked ioctl() interface", device);
|
||||
}
|
||||
let drive = LinuxDrive::cooked(device)?;
|
||||
if verbose {
|
||||
eprintln!("\t\tCDROM sensed: {}", drive.drive_model());
|
||||
}
|
||||
Ok(Box::new(drive))
|
||||
}
|
||||
|
||||
pub fn identify_device(device: &str, verbose: bool) -> Result<Box<dyn Drive>, String> {
|
||||
if let Some(img) = image_override() {
|
||||
return Ok(Box::new(img));
|
||||
}
|
||||
if verbose {
|
||||
eprintln!("Checking {} for cdrom...", device);
|
||||
}
|
||||
let meta = fs::metadata(device).map_err(|e| format!("Could not stat {}: {}", device, e))?;
|
||||
let ft = meta.file_type();
|
||||
if !ft.is_block_device() && !ft.is_char_device() {
|
||||
return Err(format!("{} is not a block or character device", device));
|
||||
}
|
||||
let sg_err = match identify_sgio(device, verbose) {
|
||||
Ok(d) => return Ok(d),
|
||||
Err(e) => e,
|
||||
};
|
||||
match identify_cooked(device, verbose) {
|
||||
Ok(d) => Ok(d),
|
||||
Err(cooked_err) => Err(format!(
|
||||
"Unable to identify {} (SG_IO: {}; cooked: {})",
|
||||
device, sg_err, cooked_err
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn identify(device: &str, verbose: bool, force_cooked: bool) -> Result<Box<dyn Drive>, String> {
|
||||
if force_cooked {
|
||||
identify_cooked(device, verbose)
|
||||
} else {
|
||||
identify_device(device, verbose)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_a_cdrom(verbose: bool) -> Option<Box<dyn Drive>> {
|
||||
if let Some(img) = image_override() {
|
||||
return Some(Box::new(img));
|
||||
}
|
||||
let mut candidates: Vec<String> = Vec::new();
|
||||
candidates.push("/dev/cdrom".to_string());
|
||||
for prefix in [
|
||||
"/dev/cdroms/cdrom",
|
||||
"/dev/sr",
|
||||
"/dev/scd",
|
||||
"/dev/hd",
|
||||
"/dev/sg",
|
||||
] {
|
||||
for i in 0..4 {
|
||||
candidates.push(format!("{}{}", prefix, i));
|
||||
candidates.push(format!("{}{}", prefix, (b'a' + i) as char));
|
||||
}
|
||||
}
|
||||
for name in [
|
||||
"/dev/cdu31a",
|
||||
"/dev/cdu535",
|
||||
"/dev/sbpcd",
|
||||
"/dev/sonycd",
|
||||
"/dev/mcd",
|
||||
"/dev/sjcd",
|
||||
"/dev/cm206cd",
|
||||
"/dev/gscd",
|
||||
"/dev/optcd",
|
||||
] {
|
||||
candidates.push(name.to_string());
|
||||
}
|
||||
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for dev in candidates {
|
||||
if !seen.insert(dev.clone()) {
|
||||
continue;
|
||||
}
|
||||
if !Path::new(&dev).exists() {
|
||||
continue;
|
||||
}
|
||||
match identify_device(&dev, verbose) {
|
||||
Ok(d) => {
|
||||
if verbose {
|
||||
eprintln!("Using {}", dev);
|
||||
}
|
||||
return Some(d);
|
||||
}
|
||||
Err(_) => {
|
||||
if verbose {
|
||||
eprintln!();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if verbose {
|
||||
let user = std::env::var("USER").unwrap_or_else(|_| "user".to_string());
|
||||
eprintln!("\n\nNo cdrom drives accessible to {} found.", user);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn interface_name(d: &dyn Drive) -> &'static str {
|
||||
match d.interface() {
|
||||
InterfaceKind::SgIo => "SG_IO",
|
||||
InterfaceKind::Cooked => "cooked ioctl",
|
||||
InterfaceKind::Image => "image",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
//! Synthetic image backend used by tests and by the hidden
|
||||
//! `CDRUSTANOIA_TEST_IMAGE` development interface.
|
||||
|
||||
use super::{
|
||||
bytes_to_samples, swap_raw16, DiscToc, Drive, InterfaceKind, ReadError, TimedRead, TocEntry,
|
||||
CD_FRAMESIZE_RAW, CD_FRAMEWORDS, MAXTRK,
|
||||
};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ImageFaults {
|
||||
/// Shift the beginning of each read by up to this many PCM words.
|
||||
pub jitter_words: i64,
|
||||
/// Only every Nth read is jittered (0 = every read when jitter_words > 0).
|
||||
pub jitter_period: u32,
|
||||
/// Leave the first N reads unjittered to establish an absolute anchor.
|
||||
pub jitter_skip: u32,
|
||||
/// Return at most this many sectors per read (0 = no limit).
|
||||
pub max_read_sectors: i64,
|
||||
/// Consistent sample discontinuity at a sector boundary.
|
||||
pub discontinuities: Vec<(i64, i64)>, // (sector, delta words)
|
||||
/// Consistently corrupt this source range.
|
||||
pub scratch: Option<(i64, i64)>, // (word offset, word length)
|
||||
/// Simulated duration of a read in milliseconds.
|
||||
pub read_ms: i32,
|
||||
/// Simulated cache-friendly timing when reading backwards.
|
||||
pub backward_ms: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ImageConfig {
|
||||
pub tracks: Vec<(i64, i64, u8)>, // start sector, end sector (inclusive), flags
|
||||
pub faults: ImageFaults,
|
||||
pub bigendianp: i32,
|
||||
pub nsectors: i64,
|
||||
}
|
||||
|
||||
impl Default for ImageConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
tracks: Vec::new(),
|
||||
faults: ImageFaults {
|
||||
read_ms: 20,
|
||||
backward_ms: 20,
|
||||
..Default::default()
|
||||
},
|
||||
bigendianp: 0,
|
||||
nsectors: 13,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ImageDrive {
|
||||
samples: Vec<i16>,
|
||||
toc: DiscToc,
|
||||
faults: ImageFaults,
|
||||
bigendianp: i32,
|
||||
nsectors: i64,
|
||||
opened: bool,
|
||||
model: String,
|
||||
read_count: u32,
|
||||
last_begin: i64,
|
||||
}
|
||||
|
||||
impl ImageDrive {
|
||||
pub fn from_bytes(data: Vec<u8>) -> Self {
|
||||
let sectors = (data.len() / CD_FRAMESIZE_RAW) as i64;
|
||||
let tracks = if sectors > 0 {
|
||||
vec![(0, sectors - 1, 0u8)]
|
||||
} else {
|
||||
vec![(0, 0, 0u8)]
|
||||
};
|
||||
Self::with_config(
|
||||
data,
|
||||
ImageConfig {
|
||||
tracks,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn with_config(data: Vec<u8>, config: ImageConfig) -> Self {
|
||||
let samples = bytes_to_samples(&data);
|
||||
let mut toc = DiscToc::default();
|
||||
let tracks = config.tracks;
|
||||
let n = tracks.len().min(MAXTRK - 1);
|
||||
for (i, (start, _end, flags)) in tracks.iter().take(n).enumerate() {
|
||||
toc.entries[i] = TocEntry {
|
||||
flags: *flags,
|
||||
track: (i + 1) as u8,
|
||||
start_sector: *start,
|
||||
};
|
||||
}
|
||||
let leadout = if n > 0 {
|
||||
tracks[n - 1].1 + 1
|
||||
} else {
|
||||
(data.len() / CD_FRAMESIZE_RAW) as i64
|
||||
};
|
||||
toc.entries[n] = TocEntry {
|
||||
flags: 0x04,
|
||||
track: 0xAA,
|
||||
start_sector: leadout,
|
||||
};
|
||||
toc.tracks = n as i32;
|
||||
let mut d = Self {
|
||||
samples,
|
||||
toc,
|
||||
faults: config.faults,
|
||||
bigendianp: config.bigendianp,
|
||||
nsectors: config.nsectors,
|
||||
opened: true,
|
||||
model: "File based test interface".to_string(),
|
||||
read_count: 0,
|
||||
last_begin: -1,
|
||||
};
|
||||
d.fix_toc();
|
||||
d
|
||||
}
|
||||
|
||||
pub fn open_path(path: &Path) -> std::io::Result<Self> {
|
||||
let data = fs::read(path)?;
|
||||
let mut config = ImageConfig::default();
|
||||
let mut sectors = (data.len() / CD_FRAMESIZE_RAW) as i64;
|
||||
if sectors < 1 {
|
||||
sectors = 1;
|
||||
}
|
||||
config.tracks = vec![(0, sectors - 1, 0)];
|
||||
// Optional sidecar: one "start end flags" line per track.
|
||||
let sidecar = path.with_extension(format!(
|
||||
"{}.toc",
|
||||
path.extension().and_then(|s| s.to_str()).unwrap_or("raw")
|
||||
));
|
||||
if let Ok(text) = fs::read_to_string(&sidecar) {
|
||||
let mut tracks = Vec::new();
|
||||
for line in text.lines() {
|
||||
let line = line.trim();
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
let mut fields = line.split_whitespace();
|
||||
let start: i64 = match fields.next().and_then(|s| s.parse().ok()) {
|
||||
Some(v) => v,
|
||||
None => continue,
|
||||
};
|
||||
let end: i64 = fields.next().and_then(|s| s.parse().ok()).unwrap_or(start);
|
||||
let flags: u8 = fields
|
||||
.next()
|
||||
.and_then(|s| u8::from_str_radix(s, 16).ok())
|
||||
.unwrap_or(0);
|
||||
tracks.push((start, end, flags));
|
||||
}
|
||||
if !tracks.is_empty() {
|
||||
config.tracks = tracks;
|
||||
}
|
||||
}
|
||||
let mut d = Self::with_config(data, config);
|
||||
d.model = format!("Image {}", path.display());
|
||||
d.opened = true;
|
||||
Ok(d)
|
||||
}
|
||||
|
||||
pub fn source_samples(&self) -> &[i16] {
|
||||
&self.samples
|
||||
}
|
||||
|
||||
fn fix_toc(&mut self) {
|
||||
let tracks = self.toc.tracks.max(0) as usize;
|
||||
self.toc.fixup(tracks + 1);
|
||||
}
|
||||
|
||||
fn mapped_sample(&self, target_word: i64) -> Option<i16> {
|
||||
if target_word < 0 {
|
||||
return None;
|
||||
}
|
||||
let mut source = target_word;
|
||||
for (sector, delta) in &self.faults.discontinuities {
|
||||
if target_word / CD_FRAMEWORDS >= *sector {
|
||||
source += *delta;
|
||||
}
|
||||
}
|
||||
if let Some((start, len)) = self.faults.scratch {
|
||||
if target_word >= start && target_word < start + len {
|
||||
// A deterministic, repeated-ish garbage pattern.
|
||||
let x = (target_word as u64).wrapping_mul(0x9E3779B97F4A7C15);
|
||||
return Some((x >> 48) as i16);
|
||||
}
|
||||
}
|
||||
self.samples.get(source as usize).copied()
|
||||
}
|
||||
|
||||
fn read_words(&mut self, begin_sector: i64, sectors: i64) -> Vec<i16> {
|
||||
let words = sectors.max(0) * CD_FRAMEWORDS;
|
||||
let mut out = Vec::with_capacity(words as usize);
|
||||
let mut jitter = 0i64;
|
||||
if self.faults.jitter_words > 0 {
|
||||
self.read_count = self.read_count.wrapping_add(1);
|
||||
let period = if self.faults.jitter_period == 0 {
|
||||
1
|
||||
} else {
|
||||
self.faults.jitter_period
|
||||
};
|
||||
if self.read_count.is_multiple_of(period) {
|
||||
let max = self.faults.jitter_words;
|
||||
let x = (self.read_count as u64).wrapping_mul(6364136223846793005);
|
||||
jitter = (x % (2 * max as u64 + 1)) as i64 - max;
|
||||
}
|
||||
if self.read_count <= self.faults.jitter_skip {
|
||||
jitter = 0;
|
||||
}
|
||||
}
|
||||
let start = begin_sector * CD_FRAMEWORDS + jitter;
|
||||
for w in 0..words {
|
||||
out.push(self.mapped_sample(start + w).unwrap_or(0));
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
impl Drive for ImageDrive {
|
||||
fn toc(&self) -> &DiscToc {
|
||||
&self.toc
|
||||
}
|
||||
fn toc_mut(&mut self) -> &mut DiscToc {
|
||||
&mut self.toc
|
||||
}
|
||||
fn opened(&self) -> bool {
|
||||
self.opened
|
||||
}
|
||||
fn read_audio(
|
||||
&mut self,
|
||||
buffer: Option<&mut [u8]>,
|
||||
begin: i64,
|
||||
sectors: i64,
|
||||
) -> Result<usize, ReadError> {
|
||||
if !self.opened {
|
||||
return Err(ReadError::closed());
|
||||
}
|
||||
if sectors <= 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
let mut sectors = sectors;
|
||||
if self.faults.max_read_sectors > 0 {
|
||||
sectors = sectors.min(self.faults.max_read_sectors);
|
||||
}
|
||||
let words = self.read_words(begin, sectors);
|
||||
if let Some(buf) = buffer {
|
||||
let bytes = super::samples_to_bytes(&words);
|
||||
let n = bytes.len().min(buf.len());
|
||||
buf[..n].copy_from_slice(&bytes[..n]);
|
||||
if super::host_big_endian() {
|
||||
swap_raw16(&mut buf[..n]);
|
||||
}
|
||||
}
|
||||
self.last_begin = begin;
|
||||
Ok(sectors as usize)
|
||||
}
|
||||
fn read_audio_raw(
|
||||
&mut self,
|
||||
buffer: Option<&mut [u8]>,
|
||||
begin: i64,
|
||||
sectors: i64,
|
||||
) -> Result<usize, ReadError> {
|
||||
if !self.opened {
|
||||
return Err(ReadError::closed());
|
||||
}
|
||||
if sectors <= 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
let mut sectors = sectors;
|
||||
if self.faults.max_read_sectors > 0 {
|
||||
sectors = sectors.min(self.faults.max_read_sectors);
|
||||
}
|
||||
let words = self.read_words(begin, sectors);
|
||||
if let Some(buf) = buffer {
|
||||
let bytes = super::samples_to_bytes(&words);
|
||||
let n = bytes.len().min(buf.len());
|
||||
buf[..n].copy_from_slice(&bytes[..n]);
|
||||
}
|
||||
self.last_begin = begin;
|
||||
Ok(sectors as usize)
|
||||
}
|
||||
fn read_timed(&mut self, buffer: Option<&mut [u8]>, begin: i64, sectors: i64) -> TimedRead {
|
||||
let start = Instant::now();
|
||||
let previous_begin = self.last_begin;
|
||||
let result = self.read_audio(buffer, begin, sectors);
|
||||
let _elapsed = start.elapsed();
|
||||
let ms = if begin < previous_begin {
|
||||
self.faults.backward_ms
|
||||
} else {
|
||||
self.faults.read_ms
|
||||
};
|
||||
TimedRead {
|
||||
result,
|
||||
milliseconds: ms,
|
||||
}
|
||||
}
|
||||
fn speed_set(&mut self, _speed: i32) -> i32 {
|
||||
0
|
||||
}
|
||||
fn close(&mut self) {
|
||||
self.opened = false;
|
||||
}
|
||||
fn drive_model(&self) -> &str {
|
||||
&self.model
|
||||
}
|
||||
fn bigendianp(&self) -> i32 {
|
||||
self.bigendianp
|
||||
}
|
||||
fn set_bigendianp(&mut self, value: i32) {
|
||||
self.bigendianp = value;
|
||||
}
|
||||
fn nsectors(&self) -> i64 {
|
||||
self.nsectors
|
||||
}
|
||||
fn set_nsectors(&mut self, value: i64) {
|
||||
self.nsectors = value;
|
||||
}
|
||||
fn interface(&self) -> InterfaceKind {
|
||||
InterfaceKind::Image
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,422 @@
|
||||
//! 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>,
|
||||
pub milliseconds: i32,
|
||||
}
|
||||
|
||||
/// 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>;
|
||||
/// 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);
|
||||
fn interface(&self) -> InterfaceKind;
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#![allow(
|
||||
clippy::too_many_arguments,
|
||||
clippy::missing_safety_doc,
|
||||
clippy::while_let_loop,
|
||||
clippy::unnecessary_cast,
|
||||
clippy::field_reassign_with_default
|
||||
)]
|
||||
|
||||
//! cdrustanoia core library.
|
||||
//!
|
||||
//! This is a Rust port of the cdparanoia III 10.2 interface and paranoia
|
||||
//! libraries, plus the command line helper code.
|
||||
|
||||
pub mod callback;
|
||||
pub mod device;
|
||||
pub mod paranoia;
|
||||
|
||||
pub const VERSIONNUM: &str = "10.2";
|
||||
pub const VERSION: &str = "cdparanoia III release 10.2 (September 11, 2008)";
|
||||
|
||||
pub fn paranoia_version() -> &'static str {
|
||||
VERSIONNUM
|
||||
}
|
||||
|
||||
pub fn cdda_version() -> &'static str {
|
||||
VERSIONNUM
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
//! c_block/v_fragment/root data structures and vector operations.
|
||||
|
||||
pub const FLAGS_EDGE: u8 = 0x1;
|
||||
pub const FLAGS_UNREAD: u8 = 0x2;
|
||||
pub const FLAGS_VERIFIED: u8 = 0x4;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CBlock {
|
||||
pub vector: Vec<i16>,
|
||||
pub flags: Option<Vec<u8>>,
|
||||
pub begin: i64,
|
||||
pub size: i64,
|
||||
pub lastsector: bool,
|
||||
}
|
||||
|
||||
impl Default for CBlock {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl CBlock {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
vector: Vec::new(),
|
||||
flags: None,
|
||||
begin: 0,
|
||||
size: 0,
|
||||
lastsector: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_parts(vector: Vec<i16>, begin: i64, lastsector: bool) -> Self {
|
||||
let size = vector.len() as i64;
|
||||
Self {
|
||||
vector,
|
||||
flags: None,
|
||||
begin,
|
||||
size,
|
||||
lastsector,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn end(&self) -> i64 {
|
||||
self.begin + self.size
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn begin(&self) -> i64 {
|
||||
self.begin
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn size(&self) -> i64 {
|
||||
self.size
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, pos: i64, data: &[i16]) {
|
||||
let vs = self.size;
|
||||
if pos < 0 || pos > vs {
|
||||
return;
|
||||
}
|
||||
let pos = pos as usize;
|
||||
self.vector.splice(pos..pos, data.iter().copied());
|
||||
self.size += data.len() as i64;
|
||||
if let Some(flags) = self.flags.as_mut() {
|
||||
let n = data.len();
|
||||
let pos = pos.min(flags.len());
|
||||
flags.splice(pos..pos, std::iter::repeat_n(0, n));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, cutpos: i64, mut cutsize: i64) {
|
||||
let vs = self.size;
|
||||
if cutpos < 0 || cutpos > vs {
|
||||
return;
|
||||
}
|
||||
if cutpos + cutsize > vs {
|
||||
cutsize = vs - cutpos;
|
||||
}
|
||||
if cutsize < 0 {
|
||||
cutsize = vs - cutpos;
|
||||
}
|
||||
if cutsize < 1 {
|
||||
return;
|
||||
}
|
||||
let pos = cutpos as usize;
|
||||
let n = cutsize as usize;
|
||||
self.vector.drain(pos..pos + n);
|
||||
self.size -= cutsize;
|
||||
if let Some(flags) = self.flags.as_mut() {
|
||||
let pos = pos.min(flags.len());
|
||||
let end = (pos + n).min(flags.len());
|
||||
flags.drain(pos..end);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn overwrite(&mut self, pos: i64, data: &[i16]) {
|
||||
if pos < 0 {
|
||||
return;
|
||||
}
|
||||
let mut size = data.len() as i64;
|
||||
if pos + size > self.size {
|
||||
size = self.size - pos;
|
||||
}
|
||||
if size <= 0 {
|
||||
return;
|
||||
}
|
||||
let p = pos as usize;
|
||||
let n = size as usize;
|
||||
self.vector[p..p + n].copy_from_slice(&data[..n]);
|
||||
}
|
||||
|
||||
pub fn append(&mut self, data: &[i16]) {
|
||||
self.vector.extend_from_slice(data);
|
||||
self.size += data.len() as i64;
|
||||
if let Some(flags) = self.flags.as_mut() {
|
||||
flags.extend(std::iter::repeat_n(0, data.len()));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_front(&mut self, cut: i64) {
|
||||
if cut <= 0 {
|
||||
return;
|
||||
}
|
||||
let n = (cut as usize).min(self.vector.len());
|
||||
self.vector.drain(..n);
|
||||
self.size -= n as i64;
|
||||
self.begin += n as i64;
|
||||
if let Some(flags) = self.flags.as_mut() {
|
||||
let n = n.min(flags.len());
|
||||
flags.drain(..n);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn flag_at(&self, abs_word: i64) -> u8 {
|
||||
let idx = abs_word - self.begin;
|
||||
if idx < 0 {
|
||||
return 0;
|
||||
}
|
||||
self.flags
|
||||
.as_ref()
|
||||
.and_then(|f| f.get(idx as usize).copied())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct VFragment {
|
||||
pub vector: Vec<i16>,
|
||||
pub begin: i64,
|
||||
pub size: i64,
|
||||
pub lastsector: bool,
|
||||
}
|
||||
|
||||
impl VFragment {
|
||||
pub fn end(&self) -> i64 {
|
||||
self.begin + self.size
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct RootBlock {
|
||||
pub returned_limit: i64,
|
||||
pub lastsector: bool,
|
||||
pub vector: Option<CBlock>,
|
||||
pub silence_flag: bool,
|
||||
pub silence_begin: i64,
|
||||
}
|
||||
|
||||
impl RootBlock {
|
||||
#[inline]
|
||||
pub fn begin(&self) -> i64 {
|
||||
self.vector.as_ref().map(|v| v.begin).unwrap_or(-1)
|
||||
}
|
||||
#[inline]
|
||||
pub fn end(&self) -> i64 {
|
||||
self.vector.as_ref().map(|v| v.end()).unwrap_or(-1)
|
||||
}
|
||||
#[inline]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.vector.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct Offsets {
|
||||
pub offpoints: i64,
|
||||
pub newpoints: i64,
|
||||
pub offaccum: i64,
|
||||
pub offdiff: i64,
|
||||
pub offmin: i64,
|
||||
pub offmax: i64,
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,87 @@
|
||||
//! Sample-value index, a safe Rust equivalent of isort.c.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct SortIndex {
|
||||
buckets: HashMap<i16, Vec<u32>>,
|
||||
pub begin: i64,
|
||||
pub size: usize,
|
||||
cursor: Option<(i16, usize, usize)>, // value, next bucket index, hi
|
||||
}
|
||||
|
||||
impl SortIndex {
|
||||
pub fn new(_maxsize: usize) -> Self {
|
||||
Self {
|
||||
buckets: HashMap::new(),
|
||||
begin: 0,
|
||||
size: 0,
|
||||
cursor: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setup(&mut self, samples: &[i16], begin_abs: i64, sortlo: i64, sorthi: i64) {
|
||||
self.buckets.clear();
|
||||
self.begin = begin_abs;
|
||||
self.size = samples.len();
|
||||
self.cursor = None;
|
||||
let lo = ((sortlo - begin_abs).max(0) as usize).min(samples.len());
|
||||
let hi = ((sorthi - begin_abs).max(0) as usize).min(samples.len());
|
||||
if lo >= hi {
|
||||
return;
|
||||
}
|
||||
for (i, sample) in samples.iter().enumerate().take(hi).skip(lo) {
|
||||
self.buckets.entry(*sample).or_default().push(i as u32);
|
||||
}
|
||||
}
|
||||
|
||||
/// Equivalent of sort_getmatch(). Returns the first matching relative
|
||||
/// position, and leaves a cursor for sort_nextmatch().
|
||||
pub fn getmatch(&mut self, post: i64, overlap: i64, value: i16) -> Option<usize> {
|
||||
let post = post.clamp(0, self.size as i64);
|
||||
let lo = (post - overlap).max(0) as usize;
|
||||
let hi = ((post + overlap).min(self.size as i64)).max(0) as usize;
|
||||
self.cursor = None;
|
||||
let positions = self.buckets.get(&value)?;
|
||||
let idx = positions.partition_point(|p| (*p as usize) < lo);
|
||||
if idx >= positions.len() {
|
||||
return None;
|
||||
}
|
||||
let pos = positions[idx] as usize;
|
||||
if pos >= hi {
|
||||
return None;
|
||||
}
|
||||
self.cursor = Some((value, idx + 1, hi));
|
||||
Some(pos)
|
||||
}
|
||||
|
||||
pub fn nextmatch(&mut self) -> Option<usize> {
|
||||
let (value, idx, hi) = self.cursor?;
|
||||
let positions = self.buckets.get(&value)?;
|
||||
if idx >= positions.len() {
|
||||
self.cursor = None;
|
||||
return None;
|
||||
}
|
||||
let pos = positions[idx] as usize;
|
||||
if pos >= hi {
|
||||
self.cursor = None;
|
||||
return None;
|
||||
}
|
||||
self.cursor = Some((value, idx + 1, hi));
|
||||
Some(pos)
|
||||
}
|
||||
|
||||
/// Position most recently returned by getmatch()/advance().
|
||||
pub fn cursor_position(&self) -> Option<i64> {
|
||||
let (value, idx, _hi) = self.cursor?;
|
||||
let positions = self.buckets.get(&value)?;
|
||||
if idx == 0 || idx > positions.len() {
|
||||
return None;
|
||||
}
|
||||
Some(positions[idx - 1] as i64)
|
||||
}
|
||||
|
||||
/// Advance to the next match. Returns false when exhausted.
|
||||
pub fn advance(&mut self) -> bool {
|
||||
self.nextmatch().is_some()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user