Skip to main content

cd_da_reader/
lib.rs

1//! # CD-DA (audio CD) reading library
2//!
3//! This library provides cross-platform audio CD reading capabilities
4//! (tested on Windows, macOS and Linux). It was written to enable CD ripping,
5//! but you can also implement a live audio CD player with its help.
6//! The library works through platform CD-drive APIs on macOS and issuing direct
7//! SCSI commands on Windows and Linux and abstracts both access to the CD drive
8//! and reading the actual data from it, so you don't deal with the hardware directly.
9//!
10//! All operations happen in this order:
11//!
12//! 1. Get a CD drive's handle
13//! 2. Read the ToC (table of contents) of the audio CD
14//! 3. Read track data using ranges from the ToC
15//!
16//! ## CD access
17//!
18//! The easiest way to open a drive is to use [`CdReader::open_default`], which scans
19//! all drives and opens the first one that contains an audio CD:
20//!
21//! ```no_run
22//! use cd_da_reader::CdReader;
23//!
24//! let reader = CdReader::open_default()?;
25//! # Ok::<(), Box<dyn std::error::Error>>(())
26//! ```
27//!
28//! If you need to pick a specific drive, use [`CdReader::list_drives`] followed
29//! by calling [`CdReader::open`] with the specific drive:
30//!
31//! ```no_run
32//! use cd_da_reader::CdReader;
33//!
34//! // Windows / Linux: enumerate drives and inspect the has_audio_cd field
35//! let drives = CdReader::list_drives()?;
36//!
37//! // Any platform: open a known path directly
38//! // Windows:  r"\\.\E:"
39//! // macOS:    "disk6"
40//! // Linux:    "/dev/sr0"
41//! let reader = CdReader::open("disk6")?;
42//! # Ok::<(), Box<dyn std::error::Error>>(())
43//! ```
44//!
45//! ## Reading ToC
46//!
47//! Each audio CD carries a Table of Contents with the block address of every
48//! track. You need to read it first before issuing any track read commands:
49//!
50//! ```no_run
51//! use cd_da_reader::CdReader;
52//!
53//! let reader = CdReader::open_default()?;
54//! let toc = reader.read_toc()?;
55//! # Ok::<(), Box<dyn std::error::Error>>(())
56//! ```
57//!
58//! The returned [`Toc`] contains a [`Vec<Track>`](Track) where each entry has
59//! two equivalent address fields:
60//!
61//! - **`start_lba`** -- Logical Block Address, which is a sector index.
62//!   LBA 0 is the first readable sector after the 2-second lead-in pre-gap.
63//!   This is the format used internally for read commands.
64//! - **`start_msf`** โ€” Minutes/Seconds/Frames, a time-based address inherited
65//!   from the physical disc layout. A "frame" is one sector; the spec defines
66//!   75 frames per second. MSF includes a fixed 2-second (150-frame) lead-in
67//!   offset, so `(0, 2, 0)` corresponds to LBA 0. You can convert between them easily:
68//!   `LBA + 150 = total frames`, then divide by 75 and 60 for M/S/F.
69//!
70//! ## Reading tracks
71//!
72//! Pass the [`Toc`] and a track number to [`CdReader::read_track`]. The
73//! library calculates the sector boundaries automatically. On CD-Extra discs
74//! where the last audio track is followed only by data tracks, the trailing
75//! audio/data session gap is excluded from the audio read.
76//!
77//! ```no_run
78//! use cd_da_reader::CdReader;
79//!
80//! let reader = CdReader::open_default()?;
81//! let toc = reader.read_toc()?;
82//! let data = reader.read_track(&toc, 1)?; // we assume track #1 exists and is audio
83//! # Ok::<(), Box<dyn std::error::Error>>(())
84//! ```
85//!
86//! This is a blocking call. For a live-playback or progress-reporting use case,
87//! use the streaming API instead:
88//!
89//! ```no_run
90//! use cd_da_reader::{CdReader, RetryConfig, TrackStreamConfig};
91//!
92//! let reader = CdReader::open_default()?;
93//! let toc = reader.read_toc()?;
94//!
95//! let cfg = TrackStreamConfig {
96//!     sectors_per_chunk: 27, // ~64 KB per chunk
97//!     retry: RetryConfig::default(),
98//! };
99//!
100//! let mut stream = reader.open_track_stream(&toc, 1, cfg)?;
101//! while let Some(chunk) = stream.next_chunk()? {
102//!     // process chunk โ€” raw PCM, 2 352 bytes per sector
103//! }
104//! # Ok::<(), Box<dyn std::error::Error>>(())
105//! ```
106//!
107//! ## Track format
108//!
109//! Track data is raw [PCM](https://en.wikipedia.org/wiki/Pulse-code_modulation),
110//! the same format used inside WAV files. Audio CDs use 16-bit stereo PCM
111//! sampled at 44 100 Hz:
112//!
113//! ```text
114//! 44 100 samples * 2 channels * 2 bytes = 176 400 bytes/second
115//! ```
116//!
117//! Each sector holds exactly 2 352 bytes (176 400 รท 75 = 2 352), that's where
118//! 75 sectors per second comes from. A typical 3-minute track is
119//! ~31 MB; a full 74-minute CD is ~650 MB.
120//!
121//! Converting raw PCM to a playable WAV file only requires prepending a 44-byte
122//! RIFF header โ€” [`CdReader::create_wav`] does exactly that:
123//!
124//! ```no_run
125//! use cd_da_reader::CdReader;
126//!
127//! let reader = CdReader::open_default()?;
128//! let toc = reader.read_toc()?;
129//! let data = reader.read_track(&toc, 1)?;
130//! let wav = CdReader::create_wav(data);
131//! std::fs::write("track01.wav", wav)?;
132//! # Ok::<(), Box<dyn std::error::Error>>(())
133//! ```
134//!
135//! ## Metadata
136//!
137//! Audio CDs carry almost no semantic metadata. [CD-TEXT] exists but is
138//! unreliable and because of that is not provided by this lbirary. The practical approach is to
139//! calculate a Disc ID from the ToC and look it up on a service such as
140//! [MusicBrainz]. The [`Toc`] struct exposes everything required for the
141//! [MusicBrainz disc ID algorithm].
142//!
143//! [CD-TEXT]: https://en.wikipedia.org/wiki/CD-Text
144//! [MusicBrainz]: https://musicbrainz.org/
145//! [MusicBrainz disc ID algorithm]: https://musicbrainz.org/doc/Disc_ID_Calculation
146#[cfg(target_os = "linux")]
147mod linux;
148#[cfg(target_os = "macos")]
149mod macos;
150#[cfg(target_os = "windows")]
151mod windows;
152
153mod discovery;
154mod errors;
155mod retry;
156mod stream;
157mod utils;
158pub use discovery::DriveInfo;
159pub use errors::{CdReaderError, ScsiError, ScsiOp};
160pub use retry::RetryConfig;
161pub use stream::{TrackStream, TrackStreamConfig};
162
163mod parse_toc;
164
165#[cfg(target_os = "windows")]
166mod windows_read_track;
167
168/// Representation of the track from TOC, purely in terms of data location on the CD.
169#[derive(Debug)]
170pub struct Track {
171    /// Track number from the Table of Contents (read from the CD itself).
172    /// It usually starts with 1, but you should read this value directly when
173    /// reading raw track data. There might be gaps, and also in the future
174    /// there might be hidden track support, which will be located at number 0.
175    pub number: u8,
176    /// starting offset, unnecessary to use directly
177    pub start_lba: u32,
178    /// starting offset, but in (minute, second, frame) format
179    pub start_msf: (u8, u8, u8),
180    pub is_audio: bool,
181}
182
183/// Table of Contents, read directly from the Audio CD. The most important part
184/// is the `tracks` vector, which allows you to read raw track data.
185#[derive(Debug)]
186pub struct Toc {
187    /// Helper value with the first track number
188    pub first_track: u8,
189    /// Helper value with the last track number. You should not use it directly to
190    /// iterate over all available tracks, as there might be gaps.
191    pub last_track: u8,
192    /// List of tracks with LBA and MSF offsets
193    pub tracks: Vec<Track>,
194    /// Lead-out LBA reported by the drive for the disc TOC. You'll also need this
195    /// in order to calculate MusicBrainz ID.
196    pub leadout_lba: u32,
197}
198
199/// Helper struct to interact with the audio CD. While it doesn't hold any internal data
200/// directly, it implements `Drop` trait, so that the CD drive handle is properly closed.
201///
202/// Please note that you should not read multiple CDs at the same time, and preferably do
203/// not use it in multiple threads. CD drives are physical devices, so currently only
204/// sequential access is properly tested and supported.
205pub struct CdReader {}
206
207impl CdReader {
208    /// Opens a CD drive at the specified path in order to read data.
209    ///
210    /// It is crucial to call this function and not to create the Reader
211    /// by yourself, as each OS needs its own way of handling the drive access.
212    ///
213    /// You don't need to close the drive; it will be handled automatically
214    /// when the `CdReader` is dropped.
215    ///
216    /// # Arguments
217    ///
218    /// * `path` - The device path (e.g., "/dev/sr0" on Linux, "disk6" on macOS, and r"\\.\E:" on Windows)
219    ///
220    /// # Errors
221    ///
222    /// Returns an error if the drive cannot be opened
223    pub fn open(path: &str) -> std::io::Result<Self> {
224        #[cfg(target_os = "windows")]
225        {
226            windows::open_drive(path)?;
227            Ok(Self {})
228        }
229
230        #[cfg(target_os = "macos")]
231        {
232            macos::open_drive(path)?;
233            Ok(Self {})
234        }
235
236        #[cfg(target_os = "linux")]
237        {
238            linux::open_drive(path)?;
239            Ok(Self {})
240        }
241
242        #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
243        {
244            compile_error!("Unsupported platform")
245        }
246    }
247
248    /// While this is a low-level library and does not include any codecs to compress the audio,
249    /// it includes a helper function to convert raw PCM data into a wav file, which is done by
250    /// prepending a 44 RIFF bytes header
251    ///
252    /// # Arguments
253    ///
254    /// * `data` - vector of bytes received from `read_track` function
255    pub fn create_wav(data: Vec<u8>) -> Vec<u8> {
256        let mut header = utils::create_wav_header(data.len() as u32);
257        header.extend_from_slice(&data);
258        header
259    }
260
261    /// Read Table of Contents for the opened drive. You'll likely only need to access
262    /// `tracks` from the returned value in order to iterate and read each track's raw data.
263    /// Please note that each track in the vector has `number` property, which you should use
264    /// when calling `read_track`, as it doesn't start with 0. It is important to do so,
265    /// because in the future it might include 0 for the hidden track.
266    pub fn read_toc(&self) -> Result<Toc, CdReaderError> {
267        #[cfg(target_os = "windows")]
268        {
269            windows::read_toc()
270        }
271
272        #[cfg(target_os = "macos")]
273        {
274            macos::read_toc()
275        }
276
277        #[cfg(target_os = "linux")]
278        {
279            linux::read_toc()
280        }
281
282        #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
283        {
284            compile_error!("Unsupported platform")
285        }
286    }
287
288    /// Read raw data for the specified track number from the TOC.
289    /// It returns raw PCM data, but if you want to save it directly and make it playable,
290    /// wrap the result with `create_wav` function, that will prepend a RIFF header and
291    /// make it a proper music file.
292    pub fn read_track(&self, toc: &Toc, track_no: u8) -> Result<Vec<u8>, CdReaderError> {
293        self.read_track_with_retry(toc, track_no, &RetryConfig::default())
294    }
295
296    /// Read raw data for the specified track number from the TOC using explicit retry config.
297    pub fn read_track_with_retry(
298        &self,
299        toc: &Toc,
300        track_no: u8,
301        cfg: &RetryConfig,
302    ) -> Result<Vec<u8>, CdReaderError> {
303        #[cfg(target_os = "windows")]
304        {
305            windows::read_track_with_retry(toc, track_no, cfg)
306        }
307
308        #[cfg(target_os = "macos")]
309        {
310            macos::read_track_with_retry(toc, track_no, cfg)
311        }
312
313        #[cfg(target_os = "linux")]
314        {
315            linux::read_track_with_retry(toc, track_no, cfg)
316        }
317
318        #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
319        {
320            compile_error!("Unsupported platform")
321        }
322    }
323
324    pub(crate) fn read_sectors_with_retry(
325        &self,
326        start_lba: u32,
327        sectors: u32,
328        cfg: &RetryConfig,
329    ) -> Result<Vec<u8>, CdReaderError> {
330        #[cfg(target_os = "windows")]
331        {
332            windows::read_sectors_with_retry(start_lba, sectors, cfg)
333        }
334
335        #[cfg(target_os = "macos")]
336        {
337            macos::read_sectors_with_retry(start_lba, sectors, cfg)
338        }
339
340        #[cfg(target_os = "linux")]
341        {
342            linux::read_sectors_with_retry(start_lba, sectors, cfg)
343        }
344
345        #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
346        {
347            compile_error!("Unsupported platform")
348        }
349    }
350}
351
352impl Drop for CdReader {
353    fn drop(&mut self) {
354        #[cfg(target_os = "windows")]
355        {
356            windows::close_drive();
357        }
358
359        #[cfg(target_os = "macos")]
360        {
361            macos::close_drive();
362        }
363
364        #[cfg(target_os = "linux")]
365        {
366            linux::close_drive();
367        }
368    }
369}