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