diff --git a/build.rs b/build.rs index 43b900a..e95698c 100644 --- a/build.rs +++ b/build.rs @@ -13,6 +13,7 @@ fn main() { println!("cargo:rerun-if-changed={NATIVE_DIR}/toc_reader.c"); println!("cargo:rerun-if-changed={NATIVE_DIR}/track_information.c"); println!("cargo:rerun-if-changed={NATIVE_DIR}/read_cd.c"); + println!("cargo:rerun-if-changed={NATIVE_DIR}/request_read_speed.c"); println!("cargo:rustc-link-lib=framework=IOKit"); println!("cargo:rustc-link-lib=framework=CoreFoundation"); @@ -22,6 +23,7 @@ fn main() { .file(format!("{NATIVE_DIR}/toc_reader.c")) .file(format!("{NATIVE_DIR}/track_information.c")) .file(format!("{NATIVE_DIR}/read_cd.c")) + .file(format!("{NATIVE_DIR}/request_read_speed.c")) .include(NATIVE_DIR) // force C compilation .flag("-x") diff --git a/examples/read_speed.rs b/examples/read_speed.rs new file mode 100644 index 0000000..6837626 --- /dev/null +++ b/examples/read_speed.rs @@ -0,0 +1,50 @@ +/// Read the first audio track at 10x speed, and read the second audio track at `Optimal` +/// speed. +mod common; + +use cd_da_reader::{CdReader, ReadOptions, ReadSpeed, Track}; + +fn main() -> Result<(), Box> { + let output_dir = common::fresh_output_dir("read_speed")?; + let reader = CdReader::open_default()?; + let toc = reader.read_toc()?; + + let audio_tracks: Vec<&Track> = toc.tracks.iter().filter(|t| t.is_audio).collect(); + + if audio_tracks.len() < 2 { + panic!("This example requires at least two audio tracks"); + } + + let first_track = audio_tracks[0]; + let second_track = audio_tracks[1]; + + // Read the first track with 10x speed + { + let options_10x = ReadOptions::default().with_read_speed(ReadSpeed::CustomMultiplier(10)); + + println!("Reading track {} with 10x speed...", first_track.number); + let data = reader.read_track_with_options(&toc, first_track.number, &options_10x)?; + + let wav = CdReader::create_wav(data); + let output_path = output_dir.join(format!("track{:02}.wav", first_track.number)); + std::fs::write(&output_path, wav)?; + println!("Saved {}", output_path.display()); + } + + // Read the second track with "optimal" speed + { + let options_optimal = ReadOptions::default().with_read_speed(ReadSpeed::Optimal); + println!( + "Reading track {} with optimal speed...", + second_track.number + ); + let data = reader.read_track_with_options(&toc, second_track.number, &options_optimal)?; + + let wav = CdReader::create_wav(data); + let output_path = output_dir.join(format!("track{:02}.wav", second_track.number)); + std::fs::write(&output_path, wav)?; + println!("Saved {}", output_path.display()); + } + + Ok(()) +} diff --git a/src/data_reader/mod.rs b/src/data_reader/mod.rs index 2a435f0..a9bcaba 100644 --- a/src/data_reader/mod.rs +++ b/src/data_reader/mod.rs @@ -1,8 +1,10 @@ mod detect; mod raw_sector; +mod read_speed; mod sector_read_format; pub(crate) mod track_information; +pub use read_speed::ReadSpeed; pub use sector_read_format::SectorReadFormat; use crate::retry::RetryConfig; @@ -16,6 +18,7 @@ use crate::{CdReaderError, Track}; pub struct ReadOptions { format: SectorReadFormat, retry: RetryConfig, + read_speed: ReadSpeed, } impl ReadOptions { @@ -31,6 +34,16 @@ impl ReadOptions { self } + /// Set the read speed to request from the drive. See [`ReadSpeed`] for + /// details. + /// + /// # Note + /// For simplicity, this crate doesn't restore the previous speed setting. + pub fn with_read_speed(mut self, read_speed: ReadSpeed) -> Self { + self.read_speed = read_speed; + self + } + pub(crate) fn format(&self) -> SectorReadFormat { self.format } @@ -38,6 +51,10 @@ impl ReadOptions { pub(crate) fn retry(&self) -> &RetryConfig { &self.retry } + + pub(crate) fn read_speed(&self) -> ReadSpeed { + self.read_speed + } } impl Default for ReadOptions { @@ -45,6 +62,7 @@ impl Default for ReadOptions { Self { format: SectorReadFormat::Audio, retry: RetryConfig::default(), + read_speed: ReadSpeed::Unchanged, } } } diff --git a/src/data_reader/read_speed.rs b/src/data_reader/read_speed.rs new file mode 100644 index 0000000..e0033c0 --- /dev/null +++ b/src/data_reader/read_speed.rs @@ -0,0 +1,44 @@ +/// Representation of read speed requested by the `SET CD SPEED` (0xBB) command. +/// +/// # Note +/// +/// According to the MMC-3 specification, the requested speed doesn't necessarily +/// match the actual read speed. The drive may select the specified read speed +/// or any higher rate. +/// +/// Therefore, this enum represents a requested speed, not a guaranteed actual +/// read speed. The actual behaviour is drive-dependent. +#[derive(Debug, Clone, Copy)] +pub enum ReadSpeed { + /// Don't change the speed + /// + /// The speed depends on the OS, previous configuration, and other factors. + Unchanged, + + /// Request the drive-selected/optimal speed. + /// + /// According to the MMC-3 specification, the drive can select its optimal + /// speed when the `SET CD SPEED` command is executed with the read + /// speed (KB/s) set to 0xFFFF. + /// + /// On macOS, this variant requests `SET CD SPEED` with 0xFFFF. + /// + /// On Linux, the read speed is selected by the `CDROM_SELECT_SPEED` + /// ioctl with speed = 0. It requests automatic speed selection. + Optimal, + + /// Use a custom speed with the specified multiplier. + /// + /// Although the CD-DA read speed should be requested in KB/s according to + /// the specification, this variant uses a multiplier for simplicity. + /// + /// For example, `ReadSpeed::CustomMultiplier(1)` represents the nominal + /// CD-DA 1x read rate (176.4 KB/s). The exact conversion is + /// platform-dependent. + /// + /// Another example: `ReadSpeed::CustomMultiplier(10)` represents 10x speed. + /// + /// `ReadSpeed::CustomMultiplier(0)` is equivalent to `ReadSpeed::Optimal`. + /// The value 0 is used as a sentinel. + CustomMultiplier(u8), +} diff --git a/src/lib.rs b/src/lib.rs index ddc4671..bd7883a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -151,7 +151,7 @@ pub use backend::{ AudioSectorReader, AudioTrackStream, TrackBounds, open_track_stream, open_track_stream_at, open_track_stream_with_bounds, read_track, read_track_with_bounds, }; -pub use data_reader::{ReadOptions, SectorReadFormat}; +pub use data_reader::{ReadOptions, ReadSpeed, SectorReadFormat}; pub use discovery::DriveInfo; pub use errors::{CdReaderError, ScsiError, ScsiOp}; pub use retry::RetryConfig; @@ -319,6 +319,7 @@ impl CdReader { options: &ReadOptions, ) -> Result, CdReaderError> { let format = options.format(); + self.drive.request_read_speed(options.read_speed())?; read_loop::read_sectors_chunked( start_lba, sectors, diff --git a/src/platform/linux/mod.rs b/src/platform/linux/mod.rs index 25da844..4664456 100644 --- a/src/platform/linux/mod.rs +++ b/src/platform/linux/mod.rs @@ -1,6 +1,7 @@ mod device; mod read_cd; mod sg_io; +mod speed; mod toc; mod track_information; @@ -28,4 +29,11 @@ impl Drive { ) -> Result, CdReaderError> { read_cd::read_cd_chunk(self, lba, sectors, format) } + + pub(crate) fn request_read_speed( + &self, + read_speed: crate::data_reader::ReadSpeed, + ) -> Result<(), CdReaderError> { + speed::request_read_speed(self, read_speed) + } } diff --git a/src/platform/linux/speed.rs b/src/platform/linux/speed.rs new file mode 100644 index 0000000..edeb40c --- /dev/null +++ b/src/platform/linux/speed.rs @@ -0,0 +1,29 @@ +use super::device::Drive; +use crate::CdReaderError; +use crate::data_reader::ReadSpeed; +use std::os::fd::RawFd; + +// From linux/include/uapi/linux/cdrom.h +const CDROM_SELECT_SPEED: libc::c_ulong = 0x5322; + +pub(super) fn request_read_speed( + drive: &Drive, + target_read_speed: ReadSpeed, +) -> Result<(), CdReaderError> { + let multiplier = match target_read_speed { + ReadSpeed::Unchanged => return Ok(()), + ReadSpeed::Optimal => 0, + ReadSpeed::CustomMultiplier(x) => x, + }; + + execute_request_read_speed(drive.fd(), multiplier) +} + +fn execute_request_read_speed(fd: RawFd, multiplier: u8) -> Result<(), CdReaderError> { + let result = unsafe { libc::ioctl(fd, CDROM_SELECT_SPEED, multiplier as libc::c_ulong) }; + if result < 0 { + Err(CdReaderError::Io(std::io::Error::last_os_error())) + } else { + Ok(()) + } +} diff --git a/src/platform/macos/ffi.rs b/src/platform/macos/ffi.rs index b4b31bd..5277725 100644 --- a/src/platform/macos/ffi.rs +++ b/src/platform/macos/ffi.rs @@ -43,6 +43,7 @@ unsafe extern "C" { out_len: *mut u32, out_err: *mut MacScsiError, ) -> bool; + pub(super) fn request_cd_read_speed(fd: libc::c_int, multiplier: libc::c_ushort) -> bool; pub(super) fn cd_free(pointer: *mut libc::c_void); pub(super) fn list_cd_drives(out_drives: *mut *mut MacDriveInfo, out_count: *mut u32) -> bool; pub(super) fn open_cd_raw_device(bsd_name: *const libc::c_char) -> libc::c_int; diff --git a/src/platform/macos/mod.rs b/src/platform/macos/mod.rs index a780fc1..65ff721 100644 --- a/src/platform/macos/mod.rs +++ b/src/platform/macos/mod.rs @@ -1,6 +1,7 @@ mod device; mod ffi; mod read_cd; +mod speed; mod toc; mod track_information; @@ -28,4 +29,11 @@ impl Drive { ) -> Result, CdReaderError> { read_cd::read_cd_chunk(self, lba, sectors, format) } + + pub(crate) fn request_read_speed( + &self, + read_speed: crate::data_reader::ReadSpeed, + ) -> Result<(), CdReaderError> { + speed::request_read_speed(self, read_speed) + } } diff --git a/src/platform/macos/native/request_read_speed.c b/src/platform/macos/native/request_read_speed.c new file mode 100644 index 0000000..8ec2f63 --- /dev/null +++ b/src/platform/macos/native/request_read_speed.c @@ -0,0 +1,12 @@ +#include "shim_common.h" +#include + +Boolean request_cd_read_speed(int fd, uint16_t target_speed_kbs) { + int ret = ioctl(fd, DKIOCCDSETSPEED, &target_speed_kbs); + if (ret < 0) { + fprintf(stderr, "[SPEED] DKIOCCDSETSPEED failed (errno=%d)\n", errno); + return false; + } + + return true; +} diff --git a/src/platform/macos/speed.rs b/src/platform/macos/speed.rs new file mode 100644 index 0000000..771aa02 --- /dev/null +++ b/src/platform/macos/speed.rs @@ -0,0 +1,27 @@ +use super::device::Drive; +use crate::CdReaderError; +use crate::data_reader::ReadSpeed; + +pub(super) fn request_read_speed( + drive: &Drive, + target_read_speed: ReadSpeed, +) -> Result<(), CdReaderError> { + let multiplier = match target_read_speed { + ReadSpeed::Unchanged => return Ok(()), + ReadSpeed::Optimal => 0, + ReadSpeed::CustomMultiplier(x) => x as u32, + }; + let target_speed_kbs = if multiplier == 0 { + 0xffff + } else { + multiplier * 176400 / 1000 + }; + let response = + unsafe { super::ffi::request_cd_read_speed(drive.fd(), target_speed_kbs as u16) }; + + if !response { + return Err(CdReaderError::Io(std::io::Error::last_os_error())); + } + + Ok(()) +} diff --git a/src/platform/windows/mod.rs b/src/platform/windows/mod.rs index 2efbe9f..4dd7ee0 100644 --- a/src/platform/windows/mod.rs +++ b/src/platform/windows/mod.rs @@ -1,5 +1,6 @@ mod device; mod read_cd; +mod speed; mod spti; mod toc; mod track_information; @@ -28,4 +29,11 @@ impl Drive { ) -> Result, CdReaderError> { read_cd::read_cd_chunk(self, lba, sectors, format) } + + pub(crate) fn request_read_speed( + &self, + read_speed: crate::data_reader::ReadSpeed, + ) -> Result<(), CdReaderError> { + speed::request_read_speed(self, read_speed) + } } diff --git a/src/platform/windows/speed.rs b/src/platform/windows/speed.rs new file mode 100644 index 0000000..fa438a2 --- /dev/null +++ b/src/platform/windows/speed.rs @@ -0,0 +1,18 @@ +use super::device::Drive; +use crate::CdReaderError; +use crate::data_reader::ReadSpeed; + +pub(super) fn request_read_speed( + drive: &Drive, + target_read_speed: ReadSpeed, +) -> Result<(), CdReaderError> { + // stub + /* + let multiplier = match target_read_speed { + ReadSpeed::Unchanged => return Ok(()), + ReadSpeed::Optimal => 0, + ReadSpeed::CustomMultiplier(x) => x, + }; + */ + Ok(()) +}