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}