1mod common;
36
37use std::error::Error;
38use std::fs::File;
39use std::io;
40use std::path::{Path, PathBuf};
41
42use cd_da_reader::{
43 AudioSectorReader, Toc, Track, TrackBounds, create_wav, lba_to_msf,
44 open_track_stream_with_bounds, read_track_with_bounds,
45};
46
47const SECTOR_SIZE: usize = 2352;
49const SECTORS_PER_SECOND: u32 = 75;
50
51struct BinImage {
57 bin: File,
58}
59
60impl AudioSectorReader for BinImage {
61 type Error = io::Error;
62
63 fn read_audio_sectors(&self, start_lba: u32, count: u32) -> Result<Vec<u8>, Self::Error> {
64 let mut buffer = vec![0u8; count as usize * SECTOR_SIZE];
65 read_exact_at(
66 &self.bin,
67 &mut buffer,
68 start_lba as u64 * SECTOR_SIZE as u64,
69 )?;
70 Ok(buffer)
71 }
72}
73
74fn read_exact_at(file: &File, buffer: &mut [u8], offset: u64) -> io::Result<()> {
77 #[cfg(unix)]
78 {
79 use std::os::unix::fs::FileExt;
80 file.read_exact_at(buffer, offset)
81 }
82
83 #[cfg(windows)]
84 {
85 use std::os::windows::fs::FileExt;
86
87 let mut filled = 0;
88 while filled < buffer.len() {
89 match file.seek_read(&mut buffer[filled..], offset + filled as u64) {
90 Ok(0) => {
91 return Err(io::Error::new(
92 io::ErrorKind::UnexpectedEof,
93 "read past the end of the .bin",
94 ));
95 }
96 Ok(read) => filled += read,
97 Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
98 Err(e) => return Err(e),
99 }
100 }
101 Ok(())
102 }
103}
104
105fn main() -> Result<(), Box<dyn Error>> {
106 let output_dir = common::fresh_output_dir("bin_cue_backend")?;
107
108 let cue_path = match std::env::args().nth(1) {
109 Some(path) => PathBuf::from(path),
110 None => {
111 println!("No .cue argument given — writing a demo image to work against.\n");
112 write_demo_image(&output_dir)?
113 }
114 };
115
116 let (bin_path, toc) = parse_cue(&cue_path)?;
117 println!("Cue: {}", cue_path.display());
118 println!("Bin: {}", bin_path.display());
119 println!(
120 "Toc: {} tracks, leadout at LBA {}\n",
121 toc.tracks.len(),
122 toc.leadout_lba
123 );
124
125 let image = BinImage {
126 bin: File::open(&bin_path)?,
127 };
128
129 let bounds = TrackBounds::Gapless;
132 explain_bounds(&image, &toc)?;
133
134 for track in toc.tracks.iter().filter(|track| track.is_audio) {
135 let pcm = read_track_with_bounds(&image, &toc, track.number, bounds)?;
136 let wav_path = output_dir.join(format!("track{:02}.wav", track.number));
137 std::fs::write(&wav_path, create_wav(pcm))?;
138
139 println!("track {:02}: wrote {}", track.number, wav_path.display());
140 }
141
142 if let Some(first_audio) = toc.tracks.iter().find(|track| track.is_audio) {
145 let mut stream = open_track_stream_with_bounds(&image, &toc, first_audio.number, bounds)?;
146 let (mut chunks, mut bytes) = (0u32, 0usize);
147 while let Some(chunk) = stream.next_chunk()? {
148 chunks += 1;
149 bytes += chunk.len();
150 }
151 println!(
152 "\nstreamed track {:02}: {bytes} bytes in {chunks} chunks ({:.1}s)",
153 first_audio.number,
154 stream.total_seconds()
155 );
156 }
157
158 Ok(())
159}
160
161fn explain_bounds(image: &BinImage, toc: &Toc) -> Result<(), Box<dyn Error>> {
166 let Some(track) = last_audio_before_data(toc) else {
167 return Ok(());
168 };
169
170 let gapless =
171 open_track_stream_with_bounds(image, toc, track, TrackBounds::Gapless)?.total_sectors();
172 let physical = match open_track_stream_with_bounds(image, toc, track, TrackBounds::SessionGap) {
173 Ok(stream) => format!("{} sectors", stream.total_sectors()),
174 Err(e) => format!("fails ({e})"),
176 };
177
178 println!(
179 "Track {track:02} is the last audio track before a data track, the one track \
180 the two bounds policies disagree on:\n \
181 Gapless (used here): {gapless} sectors\n \
182 SessionGap: {physical}\n"
183 );
184 Ok(())
185}
186
187fn last_audio_before_data(toc: &Toc) -> Option<u8> {
188 let last_audio = toc.tracks.iter().rposition(|track| track.is_audio)?;
189 let has_trailing_data = last_audio + 1 < toc.tracks.len();
190
191 has_trailing_data.then(|| toc.tracks[last_audio].number)
192}
193
194fn parse_cue(cue_path: &Path) -> Result<(PathBuf, Toc), Box<dyn Error>> {
210 let text = std::fs::read_to_string(cue_path)?;
211 let cue_dir = cue_path.parent().unwrap_or(Path::new("."));
212
213 let mut bin_path: Option<PathBuf> = None;
214 let mut pending: Option<(u8, bool)> = None;
215 let mut tracks: Vec<Track> = Vec::new();
216
217 for line in text.lines() {
218 let line = line.trim();
219 let mut fields = line.split_whitespace();
220 let Some(keyword) = fields.next() else {
221 continue;
222 };
223
224 match keyword.to_ascii_uppercase().as_str() {
225 "FILE" => {
226 if bin_path.is_some() {
227 return Err(
228 "multi-FILE cue sheets (one file per track) are not handled by \
229 this example; it assumes a single .bin addressed by sector"
230 .into(),
231 );
232 }
233 bin_path = Some(cue_dir.join(quoted_or_first_field(line)?));
234 }
235
236 "TRACK" => {
237 let number: u8 = fields.next().ok_or("TRACK line has no number")?.parse()?;
239 let mode = fields.next().ok_or("TRACK line has no mode")?;
240
241 let sector_size = mode_sector_size(mode);
245 if sector_size != Some(SECTOR_SIZE) {
246 return Err(format!(
247 "track {number} is `{mode}`, which is not stored as 2352-byte sectors; \
248 this example needs a fully raw image"
249 )
250 .into());
251 }
252
253 pending = Some((number, mode.eq_ignore_ascii_case("AUDIO")));
254 }
255
256 "INDEX" => {
258 let index = fields.next().ok_or("INDEX line has no number")?;
259 let msf = fields.next().ok_or("INDEX line has no timestamp")?;
260 if index != "01" {
261 continue;
262 }
263
264 let (number, is_audio) = pending
265 .take()
266 .ok_or("INDEX 01 appeared before any TRACK line")?;
267 let start_lba = msf_to_frames(msf)?;
268
269 tracks.push(Track {
270 number,
271 start_lba,
272 start_msf: lba_to_msf(start_lba),
273 is_audio,
274 });
275 }
276
277 _ => {}
278 }
279 }
280
281 let bin_path = bin_path.ok_or("cue sheet has no FILE line")?;
282 if tracks.is_empty() {
283 return Err("cue sheet declares no tracks".into());
284 }
285 tracks.sort_by_key(|track| track.start_lba);
286
287 let bin_bytes = std::fs::metadata(&bin_path)
289 .map_err(|e| format!("cannot open {}: {e}", bin_path.display()))?
290 .len();
291 let leadout_lba = (bin_bytes / SECTOR_SIZE as u64) as u32;
292
293 let toc = Toc {
294 first_track: tracks.first().map_or(1, |track| track.number),
295 last_track: tracks.last().map_or(1, |track| track.number),
296 tracks,
297 leadout_lba,
298 };
299
300 Ok((bin_path, toc))
301}
302
303fn mode_sector_size(mode: &str) -> Option<usize> {
307 if mode.eq_ignore_ascii_case("AUDIO") {
308 return Some(SECTOR_SIZE);
309 }
310
311 mode.split_once('/').and_then(|(_, size)| size.parse().ok())
312}
313
314fn msf_to_frames(msf: &str) -> Result<u32, Box<dyn Error>> {
317 let parts: Vec<&str> = msf.split(':').collect();
318 let [minutes, seconds, frames] = parts[..] else {
319 return Err(format!("expected a MM:SS:FF timestamp, got `{msf}`").into());
320 };
321
322 let minutes: u32 = minutes.parse()?;
323 let seconds: u32 = seconds.parse()?;
324 let frames: u32 = frames.parse()?;
325
326 Ok(((minutes * 60) + seconds) * SECTORS_PER_SECOND + frames)
327}
328
329fn quoted_or_first_field(line: &str) -> Result<String, Box<dyn Error>> {
332 if let Some((_, rest)) = line.split_once('"') {
333 return rest
334 .rsplit_once('"')
335 .map(|(name, _)| name.to_string())
336 .ok_or_else(|| "FILE line has an unterminated quote".into());
337 }
338
339 line.split_whitespace()
340 .nth(1)
341 .map(str::to_string)
342 .ok_or_else(|| "FILE line has no filename".into())
343}
344
345fn write_demo_image(output_dir: &Path) -> Result<PathBuf, Box<dyn Error>> {
351 let layout = [
352 (1u8, "AUDIO", 2 * SECTORS_PER_SECOND, Some(440.0)),
353 (2, "AUDIO", 2 * SECTORS_PER_SECOND, Some(523.25)),
354 (3, "MODE1/2352", SECTORS_PER_SECOND, None),
355 ];
356
357 let mut bin = Vec::new();
358 let mut cue = String::from("FILE \"demo.bin\" BINARY\n");
359 let mut start_lba = 0;
360
361 for (number, mode, sectors, tone_hz) in layout {
362 cue += &format!(
363 " TRACK {number:02} {mode}\n INDEX 01 {}\n",
364 frames_to_msf(start_lba)
365 );
366
367 match tone_hz {
368 Some(hz) => bin.extend_from_slice(&tone(sectors, hz)),
369 None => bin.resize(bin.len() + sectors as usize * SECTOR_SIZE, 0xAA),
372 }
373 start_lba += sectors;
374 }
375
376 let cue_path = output_dir.join("demo.cue");
377 std::fs::write(output_dir.join("demo.bin"), &bin)?;
378 std::fs::write(&cue_path, cue)?;
379
380 Ok(cue_path)
381}
382
383fn tone(sectors: u32, hz: f32) -> Vec<u8> {
385 let frames = sectors as usize * 588;
387 let mut pcm = Vec::with_capacity(frames * 4);
388
389 for frame in 0..frames {
390 let t = frame as f32 / 44_100.0;
391 let sample = ((t * hz * std::f32::consts::TAU).sin() * 8_000.0) as i16;
392 pcm.extend_from_slice(&sample.to_le_bytes()); pcm.extend_from_slice(&sample.to_le_bytes()); }
395
396 pcm
397}
398
399fn frames_to_msf(frames: u32) -> String {
401 let seconds = frames / SECTORS_PER_SECOND;
402 format!(
403 "{:02}:{:02}:{:02}",
404 seconds / 60,
405 seconds % 60,
406 frames % SECTORS_PER_SECOND
407 )
408}