first draft

This commit is contained in:
tux
2026-11-03 20:19:21 +01:00
commit 158e115242
44 changed files with 10785 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "cdrustanoia_core"
version.workspace = true
edition.workspace = true
license = "LGPL-2.1-or-later"
[dependencies]
libc.workspace = true
[dev-dependencies]
tempfile = "3"
+16
View File
@@ -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;
+138
View File
@@ -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",
}
}
+323
View File
@@ -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
+422
View File
@@ -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);
}
}
+27
View File
@@ -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
}
+195
View File
@@ -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
+87
View File
@@ -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()
}
}
+130
View File
@@ -0,0 +1,130 @@
use cdrustanoia_core::device::image::ImageDrive;
use cdrustanoia_core::device::{bytes_to_samples, samples_to_bytes, Drive, CD_FRAMESIZE_RAW};
use cdrustanoia_core::paranoia::{Paranoia, PARANOIA_MODE_FULL};
fn make_source(sectors: usize) -> Vec<i16> {
let mut out = Vec::with_capacity(sectors * 1176);
let mut x: u64 = 0x1234_5678_9abc_def0;
for i in 0..sectors * 1176 {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
// Mix in some silence and a distinct waveform.
let v = if i % 5000 < 1000 {
0
} else {
(x as i16).wrapping_add((i as i16).wrapping_mul(7))
};
out.push(v);
}
out
}
#[test]
fn clean_image_roundtrip() {
let sectors = 80usize;
let source = make_source(sectors);
let bytes = samples_to_bytes(&source);
let drive = ImageDrive::from_bytes(bytes);
let mut p = Paranoia::new(Box::new(drive));
p.modeset(PARANOIA_MODE_FULL);
let mut got = Vec::with_capacity(sectors * 1176);
let mut cb = |_: i64, _: i32| {};
for _ in 0..sectors {
let sec = p.read(&mut cb).expect("read sector");
got.extend_from_slice(sec);
}
assert_eq!(got.len(), source.len());
assert_eq!(got, source);
}
#[test]
fn drive_toc_helpers() {
let bytes = vec![0u8; CD_FRAMESIZE_RAW * 10];
let drive = ImageDrive::from_bytes(bytes);
assert_eq!(drive.tracks(), 1);
assert_eq!(drive.disc_firstsector(), 0);
assert_eq!(drive.disc_lastsector(), 9);
assert_eq!(drive.track_firstsector(1), 0);
assert_eq!(drive.track_lastsector(1), 9);
let _ = bytes_to_samples(&[0, 0]);
}
fn run_config(sectors: usize, config: cdrustanoia_core::device::image::ImageConfig) -> Vec<i16> {
let source = make_source(sectors);
let bytes = samples_to_bytes(&source);
let drive = ImageDrive::with_config(bytes, config);
let mut p = Paranoia::new(Box::new(drive));
p.modeset(PARANOIA_MODE_FULL);
let start = 5usize.min(sectors);
p.seek(start as i64, 0);
let mut got = Vec::with_capacity((sectors - start) * 1176);
let mut cb = |_: i64, _: i32| {};
for _ in start..sectors {
let sec = p.read(&mut cb).expect("read sector");
got.extend_from_slice(sec);
}
let source = &source[start * 1176..];
assert_eq!(got.len(), source.len());
if got != source {
let mut best: Option<(i64, usize)> = None;
for shift in -2000i64..=2000 {
let mut bad = 0usize;
for (i, value) in got.iter().enumerate().take(source.len()) {
let j = i as i64 + shift;
if j < 0 || j as usize >= source.len() || *value != source[j as usize] {
bad += 1;
if bad > 8 {
break;
}
}
}
if bad <= 8 {
best = Some((shift, bad));
break;
}
}
if let Some((shift, bad)) = best {
panic!(
"data shifted by {} words ({} mismatches); engine did not align absolute position",
shift, bad
);
}
let idx = got
.iter()
.zip(source.iter())
.position(|(a, b)| a != b)
.unwrap_or(0);
let where0 = source.iter().position(|x| *x == got[0]);
panic!(
"mismatch at word {} (sector {} offset {}): got {} expected {}; got0 found at {:?}; got[..8]={:?} src[..8]={:?}",
idx,
idx / 1176,
idx % 1176,
got[idx],
source[idx],
where0,
&got[..got.len().min(8)],
&source[..source.len().min(8)]
);
}
got
}
#[test]
fn jittered_image_roundtrip() {
use cdrustanoia_core::device::image::{ImageConfig, ImageFaults};
let sectors = 20usize;
let config = ImageConfig {
tracks: vec![(0, sectors as i64 - 1, 0)],
faults: ImageFaults {
jitter_words: 180,
jitter_period: 1,
jitter_skip: 10,
max_read_sectors: 0,
..Default::default()
},
..Default::default()
};
run_config(sectors, config);
}