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