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}