cdtoc 0.1.5

Parser and tools for CDTOC metadata tags.
Documentation
/*!
# CDTOC: Track
*/

use crate::Duration;
use std::ops::Range;



#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
/// # Audio Track.
///
/// This struct holds the details for an audio track, allowing you to fetch
/// things like duration, sector positioning, etc.
///
/// It is the return value of [`Toc::audio_track`](crate::Toc::audio_track).
pub struct Track {
	pub(super) num: u8,
	pub(super) pos: TrackPosition,
	pub(super) from: u32,
	pub(super) to: u32,
}

impl Track {
	#[must_use]
	/// # Byte Size.
	///
	/// Return the equivalent RAW PCM byte size for this track.
	///
	/// ## Examples
	///
	/// ```
	/// use cdtoc::Toc;
	///
	/// let toc = Toc::from_cdtoc("9+96+5766+A284+E600+11FE5+15913+19A98+1E905+240CB+26280").unwrap();
	/// let track = toc.audio_track(9).unwrap();
	/// assert_eq!(
	///     track.bytes(),
	///     20_295_408,
	/// );
	/// ```
	pub const fn bytes(self) -> u64 { self.sectors() as u64 * 2352 }

	#[must_use]
	/// # Duration.
	///
	/// Return the track duration (seconds + frames).
	///
	/// ## Examples
	///
	/// ```
	/// use cdtoc::Toc;
	///
	/// let toc = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
	/// let track = toc.audio_track(1).unwrap();
	/// assert_eq!(track.duration().to_string(), "00:02:32+13");
	/// ```
	pub const fn duration(&self) -> Duration { Duration(self.sectors() as u64) }

	#[must_use]
	/// # Number.
	///
	/// Return the track number.
	///
	/// ## Examples
	///
	/// ```
	/// use cdtoc::Toc;
	///
	/// let toc = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
	/// assert_eq!(toc.audio_track(2).unwrap().number(), 2_u8);
	/// ```
	pub const fn number(&self) -> u8 { self.num }

	#[must_use]
	/// # Disc Position.
	///
	/// Return whether or not this track appears first, last, or somewhere in
	/// between on the disc.
	///
	/// ## Examples
	///
	/// ```
	/// use cdtoc::{Toc, TrackPosition};
	///
	/// let toc = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
	/// assert_eq!(toc.audio_track(1).unwrap().position(), TrackPosition::First);
	/// assert_eq!(toc.audio_track(2).unwrap().position(), TrackPosition::Middle);
	/// assert_eq!(toc.audio_track(3).unwrap().position(), TrackPosition::Middle);
	/// assert_eq!(toc.audio_track(4).unwrap().position(), TrackPosition::Last);
	/// ```
	pub const fn position(&self) -> TrackPosition { self.pos }

	#[must_use]
	/// # Total Samples.
	///
	/// Return the total number of samples.
	///
	/// ## Examples
	///
	/// ```
	/// use cdtoc::Toc;
	///
	/// let toc = Toc::from_cdtoc("9+96+5766+A284+E600+11FE5+15913+19A98+1E905+240CB+26280").unwrap();
	/// let track = toc.audio_track(9).unwrap();
	/// assert_eq!(
	///     track.samples(),
	///     5_073_852,
	/// );
	/// ```
	pub const fn samples(self) -> u64 { self.duration().samples() }

	#[must_use]
	/// # Sector Size.
	///
	/// Return the number of sectors occupied by this track.
	///
	/// ## Examples
	///
	/// ```
	/// use cdtoc::Toc;
	///
	/// let toc = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
	/// let track = toc.audio_track(1).unwrap();
	/// assert_eq!(track.sectors(), 11_413_u32);
	/// ```
	pub const fn sectors(&self) -> u32 { self.to - self.from }

	#[must_use]
	/// # Sector Range.
	///
	/// Return the range of sectors — `start..end` — occupied by this track.
	///
	/// ## Examples
	///
	/// ```
	/// use cdtoc::Toc;
	///
	/// let toc = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
	/// let track = toc.audio_track(1).unwrap();
	/// assert_eq!(track.sector_range(), 150..11_563);
	///
	/// // If you just want the length, sectors() can get that more
	/// // directly, but it works out the same either way:
	/// assert_eq!(track.sector_range().len(), track.sectors() as usize);
	/// ```
	pub const fn sector_range(&self) -> Range<u32> { self.from..self.to }
}



