Skip to main content

cd_da_reader/
lib.rs

1//! # CD-DA (or audio CD) reading library
2//!
3//! This library provides cross-platform audio CD reading capability,
4//! it works on Windows, macOS and Linux.
5//! It is intended to be a low-level library, and only allows you read
6//! TOC and tracks, and you need to provide valid CD drive name.
7//! Currently, the functionality is very basic, and there is no way to
8//! specify subchannel info, access hidden track or read CD text.
9//!
10//! The library works by issuing direct SCSI commands.
11//!
12//! ## Example
13//!
14//! ```
15//! use cd_da_reader::CdReader;
16//!
17//! fn read_cd() -> Result<(), Box<dyn std::error::Error>> {
18//!   let reader = CdReader::open(r"\\.\E:")?;
19//!   let toc = reader.read_toc()?;
20//!   println!("{:#?}", toc);
21//!   let data = reader.read_track(&toc, 11)?;
22//!   let wav_track = CdReader::create_wav(data);
23//!   std::fs::write("myfile.wav", wav_track)?;
24//!   Ok(())
25//! }
26//! ```
27//!
28//! This function reads an audio CD on Windows, you can check your drive letter
29//! in the File Explorer. On macOS, you can run `diskutil list` and look for the
30//! Audio CD in the list (it should be something like "disk4"), and on Linux you
31//! can check it using `cat /proc/sys/dev/cdrom/info`, it will be like "/dev/sr0".
32//!
33//! ## Metadata
34//!
35//! This library does not provide any direct metadata, and audio CDs typically do
36//! not carry it by themselves. To obtain it, you'd need to get it from a place like
37//! [MusicBrainz](https://musicbrainz.org/). You should have all necessary information
38//! in the TOC struct to calculate the audio CD ID.
39#[cfg(target_os = "linux")]
40mod linux;
41#[cfg(target_os = "macos")]
42mod macos;
43#[cfg(target_os = "windows")]
44mod windows;
45
46mod discovery;
47mod errors;
48mod retry;
49mod stream;
50mod utils;
51pub use discovery::DriveInfo;
52pub use errors::{CdReaderError, ScsiError, ScsiOp};
53pub use retry::RetryConfig;
54pub use stream::{TrackStream, TrackStreamConfig};
55
56mod parse_toc;
57
58#[cfg(target_os = "windows")]
59mod windows_read_track;
60
61/// Representation of the track from TOC, purely in terms of data location on the CD.
62#[derive(Debug)]
63pub struct Track {
64    /// Track number from the Table of Contents (read from the CD itself).
65    /// It usually starts with 1, but you should read this value directly when
66    /// reading raw track data. There might be gaps, and also in the future
67    /// there might be hidden track support, which will be located at number 0.
68    pub number: u8,
69    /// starting offset, unnecessary to use directly
70    pub start_lba: u32,
71    /// starting offset, but in (minute, second, frame) format
72    pub start_msf: (u8, u8, u8),
73    pub is_audio: bool,
74}
75
76/// Table of Contents, read directly from the Audio CD. The most important part
77/// is the `tracks` vector, which allows you to read raw track data.
78#[derive(Debug)]
79pub struct Toc {
80    /// Helper value with the first track number
81    pub first_track: u8,
82    /// Helper value with the last track number. You should not use it directly to
83    /// iterate over all available tracks, as there might be gaps.
84    pub last_track: u8,
85    /// List of tracks with LBA and MSF offsets
86    pub tracks: Vec<Track>,
87    /// Used to calculate number of sectors for the last track. You'll also need this
88    /// in order to calculate MusicBrainz ID.
89    pub leadout_lba: u32,
90}
91
92/// Helper struct to interact with the audio CD. While it doesn't hold any internal data
93/// directly, it implements `Drop` trait, so that the CD drive handle is properly closed.
94///
95/// Please note that you should not read multiple CDs at the same time, and preferably do
96/// not use it in multiple threads. CD drives are a physical thing and they really want to
97/// have exclusive access, because of that currently only sequential access is supported.
98///
99/// This is especially true on macOS, where releasing exclusive lock on the audio CD will
100/// cause it to remount, and the default application (very likely Apple Music) will get
101/// the exclusive access and it will be challenging to implement a reliable waiting strategy.
102pub struct CdReader {}
103
104impl CdReader {
105    /// Opens a CD drive at the specified path in order to read data.
106    ///
107    /// It is crucial to call this function and not to create the Reader
108    /// by yourself, as each OS needs its own way of handling the drive access.
109    ///
110    /// You don't need to close the drive, it will be handled automatically
111    /// when the `CdReader` is dropped. On macOS, that will cause the CD drive
112    /// to be remounted, and the default application (like Apple Music) will
113    /// be called.
114    ///
115    /// # Arguments
116    ///
117    /// * `path` - The device path (e.g., "/dev/sr0" on Linux, "disk6" on macOS, and r"\\.\E:" on Windows)
118    ///
119    /// # Errors
120    ///
121    /// Returns an error if the drive cannot be opened
122    pub fn open(path: &str) -> std::io::Result<Self> {
123        #[cfg(target_os = "windows")]
124        {
125            windows::open_drive(path)?;
126            Ok(Self {})
127        }
128
129        #[cfg(target_os = "macos")]
130        {
131            macos::open_drive(path)?;
132            Ok(Self {})
133        }
134
135        #[cfg(target_os = "linux")]
136        {
137            linux::open_drive(path)?;
138            Ok(Self {})
139        }
140
141        #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
142        {
143            compile_error!("Unsupported platform")
144        }
145    }
146
147    /// While this is a low-level library and does not include any codecs to compress the audio,
148    /// it includes a helper function to convert raw PCM data into a wav file, which is done by
149    /// prepending a 44 RIFF bytes header
150    ///
151    /// # Arguments
152    ///
153    /// * `data` - vector of bytes received from `read_track` function
154    pub fn create_wav(data: Vec<u8>) -> Vec<u8> {
155        let mut header = utils::create_wav_header(data.len() as u32);
156        header.extend_from_slice(&data);
157        header
158    }
159
160    /// Read Table of Contents for the opened drive. You'll likely only need to access
161    /// `tracks` from the returned value in order to iterate and read each track's raw data.
162    /// Please note that each track in the vector has `number` property, which you should use
163    /// when calling `read_track`, as it doesn't start with 0. It is important to do so,
164    /// because in the future it might include 0 for the hidden track.
165    pub fn read_toc(&self) -> Result<Toc, CdReaderError> {
166        #[cfg(target_os = "windows")]
167        {
168            windows::read_toc()
169        }
170
171        #[cfg(target_os = "macos")]
172        {
173            macos::read_toc()
174        }
175
176        #[cfg(target_os = "linux")]
177        {
178            linux::read_toc()
179        }
180
181        #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
182        {
183            compile_error!("Unsupported platform")
184        }
185    }
186
187    /// Read raw data for the specified track number from the TOC.
188    /// It returns raw PCM data, but if you want to save it directly and make it playable,
189    /// wrap the result with `create_wav` function, that will prepend a RIFF header and
190    /// make it a proper music file.
191    pub fn read_track(&self, toc: &Toc, track_no: u8) -> Result<Vec<u8>, CdReaderError> {
192        self.read_track_with_retry(toc, track_no, &RetryConfig::default())
193    }
194
195    /// Read raw data for the specified track number from the TOC using explicit retry config.
196    pub fn read_track_with_retry(
197        &self,
198        toc: &Toc,
199        track_no: u8,
200        cfg: &RetryConfig,
201    ) -> Result<Vec<u8>, CdReaderError> {
202        #[cfg(target_os = "windows")]
203        {
204            windows::read_track_with_retry(toc, track_no, cfg)
205        }
206
207        #[cfg(target_os = "macos")]
208        {
209            macos::read_track_with_retry(toc, track_no, cfg)
210        }
211
212        #[cfg(target_os = "linux")]
213        {
214            linux::read_track_with_retry(toc, track_no, cfg)
215        }
216
217        #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
218        {
219            compile_error!("Unsupported platform")
220        }
221    }
222
223    pub(crate) fn read_sectors_with_retry(
224        &self,
225        start_lba: u32,
226        sectors: u32,
227        cfg: &RetryConfig,
228    ) -> Result<Vec<u8>, CdReaderError> {
229        #[cfg(target_os = "windows")]
230        {
231            windows::read_sectors_with_retry(start_lba, sectors, cfg)
232        }
233
234        #[cfg(target_os = "macos")]
235        {
236            macos::read_sectors_with_retry(start_lba, sectors, cfg)
237        }
238
239        #[cfg(target_os = "linux")]
240        {
241            linux::read_sectors_with_retry(start_lba, sectors, cfg)
242        }
243
244        #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
245        {
246            compile_error!("Unsupported platform")
247        }
248    }
249}
250
251impl Drop for CdReader {
252    fn drop(&mut self) {
253        #[cfg(target_os = "windows")]
254        {
255            windows::close_drive();
256        }
257
258        #[cfg(target_os = "macos")]
259        {
260            macos::close_drive();
261        }
262
263        #[cfg(target_os = "linux")]
264        {
265            linux::close_drive();
266        }
267    }
268}