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 utils;
50pub use discovery::DriveInfo;
51pub use errors::{CdReaderError, ScsiError, ScsiOp};
52pub use retry::RetryConfig;
53
54mod parse_toc;
55
56#[cfg(target_os = "windows")]
57mod windows_read_track;
58
59/// Representation of the track from TOC, purely in terms of data location on the CD.
60#[derive(Debug)]
61pub struct Track {
62    /// Track number from the Table of Contents (read from the CD itself).
63    /// It usually starts with 1, but you should read this value directly when
64    /// reading raw track data. There might be gaps, and also in the future
65    /// there might be hidden track support, which will be located at number 0.
66    pub number: u8,
67    /// starting offset, unnecessary to use directly
68    pub start_lba: u32,
69    /// starting offset, but in (minute, second, frame) format
70    pub start_msf: (u8, u8, u8),
71    pub is_audio: bool,
72}
73
74/// Table of Contents, read directly from the Audio CD. The most important part
75/// is the `tracks` vector, which allows you to read raw track data.
76#[derive(Debug)]
77pub struct Toc {
78    /// Helper value with the first track number
79    pub first_track: u8,
80    /// Helper value with the last track number. You should not use it directly to
81    /// iterate over all available tracks, as there might be gaps.
82    pub last_track: u8,
83    /// List of tracks with LBA and MSF offsets
84    pub tracks: Vec<Track>,
85    /// Used to calculate number of sectors for the last track. You'll also need this
86    /// in order to calculate MusicBrainz ID.
87    pub leadout_lba: u32,
88}
89
90/// Helper struct to interact with the audio CD. While it doesn't hold any internal data
91/// directly, it implements `Drop` trait, so that the CD drive handle is properly closed.
92///
93/// Please note that you should not read multiple CDs at the same time, and preferably do
94/// not use it in multiple threads. CD drives are a physical thing and they really want to
95/// have exclusive access, because of that currently only sequential access is supported.
96pub struct CdReader {}
97
98impl CdReader {
99    /// Opens a CD drive at the specified path in order to read data.
100    ///
101    /// It is crucial to call this function and not to create the Reader
102    /// by yourself, as each OS needs its own way of handling the drive acess.
103    ///
104    /// You don't need to close the drive, it will be handled automatically
105    /// when the `CdReader` is dropped. On macOS, that will cause the CD drive
106    /// to be remounted, and the default application (like Apple Music) will
107    /// be called.
108    ///
109    /// # Arguments
110    ///
111    /// * `path` - The device path (e.g., "/dev/sr0" on Linux, "disk6" on macOS, and r"\\.\E:" on Windows)
112    ///
113    /// # Errors
114    ///
115    /// Returns an error if the drive cannot be opened
116    pub fn open(path: &str) -> std::io::Result<Self> {
117        #[cfg(target_os = "windows")]
118        {
119            windows::open_drive(path)?;
120            Ok(Self {})
121        }
122
123        #[cfg(target_os = "macos")]
124        {
125            macos::open_drive(path)?;
126            Ok(Self {})
127        }
128
129        #[cfg(target_os = "linux")]
130        {
131            linux::open_drive(path)?;
132            Ok(Self {})
133        }
134
135        #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
136        {
137            compile_error!("Unsupported platform")
138        }
139    }
140
141    /// While this is a low-level library and does not include any codecs to compress the audio,
142    /// it includes a helper function to convert raw PCM data into a wav file, which is done by
143    /// prepending a 44 RIFF bytes header
144    ///
145    /// # Arguments
146    ///
147    /// * `data` - vector of bytes received from `read_track` function
148    pub fn create_wav(data: Vec<u8>) -> Vec<u8> {
149        let mut header = utils::create_wav_header(data.len() as u32);
150        header.extend_from_slice(&data);
151        header
152    }
153
154    /// Read Table of Contents for the opened drive. You'll likely only need to access
155    /// `tracks` from the returned value in order to iterate and read each track's raw data.
156    /// Please note that each track in the vector has `number` property, which you should use
157    /// when calling `read_track`, as it doesn't start with 0. It is important to do so,
158    /// because in the future it might include 0 for the hidden track.
159    pub fn read_toc(&self) -> Result<Toc, CdReaderError> {
160        #[cfg(target_os = "windows")]
161        {
162            windows::read_toc()
163        }
164
165        #[cfg(target_os = "macos")]
166        {
167            macos::read_toc()
168        }
169
170        #[cfg(target_os = "linux")]
171        {
172            linux::read_toc()
173        }
174
175        #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
176        {
177            compile_error!("Unsupported platform")
178        }
179    }
180
181    /// Read raw data for the specified track number from the TOC.
182    /// It returns raw PCM data, but if you want to save it directly and make it playable,
183    /// wrap the result with `create_wav` function, that will prepend a RIFF header and
184    /// make it a proper music file.
185    pub fn read_track(&self, toc: &Toc, track_no: u8) -> Result<Vec<u8>, CdReaderError> {
186        self.read_track_with_retry(toc, track_no, &RetryConfig::default())
187    }
188
189    /// Read raw data for the specified track number from the TOC using explicit retry config.
190    pub fn read_track_with_retry(
191        &self,
192        toc: &Toc,
193        track_no: u8,
194        cfg: &RetryConfig,
195    ) -> Result<Vec<u8>, CdReaderError> {
196        #[cfg(target_os = "windows")]
197        {
198            windows::read_track_with_retry(toc, track_no, cfg)
199        }
200
201        #[cfg(target_os = "macos")]
202        {
203            macos::read_track_with_retry(toc, track_no, cfg)
204        }
205
206        #[cfg(target_os = "linux")]
207        {
208            linux::read_track_with_retry(toc, track_no, cfg)
209        }
210
211        #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
212        {
213            compile_error!("Unsupported platform")
214        }
215    }
216}
217
218impl Drop for CdReader {
219    fn drop(&mut self) {
220        #[cfg(target_os = "windows")]
221        {
222            windows::close_drive();
223        }
224
225        #[cfg(target_os = "macos")]
226        {
227            macos::close_drive();
228        }
229
230        #[cfg(target_os = "linux")]
231        {
232            linux::close_drive();
233        }
234    }
235}