#[derive(Debug)]
/// # Audio Tracks Iterator.
///
/// This is an iterator of [`Track`] details for a given [`Toc`](crate::Toc).
///
/// It is the return value of [`Toc::audio_tracks`](crate::Toc::audio_tracks).
pub struct Tracks<'a> {
	tracks: &'a [u32],
	leadout: u32,
	pos: usize,
}

impl Iterator for Tracks<'_> {
	type Item = Track;

	#[allow(clippy::cast_possible_truncation)]
	fn next(&mut self) -> Option<Self::Item> {
		let len = self.tracks.len();
		if len <= self.pos { return None; }

		let num = (self.pos + 1) as u8;
		let pos = TrackPosition::from((self.pos + 1, len));
		let from = self.tracks[self.pos];
		let to =
			if self.pos + 1 < len { self.tracks[self.pos + 1] }
			else { self.leadout };

		self.pos += 1;
		Some(Track { num, pos, from, to })
	}

	fn size_hint(&self) -> (usize, Option<usize>) {
		let len = self.len();
		(len, Some(len))
	}
}

impl ExactSizeIterator for Tracks<'_> {
	fn len(&self) -> usize {
		self.tracks.len().saturating_sub(self.pos)
	}
}

impl<'a> Tracks<'a> {
	/// # New.
	pub(super) const fn new(tracks: &'a [u32], leadout: u32) -> Self {
		Self { tracks, leadout, pos: 0 }
	}
}





#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
/// # Track Position.
///
/// This enum is used to differentiate between first, middle, and final track
/// positions within the context of a given table of contents.
///
/// Variants of this type are returned by [`Track::position`].
pub enum TrackPosition {
	/// # Invalid.
	///
	/// This is used for track numbers that are out of range, including pre-gap
	/// hidden tracks (#0).
	Invalid,

	/// # The First Track.
	First,

	/// # Somewhere in the Middle.
	Middle,

	/// # The Last Track.
	Last,

	/// # The Only Track.
	Only,
}

macro_rules! pos_tuple {
	($($ty:ty),+) => ($(
		impl From<($ty, $ty)> for TrackPosition {
			fn from(src: ($ty, $ty)) -> Self {
				if src.0 == 0 || src.1 < src.0 { Self::Invalid }
				else if src.0 == 1 {
					if src.1 == 1 { Self::Only }
					else { Self::First }
				}
				else if src.0 == src.1 { Self::Last }
				else { Self::Middle }
			}
		}
	)+);
}

pos_tuple!(u8, u16, u32, u64, usize);

impl TrackPosition {
	#[must_use]
	/// # Is Valid?
	///
	/// Returns `true` if the position is anything other than [`TrackPosition::Invalid`].
	pub const fn is_valid(self) -> bool { ! matches!(self, Self::Invalid) }

	#[must_use]
	/// # Is First?
	///
	/// This returns `true` if the track appears at spot #1 on the disc.
	///
	/// ## Examples
	///
	/// ```
	/// use cdtoc::TrackPosition;
	///
	/// // Yep!
	/// assert!(TrackPosition::First.is_first());
	/// assert!(TrackPosition::Only.is_first());
	///
	/// // Nope!
	/// assert!(! TrackPosition::Middle.is_first());
	/// assert!(! TrackPosition::Last.is_first());
	/// ```
	pub const fn is_first(self) -> bool { matches!(self, Self::First | Self::Only) }

	#[must_use]
	/// # Is Last?
	///
	/// This returns `true` if the track appears at the end of the disc.
	///
	/// ## Examples
	///
	/// ```
	/// use cdtoc::TrackPosition;
	///
	/// // Yep!
	/// assert!(TrackPosition::Last.is_last());
	/// assert!(TrackPosition::Only.is_last());
	///
	/// // Nope!
	/// assert!(! TrackPosition::First.is_last());
	/// assert!(! TrackPosition::Middle.is_last());
	/// ```
	pub const fn is_last(self) -> bool { matches!(self, Self::Last | Self::Only) }
}