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}