diff --git a/Cargo.lock b/Cargo.lock index 22bb23b..9e60cd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,9 +18,12 @@ checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" name = "cd_read" version = "0.1.0" dependencies = [ + "hound", + "md5", "nix", "num-derive", "num-traits", + "thiserror", ] [[package]] @@ -35,12 +38,24 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "hound" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" + [[package]] name = "libc" version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "nix" version = "0.29.0" @@ -102,6 +117,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "unicode-ident" version = "1.0.15" diff --git a/Cargo.toml b/Cargo.toml index f5b8b7c..8186a79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,15 @@ version = "0.1.0" edition = "2021" [dependencies] +hound = "3.5.1" +md5 = "0.7.0" nix = { version = "0.29.0", features = ["ioctl"] } num-derive = "0.4.2" num-traits = "0.2.19" +thiserror = "2.0.11" + +[profile.release] +strip = true # Automatically strip symbols from the binary. +opt-level = "z" # Optimize for size. +lto = true +codegen-units = 1 diff --git a/src/constants.rs b/src/constants.rs index 889ba9a..d4a7a99 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -3,13 +3,15 @@ use num_traits::ToPrimitive; +pub const EDRIVE_CANT_DO_THIS: i32 = nix::errno::Errno::EOPNOTSUPP as i32; + /// CDROM ioctl byte, from pub const IOC_BYTE: u8 = 0x53; #[repr(u8)] #[derive(FromPrimitive, ToPrimitive)] -#[derive(Debug, Clone, Copy)] -pub enum Operations { +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Operation { /// Pause Audio Operation Pause = 0x01, /// Resume paused Audio Operation @@ -136,18 +138,16 @@ pub enum Operations { TimedMediaChange = 0x96, } -impl Operations { - /// Turns the byte into its full IOCTL representation - pub fn to_full(&self) -> u64 { - let value = self.to_u8().unwrap(); +/// Turns the byte into its full IOCTL representation +pub fn op_to_ioctl(op: Operation) -> u64 { + let value = op.to_u8().unwrap(); - ((IOC_BYTE as u64) << 8) + value as u64 - } + ((IOC_BYTE as u64) << 8) + value as u64 } /// Drive status possibilities returned by CDROM_DRIVE_STATUS ioctl #[derive(FromPrimitive, ToPrimitive)] -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Status { NoInfo = 0, NoDisc = 1, @@ -155,3 +155,88 @@ pub enum Status { DriveNotReady = 3, DiscOK = 4 } + +/// Disc status possibilities returned by CDROM_DISC_STATUS ioctl +#[derive(FromPrimitive, ToPrimitive)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DiscStatus { + NoInfo = 0, + Audio = 100, + Data1 = 101, + Data2 = 102, + XA21 = 103, + XA22 = 104, + Mixed = 105, +} + +pub const CD_MINS: i32 = 74; +pub const CD_SECS: i32 = 60; +pub const CD_FRAMES: i32 = 75; +pub const CD_SYNC_SIZE: i32 = 12; +pub const CD_MSF_OFFSET: i32 = 150; +pub const CD_CHUNK_SIZE: i32 = 24; +pub const CD_NUM_OF_CHUNKS: i32 = 98; +pub const CD_FRAMESIZE_SUB: i32 = 96; +pub const CD_HEAD_SIZE: i32 = 4; +pub const CD_SUBHEAD_SIZE: i32 = 8; +pub const CD_EDC_SIZE: i32 = 4; +pub const CD_ZERO_SIZE: i32 = 8; +pub const CD_ECC_SIZE: i32 = 276; +pub const CD_FRAMESIZE: i32 = 2048; +pub const CD_FRAMESIZE_RAW: i32 = 2352; +pub const CD_FRAMESIZE_RAWER: i32 = 2646; +pub const CD_FRAMESIZE_RAW1: i32 = CD_FRAMESIZE_RAW - CD_SYNC_SIZE; +pub const CD_FRAMESIZE_RAW0: i32 = CD_FRAMESIZE_RAW - CD_SYNC_SIZE - CD_HEAD_SIZE; + +pub const CD_XA_HEAD: i32 = CD_HEAD_SIZE + CD_SUBHEAD_SIZE; +pub const CD_XA_TAIL: i32 = CD_EDC_SIZE + CD_ECC_SIZE; +pub const CD_XA_SYNC_HEAD: i32 = CD_SYNC_SIZE + CD_XA_HEAD; + +#[repr(u8)] +#[derive(FromPrimitive, ToPrimitive)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AddressType { + Lba = 0x01, + Msf = 0x02, +} + +#[derive(FromPrimitive, ToPrimitive)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AudioStates { + Invalid = 0x00, + Play = 0x11, + Paused = 0x12, + Completed = 0x13, + Error = 0x14, + NoStatus = 0x15, +} + +pub enum Capability { + CloseTray = 0x01, + OpenTray = 0x02, + Lock = 0x04, + SelectSpeed = 0x08, + SelectDisc = 0x10, + MultiSession = 0x20, + Mcn = 0x40, + MediaChanged = 0x80, + PlayAudio = 0x100, + Reset = 0x200, + DriveStatus = 0x800, + GenericPacket = 0x1000, + CdR = 0x2000, + CdRW = 0x4000, + Dvd = 0x8000, + DvdR = 0x10000, + DvdRam = 0x20000, + MODrive = 0x40000, + Mrw = 0x80000, + MrwW = 0x1000000, + Ram = 0x2000000, +} + +impl Capability { + pub fn compare() { + + } +} diff --git a/src/main.rs b/src/main.rs index 21d2688..d3f164e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,65 +1,336 @@ mod constants; mod structures; +use std::io::Write; +use std::os::fd::RawFd; use std::os::{fd::IntoRawFd, unix::fs::OpenOptionsExt}; use std::fs::OpenOptions; +use std::ptr::addr_of_mut; -use constants::{Operations, IOC_BYTE}; -use nix::{ioctl_none, ioctl_none_bad, ioctl_readwrite_bad, libc}; +use constants::{op_to_ioctl, AddressType, DiscStatus, Operation, Status}; +use nix::errno::Errno; +use nix::{ioctl_none_bad, ioctl_read_bad, ioctl_readwrite_bad, ioctl_write_int_bad, libc}; -use nix::libc::c_int; use num_traits::FromPrimitive as _; -use structures::{Addr, Msf0, ReadAudio}; +use structures::{Addr, AddrUnion, Msf, MsfLong, RawResult, ReadAudio, TocEntry, TocHeader, _TocEntry}; +use thiserror::Error; #[macro_use] extern crate num_derive; -ioctl_none_bad!(cdrom_stop, Operations::to_full(&Operations::Stop)); -ioctl_none_bad!(cdrom_start, Operations::to_full(&Operations::Start)); -ioctl_none_bad!(cdrom_eject, Operations::to_full(&Operations::Eject)); -ioctl_none_bad!(cdrom_close_tray, Operations::to_full(&Operations::CloseTray)); -ioctl_none_bad!(cdrom_status, Operations::to_full(&Operations::DriveStatus)); -ioctl_readwrite_bad!(cdrom_read_audio, Operations::to_full(&Operations::ReadAudio), structures::ReadAudio); - fn main() { - const FRAMES: i32 = 75; - const BYTES_PER_FRAME: i32 = 2352; + let mut cd_rom = CDRom::new().unwrap(); - let time = Msf0 { - minute: 2, - second: 45, - frame: 0, - }; + let status = cd_rom.status().unwrap(); + println!("Drive status: {:?}", status); + cd_rom.close().unwrap(); - let address = Addr { - msf: time, - }; + println!("Disc status: {:?}", cd_rom.disc_type()); - let mut buff = [0u8; FRAMES as usize * BYTES_PER_FRAME as usize]; //Frames per second (75) * bytes per frame (2352) - - let mut ra = ReadAudio { - addr: address, - addr_format: 0x02, - nframes: FRAMES, - buf: buff.as_mut_ptr() - }; - - let cdrom = OpenOptions::new() - .read(true) - .custom_flags(libc::O_NONBLOCK | libc::O_RDONLY) - .open("/dev/sr0") - .unwrap(); - let cdrom_fd: c_int = cdrom.into_raw_fd(); - - unsafe { - let result = constants::Status::from_i32(cdrom_status(cdrom_fd).unwrap()).unwrap(); - dbg!(result); - //dbg!(cdrom_start(cdrom_fd).unwrap_or_default()); - - //std::thread::sleep(std::time::Duration::from_secs(1)); - - dbg!(cdrom_read_audio(cdrom_fd, std::ptr::addr_of_mut!(ra)).unwrap()); + let header = cd_rom.toc_header().unwrap(); + for i in header.first_track..header.last_track { + let entry = cd_rom.toc_entry(i); + println!("{:?}", entry.addr); } - println!("{:02X?}", &buff[0..10]); + cd_rom.set_lock(false).unwrap(); +} + +fn rip_cd() { + let mut cd_rom = CDRom::new().unwrap(); + + println!("Drive status: {:?}", cd_rom.status().unwrap()); + println!("Disc status: {:?}", cd_rom.disc_type().unwrap()); + + let spec = hound::WavSpec { + channels: 2, + sample_rate: 44100, + bits_per_sample: 16, + sample_format: hound::SampleFormat::Int, + }; + let mut writer = hound::WavWriter::create("cd_audio.wav", spec).unwrap(); + let mut buf = vec![0i16; (75 * constants::CD_FRAMESIZE_RAW as usize) / 2]; + + // Checksum calculator + let mut context = md5::Context::new(); + + println!("Begin ripping\n"); + for i in 0..4500u32 { + let minute = ((i + 2) / 60) as u8; + let second = ((i + 2) % 60) as u8; + print!("Reading {minute:02}:{second:02}\r"); + std::io::stdout().flush().unwrap(); + + match cd_rom.read_audio_into( + Addr::Msf(Msf { minute, second, frame: 0 }), + constants::CD_FRAMES as usize, + &mut buf, + ) { + Ok(s) => s, + Err(_) => break, + }; + + let audio_u8: Vec = buf.iter().flat_map(|s| s.to_le_bytes()).collect(); + context.consume(audio_u8); + + let mut writer = writer.get_i16_writer(buf.len() as u32); + for sample in &buf { + writer.write_sample(*sample); + } + writer.flush().unwrap(); + } + + let sum = context.compute(); + let mut string_sum = String::new(); + sum.iter().for_each(|b| string_sum.push_str(format!("{:0x}", b).as_str())); + println!("\nFinished!\n\n\tChecksum: {}", string_sum); + + writer.flush().unwrap(); + writer.finalize().unwrap(); +} + +/// Access to a CD-ROM drive on the system. +pub struct CDRom { + drive_fd: RawFd, +} + +#[derive(Error, Debug, Clone)] +pub enum CDRomError { + #[error("internal system error")] + Errno(#[from] nix::errno::Errno), + + #[error("no disc in drive to read")] + NoDisc, + + #[error("the CD does not contain cd-audio")] + NotAudioCD, + + #[error("the drive's door is locked for some reason")] + DoorLocked, + + #[error("this drive does not support the function")] + Unsupported, + + #[error("the drive is in use by another user")] + Busy, +} + +ioctl_none_bad!(cdrom_stop, op_to_ioctl(Operation::Stop)); +ioctl_none_bad!(cdrom_start, op_to_ioctl(Operation::Start)); +ioctl_none_bad!(cdrom_eject, op_to_ioctl(Operation::Eject)); +ioctl_write_int_bad!(cdrom_lock_door, op_to_ioctl(Operation::LockDoor)); +ioctl_none_bad!(cdrom_close_tray, op_to_ioctl(Operation::CloseTray)); +ioctl_none_bad!(cdrom_status, op_to_ioctl(Operation::DriveStatus)); +ioctl_none_bad!(cdrom_disc_status, op_to_ioctl(Operation::DiscStatus)); +ioctl_readwrite_bad!(cdrom_read_audio, op_to_ioctl(Operation::ReadAudio), structures::ReadAudio); +ioctl_readwrite_bad!(cdrom_read_raw, op_to_ioctl(Operation::ReadRaw), structures::RawResult); +ioctl_read_bad!(cdrom_get_mcn, op_to_ioctl(Operation::GetMcn), [u8; 14]); +ioctl_read_bad!(cdrom_read_toc_header, op_to_ioctl(Operation::ReadTocHeader), structures::TocHeader); +ioctl_read_bad!(cdrom_read_toc_entry, op_to_ioctl(Operation::ReadTocEntry), structures::_TocEntry); + +impl CDRom { + /// Creates a new interface to a system CD-ROM drive. + pub fn new() -> Option { + let drive_file = OpenOptions::new() + .read(true) + .custom_flags(libc::O_NONBLOCK | libc::O_RDONLY) + .open("/dev/sr0") + .ok()?; + + Some(Self { + drive_fd: drive_file.into_raw_fd(), + }) + } + + /// Get the currently reported status of the drive. + pub fn status(&mut self) -> Option { + let status = unsafe { + cdrom_status(self.drive_fd).unwrap() + }; + + Status::from_i32(status) + } + + /// Get the type of disc currently in the drive + pub fn disc_type(&mut self) -> Option { + let status = unsafe { + cdrom_disc_status(self.drive_fd).ok()? + }; + + DiscStatus::from_i32(status) + } + + /// Get the Media Catalog Number of the current disc. + /// + /// Many discs do not contain this information. + pub fn mcn(&mut self) -> Option { + let mut buffer = [0u8; 14]; + + unsafe { + cdrom_get_mcn(self.drive_fd, addr_of_mut!(buffer)).ok()?; + } + + let string = String::from_utf8_lossy(&buffer[..buffer.len() - 1]).into_owned(); + Some(string) + } + + pub fn toc_header(&mut self) -> Result { + let mut header = TocHeader::default(); + + if unsafe { + cdrom_read_toc_header(self.drive_fd, addr_of_mut!(header)) + }.is_err_and(|e| e == Errno::ENOMEDIUM) { + return Err(CDRomError::NoDisc) + } + + Ok(header) + } + + pub fn toc_entry(&mut self, index: u8) -> TocEntry { + let mut header = _TocEntry::default(); + header.track = index; + header.format = AddressType::Lba as u8; + + unsafe { + cdrom_read_toc_entry(self.drive_fd, addr_of_mut!(header)).unwrap(); + } + + let entry = TocEntry { + track: header.track, + adr: header.adr_ctrl >> 4, + ctrl: header.adr_ctrl & 0x0F, + addr: unsafe { + match header.format { + d if d == AddressType::Lba as u8 => Addr::Lba(header.addr.lba), + d if d == AddressType::Msf as u8 => Addr::Msf(header.addr.msf), + _ => panic!("Impossible value returned!") + } + }, + datamode: header.datamode, + }; + + entry + } + + pub fn set_lock(&mut self, locked: bool) -> Result<(), CDRomError> { + let result = match unsafe { + cdrom_lock_door(self.drive_fd, locked as i32) + } { + Ok(v) => v, + Err(e) => match e { + Errno::EBUSY => return Err(CDRomError::Busy), + _ => return Err(CDRomError::Errno(e)), + }, + }; + + match result { + constants::EDRIVE_CANT_DO_THIS => Err(CDRomError::Unsupported), + _ => Ok(()) + } + } + + pub fn eject(&mut self) -> Result<(), CDRomError> { + let status = unsafe { + cdrom_eject(self.drive_fd).unwrap() + }; + + if status == 2 { + return Err(CDRomError::DoorLocked) + } + + Ok(()) + } + + pub fn close(&mut self) -> Result<(), CDRomError> { + let status = unsafe { + cdrom_close_tray(self.drive_fd).unwrap() + }; + + match status { + d if d == Errno::ENOSYS as i32 => Err(CDRomError::Unsupported), + libc::EBUSY => Err(CDRomError::DoorLocked), + _ => Ok(()), + } + } + + /// Read audio from the CD. + /// + /// The buffer will be constructed automatically. + pub fn read_audio(&mut self, address: Addr, frames: usize) -> Result, CDRomError> { + let mut buf = vec![0i16; (frames * constants::CD_FRAMESIZE_RAW as usize) / 2]; + + self.read_audio_into(address, frames, &mut buf)?; + + Ok(buf) + } + + /// Read audio from the CD into a preallocated buffer. + /// + /// The buffer must be large enough to hold the audio for all the frames you want to read. + /// Since the values are [`i16`]s, the equation for the buffer size is `(n_frames * 2352) / 2` + pub fn read_audio_into(&mut self, address: Addr, frames: usize, buf: &mut [i16]) -> Result<(), CDRomError> { + let (addr, addr_format) = match address { + Addr::Lba(lba) => (AddrUnion { lba }, AddressType::Lba), + Addr::Msf(msf) => { + if msf.minute == 0 && msf.second < 2 { + panic!("MSF second cannot be less than 2!") + } + + (AddrUnion { msf }, AddressType::Msf) + }, + }; + + if frames < 1 || frames > 75 { + panic!("Invalid number of frames!") + } + + if buf.len() < (frames * constants::CD_FRAMESIZE_RAW as usize) / 2 { + panic!("Buffer is too small!") + } + + let mut ra = ReadAudio { + addr, + addr_format, + nframes: frames as i32, + buf: buf.as_mut_ptr() + }; + + let status = unsafe { + cdrom_read_audio(self.drive_fd, addr_of_mut!(ra)) + }?; + + if status != 0 { + return Err(Errno::from_raw(status).into()); + } + + Ok(()) + } + + pub fn read_raw(&mut self, address: Msf) -> Box<[u8; 2352]> { + // TODO: Make this take LBA values too + // MSF values are converted to LBA values via this formula: + // lba = (((m * CD_SECS) + s) * CD_FRAMES + f) - CD_MSF_OFFSET; + // + + if address.minute == 0 && address.second < 2 { + panic!("MSF second cannot be less than 2!") + } + + let mut argument = RawResult { + cdrom_msf: MsfLong { + min0: address.minute, + sec0: address.second, + frame0: address.frame, + ..Default::default() + } + }; + + let result = unsafe { + cdrom_read_raw(self.drive_fd, addr_of_mut!(argument)).unwrap(); + + argument.buffer + }; + + Box::new(result) + } } diff --git a/src/structures.rs b/src/structures.rs index 9e9e2bf..f890bb2 100644 --- a/src/structures.rs +++ b/src/structures.rs @@ -1,9 +1,11 @@ -use std::ffi::c_int; +use std::{ffi::c_int, mem}; + +use crate::constants::AddressType; /// Address in MSF format #[repr(C)] -#[derive(Clone, Copy)] -pub struct Msf0 { +#[derive(Debug, Clone, Copy)] +pub struct Msf { pub minute: u8, pub second: u8, pub frame: u8, @@ -12,15 +14,21 @@ pub struct Msf0 { /// Address in either MSF or logical format #[repr(C)] #[derive(Clone, Copy)] -pub union Addr { +pub union AddrUnion { pub lba: c_int, - pub msf: Msf0, + pub msf: Msf, +} + +#[derive(Debug, Clone, Copy)] +pub enum Addr { + Lba(i32), + Msf(Msf), } /// This struct is used by [`crate::constants::PLAY_MSF`] #[repr(C)] -#[derive(Clone, Copy)] -pub struct Msf { +#[derive(Clone, Copy, Default)] +pub struct MsfLong { /// Start minute pub min0: u8, /// Start second @@ -37,11 +45,12 @@ pub struct Msf { #[repr(C)] pub union RawResult { - pub cdrom_msf: Msf, - pub buffer: [u8; 2646], + pub cdrom_msf: MsfLong, + pub buffer: [u8; 2352], } /// This struct is used by [`crate::constants::PLAY_TRACK_INDEX`] +#[repr(C)] struct TrackIndex { /// Start track trk0: u8, @@ -54,9 +63,45 @@ struct TrackIndex { } /// This struct is used by [`crate::constants::READ_TOC_HEADER`] -struct TocHeader { - trk0: u8, - trk1: u8, +#[repr(C)] +#[derive(Default, Debug)] +pub struct TocHeader { + pub first_track: u8, + pub last_track: u8, +} + +// This struct is used by the [`crate::constants::READTOCENTRY`] ioctl +#[repr(C)] +pub(crate) struct _TocEntry { + pub track: u8, + pub adr_ctrl: u8, + pub format: u8, + pub addr: AddrUnion, + pub datamode: u8, +} + +impl Default for _TocEntry { + fn default() -> Self { + unsafe { + Self { + track: 0, + adr_ctrl: 0, + format: AddressType::Msf as u8, + addr: mem::zeroed(), + datamode: 0 + } + } + } +} + +// Actually public version of [`_TocEntry`]. +#[derive(Debug)] +pub struct TocEntry { + pub track: u8, + pub adr: u8, + pub ctrl: u8, + pub addr: Addr, + pub datamode: u8, } struct VolCtl { @@ -67,11 +112,19 @@ struct VolCtl { #[derive(Clone, Copy)] pub struct ReadAudio { /// Frame address - pub addr: Addr, + pub addr: AddrUnion, /// CDROM_LBA or CDROM_MSF - pub addr_format: u8, + pub addr_format: AddressType, /// Number of 2352-byte-frames to read at once pub nframes: i32, /// Pointer to frame buffer (size: nframes*2352 bytes) - pub buf: *mut u8, + pub buf: *mut i16, +} + +/// This struct is used with the CDROM_GET_MCN ioctl. +/// Very few audio discs actually have Universal Product Code information, +/// which should just be the Medium Catalog Number on the box. Also note +/// that the way the codeis written on CD is _not_ uniform across all discs! +struct Mcn { + medium_catalog_number: [u8; 14] }