Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 2 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,13 @@ use cd_da_reader::{CdReader};
let drives = CdReader::list_drives()?;
```

This will give you a vector of drives, and the struct will have `has_audio_cd` field for audio CDs. Unfortunately, this does not work on macOS due to how CD drive handles are treated. When we execute any command to a CD drive (which we need to check whether the CD is audio or not), we need to claim exclusivity, which will cause it to unmount. If we release the handle, it will cause it to remount, and that will do 2 things:
This will give you a vector of drives, and the struct will have `has_audio_cd` field for audio CDs.

1. call the default application for an audio CD (probably Apple Music)
2. that app will claim exclusivity, so we won't be able to get it back for some time

Because of that, on macOS you should either provide the name by yourself, or get the default drive:
If you already know the drive name, you can open it directly:

```rust
use cd_da_reader::{CdReader};

// get the default drive, which should be what you want
let reader = CdReader::open_default()?;

// or read the disk directly
let reader = CdReader::open("disk14")?;
```

Expand Down
3 changes: 0 additions & 3 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,14 @@
fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=src/mac/shim_common.h");
println!("cargo:rerun-if-changed=src/mac/da_guard.c");
println!("cargo:rerun-if-changed=src/mac/device_service.c");
println!("cargo:rerun-if-changed=src/mac/list_drives.c");
println!("cargo:rerun-if-changed=src/mac/toc_reader.c");
println!("cargo:rerun-if-changed=src/mac/audio_reader.c");

println!("cargo:rustc-link-lib=framework=IOKit");
println!("cargo:rustc-link-lib=framework=CoreFoundation");
println!("cargo:rustc-link-lib=framework=DiskArbitration");
cc::Build::new()
.file("src/mac/da_guard.c")
.file("src/mac/device_service.c")
.file("src/mac/list_drives.c")
.file("src/mac/toc_reader.c")
Expand Down
38 changes: 15 additions & 23 deletions src/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pub struct DriveInfo {
pub path: String,
/// Just the device name, without the full path for the OS
pub display_name: Option<String>,
/// We load the disc and issue a TOC command, which is supported only on media CDs
/// Whether the current disc appears to contain at least one audio track.
pub has_audio_cd: bool,
}

Expand Down Expand Up @@ -71,31 +71,23 @@ impl CdReader {

/// Open the first discovered drive that currently has an audio CD.
///
/// On macOS, we open each drive returned from `diskutil list`, and
/// evaluate each disk. Once we are able to open it and read correct TOC,
/// we return it back with already acquired exclusivity.
/// On macOS, we use the passive `IOCDMedia` discovery path and then open
/// the matching BSD device name without claiming exclusive access.
#[cfg(target_os = "macos")]
pub fn open_default() -> Result<Self, CdReaderError> {
let mut paths = crate::macos::list_drive_paths().map_err(CdReaderError::Io)?;
paths.sort();
paths.dedup();

for path in paths {
let Ok(reader) = Self::open(&path) else {
continue;
};
let Ok(toc) = reader.read_toc() else {
continue;
};
if toc.tracks.iter().any(|track| track.is_audio) {
return Ok(reader);
}
}
let drives = Self::list_drives()?;
let chosen = drives
.iter()
.find(|drive| drive.has_audio_cd)
.map(|drive| drive.path.as_str())
.ok_or_else(|| {
CdReaderError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
"no usable audio CD drive found",
))
})?;

Err(CdReaderError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
"no usable audio CD drive found",
)))
Self::open(chosen).map_err(CdReaderError::Io)
}

/// Open the first discovered drive that currently has an audio CD.
Expand Down
25 changes: 7 additions & 18 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
//! This library provides cross-platform audio CD reading capabilities
//! (tested on Windows, macOS and Linux). It was written to enable CD ripping,
//! but you can also implement a live audio CD player with its help.
//! The library works by issuing direct SCSI commands and abstracts both
//! access to the CD drive and reading the actual data from it, so you don't
//! deal with the hardware directly.
//! The library works through platform CD-drive APIs on macOS and issuing direct
//! SCSI commands on Windows and Linux and abstracts both access to the CD drive
//! and reading the actual data from it, so you don't deal with the hardware directly.
//!
//! All operations happen in this order:
//!
Expand Down Expand Up @@ -42,11 +42,6 @@
//! # Ok::<(), Box<dyn std::error::Error>>(())
//! ```
//!
//! > **macOS note:** querying drives requires claiming exclusive access, which
//! > unmounts the disc. Releasing it triggers a remount that hands control to
//! > the default app (usually Apple Music). Use `open_default` or `open` with a
//! > known path instead of `list_drives` on macOS.
//!
//! ## Reading ToC
//!
//! Each audio CD carries a Table of Contents with the block address of every
Expand Down Expand Up @@ -203,12 +198,8 @@ pub struct Toc {
/// directly, it implements `Drop` trait, so that the CD drive handle is properly closed.
///
/// Please note that you should not read multiple CDs at the same time, and preferably do
/// not use it in multiple threads. CD drives are a physical thing and they really want to
/// have exclusive access, because of that currently only sequential access is supported.
///
/// This is especially true on macOS, where releasing exclusive lock on the audio CD will
/// cause it to remount, and the default application (very likely Apple Music) will get
/// the exclusive access and it will be challenging to implement a reliable waiting strategy.
/// not use it in multiple threads. CD drives are physical devices, so currently only
/// sequential access is properly tested and supported.
pub struct CdReader {}

impl CdReader {
Expand All @@ -217,10 +208,8 @@ impl CdReader {
/// It is crucial to call this function and not to create the Reader
/// by yourself, as each OS needs its own way of handling the drive access.
///
/// You don't need to close the drive, it will be handled automatically
/// when the `CdReader` is dropped. On macOS, that will cause the CD drive
/// to be remounted, and the default application (like Apple Music) will
/// be called.
/// You don't need to close the drive; it will be handled automatically
/// when the `CdReader` is dropped.
///
/// # Arguments
///
Expand Down
130 changes: 24 additions & 106 deletions src/mac/audio_reader.c
Original file line number Diff line number Diff line change
@@ -1,55 +1,12 @@
#include "shim_common.h"

static uint8_t task_status_to_scsi_status(SCSITaskStatus status) {
switch (status) {
case kSCSITaskStatus_GOOD: return 0x00;
case kSCSITaskStatus_CHECK_CONDITION: return 0x02;
case kSCSITaskStatus_BUSY: return 0x08;
case kSCSITaskStatus_RESERVATION_CONFLICT: return 0x18;
case kSCSITaskStatus_TASK_SET_FULL: return 0x28;
case kSCSITaskStatus_ACA_ACTIVE: return 0x30;
default: return 0xFF;
}
}

static void fill_scsi_error(CdScsiError *outErr, kern_return_t ex, SCSITaskStatus status, SCSI_Sense_Data *sense) {
if (!outErr) return;

outErr->has_scsi_error = 1;
outErr->exec_error = (uint32_t)ex;
outErr->task_status = (uint32_t)status;
outErr->scsi_status = task_status_to_scsi_status(status);

const uint8_t *sense_bytes = (const uint8_t *)sense;
bool has_sense = false;
for (size_t i = 0; i < sizeof(SCSI_Sense_Data); i++) {
if (sense_bytes[i] != 0) {
has_sense = true;
break;
}
}

outErr->has_sense = has_sense ? 1 : 0;
if (has_sense && sizeof(SCSI_Sense_Data) >= 14) {
outErr->sense_key = sense_bytes[2] & 0x0F;
outErr->asc = sense_bytes[12];
outErr->ascq = sense_bytes[13];
}
}

bool read_cd_audio(uint32_t lba, uint32_t sectors, uint8_t **outBuf, uint32_t *outLen, CdScsiError *outErr) {
*outBuf = NULL;
*outLen = 0;
if (outErr) {
memset(outErr, 0, sizeof(CdScsiError));
}

SCSITaskDeviceInterface **dev = globalDev;
if (!dev) {
fprintf(stderr, "[READ] Device session is not open\n");
goto fail;
}

const uint32_t SECTOR_SZ = 2352;
if (sectors == 0) {
fprintf(stderr, "[READ] sectors == 0\n");
Expand All @@ -69,76 +26,37 @@ bool read_cd_audio(uint32_t lba, uint32_t sectors, uint8_t **outBuf, uint32_t *o
goto fail;
}

const uint32_t MAX_SECTORS_PER_CMD = 27;

uint32_t remaining = sectors;
uint32_t curLBA = lba;
uint32_t written = 0;

while (remaining > 0) {
uint32_t xfer = (remaining > MAX_SECTORS_PER_CMD) ? MAX_SECTORS_PER_CMD : remaining;
uint32_t bytes = xfer * SECTOR_SZ;

// READ CD (0xBE) 12-byte CDB for CD-DA.
uint8_t cdb[12] = {0};
cdb[0] = 0xBE;
cdb[1] = 0x00;
cdb[2] = (uint8_t)((curLBA >> 24) & 0xFF);
cdb[3] = (uint8_t)((curLBA >> 16) & 0xFF);
cdb[4] = (uint8_t)((curLBA >> 8) & 0xFF);
cdb[5] = (uint8_t)((curLBA >> 0) & 0xFF);
cdb[6] = (uint8_t)((xfer >> 16) & 0xFF);
cdb[7] = (uint8_t)((xfer >> 8) & 0xFF);
cdb[8] = (uint8_t)((xfer >> 0) & 0xFF);
cdb[9] = 0x10; // USER DATA only (2352 bytes/sector)
cdb[10] = 0x00;
cdb[11] = 0x00;

SCSITaskInterface **task = (*dev)->CreateSCSITask(dev);
if (!task) {
fprintf(stderr, "[READ] CreateSCSITask failed\n");
free(dst);
goto fail;
}

IOVirtualRange vr = {0};
vr.address = (IOVirtualAddress)(dst + written);
vr.length = bytes;

if ((*task)->SetCommandDescriptorBlock(task, cdb, sizeof(cdb)) != kIOReturnSuccess) {
fprintf(stderr, "[READ] SetCommandDescriptorBlock failed\n");
(*task)->Release(task);
free(dst);
goto fail;
}
int fd = open_cd_raw_device();
if (fd < 0) {
fprintf(stderr, "[READ] open raw CD device failed (errno=%d)\n", errno);
free(dst);
goto fail;
}

// dir=2 means from device in SCSITaskLib.
if ((*task)->SetScatterGatherEntries(task, &vr, 1, bytes, 2) != kIOReturnSuccess) {
fprintf(stderr, "[READ] SetScatterGatherEntries failed\n");
(*task)->Release(task);
free(dst);
goto fail;
}
dk_cd_read_t read = {0};
read.offset = (uint64_t)lba * (uint64_t)SECTOR_SZ;
read.sectorArea = kCDSectorAreaUser;
read.sectorType = kCDSectorTypeCDDA;
read.bufferLength = totalBytes;
read.buffer = dst;

SCSI_Sense_Data sense = {0};
SCSITaskStatus status = kSCSITaskStatus_No_Status;
kern_return_t ex = (*task)->ExecuteTaskSync(task, &sense, &status, NULL);
(*task)->Release(task);
int ret = ioctl(fd, DKIOCCDREAD, &read);
close(fd);

if (ex != kIOReturnSuccess || status != kSCSITaskStatus_GOOD) {
fill_scsi_error(outErr, ex, status, &sense);
fprintf(stderr, "[READ] ExecuteTaskSync failed (ex=0x%x, status=%u)\n", ex, status);
free(dst);
goto fail;
}
if (ret < 0) {
fprintf(stderr, "[READ] DKIOCCDREAD failed (errno=%d)\n", errno);
free(dst);
goto fail;
}

written += bytes;
curLBA += xfer;
remaining -= xfer;
if (read.bufferLength != totalBytes) {
fprintf(stderr, "[READ] short read: requested=%u actual=%u\n", totalBytes, read.bufferLength);
free(dst);
goto fail;
}

*outBuf = dst;
*outLen = written;
*outLen = totalBytes;

return true;

Expand Down
84 changes: 0 additions & 84 deletions src/mac/da_guard.c

This file was deleted.

Loading
Loading