use std::cmp::min;
use crate::{CdReader, CdReaderError, ReadOptions, Toc, utils};
impl AudioSectorReader for CdReader {
type Error = CdReaderError;
fn read_audio_sectors(&self, start_lba: u32, count: u32) -> Result<Vec<u8>, Self::Error> {
self.read_sector_range(start_lba, count, &ReadOptions::default())
}
}
pub trait AudioSectorReader {
type Error: std::error::Error + Send + Sync + 'static;
fn read_audio_sectors(&self, start_lba: u32, count: u32) -> Result<Vec<u8>, Self::Error>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TrackBounds {
SessionGap,
Gapless,
}
impl TrackBounds {
fn resolve(self, toc: &Toc, track_no: u8) -> std::io::Result<(u32, u32)> {
match self {
TrackBounds::SessionGap => utils::get_track_bounds(toc, track_no),
TrackBounds::Gapless => utils::get_gapless_track_bounds(toc, track_no),
}
}
}
pub fn read_track<R: AudioSectorReader>(
src: &R,
toc: &Toc,
track_no: u8,
) -> Result<Vec<u8>, CdReaderError> {
read_track_with_bounds(src, toc, track_no, TrackBounds::SessionGap)
}
pub fn read_track_with_bounds<R: AudioSectorReader>(
src: &R,
toc: &Toc,
track_no: u8,
bounds: TrackBounds,
) -> Result<Vec<u8>, CdReaderError> {
let (start_lba, sectors) = bounds.resolve(toc, track_no).map_err(CdReaderError::Io)?;
src.read_audio_sectors(start_lba, sectors)
.map_err(|e| CdReaderError::Backend(Box::new(e)))
}
pub struct AudioTrackStream<'a, R: AudioSectorReader> {
src: &'a R,
start_lba: u32,
next_lba: u32,
remaining_sectors: u32,
total_sectors: u32,
sectors_per_chunk: u32,
}
impl<'a, R: AudioSectorReader> AudioTrackStream<'a, R> {
const DEFAULT_SECTORS_PER_CHUNK: u32 = 27;
const SECTORS_PER_SECOND: f32 = 75.0;
fn new(src: &'a R, start_lba: u32, sectors: u32) -> Self {
Self {
src,
start_lba,
next_lba: start_lba,
remaining_sectors: sectors,
total_sectors: sectors,
sectors_per_chunk: Self::DEFAULT_SECTORS_PER_CHUNK,
}
}
pub fn with_sectors_per_chunk(mut self, sectors: u32) -> Self {
self.sectors_per_chunk = sectors.max(1);
self
}
pub fn next_chunk(&mut self) -> Result<Option<Vec<u8>>, CdReaderError> {
if self.remaining_sectors == 0 {
return Ok(None);
}
let sectors = min(self.remaining_sectors, self.sectors_per_chunk);
let chunk = self
.src
.read_audio_sectors(self.next_lba, sectors)
.map_err(|e| CdReaderError::Backend(Box::new(e)))?;
self.next_lba += sectors;
self.remaining_sectors -= sectors;
Ok(Some(chunk))
}
pub fn total_sectors(&self) -> u32 {
self.total_sectors
}
pub fn current_sector(&self) -> u32 {
self.total_sectors - self.remaining_sectors
}
pub fn seek_to_sector(&mut self, sector: u32) -> Result<(), CdReaderError> {
if sector > self.total_sectors {
return Err(CdReaderError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"seek sector is out of track bounds",
)));
}
self.next_lba = self.start_lba + sector;
self.remaining_sectors = self.total_sectors - sector;
Ok(())
}
pub fn current_seconds(&self) -> f32 {
self.current_sector() as f32 / Self::SECTORS_PER_SECOND
}
pub fn total_seconds(&self) -> f32 {
self.total_sectors as f32 / Self::SECTORS_PER_SECOND
}
pub fn seek_to_seconds(&mut self, seconds: f32) -> Result<(), CdReaderError> {
if !seconds.is_finite() || seconds < 0.0 {
return Err(CdReaderError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"seek seconds must be a finite non-negative number",
)));
}
let target_sector = (seconds * Self::SECTORS_PER_SECOND).round() as u32;
self.seek_to_sector(target_sector.min(self.total_sectors))
}
}
pub fn open_track_stream<'a, R: AudioSectorReader>(
src: &'a R,
toc: &Toc,
track_no: u8,
) -> Result<AudioTrackStream<'a, R>, CdReaderError> {
open_track_stream_with_bounds(src, toc, track_no, TrackBounds::SessionGap)
}
pub fn open_track_stream_with_bounds<'a, R: AudioSectorReader>(
src: &'a R,
toc: &Toc,
track_no: u8,
bounds: TrackBounds,
) -> Result<AudioTrackStream<'a, R>, CdReaderError> {
let (start_lba, sectors) = bounds.resolve(toc, track_no).map_err(CdReaderError::Io)?;
Ok(AudioTrackStream::new(src, start_lba, sectors))
}
pub fn open_track_stream_at<R: AudioSectorReader>(
src: &R,
start_lba: u32,
sectors: u32,
) -> AudioTrackStream<'_, R> {
AudioTrackStream::new(src, start_lba, sectors)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Track, create_wav, lba_to_msf};
struct MemDisc {
pcm: Vec<u8>,
}
impl AudioSectorReader for MemDisc {
type Error = std::io::Error;
fn read_audio_sectors(&self, start_lba: u32, count: u32) -> Result<Vec<u8>, Self::Error> {
let start = start_lba as usize * 2352;
let end = start + count as usize * 2352;
self.pcm.get(start..end).map(<[u8]>::to_vec).ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "read past end of disc")
})
}
}
fn toc_two_tracks(t1_sectors: u32, t2_sectors: u32) -> Toc {
Toc {
first_track: 1,
last_track: 2,
tracks: vec![
Track {
number: 1,
start_lba: 0,
start_msf: lba_to_msf(0),
is_audio: true,
},
Track {
number: 2,
start_lba: t1_sectors,
start_msf: lba_to_msf(t1_sectors),
is_audio: true,
},
],
leadout_lba: t1_sectors + t2_sectors,
}
}
#[test]
fn reads_track_bytes_for_the_right_range() {
let (t1, t2) = (100u32, 50u32);
let disc = MemDisc {
pcm: vec![0u8; (t1 + t2) as usize * 2352],
};
let toc = toc_two_tracks(t1, t2);
let track1 = read_track(&disc, &toc, 1).unwrap();
let track2 = read_track(&disc, &toc, 2).unwrap();
assert_eq!(track1.len(), t1 as usize * 2352);
assert_eq!(track2.len(), t2 as usize * 2352);
}
#[test]
fn create_wav_wraps_backend_pcm() {
let disc = MemDisc {
pcm: vec![0u8; 10 * 2352],
};
let toc = Toc {
first_track: 1,
last_track: 1,
tracks: vec![Track {
number: 1,
start_lba: 0,
start_msf: lba_to_msf(0),
is_audio: true,
}],
leadout_lba: 10,
};
let pcm = read_track(&disc, &toc, 1).unwrap();
let wav = create_wav(pcm);
assert_eq!(&wav[0..4], b"RIFF");
assert_eq!(&wav[8..12], b"WAVE");
assert_eq!(wav.len(), 44 + 10 * 2352);
}
#[test]
fn missing_track_is_an_io_error() {
let disc = MemDisc {
pcm: vec![0u8; 2352],
};
let toc = toc_two_tracks(1, 0);
match read_track(&disc, &toc, 99) {
Err(CdReaderError::Io(e)) => assert_eq!(e.kind(), std::io::ErrorKind::NotFound),
other => panic!("expected Io(NotFound), got {other:?}"),
}
}
#[test]
fn backend_failure_is_a_backend_error() {
let disc = MemDisc {
pcm: vec![0u8; 2352],
};
let toc = toc_two_tracks(5, 10);
match read_track(&disc, &toc, 1) {
Err(CdReaderError::Backend(e)) => {
let io = e
.downcast_ref::<std::io::Error>()
.expect("backend error preserves the io::Error");
assert_eq!(io.kind(), std::io::ErrorKind::UnexpectedEof);
}
other => panic!("expected Backend error, got {other:?}"),
}
}
#[test]
fn stream_pulls_sector_aligned_chunks() {
let sectors = 100u32;
let disc = MemDisc {
pcm: vec![0u8; sectors as usize * 2352],
};
let mut stream = open_track_stream_at(&disc, 0, sectors).with_sectors_per_chunk(27);
assert_eq!(stream.total_sectors(), sectors);
let mut total = 0usize;
let mut chunks = 0usize;
while let Some(chunk) = stream.next_chunk().unwrap() {
assert_eq!(chunk.len() % 2352, 0);
total += chunk.len();
chunks += 1;
}
assert_eq!(total, sectors as usize * 2352);
assert_eq!(chunks, 4); assert!(stream.next_chunk().unwrap().is_none());
}
#[test]
fn stream_seek_repositions() {
let disc = MemDisc {
pcm: vec![0u8; 310 * 2352],
};
let mut stream = open_track_stream_at(&disc, 10, 300).with_sectors_per_chunk(1000);
stream.seek_to_sector(250).unwrap();
assert_eq!(stream.current_sector(), 250);
assert!((stream.current_seconds() - 250.0 / 75.0).abs() < f32::EPSILON);
let chunk = stream.next_chunk().unwrap().unwrap();
assert_eq!(chunk.len(), 50 * 2352); assert!(stream.next_chunk().unwrap().is_none());
assert!(stream.seek_to_sector(301).is_err());
}
#[test]
fn open_track_stream_resolves_toc_bounds() {
let (t1, t2) = (40u32, 60u32);
let disc = MemDisc {
pcm: vec![0u8; (t1 + t2) as usize * 2352],
};
let toc = toc_two_tracks(t1, t2);
let stream = open_track_stream(&disc, &toc, 2).unwrap();
assert_eq!(stream.total_sectors(), t2);
}
}