diff --git a/Cargo.toml b/Cargo.toml index a0043bbe7..d5ee0e6ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ resolver = "3" members = [ + "aud_io", "lofty", "lofty_attr", "ogg_pager", @@ -10,17 +11,21 @@ members = [ ] [workspace.package] +authors = ["Serial <69764315+Serial-ATA@users.noreply.github.com>"] edition = "2024" rust-version = "1.85" repository = "https://github.com/Serial-ATA/lofty-rs" license = "MIT OR Apache-2.0" [workspace.dependencies] +aud_io = { version = "0.1.0", path = "aud_io" } lofty = { version = "0.22.4", path = "lofty" } lofty_attr = { version = "0.11.1", path = "lofty_attr" } ogg_pager = { version = "0.7.0", path = "ogg_pager" } byteorder = "1.5.0" +log = "0.4.28" +test-log = "0.2.18" [workspace.lints.rust] missing_docs = "deny" diff --git a/aud_io/Cargo.toml b/aud_io/Cargo.toml new file mode 100644 index 000000000..7db33e81d --- /dev/null +++ b/aud_io/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "aud_io" +version = "0.1.0" +description = "" # TODO +keywords = ["audio", "mp4"] +categories = ["multimedia", "multimedia::audio", "parser-implementations"] +readme = "" # TODO +include = ["src", "../LICENSE-APACHE", "../LICENSE-MIT"] +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +byteorder = { workspace = true } +log = { workspace = true } + +[dev-dependencies] +test-log = { workspace = true } + +# TODO +#[lints] +#workspace = true diff --git a/aud_io/src/aac/error.rs b/aud_io/src/aac/error.rs new file mode 100644 index 000000000..4161289cb --- /dev/null +++ b/aud_io/src/aac/error.rs @@ -0,0 +1,18 @@ +use std::fmt::Display; + +#[derive(Debug)] +pub enum AacError { + BadSampleRate, +} + +impl Display for AacError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + AacError::BadSampleRate => { + f.write_str("File contains an invalid sample frequency index") + }, + } + } +} + +impl core::error::Error for AacError {} diff --git a/lofty/src/aac/header.rs b/aud_io/src/aac/header.rs similarity index 81% rename from lofty/src/aac/header.rs rename to aud_io/src/aac/header.rs index f7411cd90..75c9c79f0 100644 --- a/lofty/src/aac/header.rs +++ b/aud_io/src/aac/header.rs @@ -1,33 +1,33 @@ +use super::error::AacError; use crate::config::ParsingMode; use crate::error::Result; -use crate::macros::decode_err; use crate::mp4::{AudioObjectType, SAMPLE_RATES}; use crate::mpeg::MpegVersion; -use std::io::{Read, Seek, SeekFrom}; - -// Used to compare the headers up to the home bit. -// If they aren't equal, something is broken. -pub(super) const HEADER_MASK: u32 = 0xFFFF_FFE0; +use std::io::Read; #[derive(Copy, Clone)] -pub(crate) struct ADTSHeader { - pub(crate) version: MpegVersion, - pub(crate) audio_object_ty: AudioObjectType, - pub(crate) sample_rate: u32, - pub(crate) channels: u8, - pub(crate) copyright: bool, - pub(crate) original: bool, - pub(crate) len: u16, - pub(crate) bitrate: u32, - pub(crate) bytes: [u8; 7], - pub(crate) has_crc: bool, +pub struct ADTSHeader { + pub version: MpegVersion, + pub audio_object_ty: AudioObjectType, + pub sample_rate: u32, + pub channels: u8, + pub copyright: bool, + pub original: bool, + pub len: u16, + pub bitrate: u32, + pub bytes: [u8; 7], + pub has_crc: bool, } impl ADTSHeader { - pub(super) fn read(reader: &mut R, _parse_mode: ParsingMode) -> Result> + /// Used to compare ADTS headers up to the `home` bit. If they aren't equal, then something's broken + /// in the input. + pub const COMPARISON_MASK: u32 = 0xFFFF_FFE0; + + pub fn read(reader: &mut R, _parse_mode: ParsingMode) -> Result> where - R: Read + Seek, + R: Read, { // The ADTS header consists of 7 bytes, or 9 bytes with a CRC let mut needs_crc_skip = false; @@ -81,7 +81,7 @@ impl ADTSHeader { let sample_rate_idx = (byte3 >> 2) & 0b1111; if sample_rate_idx == 15 { // 15 is forbidden - decode_err!(@BAIL Aac, "File contains an invalid sample frequency index"); + return Err(AacError::BadSampleRate.into()); } let sample_rate = SAMPLE_RATES[sample_rate_idx as usize]; @@ -106,7 +106,7 @@ impl ADTSHeader { if needs_crc_skip { log::debug!("Skipping CRC"); - reader.seek(SeekFrom::Current(2))?; + reader.read_exact(&mut [0; 2])?; } Ok(Some(ADTSHeader { diff --git a/aud_io/src/aac/mod.rs b/aud_io/src/aac/mod.rs new file mode 100644 index 000000000..cb0802097 --- /dev/null +++ b/aud_io/src/aac/mod.rs @@ -0,0 +1,4 @@ +mod header; +pub use header::*; + +pub mod error; diff --git a/lofty/src/util/alloc.rs b/aud_io/src/alloc.rs similarity index 71% rename from lofty/src/util/alloc.rs rename to aud_io/src/alloc.rs index 0f7941ae1..68777334d 100644 --- a/lofty/src/util/alloc.rs +++ b/aud_io/src/alloc.rs @@ -1,12 +1,11 @@ -use crate::error::Result; -use crate::macros::err; - use crate::config::global_options; +use crate::err; +use crate::error::Result; -/// Provides the `fallible_repeat` method on `Vec` +/// Provides the `fallible_repeat` method on [`Vec`] /// -/// It is intended to be used in [`try_vec!`](crate::macros::try_vec). -trait VecFallibleRepeat: Sized { +/// It is intended to be used in [`try_vec!`](crate::try_vec). +pub trait VecFallibleRepeat: Sized { fn fallible_repeat(self, element: T, expected_size: usize) -> Result where T: Clone; @@ -46,23 +45,24 @@ impl VecFallibleRepeat for Vec { /// **DO NOT USE DIRECTLY** /// -/// Creates a `Vec` of the specified length, containing copies of `element`. +/// Creates a [`Vec`] of the specified length, containing copies of `element`. /// -/// This should be used through [`try_vec!`](crate::macros::try_vec) -pub(crate) fn fallible_vec_from_element(element: T, expected_size: usize) -> Result> +/// This should be used through [`try_vec!`](crate::try_vec) +#[doc(hidden)] +pub fn fallible_vec_from_element(element: T, expected_size: usize) -> Result> where T: Clone, { Vec::new().fallible_repeat(element, expected_size) } -/// Provides the `try_with_capacity` method on `Vec` +/// Provides the `try_with_capacity` method on [`Vec`] /// /// This can be used directly. -pub(crate) trait VecFallibleCapacity: Sized { - /// Same as `Vec::with_capacity`, but takes `GlobalOptions::allocation_limit` into account. +pub trait VecFallibleCapacity: Sized { + /// Same as [`Vec::with_capacity()`], but takes [`GlobalOptions::allocation_limit()`] into account. /// - /// Named `try_with_capacity_stable` to avoid conflicts with the nightly `Vec::try_with_capacity`. + /// Named `try_with_capacity_stable` to avoid conflicts with the nightly [`Vec::try_with_capacity()`]. fn try_with_capacity_stable(capacity: usize) -> Result; } @@ -81,7 +81,7 @@ impl VecFallibleCapacity for Vec { #[cfg(test)] mod tests { - use crate::util::alloc::fallible_vec_from_element; + use super::fallible_vec_from_element; #[test_log::test] fn vec_fallible_repeat() { diff --git a/aud_io/src/config/global_options.rs b/aud_io/src/config/global_options.rs new file mode 100644 index 000000000..fc7d83c25 --- /dev/null +++ b/aud_io/src/config/global_options.rs @@ -0,0 +1,103 @@ +use std::cell::UnsafeCell; + +thread_local! { + static GLOBAL_OPTIONS: UnsafeCell = UnsafeCell::new(GlobalOptions::default()); +} + +pub(crate) unsafe fn global_options() -> &'static GlobalOptions { + GLOBAL_OPTIONS.with(|global_options| unsafe { &*global_options.get() }) +} + +/// Options that control all interactions with `aud_io` for the current thread +/// +/// # Examples +/// +/// ```rust +/// use aud_io::config::{GlobalOptions, apply_global_options}; +/// +/// // I want to double the allocation limit +/// let global_options = +/// GlobalOptions::new().allocation_limit(GlobalOptions::DEFAULT_ALLOCATION_LIMIT * 2); +/// apply_global_options(global_options); +/// ``` +#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] +#[non_exhaustive] +pub struct GlobalOptions { + pub(crate) allocation_limit: usize, +} + +impl GlobalOptions { + /// Default allocation limit for any single allocation + pub const DEFAULT_ALLOCATION_LIMIT: usize = 16 * 1024 * 1024; + + /// Creates a new `GlobalOptions`, alias for `Default` implementation + /// + /// See also: [`GlobalOptions::default`] + /// + /// # Examples + /// + /// ```rust + /// use aud_io::config::GlobalOptions; + /// + /// let global_options = GlobalOptions::new(); + /// ``` + #[must_use] + pub const fn new() -> Self { + Self { + allocation_limit: Self::DEFAULT_ALLOCATION_LIMIT, + } + } + + /// The maximum number of bytes to allocate for any single allocation + /// + /// This is a safety measure to prevent allocating too much memory for a single allocation. If an allocation + /// exceeds this limit, the allocator will return [`AudioError::TooMuchData`](crate::error::AudioError::TooMuchData). + /// + /// # Examples + /// + /// ```rust + /// use aud_io::config::{GlobalOptions, apply_global_options}; + /// + /// // I have files with gigantic images, I'll double the allocation limit! + /// let global_options = + /// GlobalOptions::new().allocation_limit(GlobalOptions::DEFAULT_ALLOCATION_LIMIT * 2); + /// apply_global_options(global_options); + /// ``` + pub fn allocation_limit(&mut self, allocation_limit: usize) -> Self { + self.allocation_limit = allocation_limit; + *self + } +} + +impl Default for GlobalOptions { + /// The default implementation for `GlobalOptions` + /// + /// The defaults are as follows: + /// + /// ```rust,ignore + /// GlobalOptions { + /// allocation_limit: Self::DEFAULT_ALLOCATION_LIMIT, + /// } + /// ``` + fn default() -> Self { + Self::new() + } +} + +/// Applies the given `GlobalOptions` to the current thread +/// +/// # Examples +/// +/// ```rust +/// use aud_io::config::{GlobalOptions, apply_global_options}; +/// +/// // I want to double the allocation limit +/// let global_options = +/// GlobalOptions::new().allocation_limit(GlobalOptions::DEFAULT_ALLOCATION_LIMIT * 2); +/// apply_global_options(global_options); +/// ``` +pub fn apply_global_options(options: GlobalOptions) { + GLOBAL_OPTIONS.with(|global_options| unsafe { + *global_options.get() = options; + }); +} diff --git a/aud_io/src/config/mod.rs b/aud_io/src/config/mod.rs new file mode 100644 index 000000000..54af2e2d4 --- /dev/null +++ b/aud_io/src/config/mod.rs @@ -0,0 +1,5 @@ +mod global_options; +pub use global_options::*; + +mod parse; +pub use parse::*; diff --git a/aud_io/src/config/parse.rs b/aud_io/src/config/parse.rs new file mode 100644 index 000000000..e9e6fa61b --- /dev/null +++ b/aud_io/src/config/parse.rs @@ -0,0 +1,52 @@ +/// The parsing strictness mode +/// +/// This can be set with [`Probe::options`](crate::probe::Probe). +/// +/// # Examples +/// +/// ```rust,no_run +/// use lofty::config::{ParseOptions, ParsingMode}; +/// use lofty::probe::Probe; +/// +/// # fn main() -> lofty::error::Result<()> { +/// // We only want to read spec-compliant inputs +/// let parsing_options = ParseOptions::new().parsing_mode(ParsingMode::Strict); +/// let tagged_file = Probe::open("foo.mp3")?.options(parsing_options).read()?; +/// # Ok(()) } +/// ``` +#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Default)] +#[non_exhaustive] +pub enum ParsingMode { + /// Will eagerly error on invalid input + /// + /// This mode will eagerly error on any non-spec-compliant input. + /// + /// ## Examples of behavior + /// + /// * Unable to decode text - The parser will error and the entire input is discarded + /// * Unable to determine the sample rate - The parser will error and the entire input is discarded + Strict, + /// Default mode, less eager to error on recoverably malformed input + /// + /// This mode will attempt to fill in any holes where possible in otherwise valid, spec-compliant input. + /// + /// NOTE: A readable input does *not* necessarily make it writeable. + /// + /// ## Examples of behavior + /// + /// * Unable to decode text - If valid otherwise, the field will be replaced by an empty string and the parser moves on + /// * Unable to determine the sample rate - The sample rate will be 0 + #[default] + BestAttempt, + /// Least eager to error, may produce invalid/partial output + /// + /// This mode will discard any invalid fields, and ignore the majority of non-fatal errors. + /// + /// If the input is malformed, the resulting tags may be incomplete, and the properties zeroed. + /// + /// ## Examples of behavior + /// + /// * Unable to decode text - The entire item is discarded and the parser moves on + /// * Unable to determine the sample rate - The sample rate will be 0 + Relaxed, +} diff --git a/aud_io/src/error.rs b/aud_io/src/error.rs new file mode 100644 index 000000000..5c6a0d7ac --- /dev/null +++ b/aud_io/src/error.rs @@ -0,0 +1,120 @@ +use std::fmt::{Display, Formatter}; + +/// Alias for `Result` +pub type Result = std::result::Result; + +#[derive(Debug)] +pub enum AudioError { + // File data related errors + /// Attempting to read/write an abnormally large amount of data + TooMuchData, + /// Expected the data to be a different size than provided + /// + /// This occurs when the size of an item is written as one value, but that size is either too + /// big or small to be valid within the bounds of that item. + // TODO: Should probably have context + SizeMismatch, + + /// Errors that arise while decoding text + TextDecode(&'static str), + + // Format-specific + Aac(crate::aac::error::AacError), + Mpeg(crate::mpeg::error::MpegError), + Musepack(crate::musepack::error::MusePackError), + /// Arises when an MP4 atom contains invalid data + BadAtom(&'static str), + + // Conversions for external errors + /// Represents all cases of [`std::io::Error`]. + Io(std::io::Error), + /// Unable to convert bytes to a String + StringFromUtf8(std::string::FromUtf8Error), + /// Unable to convert bytes to a str + StrFromUtf8(std::str::Utf8Error), + /// Failure to allocate enough memory + Alloc(std::collections::TryReserveError), + /// This should **never** be encountered + Infallible(std::convert::Infallible), +} + +impl From for AudioError { + fn from(err: crate::aac::error::AacError) -> Self { + AudioError::Aac(err) + } +} + +impl From for AudioError { + fn from(err: crate::mpeg::error::MpegError) -> Self { + AudioError::Mpeg(err) + } +} + +impl From for AudioError { + fn from(err: crate::musepack::error::MusePackError) -> Self { + AudioError::Musepack(err) + } +} + +impl From for AudioError { + fn from(input: std::io::Error) -> Self { + AudioError::Io(input) + } +} + +impl From for AudioError { + fn from(input: std::string::FromUtf8Error) -> Self { + AudioError::StringFromUtf8(input) + } +} + +impl From for AudioError { + fn from(input: std::str::Utf8Error) -> Self { + AudioError::StrFromUtf8(input) + } +} + +impl From for AudioError { + fn from(input: std::collections::TryReserveError) -> Self { + AudioError::Alloc(input) + } +} + +impl From for AudioError { + fn from(input: std::convert::Infallible) -> Self { + AudioError::Infallible(input) + } +} + +impl Display for AudioError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + AudioError::TextDecode(message) => write!(f, "Text decoding: {message}"), + + // Conversions + AudioError::StringFromUtf8(err) => write!(f, "{err}"), + AudioError::StrFromUtf8(err) => write!(f, "{err}"), + AudioError::Io(err) => write!(f, "{err}"), + AudioError::Alloc(err) => write!(f, "{err}"), + AudioError::Infallible(_) => write!(f, "An expected condition was not upheld"), + + // Files + AudioError::TooMuchData => write!( + f, + "Attempted to read/write an abnormally large amount of data" + ), + AudioError::SizeMismatch => write!( + f, + "Encountered an invalid item size, either too big or too small to be valid" + ), + + // Format-specific + AudioError::Aac(err) => write!(f, "AAC: {err}"), + AudioError::Mpeg(err) => write!(f, "MPEG: {err}"), + AudioError::Musepack(err) => write!(f, "MPC: {err}"), + AudioError::BadAtom(message) => write!(f, "MP4 Atom: {message}"), + } + } +} + +impl core::error::Error for AudioError {} diff --git a/lofty/src/util/io.rs b/aud_io/src/io.rs similarity index 57% rename from lofty/src/util/io.rs rename to aud_io/src/io.rs index c6ff46529..ef220f492 100644 --- a/lofty/src/util/io.rs +++ b/aud_io/src/io.rs @@ -1,28 +1,12 @@ //! Various traits for reading and writing to file-like objects -use crate::error::{LoftyError, Result}; -use crate::util::math::F80; +use crate::error::{AudioError, Result}; +use crate::math::F80; use std::collections::VecDeque; use std::fs::File; use std::io::{Cursor, Read, Seek, Write}; -// TODO: https://github.com/rust-lang/rust/issues/59359 -pub(crate) trait SeekStreamLen: Seek { - fn stream_len_hack(&mut self) -> crate::error::Result { - use std::io::SeekFrom; - - let current_pos = self.stream_position()?; - let len = self.seek(SeekFrom::End(0))?; - - self.seek(SeekFrom::Start(current_pos))?; - - Ok(len) - } -} - -impl SeekStreamLen for T where T: Seek {} - /// Provides a method to truncate an object to the specified length /// /// This is one component of the [`FileLike`] trait, which is used to provide implementors access to any @@ -43,7 +27,7 @@ impl SeekStreamLen for T where T: Seek {} /// ``` pub trait Truncate { /// The error type of the truncation operation - type Error: Into; + type Error: Into; /// Truncate a storage object to the specified length /// @@ -130,7 +114,7 @@ where /// ``` pub trait Length { /// The error type of the length operation - type Error: Into; + type Error: Into; /// Get the length of a storage object /// @@ -217,20 +201,20 @@ where /// trait implementations are correct. If this assumption were to be broken, files **may** become corrupted. pub trait FileLike: Read + Write + Seek + Truncate + Length where - ::Error: Into, - ::Error: Into, + ::Error: Into, + ::Error: Into, { } impl FileLike for T where T: Read + Write + Seek + Truncate + Length, - ::Error: Into, - ::Error: Into, + ::Error: Into, + ::Error: Into, { } -pub(crate) trait ReadExt: Read { +pub trait ReadExt: Read { fn read_f80(&mut self) -> Result; } @@ -246,126 +230,18 @@ where } } -#[cfg(test)] -mod tests { - use crate::config::{ParseOptions, WriteOptions}; - use crate::file::AudioFile; - use crate::mpeg::MpegFile; - use crate::tag::Accessor; - - use std::io::{Cursor, Read, Seek, Write}; - - const TEST_ASSET: &str = "tests/files/assets/minimal/full_test.mp3"; - - fn test_asset_contents() -> Vec { - std::fs::read(TEST_ASSET).unwrap() - } - - fn file() -> MpegFile { - let file_contents = test_asset_contents(); - let mut reader = Cursor::new(file_contents); - MpegFile::read_from(&mut reader, ParseOptions::new()).unwrap() - } - - fn alter_tag(file: &mut MpegFile) { - let tag = file.id3v2_mut().unwrap(); - tag.set_artist(String::from("Bar artist")); - } - - fn revert_tag(file: &mut MpegFile) { - let tag = file.id3v2_mut().unwrap(); - tag.set_artist(String::from("Foo artist")); - } - - #[test_log::test] - fn io_save_to_file() { - // Read the file and change the artist - let mut file = file(); - alter_tag(&mut file); - - let mut temp_file = tempfile::tempfile().unwrap(); - let file_content = std::fs::read(TEST_ASSET).unwrap(); - temp_file.write_all(&file_content).unwrap(); - temp_file.rewind().unwrap(); - - // Save the new artist - file.save_to(&mut temp_file, WriteOptions::new().preferred_padding(0)) - .expect("Failed to save to file"); - - // Read the file again and change the artist back - temp_file.rewind().unwrap(); - let mut file = MpegFile::read_from(&mut temp_file, ParseOptions::new()).unwrap(); - revert_tag(&mut file); - - temp_file.rewind().unwrap(); - file.save_to(&mut temp_file, WriteOptions::new().preferred_padding(0)) - .expect("Failed to save to file"); - - // The contents should be the same as the original file - temp_file.rewind().unwrap(); - let mut current_file_contents = Vec::new(); - temp_file.read_to_end(&mut current_file_contents).unwrap(); - - assert_eq!(current_file_contents, test_asset_contents()); - } - - #[test_log::test] - fn io_save_to_vec() { - // Same test as above, but using a Cursor> instead of a file - let mut file = file(); - alter_tag(&mut file); - - let file_content = std::fs::read(TEST_ASSET).unwrap(); - - let mut reader = Cursor::new(file_content); - file.save_to(&mut reader, WriteOptions::new().preferred_padding(0)) - .expect("Failed to save to vec"); - - reader.rewind().unwrap(); - let mut file = MpegFile::read_from(&mut reader, ParseOptions::new()).unwrap(); - revert_tag(&mut file); +/// Stable version of [`Seek::stream_len()`] +pub trait SeekStreamLen: Seek { + fn stream_len_hack(&mut self) -> Result { + use std::io::SeekFrom; - reader.rewind().unwrap(); - file.save_to(&mut reader, WriteOptions::new().preferred_padding(0)) - .expect("Failed to save to vec"); + let current_pos = self.stream_position()?; + let len = self.seek(SeekFrom::End(0))?; - let current_file_contents = reader.into_inner(); - assert_eq!(current_file_contents, test_asset_contents()); - } + self.seek(SeekFrom::Start(current_pos))?; - #[test_log::test] - fn io_save_using_references() { - struct File { - buf: Vec, - } - - let mut f = File { - buf: std::fs::read(TEST_ASSET).unwrap(), - }; - - // Same test as above, but using references instead of owned values - let mut file = file(); - alter_tag(&mut file); - - { - let mut reader = Cursor::new(&mut f.buf); - file.save_to(&mut reader, WriteOptions::new().preferred_padding(0)) - .expect("Failed to save to vec"); - } - - { - let mut reader = Cursor::new(&f.buf[..]); - file = MpegFile::read_from(&mut reader, ParseOptions::new()).unwrap(); - revert_tag(&mut file); - } - - { - let mut reader = Cursor::new(&mut f.buf); - file.save_to(&mut reader, WriteOptions::new().preferred_padding(0)) - .expect("Failed to save to vec"); - } - - let current_file_contents = f.buf; - assert_eq!(current_file_contents, test_asset_contents()); + Ok(len) } } + +impl SeekStreamLen for T where T: Seek {} diff --git a/aud_io/src/lib.rs b/aud_io/src/lib.rs new file mode 100644 index 000000000..f19c77de6 --- /dev/null +++ b/aud_io/src/lib.rs @@ -0,0 +1,11 @@ +pub mod aac; +pub mod alloc; +pub mod config; +pub mod error; +pub mod io; +pub(crate) mod macros; +pub mod math; +pub mod mp4; +pub mod mpeg; +pub mod musepack; +pub mod text; diff --git a/aud_io/src/macros.rs b/aud_io/src/macros.rs new file mode 100644 index 000000000..533d0f6c2 --- /dev/null +++ b/aud_io/src/macros.rs @@ -0,0 +1,19 @@ +#[macro_export] +macro_rules! try_vec { + ($elem:expr; $size:expr) => {{ $crate::alloc::fallible_vec_from_element($elem, $size)? }}; +} + +// Shorthand for `return Err(AudioError::Foo)` +// +// Usage: +// - err!(Variant) -> return Err(AudioError::::Variant) +// - err!(Variant(Message)) -> return Err(AudioError::Variant(Message)) +#[macro_export] +macro_rules! err { + ($variant:ident) => { + return Err($crate::error::AudioError::$variant.into()) + }; + ($variant:ident($reason:literal)) => { + return Err($crate::error::AudioError::$variant($reason).into()) + }; +} diff --git a/lofty/src/util/math.rs b/aud_io/src/math.rs similarity index 98% rename from lofty/src/util/math.rs rename to aud_io/src/math.rs index 229c28f5c..bd40cc689 100644 --- a/lofty/src/util/math.rs +++ b/aud_io/src/math.rs @@ -3,7 +3,7 @@ /// This is implemented for all unsigned integers. /// /// NOTE: If the result is less than 1, it will be rounded up to 1. -pub(crate) trait RoundedDivision { +pub trait RoundedDivision { type Output; fn div_round(self, rhs: Rhs) -> Self::Output; @@ -29,7 +29,7 @@ unsigned_rounded_division!(u8, u16, u32, u64, u128, usize); /// /// This is used in AIFF. #[derive(Debug, Eq, PartialEq, Copy, Clone)] -pub(crate) struct F80 { +pub struct F80 { signed: bool, // 15-bit exponent with a bias of 16383 exponent: u16, diff --git a/lofty/src/mp4/atom_info.rs b/aud_io/src/mp4/atom_info.rs similarity index 74% rename from lofty/src/mp4/atom_info.rs rename to aud_io/src/mp4/atom_info.rs index c7b3a1004..0eb96027b 100644 --- a/lofty/src/mp4/atom_info.rs +++ b/aud_io/src/mp4/atom_info.rs @@ -1,17 +1,16 @@ use crate::config::ParsingMode; -use crate::error::{ErrorKind, LoftyError, Result}; -use crate::macros::{err, try_vec}; -use crate::tag::{ItemKey, TagType}; -use crate::util::text::utf8_decode; +use crate::error::{AudioError, Result}; +use crate::text::utf8_decode; +use crate::{err, try_vec}; use std::borrow::Cow; use std::io::{Read, Seek, SeekFrom}; use byteorder::{BigEndian, ReadBytesExt}; -pub(super) const FOURCC_LEN: u64 = 4; -pub(super) const IDENTIFIER_LEN: u64 = 4; -pub(super) const ATOM_HEADER_LEN: u64 = FOURCC_LEN + IDENTIFIER_LEN; +pub const FOURCC_LEN: u64 = 4; +pub const IDENTIFIER_LEN: u64 = 4; +pub const ATOM_HEADER_LEN: u64 = FOURCC_LEN + IDENTIFIER_LEN; /// Represents an `MP4` atom identifier #[derive(Eq, PartialEq, Debug, Clone)] @@ -63,55 +62,12 @@ impl<'a> AtomIdent<'a> { } } -impl<'a> TryFrom<&'a ItemKey> for AtomIdent<'a> { - type Error = LoftyError; - - fn try_from(value: &'a ItemKey) -> std::result::Result { - if let Some(mapped_key) = value.map_key(TagType::Mp4Ilst) { - if mapped_key.starts_with("----") { - let mut split = mapped_key.split(':'); - - split.next(); - - let mean = split.next(); - let name = split.next(); - - if let (Some(mean), Some(name)) = (mean, name) { - return Ok(AtomIdent::Freeform { - mean: Cow::Borrowed(mean), - name: Cow::Borrowed(name), - }); - } - } else { - let fourcc = mapped_key.chars().map(|c| c as u8).collect::>(); - - if let Ok(fourcc) = TryInto::<[u8; 4]>::try_into(fourcc) { - return Ok(AtomIdent::Fourcc(fourcc)); - } - } - } - - err!(TextDecode( - "ItemKey does not map to a freeform or fourcc identifier" - )) - } -} - -impl TryFrom for AtomIdent<'static> { - type Error = LoftyError; - - fn try_from(value: ItemKey) -> std::result::Result { - let ret: AtomIdent<'_> = (&value).try_into()?; - Ok(ret.into_owned()) - } -} - #[derive(Debug)] -pub(crate) struct AtomInfo { - pub(crate) start: u64, - pub(crate) len: u64, - pub(crate) extended: bool, - pub(crate) ident: AtomIdent<'static>, +pub struct AtomInfo { + pub start: u64, + pub len: u64, + pub extended: bool, + pub ident: AtomIdent<'static>, } // The spec permits any characters to be used in atom identifiers. This doesn't @@ -124,7 +80,7 @@ fn is_valid_identifier_byte(b: u8) -> bool { } impl AtomInfo { - pub(crate) fn read( + pub fn read( data: &mut R, mut reader_size: u64, parse_mode: ParsingMode, @@ -145,15 +101,12 @@ impl AtomInfo { // Seek to the end, since we can't recover from this data.seek(SeekFrom::End(0))?; - match parse_mode { - ParsingMode::Strict => { - err!(BadAtom("Encountered an atom with invalid characters")); - }, - ParsingMode::BestAttempt | ParsingMode::Relaxed => { - log::warn!("Encountered an atom with invalid characters, stopping"); - return Ok(None); - }, + if parse_mode == ParsingMode::Strict { + err!(BadAtom("Encountered an atom with invalid characters")); } + + log::warn!("Encountered an atom with invalid characters, stopping"); + return Ok(None); } let (len, extended) = match len_raw { @@ -216,7 +169,7 @@ impl AtomInfo { })) } - pub(crate) fn header_size(&self) -> u64 { + pub fn header_size(&self) -> u64 { if !self.extended { return ATOM_HEADER_LEN; } @@ -286,9 +239,7 @@ where *reader_size -= len; utf8_decode(content).map_err(|_| { - LoftyError::new(ErrorKind::BadAtom( - "Found a non UTF-8 string while reading freeform identifier", - )) + AudioError::BadAtom("Found a non UTF-8 string while reading freeform identifier") }) }, _ => err!(BadAtom( diff --git a/lofty/src/mp4/read/atom_reader.rs b/aud_io/src/mp4/atom_reader.rs similarity index 80% rename from lofty/src/mp4/read/atom_reader.rs rename to aud_io/src/mp4/atom_reader.rs index c24032224..f41a04206 100644 --- a/lofty/src/mp4/read/atom_reader.rs +++ b/aud_io/src/mp4/atom_reader.rs @@ -1,8 +1,8 @@ +use super::atom_info::AtomInfo; use crate::config::ParsingMode; +use crate::err; use crate::error::Result; -use crate::macros::err; -use crate::mp4::atom_info::AtomInfo; -use crate::util::io::SeekStreamLen; +use crate::io::SeekStreamLen; use std::io::{Read, Seek, SeekFrom}; @@ -15,7 +15,7 @@ use byteorder::{BigEndian, ReadBytesExt}; /// * [`Self::next`] to read atoms. /// * `read_u*` methods to read integers without needing to specify the endianness. /// * Bounds checking on reads and seeks to prevent going outside the file. -pub(crate) struct AtomReader +pub struct AtomReader where R: Read + Seek, { @@ -31,7 +31,7 @@ where R: Read + Seek, { /// Create a new `AtomReader` - pub(crate) fn new(mut reader: R, parse_mode: ParsingMode) -> Result { + pub fn new(mut reader: R, parse_mode: ParsingMode) -> Result { let len = reader.stream_len_hack()?; Ok(Self { reader, @@ -47,38 +47,38 @@ where /// This is useful when reading an atom such as `moov`, where we only want to read it and its /// children. We can read the atom, set the bounds to the atom's length, and then read the children /// without worrying about reading past the atom's end. - pub(crate) fn reset_bounds(&mut self, start_position: u64, len: u64) { + pub fn reset_bounds(&mut self, start_position: u64, len: u64) { self.start = start_position; self.remaining_size = len; self.len = len; } - pub(crate) fn read_u8(&mut self) -> std::io::Result { + pub fn read_u8(&mut self) -> std::io::Result { self.remaining_size = self.remaining_size.saturating_sub(1); self.reader.read_u8() } - pub(crate) fn read_u16(&mut self) -> std::io::Result { + pub fn read_u16(&mut self) -> std::io::Result { self.remaining_size = self.remaining_size.saturating_sub(2); self.reader.read_u16::() } - pub(crate) fn read_u24(&mut self) -> std::io::Result { + pub fn read_u24(&mut self) -> std::io::Result { self.remaining_size = self.remaining_size.saturating_sub(3); self.reader.read_u24::() } - pub(crate) fn read_u32(&mut self) -> std::io::Result { + pub fn read_u32(&mut self) -> std::io::Result { self.remaining_size = self.remaining_size.saturating_sub(4); self.reader.read_u32::() } - pub(crate) fn read_u64(&mut self) -> std::io::Result { + pub fn read_u64(&mut self) -> std::io::Result { self.remaining_size = self.remaining_size.saturating_sub(8); self.reader.read_u64::() } - pub(crate) fn read_uint(&mut self, size: usize) -> std::io::Result { + pub fn read_uint(&mut self, size: usize) -> std::io::Result { self.remaining_size = self.remaining_size.saturating_sub(size as u64); self.reader.read_uint::(size) } @@ -86,7 +86,7 @@ where /// Read the next atom in the file /// /// This will leave the reader at the beginning of the atom content. - pub(crate) fn next(&mut self) -> Result> { + pub fn next(&mut self) -> Result> { if self.remaining_size == 0 { return Ok(None); } @@ -95,10 +95,10 @@ where err!(SizeMismatch); } - AtomInfo::read(self, self.remaining_size, self.parse_mode) + AtomInfo::read(self, self.remaining_size, self.parse_mode).map_err(Into::into) } - pub(crate) fn into_inner(self) -> R { + pub fn into_inner(self) -> R { self.reader } } diff --git a/aud_io/src/mp4/constants.rs b/aud_io/src/mp4/constants.rs new file mode 100644 index 000000000..c783555a0 --- /dev/null +++ b/aud_io/src/mp4/constants.rs @@ -0,0 +1,6 @@ +/// The 15 valid sample rate indices in MP4 +/// +/// See +pub const SAMPLE_RATES: [u32; 15] = [ + 96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350, 0, 0, +]; diff --git a/aud_io/src/mp4/mod.rs b/aud_io/src/mp4/mod.rs new file mode 100644 index 000000000..e83a90e42 --- /dev/null +++ b/aud_io/src/mp4/mod.rs @@ -0,0 +1,13 @@ +mod atom_info; +pub use atom_info::*; + +mod atom_reader; + +pub use atom_reader::*; + +mod constants; + +pub use constants::*; + +mod properties; +pub use properties::*; diff --git a/aud_io/src/mp4/properties.rs b/aud_io/src/mp4/properties.rs new file mode 100644 index 000000000..b136bdd3a --- /dev/null +++ b/aud_io/src/mp4/properties.rs @@ -0,0 +1,105 @@ +#[allow(missing_docs)] +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +#[rustfmt::skip] +#[non_exhaustive] +pub enum AudioObjectType { + // https://en.wikipedia.org/wiki/MPEG-4_Part_3#MPEG-4_Audio_Object_Types + + #[default] + NULL = 0, + AacMain = 1, // AAC Main Profile + AacLowComplexity = 2, // AAC Low Complexity + AacScalableSampleRate = 3, // AAC Scalable Sample Rate + AacLongTermPrediction = 4, // AAC Long Term Predictor + SpectralBandReplication = 5, // Spectral band Replication + AACScalable = 6, // AAC Scalable + TwinVQ = 7, // Twin VQ + CodeExcitedLinearPrediction = 8, // CELP + HarmonicVectorExcitationCoding = 9, // HVXC + TextToSpeechtInterface = 12, // TTSI + MainSynthetic = 13, // Main Synthetic + WavetableSynthesis = 14, // Wavetable Synthesis + GeneralMIDI = 15, // General MIDI + AlgorithmicSynthesis = 16, // Algorithmic Synthesis + ErrorResilientAacLowComplexity = 17, // ER AAC LC + ErrorResilientAacLongTermPrediction = 19, // ER AAC LTP + ErrorResilientAacScalable = 20, // ER AAC Scalable + ErrorResilientAacTwinVQ = 21, // ER AAC TwinVQ + ErrorResilientAacBitSlicedArithmeticCoding = 22, // ER Bit Sliced Arithmetic Coding + ErrorResilientAacLowDelay = 23, // ER AAC Low Delay + ErrorResilientCodeExcitedLinearPrediction = 24, // ER CELP + ErrorResilientHarmonicVectorExcitationCoding = 25, // ER HVXC + ErrorResilientHarmonicIndividualLinesNoise = 26, // ER HILN + ErrorResilientParametric = 27, // ER Parametric + SinuSoidalCoding = 28, // SSC + ParametricStereo = 29, // PS + MpegSurround = 30, // MPEG Surround + MpegLayer1 = 32, // MPEG Layer 1 + MpegLayer2 = 33, // MPEG Layer 2 + MpegLayer3 = 34, // MPEG Layer 3 + DirectStreamTransfer = 35, // DST Direct Stream Transfer + AudioLosslessCoding = 36, // ALS Audio Lossless Coding + ScalableLosslessCoding = 37, // SLC Scalable Lossless Coding + ScalableLosslessCodingNoneCore = 38, // SLC non-core + ErrorResilientAacEnhancedLowDelay = 39, // ER AAC ELD + SymbolicMusicRepresentationSimple = 40, // SMR Simple + SymbolicMusicRepresentationMain = 41, // SMR Main + UnifiedSpeechAudioCoding = 42, // USAC + SpatialAudioObjectCoding = 43, // SAOC + LowDelayMpegSurround = 44, // LD MPEG Surround + SpatialAudioObjectCodingDialogueEnhancement = 45, // SAOC-DE + AudioSync = 46, // Audio Sync +} + +impl TryFrom for AudioObjectType { + type Error = (); + + #[rustfmt::skip] + fn try_from(value: u8) -> std::result::Result { + match value { + 1 => Ok(Self::AacMain), + 2 => Ok(Self::AacLowComplexity), + 3 => Ok(Self::AacScalableSampleRate), + 4 => Ok(Self::AacLongTermPrediction), + 5 => Ok(Self::SpectralBandReplication), + 6 => Ok(Self::AACScalable), + 7 => Ok(Self::TwinVQ), + 8 => Ok(Self::CodeExcitedLinearPrediction), + 9 => Ok(Self::HarmonicVectorExcitationCoding), + 12 => Ok(Self::TextToSpeechtInterface), + 13 => Ok(Self::MainSynthetic), + 14 => Ok(Self::WavetableSynthesis), + 15 => Ok(Self::GeneralMIDI), + 16 => Ok(Self::AlgorithmicSynthesis), + 17 => Ok(Self::ErrorResilientAacLowComplexity), + 19 => Ok(Self::ErrorResilientAacLongTermPrediction), + 20 => Ok(Self::ErrorResilientAacScalable), + 21 => Ok(Self::ErrorResilientAacTwinVQ), + 22 => Ok(Self::ErrorResilientAacBitSlicedArithmeticCoding), + 23 => Ok(Self::ErrorResilientAacLowDelay), + 24 => Ok(Self::ErrorResilientCodeExcitedLinearPrediction), + 25 => Ok(Self::ErrorResilientHarmonicVectorExcitationCoding), + 26 => Ok(Self::ErrorResilientHarmonicIndividualLinesNoise), + 27 => Ok(Self::ErrorResilientParametric), + 28 => Ok(Self::SinuSoidalCoding), + 29 => Ok(Self::ParametricStereo), + 30 => Ok(Self::MpegSurround), + 32 => Ok(Self::MpegLayer1), + 33 => Ok(Self::MpegLayer2), + 34 => Ok(Self::MpegLayer3), + 35 => Ok(Self::DirectStreamTransfer), + 36 => Ok(Self::AudioLosslessCoding), + 37 => Ok(Self::ScalableLosslessCoding), + 38 => Ok(Self::ScalableLosslessCodingNoneCore), + 39 => Ok(Self::ErrorResilientAacEnhancedLowDelay), + 40 => Ok(Self::SymbolicMusicRepresentationSimple), + 41 => Ok(Self::SymbolicMusicRepresentationMain), + 42 => Ok(Self::UnifiedSpeechAudioCoding), + 43 => Ok(Self::SpatialAudioObjectCoding), + 44 => Ok(Self::LowDelayMpegSurround), + 45 => Ok(Self::SpatialAudioObjectCodingDialogueEnhancement), + 46 => Ok(Self::AudioSync), + _ => Err(()), + } + } +} diff --git a/lofty/src/mpeg/constants.rs b/aud_io/src/mpeg/constants.rs similarity index 100% rename from lofty/src/mpeg/constants.rs rename to aud_io/src/mpeg/constants.rs diff --git a/aud_io/src/mpeg/error.rs b/aud_io/src/mpeg/error.rs new file mode 100644 index 000000000..29aaaa7a2 --- /dev/null +++ b/aud_io/src/mpeg/error.rs @@ -0,0 +1,91 @@ +use std::fmt::Display; + +#[derive(Debug)] +pub enum MpegFrameError { + BadVersion, + BadLayer, + BadBitrate, + BadSampleRate, +} + +impl From for crate::error::AudioError { + fn from(err: MpegFrameError) -> Self { + crate::error::AudioError::Mpeg(err.into()) + } +} + +impl Display for MpegFrameError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + MpegFrameError::BadVersion => write!(f, "Invalid MPEG frame version"), + MpegFrameError::BadLayer => write!(f, "Invalid MPEG frame layer"), + MpegFrameError::BadBitrate => write!(f, "MPEG frame has an invalid bitrate index"), + MpegFrameError::BadSampleRate => write!(f, "MPEG frame has an sample rate index"), + } + } +} + +impl core::error::Error for MpegFrameError {} + +#[derive(Debug)] +pub enum VbrHeaderError { + BadXing, + BadVbri, + UnknownHeader, + + Io(std::io::Error), +} + +impl From for VbrHeaderError { + fn from(err: std::io::Error) -> Self { + VbrHeaderError::Io(err) + } +} + +impl From for crate::error::AudioError { + fn from(err: VbrHeaderError) -> Self { + crate::error::AudioError::Mpeg(err.into()) + } +} + +impl Display for VbrHeaderError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + VbrHeaderError::BadXing => write!(f, "Xing header is invalid"), + VbrHeaderError::BadVbri => write!(f, "VBRI header is invalid"), + VbrHeaderError::UnknownHeader => write!(f, "Unknown VBR header type"), + VbrHeaderError::Io(e) => write!(f, "{e}"), + } + } +} + +impl core::error::Error for VbrHeaderError {} + +#[derive(Debug)] +pub enum MpegError { + Frame(MpegFrameError), + Vbr(VbrHeaderError), +} + +impl From for MpegError { + fn from(err: MpegFrameError) -> Self { + MpegError::Frame(err) + } +} + +impl From for MpegError { + fn from(err: VbrHeaderError) -> Self { + MpegError::Vbr(err) + } +} + +impl Display for MpegError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + MpegError::Frame(err) => write!(f, "{err}"), + MpegError::Vbr(err) => write!(f, "{err}"), + } + } +} + +impl core::error::Error for MpegError {} diff --git a/aud_io/src/mpeg/header.rs b/aud_io/src/mpeg/header.rs new file mode 100644 index 000000000..c8519787b --- /dev/null +++ b/aud_io/src/mpeg/header.rs @@ -0,0 +1,244 @@ +use super::constants::{BITRATES, PADDING_SIZES, SAMPLE_RATES, SAMPLES, SIDE_INFORMATION_SIZES}; +use super::error::{MpegFrameError, VbrHeaderError}; + +use std::io::Read; + +use byteorder::{BigEndian, ReadBytesExt}; + +/// MPEG Audio version +#[derive(Default, PartialEq, Eq, Copy, Clone, Debug)] +#[allow(missing_docs)] +pub enum MpegVersion { + #[default] + V1, + V2, + V2_5, + /// Exclusive to AAC + V4, +} + +/// MPEG layer +#[derive(Default, Copy, Clone, Debug, PartialEq, Eq)] +#[allow(missing_docs)] +pub enum Layer { + Layer1 = 1, + Layer2 = 2, + #[default] + Layer3 = 3, +} + +/// Channel mode +#[derive(Default, Copy, Clone, PartialEq, Eq, Debug)] +#[allow(missing_docs)] +pub enum ChannelMode { + #[default] + Stereo = 0, + JointStereo = 1, + /// Two independent mono channels + DualChannel = 2, + SingleChannel = 3, +} + +/// A rarely-used decoder hint that the file must be de-emphasized +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[allow(missing_docs, non_camel_case_types)] +pub enum Emphasis { + /// 50/15 ms + MS5015, + Reserved, + /// CCIT J.17 + CCIT_J17, +} + +#[derive(Copy, Clone, Debug)] +pub struct FrameHeader { + pub sample_rate: u32, + pub len: u32, + pub data_start: u32, + pub samples: u16, + pub bitrate: u32, + pub version: MpegVersion, + pub layer: Layer, + pub channel_mode: ChannelMode, + pub mode_extension: Option, + pub copyright: bool, + pub original: bool, + pub emphasis: Option, +} + +impl FrameHeader { + pub fn parse(data: u32) -> Result { + let version = match (data >> 19) & 0b11 { + 0b00 => MpegVersion::V2_5, + 0b10 => MpegVersion::V2, + 0b11 => MpegVersion::V1, + _ => return Err(MpegFrameError::BadVersion), + }; + + let version_index = if version == MpegVersion::V1 { 0 } else { 1 }; + + let layer = match (data >> 17) & 0b11 { + 0b01 => Layer::Layer3, + 0b10 => Layer::Layer2, + 0b11 => Layer::Layer1, + _ => { + log::debug!("MPEG: Frame header uses a reserved layer"); + return Err(MpegFrameError::BadLayer); + }, + }; + + let mut header = FrameHeader { + sample_rate: 0, + len: 0, + data_start: 0, + samples: 0, + bitrate: 0, + version, + layer, + channel_mode: ChannelMode::default(), + mode_extension: None, + copyright: false, + original: false, + emphasis: None, + }; + + let layer_index = (header.layer as usize).saturating_sub(1); + + let bitrate_index = (data >> 12) & 0xF; + header.bitrate = BITRATES[version_index][layer_index][bitrate_index as usize]; + if header.bitrate == 0 { + return Err(MpegFrameError::BadBitrate); + } + + // Sample rate index + let sample_rate_index = (data >> 10) & 0b11; + header.sample_rate = match sample_rate_index { + // This is invalid + 0b11 => return Err(MpegFrameError::BadSampleRate), + _ => SAMPLE_RATES[header.version as usize][sample_rate_index as usize], + }; + + let has_padding = ((data >> 9) & 1) == 1; + let mut padding = 0; + + if has_padding { + padding = u32::from(PADDING_SIZES[layer_index]); + } + + header.channel_mode = match (data >> 6) & 0b11 { + 0b00 => ChannelMode::Stereo, + 0b01 => ChannelMode::JointStereo, + 0b10 => ChannelMode::DualChannel, + 0b11 => ChannelMode::SingleChannel, + _ => unreachable!(), + }; + + if let ChannelMode::JointStereo = header.channel_mode { + header.mode_extension = Some(((data >> 4) & 3) as u8); + } else { + header.mode_extension = None; + } + + header.copyright = ((data >> 3) & 1) == 1; + header.original = ((data >> 2) & 1) == 1; + + header.emphasis = match data & 0b11 { + 0b00 => None, + 0b01 => Some(Emphasis::MS5015), + 0b10 => Some(Emphasis::Reserved), + 0b11 => Some(Emphasis::CCIT_J17), + _ => unreachable!(), + }; + + header.data_start = SIDE_INFORMATION_SIZES[version_index][header.channel_mode as usize] + 4; + header.samples = SAMPLES[layer_index][version_index]; + header.len = + (u32::from(header.samples) * header.bitrate * 125 / header.sample_rate) + padding; + + Ok(header) + } + + /// Equivalent of [`cmp_header()`], but for an already constructed `Header`. + pub fn cmp(self, other: &Self) -> bool { + self.version == other.version + && self.layer == other.layer + && self.sample_rate == other.sample_rate + } +} + +#[derive(Copy, Clone)] +pub enum VbrHeaderType { + Xing, + Info, + Vbri, +} + +#[derive(Copy, Clone)] +pub struct VbrHeader { + pub ty: VbrHeaderType, + pub frames: u32, + pub size: u32, +} + +impl VbrHeader { + pub fn parse(reader: &mut R) -> Result { + let mut header = [0; 4]; + reader.read_exact(&mut header)?; + + match &header { + b"Xing" | b"Info" => { + let mut flags = [0; 4]; + reader.read_exact(&mut flags)?; + + if flags[3] & 0x03 != 0x03 { + log::debug!( + "MPEG: Xing header doesn't have required flags set (0x0001 and 0x0002)" + ); + return Err(VbrHeaderError::BadXing); + } + + let frames = reader + .read_u32::() + .map_err(|_| VbrHeaderError::BadXing)?; + let size = reader + .read_u32::() + .map_err(|_| VbrHeaderError::BadXing)?; + + let ty = match &header { + b"Xing" => VbrHeaderType::Xing, + b"Info" => VbrHeaderType::Info, + _ => unreachable!(), + }; + + Ok(Self { ty, frames, size }) + }, + b"VBRI" => { + // Skip 6 bytes + // Version ID (2) + // Delay float (2) + // Quality indicator (2) + let _info = reader + .read_uint::(6) + .map_err(|_| VbrHeaderError::BadVbri)?; + + let size = reader + .read_u32::() + .map_err(|_| VbrHeaderError::BadVbri)?; + let frames = reader + .read_u32::() + .map_err(|_| VbrHeaderError::BadVbri)?; + + Ok(Self { + ty: VbrHeaderType::Vbri, + frames, + size, + }) + }, + _ => Err(VbrHeaderError::UnknownHeader), + } + } + + pub fn is_valid(&self) -> bool { + self.frames > 0 && self.size > 0 + } +} diff --git a/aud_io/src/mpeg/mod.rs b/aud_io/src/mpeg/mod.rs new file mode 100644 index 000000000..bc8daf294 --- /dev/null +++ b/aud_io/src/mpeg/mod.rs @@ -0,0 +1,5 @@ +mod header; +pub use header::*; + +mod constants; +pub mod error; diff --git a/lofty/src/musepack/constants.rs b/aud_io/src/musepack/constants.rs similarity index 66% rename from lofty/src/musepack/constants.rs rename to aud_io/src/musepack/constants.rs index 5b3aedffd..aa39158fe 100644 --- a/lofty/src/musepack/constants.rs +++ b/aud_io/src/musepack/constants.rs @@ -7,11 +7,11 @@ // static const mpc_int32_t samplefreqs[8] = { 44100, 48000, 37800, 32000 }; // // So it's safe to just fill the rest with zeroes -pub(super) const FREQUENCY_TABLE: [u32; 8] = [44100, 48000, 37800, 32000, 0, 0, 0, 0]; +pub const FREQUENCY_TABLE: [u32; 8] = [44100, 48000, 37800, 32000, 0, 0, 0, 0]; // Taken from mpcdec /// This is the gain reference used in old ReplayGain pub const MPC_OLD_GAIN_REF: f32 = 64.82; -pub(super) const MPC_DECODER_SYNTH_DELAY: u64 = 481; -pub(super) const MPC_FRAME_LENGTH: u64 = 36 * 32; // Samples per mpc frame +pub const MPC_DECODER_SYNTH_DELAY: u64 = 481; +pub const MPC_FRAME_LENGTH: u64 = 36 * 32; // Samples per mpc frame diff --git a/aud_io/src/musepack/error.rs b/aud_io/src/musepack/error.rs new file mode 100644 index 000000000..59b05d798 --- /dev/null +++ b/aud_io/src/musepack/error.rs @@ -0,0 +1,23 @@ +use std::fmt::Display; + +#[derive(Debug)] +pub enum MusePackError { + BadPacketKey, + UnexpectedStreamVersion { expected: u8, actual: u8 }, +} + +impl Display for MusePackError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MusePackError::BadPacketKey => write!( + f, + "Packet key contains characters that are out of the allowed range" + ), + MusePackError::UnexpectedStreamVersion { expected, actual } => { + write!(f, "Expected stream version {expected}, got {actual}") + }, + } + } +} + +impl core::error::Error for MusePackError {} diff --git a/aud_io/src/musepack/mod.rs b/aud_io/src/musepack/mod.rs new file mode 100644 index 000000000..6e1109f47 --- /dev/null +++ b/aud_io/src/musepack/mod.rs @@ -0,0 +1,5 @@ +pub mod constants; +pub mod error; +pub mod sv4to6; +pub mod sv7; +pub mod sv8; diff --git a/aud_io/src/musepack/sv4to6.rs b/aud_io/src/musepack/sv4to6.rs new file mode 100644 index 000000000..537259996 --- /dev/null +++ b/aud_io/src/musepack/sv4to6.rs @@ -0,0 +1,43 @@ +use crate::error::Result; +use crate::musepack::error::MusePackError; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] +pub struct StreamHeader { + pub average_bitrate: u32, + pub intensity_stereo: bool, + pub mid_side_stereo: bool, + pub stream_version: u16, + pub max_band: u8, + pub block_size: u32, + pub frame_count: u32, +} + +impl StreamHeader { + pub fn parse(header_data: [u32; 8]) -> Result { + let mut header = Self::default(); + + header.average_bitrate = (header_data[0] >> 23) & 0x1FF; + header.intensity_stereo = (header_data[0] >> 22) & 0x1 == 1; + header.mid_side_stereo = (header_data[0] >> 21) & 0x1 == 1; + + header.stream_version = ((header_data[0] >> 11) & 0x03FF) as u16; + if !(4..=6).contains(&header.stream_version) { + return Err(MusePackError::UnexpectedStreamVersion { + expected: 4, + actual: header.stream_version as u8, + } + .into()); + } + + header.max_band = ((header_data[0] >> 6) & 0x1F) as u8; + header.block_size = header_data[0] & 0x3F; + + if header.stream_version >= 5 { + header.frame_count = header_data[1]; // 32 bit + } else { + header.frame_count = header_data[1] >> 16; // 16 bit + } + + Ok(header) + } +} diff --git a/aud_io/src/musepack/sv7.rs b/aud_io/src/musepack/sv7.rs new file mode 100644 index 000000000..c0b97fcec --- /dev/null +++ b/aud_io/src/musepack/sv7.rs @@ -0,0 +1,227 @@ +use super::constants::{FREQUENCY_TABLE, MPC_OLD_GAIN_REF}; +use crate::error::Result; +use crate::musepack::error::MusePackError; + +use std::io::Read; + +use byteorder::{LittleEndian, ReadBytesExt}; + +/// Used profile +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum Profile { + /// No profile + #[default] + None, + /// Unstable/Experimental + Unstable, + /// Profiles 2-4 + Unused, + /// Below Telephone (q= 0.0) + BelowTelephone0, + /// Below Telephone (q= 1.0) + BelowTelephone1, + /// Telephone (q= 2.0) + Telephone, + /// Thumb (q= 3.0) + Thumb, + /// Radio (q= 4.0) + Radio, + /// Standard (q= 5.0) + Standard, + /// Xtreme (q= 6.0) + Xtreme, + /// Insane (q= 7.0) + Insane, + /// BrainDead (q= 8.0) + BrainDead, + /// Above BrainDead (q= 9.0) + AboveBrainDead9, + /// Above BrainDead (q= 10.0) + AboveBrainDead10, +} + +impl TryFrom for Profile { + type Error = (); + + #[rustfmt::skip] + fn try_from(value: u8) -> std::result::Result { + match value { + 0 => Ok(Self::None), + 1 => Ok(Self::Unstable), + 2 | 3 | 4 => Ok(Self::Unused), + 5 => Ok(Self::BelowTelephone0), + 6 => Ok(Self::BelowTelephone1), + 7 => Ok(Self::Telephone), + 8 => Ok(Self::Thumb), + 9 => Ok(Self::Radio), + 10 => Ok(Self::Standard), + 11 => Ok(Self::Xtreme), + 12 => Ok(Self::Insane), + 13 => Ok(Self::BrainDead), + 14 => Ok(Self::AboveBrainDead9), + 15 => Ok(Self::AboveBrainDead10), + _ => Err(()), + } + } +} + +/// Volume description for the start and end of the title +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum Link { + /// Title starts or ends with a very low level (no live or classical genre titles) + #[default] + VeryLowStartOrEnd = 0, + /// Title ends loudly + LoudEnd = 1, + /// Title starts loudly + LoudStart = 2, + /// Title starts loudly and ends loudly + LoudStartAndEnd = 3, +} + +impl TryFrom for Link { + type Error = (); + + fn try_from(value: u8) -> std::result::Result { + match value { + 0 => Ok(Self::VeryLowStartOrEnd), + 1 => Ok(Self::LoudEnd), + 2 => Ok(Self::LoudStart), + 3 => Ok(Self::LoudStartAndEnd), + _ => Err(()), + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] +pub struct StreamHeader { + pub channels: u8, + + // Section 1 + pub frame_count: u32, + + // Section 2 + pub intensity_stereo: bool, + pub mid_side_stereo: bool, + pub max_band: u8, + pub profile: Profile, + pub link: Link, + pub sample_frequency: u32, + pub max_level: u16, + + // Section 3 + pub replaygain_title_peak: u16, + pub replaygain_title_gain: i16, + + // Section 4 + pub replaygain_album_peak: u16, + pub replaygain_album_gain: i16, + + // Section 5 + pub true_gapless: bool, + pub last_frame_length: u16, + pub fast_seeking_safe: bool, + + // Section 6 + pub encoder_version: u8, +} + +impl StreamHeader { + pub fn parse(reader: &mut R) -> Result + where + R: Read, + { + let version = reader.read_u8()? & 0x0F; + if version != 7 { + return Err(MusePackError::UnexpectedStreamVersion { + expected: 7, + actual: version, + } + .into()); + } + + let mut header = Self { + channels: 2, // Always 2 channels + ..Self::default() + }; + + // TODO: Make a Bitreader, would be nice crate-wide but especially here + // The SV7 header is split into 6 32-bit sections + + // -- Section 1 -- + header.frame_count = reader.read_u32::()?; + + // -- Section 2 -- + let chunk = reader.read_u32::()?; + + let byte1 = ((chunk & 0xFF00_0000) >> 24) as u8; + + header.intensity_stereo = ((byte1 & 0x80) >> 7) == 1; + header.mid_side_stereo = ((byte1 & 0x40) >> 6) == 1; + header.max_band = byte1 & 0x3F; + + let byte2 = ((chunk & 0xFF_0000) >> 16) as u8; + + header.profile = Profile::try_from((byte2 & 0xF0) >> 4).unwrap(); // Infallible + header.link = Link::try_from((byte2 & 0x0C) >> 2).unwrap(); // Infallible + + let sample_freq_index = byte2 & 0x03; + header.sample_frequency = FREQUENCY_TABLE[sample_freq_index as usize]; + + let remaining_bytes = (chunk & 0xFFFF) as u16; + header.max_level = remaining_bytes; + + // -- Section 3 -- + let title_peak = reader.read_u16::()?; + let title_gain = reader.read_u16::()?; + + // -- Section 4 -- + let album_peak = reader.read_u16::()?; + let album_gain = reader.read_u16::()?; + + // -- Section 5 -- + let chunk = reader.read_u32::()?; + + header.true_gapless = (chunk >> 31) == 1; + + if header.true_gapless { + header.last_frame_length = ((chunk >> 20) & 0x7FF) as u16; + } + + header.fast_seeking_safe = (chunk >> 19) & 1 == 1; + + // NOTE: Rest of the chunk is zeroed and unused + + // -- Section 6 -- + header.encoder_version = reader.read_u8()?; + + // -- End of parsing -- + + // Convert ReplayGain values + let set_replay_gain = |gain: u16| -> i16 { + if gain == 0 { + return 0; + } + + let gain = ((MPC_OLD_GAIN_REF - f32::from(gain) / 100.0) * 256.0 + 0.5) as i16; + if !(0..i16::MAX).contains(&gain) { + return 0; + } + gain + }; + let set_replay_peak = |peak: u16| -> u16 { + if peak == 0 { + return 0; + } + + ((f64::from(peak).log10() * 20.0 * 256.0) + 0.5) as u16 + }; + + header.replaygain_title_gain = set_replay_gain(title_gain); + header.replaygain_title_peak = set_replay_peak(title_peak); + header.replaygain_album_gain = set_replay_gain(album_gain); + header.replaygain_album_peak = set_replay_peak(album_peak); + + Ok(header) + } +} diff --git a/aud_io/src/musepack/sv8.rs b/aud_io/src/musepack/sv8.rs new file mode 100644 index 000000000..e8406fc9f --- /dev/null +++ b/aud_io/src/musepack/sv8.rs @@ -0,0 +1,299 @@ +use super::constants::FREQUENCY_TABLE; +use super::error::MusePackError; +use crate::err; +use crate::error::Result; + +use std::io::Read; + +use byteorder::{BigEndian, ReadBytesExt}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum PacketKey { + StreamHeader, + ReplayGain, + EncoderInfo, + SeekTableOffset, + Audio, + SeekTable, + Chapter, + StreamEnd, +} + +impl TryFrom<[u8; 2]> for PacketKey { + type Error = (); + + fn try_from(value: [u8; 2]) -> std::result::Result { + match &value { + b"SH" => Ok(PacketKey::StreamHeader), + b"RG" => Ok(PacketKey::ReplayGain), + b"EI" => Ok(PacketKey::EncoderInfo), + b"SO" => Ok(PacketKey::SeekTableOffset), + b"AP" => Ok(PacketKey::Audio), + b"ST" => Ok(PacketKey::SeekTable), + b"CT" => Ok(PacketKey::Chapter), + b"SE" => Ok(PacketKey::StreamEnd), + _ => Err(()), + } + } +} + +pub struct PacketReader { + reader: R, + capacity: u64, +} + +impl PacketReader { + pub fn new(reader: R) -> Self { + Self { + reader, + capacity: 0, + } + } + + /// Move the reader to the next packet, returning the next packet key and size + pub fn next(&mut self) -> Result<([u8; 2], u64)> { + // Discard the rest of the current packet + std::io::copy( + &mut self.reader.by_ref().take(self.capacity), + &mut std::io::sink(), + )?; + + // Packet format: + // + // Field | Size (bits) | Value + // Key | 16 | "EX" + // Size | n*8; 0 < n < 10 | 0x1A + // Payload | Size * 8 | "example" + + let mut key = [0; 2]; + self.reader.read_exact(&mut key)?; + + if !key[0].is_ascii_uppercase() || !key[1].is_ascii_uppercase() { + return Err(MusePackError::BadPacketKey.into()); + } + + let (packet_size, packet_size_byte_count) = Self::read_size(&mut self.reader)?; + + // The packet size contains the key (2) and the size (?, variable length <= 9) + self.capacity = packet_size.saturating_sub(u64::from(2 + packet_size_byte_count)); + + Ok((key, self.capacity)) + } + + /// Read the variable-length packet size + /// + /// This takes a reader since we need to both use it for packet reading *and* setting up the reader itself in `PacketReader::next` + pub fn read_size(reader: &mut R) -> Result<(u64, u8)> { + let mut current; + let mut size = 0u64; + + // bits, big-endian + // 0xxx xxxx - value 0 to 2^7-1 + // 1xxx xxxx 0xxx xxxx - value 0 to 2^14-1 + // 1xxx xxxx 1xxx xxxx 0xxx xxxx - value 0 to 2^21-1 + // 1xxx xxxx 1xxx xxxx 1xxx xxxx 0xxx xxxx - value 0 to 2^28-1 + // ... + + let mut bytes_read = 0; + loop { + current = reader.read_u8()?; + bytes_read += 1; + + // Sizes cannot go above 9 bytes + if bytes_read > 9 { + err!(TooMuchData); + } + + size = (size << 7) | u64::from(current & 0x7F); + if current & 0x80 == 0 { + break; + } + } + + Ok((size, bytes_read)) + } +} + +impl Read for PacketReader { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let bytes_read = self.reader.by_ref().take(self.capacity).read(buf)?; + self.capacity = self.capacity.saturating_sub(bytes_read as u64); + Ok(bytes_read) + } +} + +/// Information from a Stream Header packet +/// +/// This contains the information needed to decode the stream. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct StreamHeader { + /// CRC 32 of the stream header packet + /// + /// The CRC used is here: + pub crc: u32, + /// Bitstream version + pub stream_version: u8, + /// Number of samples in the stream. 0 = unknown + pub sample_count: u64, + /// Number of samples to skip at the beginning of the stream + pub beginning_silence: u64, + /// The sampling frequency + /// + /// NOTE: This is not the index into the frequency table, this is the mapped value. + pub sample_rate: u32, + /// Maximum number of bands used in the file + pub max_used_bands: u8, + /// Number of channels in the stream + pub channels: u8, + /// Whether Mid Side Stereo is enabled + pub ms_used: bool, + /// Number of frames per audio packet + pub audio_block_frames: u16, +} + +impl StreamHeader { + pub fn parse(reader: &mut PacketReader) -> Result { + // StreamHeader format: + // + // Field | Size (bits) | Value | Comment + // CRC | 32 | | CRC 32 of the block (this field excluded). 0 = invalid + // Stream version | 8 | 8 | Bitstream version + // Sample count | n*8; 0 < n < 10 | | Number of samples in the stream. 0 = unknown + // Beginning silence | n*8; 0 < n < 10 | | Number of samples to skip at the beginning of the stream + // Sample frequency | 3 | 0..7 | See table below + // Max used bands | 5 | 1..32 | Maximum number of bands used in the file + // Channel count | 4 | 1..16 | Number of channels in the stream + // MS used | 1 | | True if Mid Side Stereo is enabled + // Audio block frames | 3 | 0..7 | Number of frames per audio packet (4value=(1..16384)) + + let crc = reader.read_u32::()?; + let stream_version = reader.read_u8()?; + let (sample_count, _) = PacketReader::read_size(reader)?; + let (beginning_silence, _) = PacketReader::read_size(reader)?; + + // Sample rate and max used bands + let remaining_flags_byte_1 = reader.read_u8()?; + + let sample_rate_index = (remaining_flags_byte_1 & 0xE0) >> 5; + let sample_rate = FREQUENCY_TABLE[sample_rate_index as usize]; + + let max_used_bands = (remaining_flags_byte_1 & 0x1F) + 1; + + // Channel count, MS used, audio block frames + let remaining_flags_byte_2 = reader.read_u8()?; + + let channels = (remaining_flags_byte_2 >> 4) + 1; + let ms_used = remaining_flags_byte_2 & 0x08 == 0x08; + + let audio_block_frames_value = remaining_flags_byte_2 & 0x07; + let audio_block_frames = 4u16.pow(u32::from(audio_block_frames_value)); + + Ok(Self { + crc, + stream_version, + sample_count, + beginning_silence, + sample_rate, + max_used_bands, + channels, + ms_used, + audio_block_frames, + }) + } +} + +/// Information from a ReplayGain packet +/// +/// This contains the necessary data needed to apply ReplayGain on the current stream. +/// +/// The ReplayGain values are stored in dB in Q8.8 format. +/// A value of `0` means that this field has not been computed (no gain must be applied in this case). +/// +/// Examples: +/// +/// * ReplayGain finds that this title has a loudness of 78.56 dB. It will be encoded as $ 78.56 * 256 ~ 20111 = 0x4E8F $ +/// * For 16-bit output (range \[-32767 32768]), the max is 68813 (out of range). It will be encoded as $ 20 * log10(68813) * 256 ~ 24769 = 0x60C1 $ +/// * For float output (range \[-1 1]), the max is 0.96. It will be encoded as $ 20 * log10(0.96 * 215) * 256 ~ 23029 = 0x59F5 $ (for peak values it is suggested to round to nearest higher integer) +#[derive(Debug, Clone, Copy, PartialEq, Default)] +#[allow(missing_docs)] +pub struct ReplayGain { + /// The replay gain version + pub version: u8, + /// The loudness calculated for the title, and not the gain that the player must apply + pub title_gain: u16, + pub title_peak: u16, + /// The loudness calculated for the album + pub album_gain: u16, + pub album_peak: u16, +} + +impl ReplayGain { + pub fn parse(reader: &mut PacketReader) -> Result { + // ReplayGain format: + // + // Field | Size (bits) | Value | Comment + // ReplayGain version | 8 | 1 | The replay gain version + // Title gain | 16 | | The loudness calculated for the title, and not the gain that the player must apply + // Title peak | 16 | | + // Album gain | 16 | | The loudness calculated for the album + // Album peak | 16 | | + + let version = reader.read_u8()?; + let title_gain = reader.read_u16::()?; + let title_peak = reader.read_u16::()?; + let album_gain = reader.read_u16::()?; + let album_peak = reader.read_u16::()?; + + Ok(Self { + version, + title_gain, + title_peak, + album_gain, + album_peak, + }) + } +} + +/// Information from an Encoder Info packet +#[derive(Debug, Clone, Copy, PartialEq, Default)] +#[allow(missing_docs)] +pub struct EncoderInfo { + /// Quality in 4.3 format + pub profile: f32, + pub pns_tool: bool, + /// Major version + pub major: u8, + /// Minor version, even numbers for stable version, odd when unstable + pub minor: u8, + /// Build + pub build: u8, +} + +impl EncoderInfo { + pub fn parse(reader: &mut PacketReader) -> Result { + // EncoderInfo format: + // + // Field | Size (bits) | Value + // Profile | 7 | 0..15.875 + // PNS tool | 1 | True if enabled + // Major | 8 | 1 + // Minor | 8 | 17 + // Build | 8 | 3 + + let byte1 = reader.read_u8()?; + let profile = f32::from((byte1 & 0xFE) >> 1) / 8.0; + let pns_tool = byte1 & 0x01 == 1; + + let major = reader.read_u8()?; + let minor = reader.read_u8()?; + let build = reader.read_u8()?; + + Ok(Self { + profile, + pns_tool, + major, + minor, + build, + }) + } +} diff --git a/lofty/src/util/text.rs b/aud_io/src/text.rs similarity index 83% rename from lofty/src/util/text.rs rename to aud_io/src/text.rs index f1fd7e201..192a96d07 100644 --- a/lofty/src/util/text.rs +++ b/aud_io/src/text.rs @@ -1,5 +1,5 @@ -use crate::error::{ErrorKind, LoftyError, Result}; -use crate::macros::err; +use crate::err; +use crate::error::{AudioError, Result}; use std::io::Read; @@ -31,36 +31,20 @@ impl TextEncoding { } } - pub(crate) fn verify_latin1(text: &str) -> bool { + pub fn verify_latin1(text: &str) -> bool { text.chars().all(|c| c as u32 <= 255) } - - /// ID3v2.4 introduced two new text encodings. - /// - /// When writing ID3v2.3, we just substitute with UTF-16. - pub(crate) fn to_id3v23(self) -> Self { - match self { - Self::UTF8 | Self::UTF16BE => { - log::warn!( - "Text encoding {:?} is not supported in ID3v2.3, substituting with UTF-16", - self - ); - Self::UTF16 - }, - _ => self, - } - } } #[derive(Eq, PartialEq, Debug)] -pub(crate) struct DecodeTextResult { - pub(crate) content: String, - pub(crate) bytes_read: usize, - pub(crate) bom: [u8; 2], +pub struct DecodeTextResult { + pub content: String, + pub bytes_read: usize, + pub bom: [u8; 2], } impl DecodeTextResult { - pub(crate) fn text_or_none(self) -> Option { + pub fn text_or_none(self) -> Option { if self.content.is_empty() { return None; } @@ -83,28 +67,28 @@ const EMPTY_DECODED_TEXT: DecodeTextResult = DecodeTextResult { /// * Not expect the text to be null terminated /// * Have no byte order mark #[derive(Copy, Clone, Debug)] -pub(crate) struct TextDecodeOptions { +pub struct TextDecodeOptions { pub encoding: TextEncoding, pub terminated: bool, pub bom: [u8; 2], } impl TextDecodeOptions { - pub(crate) fn new() -> Self { + pub fn new() -> Self { Self::default() } - pub(crate) fn encoding(mut self, encoding: TextEncoding) -> Self { + pub fn encoding(mut self, encoding: TextEncoding) -> Self { self.encoding = encoding; self } - pub(crate) fn terminated(mut self, terminated: bool) -> Self { + pub fn terminated(mut self, terminated: bool) -> Self { self.terminated = terminated; self } - pub(crate) fn bom(mut self, bom: [u8; 2]) -> Self { + pub fn bom(mut self, bom: [u8; 2]) -> Self { self.bom = bom; self } @@ -120,7 +104,7 @@ impl Default for TextDecodeOptions { } } -pub(crate) fn decode_text(reader: &mut R, options: TextDecodeOptions) -> Result +pub fn decode_text(reader: &mut R, options: TextDecodeOptions) -> Result where R: Read, { @@ -173,8 +157,9 @@ where } }, TextEncoding::UTF16BE => utf16_decode_bytes(raw_bytes.as_slice(), u16::from_be_bytes)?, - TextEncoding::UTF8 => utf8_decode(raw_bytes) - .map_err(|_| LoftyError::new(ErrorKind::TextDecode("Expected a UTF-8 string")))?, + TextEncoding::UTF8 => { + utf8_decode(raw_bytes).map_err(|_| AudioError::TextDecode("Expected a UTF-8 string"))? + }, }; Ok(DecodeTextResult { @@ -224,7 +209,7 @@ pub(crate) fn latin1_decode(bytes: &[u8]) -> String { text } -pub(crate) fn utf8_decode(bytes: Vec) -> Result { +pub fn utf8_decode(bytes: Vec) -> Result { String::from_utf8(bytes) .map(|mut text| { trim_end_nulls(&mut text); @@ -233,22 +218,22 @@ pub(crate) fn utf8_decode(bytes: Vec) -> Result { .map_err(Into::into) } -pub(crate) fn utf8_decode_str(bytes: &[u8]) -> Result<&str> { +pub fn utf8_decode_str(bytes: &[u8]) -> Result<&str> { std::str::from_utf8(bytes) .map(trim_end_nulls_str) .map_err(Into::into) } -pub(crate) fn utf16_decode(words: &[u16]) -> Result { +pub fn utf16_decode(words: &[u16]) -> Result { String::from_utf16(words) .map(|mut text| { trim_end_nulls(&mut text); text }) - .map_err(|_| LoftyError::new(ErrorKind::TextDecode("Given an invalid UTF-16 string"))) + .map_err(|_| AudioError::TextDecode("Given an invalid UTF-16 string")) } -pub(crate) fn utf16_decode_bytes(bytes: &[u8], endianness: fn([u8; 2]) -> u16) -> Result { +pub fn utf16_decode_bytes(bytes: &[u8], endianness: fn([u8; 2]) -> u16) -> Result { if bytes.is_empty() { return Ok(String::new()); } @@ -277,7 +262,7 @@ pub(crate) fn utf16_decode_bytes(bytes: &[u8], endianness: fn([u8; 2]) -> u16) - /// with a BOM. /// /// If no BOM is present, the string will be decoded using `endianness`. -pub(crate) fn utf16_decode_terminated_maybe_bom( +pub fn utf16_decode_terminated_maybe_bom( reader: &mut R, endianness: fn([u8; 2]) -> u16, ) -> Result<(String, usize)> @@ -297,7 +282,7 @@ where decoded.map(|d| (d, bytes_read)) } -pub(crate) fn encode_text(text: &str, text_encoding: TextEncoding, terminated: bool) -> Vec { +pub fn encode_text(text: &str, text_encoding: TextEncoding, terminated: bool) -> Vec { match text_encoding { TextEncoding::Latin1 => { let mut out = text.chars().map(|c| c as u8).collect::>(); @@ -322,14 +307,14 @@ pub(crate) fn encode_text(text: &str, text_encoding: TextEncoding, terminated: b } } -pub(crate) fn trim_end_nulls(text: &mut String) { +pub fn trim_end_nulls(text: &mut String) { if text.ends_with('\0') { let new_len = text.trim_end_matches('\0').len(); text.truncate(new_len); } } -pub(crate) fn trim_end_nulls_str(text: &str) -> &str { +pub fn trim_end_nulls_str(text: &str) -> &str { text.trim_end_matches('\0') } @@ -358,7 +343,7 @@ fn utf16_encode( #[cfg(test)] mod tests { - use crate::util::text::{TextDecodeOptions, TextEncoding}; + use super::{TextDecodeOptions, TextEncoding}; use std::io::Cursor; const TEST_STRING: &str = "l\u{00f8}ft\u{00a5}"; diff --git a/lofty/Cargo.toml b/lofty/Cargo.toml index 84ac1de32..97d19a492 100644 --- a/lofty/Cargo.toml +++ b/lofty/Cargo.toml @@ -1,18 +1,20 @@ [package] name = "lofty" version = "0.22.4" -authors = ["Serial <69764315+Serial-ATA@users.noreply.github.com>"] description = "Audio metadata library" keywords = ["tags", "audio", "metadata", "id3", "vorbis"] categories = ["multimedia", "multimedia::audio", "parser-implementations"] readme = "../README.md" include = ["src", "LICENSE-APACHE", "LICENSE-MIT", "SUPPORTED_FORMATS.md"] +authors.workspace = true edition.workspace = true rust-version.workspace = true repository.workspace = true license.workspace = true [dependencies] +aud_io = { workspace = true } + # Vorbis comments pictures data-encoding = "2.6.0" byteorder = { workspace = true } @@ -21,7 +23,7 @@ flate2 = { version = "1.0.30", optional = true } # Proc macros lofty_attr = { workspace = true } # Debug logging -log = "0.4.22" +log = { workspace = true } # OGG Vorbis/Opus ogg_pager = "0.7.0" # Key maps @@ -46,7 +48,7 @@ rusty-fork = "0.3.0" # tag_writer example structopt = { version = "0.3.26", default-features = false } tempfile = "3.15.0" -test-log = "0.2.16" +test-log = { workspace = true } gungraun = "0.17.0" [lints] diff --git a/lofty/src/aac/mod.rs b/lofty/src/aac/mod.rs index 46b2b91c8..fed53b25e 100644 --- a/lofty/src/aac/mod.rs +++ b/lofty/src/aac/mod.rs @@ -2,7 +2,6 @@ // TODO: Currently we only support ADTS, might want to look into ADIF in the future. -mod header; mod properties; mod read; diff --git a/lofty/src/aac/properties.rs b/lofty/src/aac/properties.rs index 40c0b6e4c..f71ac6f68 100644 --- a/lofty/src/aac/properties.rs +++ b/lofty/src/aac/properties.rs @@ -1,10 +1,11 @@ -use crate::aac::header::ADTSHeader; -use crate::mp4::AudioObjectType; -use crate::mpeg::header::MpegVersion; use crate::properties::{ChannelMask, FileProperties}; use std::time::Duration; +use aud_io::aac::ADTSHeader; +use aud_io::mp4::AudioObjectType; +use aud_io::mpeg::MpegVersion; + /// An AAC file's audio properties #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct AACProperties { diff --git a/lofty/src/aac/read.rs b/lofty/src/aac/read.rs index 6418f6b92..b6e2f4969 100644 --- a/lofty/src/aac/read.rs +++ b/lofty/src/aac/read.rs @@ -1,15 +1,16 @@ use super::AacFile; -use super::header::{ADTSHeader, HEADER_MASK}; use crate::config::{ParseOptions, ParsingMode}; use crate::error::Result; use crate::id3::v2::header::Id3v2Header; use crate::id3::v2::read::parse_id3v2; use crate::id3::{ID3FindResults, find_id3v1}; -use crate::macros::{decode_err, err, parse_mode_choice}; +use crate::macros::{decode_err, parse_mode_choice}; use crate::mpeg::header::{HeaderCmpResult, cmp_header, search_for_frame_sync}; use std::io::{Read, Seek, SeekFrom}; +use aud_io::aac::ADTSHeader; +use aud_io::err as io_err; use byteorder::ReadBytesExt; #[allow(clippy::unnecessary_wraps)] @@ -47,7 +48,7 @@ where let skip_footer = header.flags.footer; let Some(new_stream_len) = stream_len.checked_sub(u64::from(header.size)) else { - err!(SizeMismatch); + io_err!(SizeMismatch); }; stream_len = new_stream_len; @@ -72,7 +73,7 @@ where log::debug!("Skipping ID3v2 footer"); let Some(new_stream_len) = stream_len.checked_sub(10) else { - err!(SizeMismatch); + io_err!(SizeMismatch); }; stream_len = new_stream_len; @@ -108,7 +109,7 @@ where if header.is_some() { let Some(new_stream_len) = stream_len.checked_sub(128) else { - err!(SizeMismatch); + io_err!(SizeMismatch); }; stream_len = new_stream_len; @@ -174,7 +175,7 @@ where header_len, u32::from(first_header.len), u32::from_be_bytes(first_header.bytes[..4].try_into().unwrap()), - HEADER_MASK, + ADTSHeader::COMPARISON_MASK, ) { HeaderCmpResult::Equal => { return Ok(Some(( diff --git a/lofty/src/ape/header.rs b/lofty/src/ape/header.rs index 23c0f45f7..8406c8169 100644 --- a/lofty/src/ape/header.rs +++ b/lofty/src/ape/header.rs @@ -1,10 +1,10 @@ use crate::error::Result; use crate::macros::decode_err; -use crate::util::io::SeekStreamLen; use std::io::{Read, Seek, SeekFrom}; use std::ops::Neg; +use aud_io::io::SeekStreamLen; use byteorder::{LittleEndian, ReadBytesExt}; #[derive(Copy, Clone)] diff --git a/lofty/src/ape/read.rs b/lofty/src/ape/read.rs index d6d3e620b..93502ff3a 100644 --- a/lofty/src/ape/read.rs +++ b/lofty/src/ape/read.rs @@ -8,10 +8,12 @@ use crate::id3::v1::tag::Id3v1Tag; use crate::id3::v2::read::parse_id3v2; use crate::id3::v2::tag::Id3v2Tag; use crate::id3::{FindId3v2Config, ID3FindResults, find_id3v1, find_id3v2, find_lyrics3v2}; -use crate::macros::{decode_err, err}; +use crate::macros::decode_err; use std::io::{Read, Seek, SeekFrom}; +use aud_io::err as io_err; + pub(crate) fn read_from(data: &mut R, parse_options: ParseOptions) -> Result where R: Read + Seek, @@ -39,7 +41,7 @@ where let Some(new_stream_length) = stream_len.checked_sub(u64::from(header.full_tag_size())) else { - err!(SizeMismatch); + io_err!(SizeMismatch); }; stream_len = new_stream_length; @@ -88,7 +90,7 @@ where let ape_header = read_ape_header(data, false)?; let Some(new_stream_length) = stream_len.checked_sub(u64::from(ape_header.size)) else { - err!(SizeMismatch); + io_err!(SizeMismatch); }; stream_len = new_stream_length; @@ -113,7 +115,7 @@ where if id3v1_header.is_some() { id3v1_tag = id3v1; let Some(new_stream_length) = stream_len.checked_sub(128) else { - err!(SizeMismatch); + io_err!(SizeMismatch); }; stream_len = new_stream_length; @@ -122,7 +124,7 @@ where // Next, check for a Lyrics3v2 tag, and skip over it, as it's no use to us let ID3FindResults(_, lyrics3v2_size) = find_lyrics3v2(data)?; let Some(new_stream_length) = stream_len.checked_sub(u64::from(lyrics3v2_size)) else { - err!(SizeMismatch); + io_err!(SizeMismatch); }; stream_len = new_stream_length; diff --git a/lofty/src/ape/tag/mod.rs b/lofty/src/ape/tag/mod.rs index 69ff98984..c99af3372 100644 --- a/lofty/src/ape/tag/mod.rs +++ b/lofty/src/ape/tag/mod.rs @@ -11,12 +11,12 @@ use crate::tag::{ Accessor, ItemKey, ItemValue, MergeTag, SplitTag, Tag, TagExt, TagItem, TagType, try_parse_year, }; use crate::util::flag_item; -use crate::util::io::{FileLike, Truncate}; use std::borrow::Cow; use std::io::Write; use std::ops::Deref; +use aud_io::io::{FileLike, Truncate}; use lofty_attr::tag; macro_rules! impl_accessor { diff --git a/lofty/src/ape/tag/read.rs b/lofty/src/ape/tag/read.rs index 6608896f6..91863905b 100644 --- a/lofty/src/ape/tag/read.rs +++ b/lofty/src/ape/tag/read.rs @@ -5,12 +5,13 @@ use crate::ape::constants::{APE_PREAMBLE, INVALID_KEYS}; use crate::ape::header::{self, ApeHeader}; use crate::config::ParseOptions; use crate::error::Result; -use crate::macros::{decode_err, err, try_vec}; +use crate::macros::decode_err; use crate::tag::ItemValue; -use crate::util::text::utf8_decode; use std::io::{Read, Seek, SeekFrom}; +use aud_io::text::utf8_decode; +use aud_io::{err as io_err, try_vec}; use byteorder::{LittleEndian, ReadBytesExt}; pub(crate) fn read_ape_tag_with_header( @@ -31,7 +32,7 @@ where let value_size = data.read_u32::()?; if value_size > remaining_size { - err!(SizeMismatch); + io_err!(SizeMismatch); } remaining_size -= 4; diff --git a/lofty/src/ape/tag/write.rs b/lofty/src/ape/tag/write.rs index 7e0eaac24..a8a5a9771 100644 --- a/lofty/src/ape/tag/write.rs +++ b/lofty/src/ape/tag/write.rs @@ -8,10 +8,11 @@ use crate::id3::{FindId3v2Config, find_id3v1, find_id3v2, find_lyrics3v2}; use crate::macros::{decode_err, err}; use crate::probe::Probe; use crate::tag::item::ItemValueRef; -use crate::util::io::{FileLike, Truncate}; use std::io::{Cursor, Seek, SeekFrom, Write}; +use aud_io::err as io_err; +use aud_io::io::{FileLike, Truncate}; use byteorder::{LittleEndian, WriteBytesExt}; #[allow(clippy::shadow_unrelated)] @@ -205,7 +206,7 @@ where let size = tag_write.get_ref().len(); if size as u64 + 32 > u64::from(u32::MAX) { - err!(TooMuchData); + io_err!(TooMuchData); } let mut footer = [0_u8; 32]; diff --git a/lofty/src/config/global_options.rs b/lofty/src/config/global_options.rs index 4f08e1dfb..a14835988 100644 --- a/lofty/src/config/global_options.rs +++ b/lofty/src/config/global_options.rs @@ -29,7 +29,8 @@ pub struct GlobalOptions { impl GlobalOptions { /// Default allocation limit for any single tag item - pub const DEFAULT_ALLOCATION_LIMIT: usize = 16 * 1024 * 1024; + pub const DEFAULT_ALLOCATION_LIMIT: usize = + aud_io::config::GlobalOptions::DEFAULT_ALLOCATION_LIMIT; /// Creates a new `GlobalOptions`, alias for `Default` implementation /// @@ -131,6 +132,12 @@ impl Default for GlobalOptions { } } +impl From for aud_io::config::GlobalOptions { + fn from(options: GlobalOptions) -> Self { + aud_io::config::GlobalOptions::new().allocation_limit(options.allocation_limit) + } +} + /// Applies the given `GlobalOptions` to the current thread /// /// # Examples @@ -143,6 +150,9 @@ impl Default for GlobalOptions { /// apply_global_options(global_options); /// ``` pub fn apply_global_options(options: GlobalOptions) { + // Propagate changes to `aud_io` as well + aud_io::config::apply_global_options(options.into()); + GLOBAL_OPTIONS.with(|global_options| unsafe { *global_options.get() = options; }); diff --git a/lofty/src/config/mod.rs b/lofty/src/config/mod.rs index 16d19fc27..3c7438152 100644 --- a/lofty/src/config/mod.rs +++ b/lofty/src/config/mod.rs @@ -5,7 +5,7 @@ mod parse_options; mod write_options; pub use global_options::{GlobalOptions, apply_global_options}; -pub use parse_options::{ParseOptions, ParsingMode}; +pub use parse_options::*; pub use write_options::WriteOptions; pub(crate) use global_options::global_options; diff --git a/lofty/src/config/parse_options.rs b/lofty/src/config/parse_options.rs index e6178d22f..6a45745dd 100644 --- a/lofty/src/config/parse_options.rs +++ b/lofty/src/config/parse_options.rs @@ -1,3 +1,5 @@ +pub use aud_io::config::ParsingMode; + /// Options to control how Lofty parses a file #[derive(Copy, Clone, Debug, Eq, PartialEq)] #[non_exhaustive] @@ -163,56 +165,3 @@ impl ParseOptions { *self } } - -/// The parsing strictness mode -/// -/// This can be set with [`Probe::options`](crate::probe::Probe). -/// -/// # Examples -/// -/// ```rust,no_run -/// use lofty::config::{ParseOptions, ParsingMode}; -/// use lofty::probe::Probe; -/// -/// # fn main() -> lofty::error::Result<()> { -/// // We only want to read spec-compliant inputs -/// let parsing_options = ParseOptions::new().parsing_mode(ParsingMode::Strict); -/// let tagged_file = Probe::open("foo.mp3")?.options(parsing_options).read()?; -/// # Ok(()) } -/// ``` -#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Default)] -#[non_exhaustive] -pub enum ParsingMode { - /// Will eagerly error on invalid input - /// - /// This mode will eagerly error on any non spec-compliant input. - /// - /// ## Examples of behavior - /// - /// * Unable to decode text - The parser will error and the entire input is discarded - /// * Unable to determine the sample rate - The parser will error and the entire input is discarded - Strict, - /// Default mode, less eager to error on recoverably malformed input - /// - /// This mode will attempt to fill in any holes where possible in otherwise valid, spec-compliant input. - /// - /// NOTE: A readable input does *not* necessarily make it writeable. - /// - /// ## Examples of behavior - /// - /// * Unable to decode text - If valid otherwise, the field will be replaced by an empty string and the parser moves on - /// * Unable to determine the sample rate - The sample rate will be 0 - #[default] - BestAttempt, - /// Least eager to error, may produce invalid/partial output - /// - /// This mode will discard any invalid fields, and ignore the majority of non-fatal errors. - /// - /// If the input is malformed, the resulting tags may be incomplete, and the properties zeroed. - /// - /// ## Examples of behavior - /// - /// * Unable to decode text - The entire item is discarded and the parser moves on - /// * Unable to determine the sample rate - The sample rate will be 0 - Relaxed, -} diff --git a/lofty/src/error.rs b/lofty/src/error.rs index 9b172489d..48656408e 100644 --- a/lofty/src/error.rs +++ b/lofty/src/error.rs @@ -7,9 +7,9 @@ use crate::file::FileType; use crate::id3::v2::FrameId; use crate::tag::ItemKey; -use std::collections::TryReserveError; use std::fmt::{Debug, Display, Formatter}; +use aud_io::error::AudioError; use ogg_pager::PageError; /// Alias for `Result` @@ -24,14 +24,6 @@ pub enum ErrorKind { UnknownFormat, // File data related errors - /// Attempting to read/write an abnormally large amount of data - TooMuchData, - /// Expected the data to be a different size than provided - /// - /// This occurs when the size of an item is written as one value, but that size is either too - /// big or small to be valid within the bounds of that item. - // TODO: Should probably have context - SizeMismatch, /// Errors that occur while decoding a file FileDecoding(FileDecodingError), /// Errors that occur while encoding a file @@ -48,33 +40,23 @@ pub enum ErrorKind { UnsupportedTag, /// Arises when a tag is expected (Ex. found an "ID3 " chunk in a WAV file), but isn't found FakeTag, - /// Errors that arise while decoding text - TextDecode(&'static str), /// Arises when decoding OR encoding a problematic [`Timestamp`](crate::tag::items::Timestamp) BadTimestamp(&'static str), /// Errors that arise while reading/writing ID3v2 tags Id3v2(Id3v2Error), - /// Arises when an atom contains invalid data - BadAtom(&'static str), /// Arises when attempting to use [`Atom::merge`](crate::mp4::Atom::merge) with mismatching identifiers AtomMismatch, // Conversions for external errors /// Errors that arise while parsing OGG pages OggPage(ogg_pager::PageError), - /// Unable to convert bytes to a String - StringFromUtf8(std::string::FromUtf8Error), - /// Unable to convert bytes to a str - StrFromUtf8(std::str::Utf8Error), + /// General Audio IO errors + AudioIo(aud_io::error::AudioError), /// Represents all cases of [`std::io::Error`]. Io(std::io::Error), /// Represents all cases of [`std::fmt::Error`]. Fmt(std::fmt::Error), - /// Failure to allocate enough memory - Alloc(TryReserveError), - /// This should **never** be encountered - Infallible(std::convert::Infallible), } /// The types of errors that can occur while interacting with ID3v2 tags @@ -477,39 +459,23 @@ impl From for LoftyError { impl From for LoftyError { fn from(input: std::io::Error) -> Self { Self { - kind: ErrorKind::Io(input), - } - } -} - -impl From for LoftyError { - fn from(input: std::fmt::Error) -> Self { - Self { - kind: ErrorKind::Fmt(input), - } - } -} - -impl From for LoftyError { - fn from(input: std::string::FromUtf8Error) -> Self { - Self { - kind: ErrorKind::StringFromUtf8(input), + kind: ErrorKind::AudioIo(AudioError::Io(input)), } } } -impl From for LoftyError { - fn from(input: std::str::Utf8Error) -> Self { +impl From for LoftyError { + fn from(input: aud_io::error::AudioError) -> Self { Self { - kind: ErrorKind::StrFromUtf8(input), + kind: ErrorKind::AudioIo(input), } } } -impl From for LoftyError { - fn from(input: TryReserveError) -> Self { +impl From for LoftyError { + fn from(input: std::fmt::Error) -> Self { Self { - kind: ErrorKind::Alloc(input), + kind: ErrorKind::Fmt(input), } } } @@ -517,7 +483,7 @@ impl From for LoftyError { impl From for LoftyError { fn from(input: std::convert::Infallible) -> Self { Self { - kind: ErrorKind::Infallible(input), + kind: ErrorKind::AudioIo(AudioError::Infallible(input)), } } } @@ -527,11 +493,9 @@ impl Display for LoftyError { match self.kind { // Conversions ErrorKind::OggPage(ref err) => write!(f, "{err}"), - ErrorKind::StringFromUtf8(ref err) => write!(f, "{err}"), - ErrorKind::StrFromUtf8(ref err) => write!(f, "{err}"), + ErrorKind::AudioIo(ref err) => write!(f, "{err}"), ErrorKind::Io(ref err) => write!(f, "{err}"), ErrorKind::Fmt(ref err) => write!(f, "{err}"), - ErrorKind::Alloc(ref err) => write!(f, "{err}"), ErrorKind::UnknownFormat => { write!(f, "No format could be determined from the provided file") @@ -545,30 +509,18 @@ impl Display for LoftyError { "Attempted to write a tag to a format that does not support it" ), ErrorKind::FakeTag => write!(f, "Reading: Expected a tag, found invalid data"), - ErrorKind::TextDecode(message) => write!(f, "Text decoding: {message}"), ErrorKind::BadTimestamp(message) => { write!(f, "Encountered an invalid timestamp: {message}") }, ErrorKind::Id3v2(ref id3v2_err) => write!(f, "{id3v2_err}"), - ErrorKind::BadAtom(message) => write!(f, "MP4 Atom: {message}"), ErrorKind::AtomMismatch => write!( f, "MP4 Atom: Attempted to use `Atom::merge()` with mismatching identifiers" ), // Files - ErrorKind::TooMuchData => write!( - f, - "Attempted to read/write an abnormally large amount of data" - ), - ErrorKind::SizeMismatch => write!( - f, - "Encountered an invalid item size, either too big or too small to be valid" - ), ErrorKind::FileDecoding(ref file_decode_err) => write!(f, "{file_decode_err}"), ErrorKind::FileEncoding(ref file_encode_err) => write!(f, "{file_encode_err}"), - - ErrorKind::Infallible(_) => write!(f, "A expected condition was not upheld"), } } } diff --git a/lofty/src/file/audio_file.rs b/lofty/src/file/audio_file.rs index 4541d1c4d..c30f60e77 100644 --- a/lofty/src/file/audio_file.rs +++ b/lofty/src/file/audio_file.rs @@ -3,11 +3,12 @@ use crate::config::{ParseOptions, WriteOptions}; use crate::error::{LoftyError, Result}; use crate::tag::TagType; -use crate::util::io::{FileLike, Length, Truncate}; use std::fs::OpenOptions; use std::io::{Read, Seek}; use std::path::Path; +use aud_io::io::{FileLike, Length, Truncate}; + /// Provides various methods for interaction with a file pub trait AudioFile: Into { /// The struct the file uses for audio properties diff --git a/lofty/src/file/tagged_file.rs b/lofty/src/file/tagged_file.rs index e61d41140..0a0fb86cc 100644 --- a/lofty/src/file/tagged_file.rs +++ b/lofty/src/file/tagged_file.rs @@ -4,10 +4,11 @@ use crate::config::{ParseOptions, WriteOptions}; use crate::error::{LoftyError, Result}; use crate::properties::FileProperties; use crate::tag::{Tag, TagExt, TagSupport, TagType}; -use crate::util::io::{FileLike, Length, Truncate}; use std::io::{Read, Seek}; +use aud_io::io::{FileLike, Length, Truncate}; + /// Provides a common interface between [`TaggedFile`] and [`BoundTaggedFile`] pub trait TaggedFileExt { /// Returns the file's [`FileType`] diff --git a/lofty/src/flac/block.rs b/lofty/src/flac/block.rs index ee2a596ad..4eba6295d 100644 --- a/lofty/src/flac/block.rs +++ b/lofty/src/flac/block.rs @@ -1,10 +1,10 @@ #![allow(dead_code)] use crate::error::Result; -use crate::macros::try_vec; use std::io::{Read, Seek, SeekFrom}; +use aud_io::try_vec; use byteorder::{BigEndian, ReadBytesExt}; pub(in crate::flac) const BLOCK_ID_STREAMINFO: u8 = 0; diff --git a/lofty/src/flac/mod.rs b/lofty/src/flac/mod.rs index 11349b4a2..9a1fe6965 100644 --- a/lofty/src/flac/mod.rs +++ b/lofty/src/flac/mod.rs @@ -17,10 +17,10 @@ use crate::ogg::tag::VorbisCommentsRef; use crate::ogg::{OggPictureStorage, VorbisComments}; use crate::picture::{Picture, PictureInformation}; use crate::tag::TagExt; -use crate::util::io::{FileLike, Length, Truncate}; use std::borrow::Cow; +use aud_io::io::{FileLike, Length, Truncate}; use lofty_attr::LoftyFile; // Exports diff --git a/lofty/src/flac/read.rs b/lofty/src/flac/read.rs index 1a7b6e714..b441ba67c 100644 --- a/lofty/src/flac/read.rs +++ b/lofty/src/flac/read.rs @@ -6,12 +6,14 @@ use crate::error::Result; use crate::flac::block::{BLOCK_ID_PICTURE, BLOCK_ID_STREAMINFO, BLOCK_ID_VORBIS_COMMENTS}; use crate::id3::v2::read::parse_id3v2; use crate::id3::{FindId3v2Config, ID3FindResults, find_id3v2}; -use crate::macros::{decode_err, err}; +use crate::macros::decode_err; use crate::ogg::read::read_comments; use crate::picture::Picture; use std::io::{Read, Seek, SeekFrom}; +use aud_io::err as io_err; + pub(super) fn verify_flac(data: &mut R) -> Result where R: Read + Seek, @@ -137,7 +139,7 @@ where // In the event that a block lies about its size, the current position could be // completely wrong. if current > end { - err!(SizeMismatch); + io_err!(SizeMismatch); } (end - current, end) diff --git a/lofty/src/flac/write.rs b/lofty/src/flac/write.rs index 333e061d6..f2fec72c7 100644 --- a/lofty/src/flac/write.rs +++ b/lofty/src/flac/write.rs @@ -2,16 +2,17 @@ use super::block::{BLOCK_ID_PADDING, BLOCK_ID_PICTURE, BLOCK_ID_VORBIS_COMMENTS, use super::read::verify_flac; use crate::config::WriteOptions; use crate::error::{LoftyError, Result}; -use crate::macros::{err, try_vec}; +use crate::macros::err; use crate::ogg::tag::VorbisCommentsRef; use crate::ogg::write::create_comments; use crate::picture::{Picture, PictureInformation}; use crate::tag::{Tag, TagType}; -use crate::util::io::{FileLike, Length, Truncate}; use std::borrow::Cow; use std::io::{Cursor, Read, Seek, SeekFrom, Write}; +use aud_io::io::{FileLike, Length, Truncate}; +use aud_io::{err as io_err, try_vec}; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; const BLOCK_HEADER_SIZE: usize = 4; @@ -195,7 +196,7 @@ fn create_comment_block( let len = (writer.get_ref().len() - 1) as u32; if len > MAX_BLOCK_SIZE { - err!(TooMuchData); + io_err!(TooMuchData); } let comment_end = writer.stream_position()?; @@ -232,7 +233,7 @@ fn create_picture_blocks( let pic_len = pic_bytes.len() as u32; if pic_len > MAX_BLOCK_SIZE { - err!(TooMuchData); + io_err!(TooMuchData); } writer.write_all(&pic_len.to_be_bytes()[1..])?; diff --git a/lofty/src/id3/mod.rs b/lofty/src/id3/mod.rs index 507e1d041..30b514f2c 100644 --- a/lofty/src/id3/mod.rs +++ b/lofty/src/id3/mod.rs @@ -6,14 +6,15 @@ pub mod v1; pub mod v2; -use crate::error::{ErrorKind, LoftyError, Result}; -use crate::macros::try_vec; -use crate::util::text::utf8_decode_str; +use crate::error::Result; use v2::header::Id3v2Header; use std::io::{Read, Seek, SeekFrom}; use std::ops::Neg; +use aud_io::text::utf8_decode_str; +use aud_io::{err as io_err, try_vec}; + pub(crate) struct ID3FindResults(pub Option
, pub Content); pub(crate) fn find_lyrics3v2(data: &mut R) -> Result> @@ -36,11 +37,9 @@ where header = Some(()); let lyrics_size = utf8_decode_str(&lyrics3v2[..7])?; - let lyrics_size = lyrics_size.parse::().map_err(|_| { - LoftyError::new(ErrorKind::TextDecode( - "Lyrics3v2 tag has an invalid size string", - )) - })?; + let Ok(lyrics_size) = lyrics_size.parse::() else { + io_err!(TextDecode("Lyrics3v2 tag has an invalid size string")); + }; size += lyrics_size; diff --git a/lofty/src/id3/v1/tag.rs b/lofty/src/id3/v1/tag.rs index 1e5b937b3..03c613587 100644 --- a/lofty/src/id3/v1/tag.rs +++ b/lofty/src/id3/v1/tag.rs @@ -2,12 +2,12 @@ use crate::config::WriteOptions; use crate::error::{LoftyError, Result}; use crate::id3::v1::constants::GENRES; use crate::tag::{Accessor, ItemKey, ItemValue, MergeTag, SplitTag, Tag, TagExt, TagItem, TagType}; -use crate::util::io::{FileLike, Length, Truncate}; use std::borrow::Cow; use std::io::Write; use std::path::Path; +use aud_io::io::{FileLike, Length, Truncate}; use lofty_attr::tag; macro_rules! impl_accessor { diff --git a/lofty/src/id3/v1/write.rs b/lofty/src/id3/v1/write.rs index 3ee434dba..d4341b1db 100644 --- a/lofty/src/id3/v1/write.rs +++ b/lofty/src/id3/v1/write.rs @@ -4,10 +4,10 @@ use crate::error::{LoftyError, Result}; use crate::id3::{ID3FindResults, find_id3v1}; use crate::macros::err; use crate::probe::Probe; -use crate::util::io::{FileLike, Length, Truncate}; use std::io::{Cursor, Seek, Write}; +use aud_io::io::{FileLike, Length, Truncate}; use byteorder::WriteBytesExt; #[allow(clippy::shadow_unrelated)] diff --git a/lofty/src/id3/v2/frame/content.rs b/lofty/src/id3/v2/frame/content.rs index 84f4253ed..04150bab1 100644 --- a/lofty/src/id3/v2/frame/content.rs +++ b/lofty/src/id3/v2/frame/content.rs @@ -8,11 +8,12 @@ use crate::id3::v2::items::{ UrlLinkFrame, }; use crate::id3::v2::{BinaryFrame, Frame, FrameFlags, FrameId}; -use crate::macros::err; -use crate::util::text::TextEncoding; use std::io::Read; +use aud_io::err as io_err; +use aud_io::text::TextEncoding; + #[rustfmt::skip] pub(super) fn parse_content( reader: &mut R, @@ -22,7 +23,7 @@ pub(super) fn parse_content( parse_mode: ParsingMode, ) -> Result>> { log::trace!("Parsing frame content for ID: {}", id); - + Ok(match id.as_str() { // The ID was previously upgraded, but the content remains unchanged, so version is necessary "APIC" => { @@ -61,7 +62,7 @@ pub(in crate::id3::v2) fn verify_encoding( } match TextEncoding::from_u8(encoding) { - None => err!(TextDecode("Found invalid encoding")), + None => io_err!(TextDecode("Found invalid encoding")), Some(e) => Ok(e), } } diff --git a/lofty/src/id3/v2/frame/conversion.rs b/lofty/src/id3/v2/frame/conversion.rs index 1162d7094..7837d7de1 100644 --- a/lofty/src/id3/v2/frame/conversion.rs +++ b/lofty/src/id3/v2/frame/conversion.rs @@ -1,4 +1,3 @@ -use crate::TextEncoding; use crate::error::{Id3v2Error, Id3v2ErrorKind, LoftyError, Result}; use crate::id3::v2::frame::{EMPTY_CONTENT_DESCRIPTOR, FrameRef, MUSICBRAINZ_UFID_OWNER}; use crate::id3::v2::tag::{ @@ -9,11 +8,13 @@ use crate::id3::v2::{ ExtendedTextFrame, ExtendedUrlFrame, Frame, FrameFlags, FrameId, PopularimeterFrame, UniqueFileIdentifierFrame, }; -use crate::macros::err; use crate::tag::{ItemKey, ItemValue, TagItem, TagType}; use std::borrow::Cow; +use aud_io::err as io_err; +use aud_io::text::TextEncoding; + fn frame_from_unknown_item(id: FrameId<'_>, item_value: ItemValue) -> Result> { match item_value { ItemValue::Text(text) => Ok(new_text_frame(id, text)), @@ -21,7 +22,7 @@ fn frame_from_unknown_item(id: FrameId<'_>, item_value: ItemValue) -> Result Ok(new_binary_frame(id, binary.clone())), diff --git a/lofty/src/id3/v2/frame/header/parse.rs b/lofty/src/id3/v2/frame/header/parse.rs index a361cae09..3a120b0fd 100644 --- a/lofty/src/id3/v2/frame/header/parse.rs +++ b/lofty/src/id3/v2/frame/header/parse.rs @@ -1,14 +1,15 @@ use super::FrameFlags; +use crate::config::ParseOptions; use crate::error::{Id3v2Error, Id3v2ErrorKind, Result}; use crate::id3::v2::FrameId; use crate::id3::v2::util::synchsafe::SynchsafeInteger; use crate::id3::v2::util::upgrade::{upgrade_v2, upgrade_v3}; -use crate::util::text::utf8_decode_str; -use crate::config::ParseOptions; use std::borrow::Cow; use std::io::Read; +use aud_io::text::utf8_decode_str; + pub(crate) fn parse_v2_header( reader: &mut R, size: &mut u32, diff --git a/lofty/src/id3/v2/frame/mod.rs b/lofty/src/id3/v2/frame/mod.rs index 787eaec6f..86cf0e385 100644 --- a/lofty/src/id3/v2/frame/mod.rs +++ b/lofty/src/id3/v2/frame/mod.rs @@ -12,13 +12,14 @@ use super::items::{ }; use crate::error::Result; use crate::id3::v2::FrameHeader; -use crate::util::text::TextEncoding; use header::FrameId; use std::borrow::Cow; use std::hash::Hash; use std::ops::Deref; +use aud_io::text::TextEncoding; + pub(super) const MUSICBRAINZ_UFID_OWNER: &str = "http://musicbrainz.org"; /// Empty content descriptor in text frame diff --git a/lofty/src/id3/v2/frame/read.rs b/lofty/src/id3/v2/frame/read.rs index 9bed13ccc..c8b80b2ca 100644 --- a/lofty/src/id3/v2/frame/read.rs +++ b/lofty/src/id3/v2/frame/read.rs @@ -7,10 +7,10 @@ use crate::id3::v2::header::Id3v2Version; use crate::id3::v2::tag::ATTACHED_PICTURE_ID; use crate::id3::v2::util::synchsafe::{SynchsafeInteger, UnsynchronizedStream}; use crate::id3::v2::{BinaryFrame, FrameFlags, FrameHeader, FrameId}; -use crate::macros::try_vec; use std::io::Read; +use aud_io::try_vec; use byteorder::{BigEndian, ReadBytesExt}; pub(crate) enum ParsedFrame<'a> { @@ -43,16 +43,15 @@ impl ParsedFrame<'_> { }, Ok(Some(some)) => some, Err(err) => { - match parse_options.parsing_mode { - ParsingMode::Strict => return Err(err), - ParsingMode::BestAttempt | ParsingMode::Relaxed => { - log::warn!("Failed to read frame header, skipping: {}", err); - - // Skip this frame and continue reading - skip_frame(reader, size)?; - return Ok(Self::Skip); - }, + if parse_options.parsing_mode == ParsingMode::Strict { + return Err(err); } + + log::warn!("Failed to read frame header, skipping: {}", err); + + // Skip this frame and continue reading + skip_frame(reader, size)?; + return Ok(Self::Skip); }, }; diff --git a/lofty/src/id3/v2/items/attached_picture_frame.rs b/lofty/src/id3/v2/items/attached_picture_frame.rs index ad0278c24..3bf37f56c 100644 --- a/lofty/src/id3/v2/items/attached_picture_frame.rs +++ b/lofty/src/id3/v2/items/attached_picture_frame.rs @@ -1,13 +1,14 @@ use crate::error::{Id3v2Error, Id3v2ErrorKind, Result}; use crate::id3::v2::header::Id3v2Version; -use crate::id3::v2::{FrameFlags, FrameHeader, FrameId}; +use crate::id3::v2::{FrameFlags, FrameHeader, FrameId, Id3TextEncodingExt}; use crate::macros::err; use crate::picture::{MimeType, Picture, PictureType}; -use crate::util::text::{TextDecodeOptions, TextEncoding, encode_text}; use std::borrow::Cow; use std::io::{Read, Write as _}; +use aud_io::err as io_err; +use aud_io::text::{TextDecodeOptions, TextEncoding, encode_text}; use byteorder::{ReadBytesExt as _, WriteBytesExt as _}; const FRAME_ID: FrameId<'static> = FrameId::Valid(Cow::Borrowed("APIC")); @@ -86,7 +87,7 @@ impl AttachedPictureFrame<'_> { }, } } else { - let mime_type_str = crate::util::text::decode_text( + let mime_type_str = aud_io::text::decode_text( reader, TextDecodeOptions::new() .encoding(TextEncoding::Latin1) @@ -98,7 +99,7 @@ impl AttachedPictureFrame<'_> { let pic_type = PictureType::from_u8(reader.read_u8()?); - let description = crate::util::text::decode_text( + let description = aud_io::text::decode_text( reader, TextDecodeOptions::new().encoding(encoding).terminated(true), )? @@ -180,7 +181,7 @@ impl AttachedPictureFrame<'_> { data.write_all(&self.picture.data)?; if data.len() as u64 > max_size { - err!(TooMuchData); + io_err!(TooMuchData); } Ok(data) diff --git a/lofty/src/id3/v2/items/audio_text_frame.rs b/lofty/src/id3/v2/items/audio_text_frame.rs index ca12a5da2..8dd3245e6 100644 --- a/lofty/src/id3/v2/items/audio_text_frame.rs +++ b/lofty/src/id3/v2/items/audio_text_frame.rs @@ -1,10 +1,11 @@ -use crate::error::{ErrorKind, Id3v2Error, Id3v2ErrorKind, LoftyError, Result}; +use crate::error::{Id3v2Error, Id3v2ErrorKind, Result}; use crate::id3::v2::{FrameFlags, FrameHeader, FrameId}; -use crate::util::text::{TextDecodeOptions, TextEncoding, decode_text, encode_text}; use std::borrow::Cow; use std::hash::{Hash, Hasher}; +use aud_io::err as io_err; +use aud_io::text::{TextDecodeOptions, TextEncoding, decode_text, encode_text}; use byteorder::ReadBytesExt as _; const FRAME_ID: FrameId<'static> = FrameId::Valid(Cow::Borrowed("ATXT")); @@ -132,8 +133,9 @@ impl AudioTextFrame<'_> { } let content = &mut &bytes[..]; - let encoding = TextEncoding::from_u8(content.read_u8()?) - .ok_or_else(|| LoftyError::new(ErrorKind::TextDecode("Found invalid encoding")))?; + let Some(encoding) = TextEncoding::from_u8(content.read_u8()?) else { + io_err!(TextDecode("Found invalid encoding")); + }; let mime_type = decode_text( content, @@ -234,9 +236,10 @@ pub fn scramble(audio_data: &mut [u8]) { #[cfg(test)] mod tests { - use crate::TextEncoding; use crate::id3::v2::{AudioTextFrame, AudioTextFrameFlags, FrameFlags}; + use aud_io::text::TextEncoding; + fn expected() -> AudioTextFrame<'static> { AudioTextFrame { header: super::FrameHeader::new(super::FRAME_ID, FrameFlags::default()), diff --git a/lofty/src/id3/v2/items/encapsulated_object.rs b/lofty/src/id3/v2/items/encapsulated_object.rs index 35874e6b0..5825bb32f 100644 --- a/lofty/src/id3/v2/items/encapsulated_object.rs +++ b/lofty/src/id3/v2/items/encapsulated_object.rs @@ -1,9 +1,11 @@ -use crate::error::{ErrorKind, Id3v2Error, Id3v2ErrorKind, LoftyError, Result}; +use crate::error::{Id3v2Error, Id3v2ErrorKind, Result}; use crate::id3::v2::{FrameFlags, FrameHeader, FrameId}; -use crate::util::text::{TextDecodeOptions, TextEncoding, decode_text, encode_text}; use std::io::{Cursor, Read}; +use aud_io::err as io_err; +use aud_io::text::{TextDecodeOptions, TextEncoding, decode_text, encode_text}; + const FRAME_ID: FrameId<'static> = FrameId::Valid(std::borrow::Cow::Borrowed("GEOB")); /// Allows for encapsulation of any file type inside an ID3v2 tag @@ -69,8 +71,9 @@ impl GeneralEncapsulatedObject<'_> { return Err(Id3v2Error::new(Id3v2ErrorKind::BadFrameLength).into()); } - let encoding = TextEncoding::from_u8(data[0]) - .ok_or_else(|| LoftyError::new(ErrorKind::TextDecode("Found invalid encoding")))?; + let Some(encoding) = TextEncoding::from_u8(data[0]) else { + io_err!(TextDecode("Found invalid encoding")); + }; let mut cursor = Cursor::new(&data[1..]); @@ -129,7 +132,7 @@ impl GeneralEncapsulatedObject<'_> { #[cfg(test)] mod tests { use crate::id3::v2::{FrameFlags, FrameHeader, GeneralEncapsulatedObject}; - use crate::util::text::TextEncoding; + use aud_io::text::TextEncoding; fn expected() -> GeneralEncapsulatedObject<'static> { GeneralEncapsulatedObject { diff --git a/lofty/src/id3/v2/items/extended_text_frame.rs b/lofty/src/id3/v2/items/extended_text_frame.rs index d19357115..b0db3771b 100644 --- a/lofty/src/id3/v2/items/extended_text_frame.rs +++ b/lofty/src/id3/v2/items/extended_text_frame.rs @@ -1,16 +1,14 @@ use crate::error::{Id3v2Error, Id3v2ErrorKind, LoftyError, Result}; use crate::id3::v2::frame::content::verify_encoding; use crate::id3::v2::header::Id3v2Version; -use crate::id3::v2::{FrameFlags, FrameHeader, FrameId}; -use crate::macros::err; -use crate::util::text::{ - TextDecodeOptions, TextEncoding, decode_text, encode_text, utf16_decode_bytes, -}; +use crate::id3::v2::{FrameFlags, FrameHeader, FrameId, Id3TextEncodingExt}; use std::borrow::Cow; use std::hash::{Hash, Hasher}; use std::io::Read; +use aud_io::err as io_err; +use aud_io::text::{TextDecodeOptions, TextEncoding, decode_text, encode_text, utf16_decode_bytes}; use byteorder::ReadBytesExt; const FRAME_ID: FrameId<'static> = FrameId::Valid(Cow::Borrowed("TXXX")); @@ -142,7 +140,7 @@ impl ExtendedTextFrame<'_> { }, [0x00, 0x00] => { debug_assert!(description.content.is_empty()); - err!(TextDecode("UTF-16 string has no BOM")); + io_err!(TextDecode("UTF-16 string has no BOM")); }, [0xFF, 0xFE] => u16::from_le_bytes, [0xFE, 0xFF] => u16::from_be_bytes, diff --git a/lofty/src/id3/v2/items/extended_url_frame.rs b/lofty/src/id3/v2/items/extended_url_frame.rs index 9fde67afb..3bd692819 100644 --- a/lofty/src/id3/v2/items/extended_url_frame.rs +++ b/lofty/src/id3/v2/items/extended_url_frame.rs @@ -1,13 +1,13 @@ use crate::error::Result; use crate::id3::v2::frame::content::verify_encoding; use crate::id3::v2::header::Id3v2Version; -use crate::id3::v2::{FrameFlags, FrameHeader, FrameId}; -use crate::util::text::{TextDecodeOptions, TextEncoding, decode_text, encode_text}; +use crate::id3::v2::{FrameFlags, FrameHeader, FrameId, Id3TextEncodingExt}; use std::borrow::Cow; use std::hash::{Hash, Hasher}; use std::io::Read; +use aud_io::text::{TextDecodeOptions, TextEncoding, decode_text, encode_text}; use byteorder::ReadBytesExt; const FRAME_ID: FrameId<'static> = FrameId::Valid(Cow::Borrowed("WXXX")); diff --git a/lofty/src/id3/v2/items/key_value_frame.rs b/lofty/src/id3/v2/items/key_value_frame.rs index dc5b65191..4add6b594 100644 --- a/lofty/src/id3/v2/items/key_value_frame.rs +++ b/lofty/src/id3/v2/items/key_value_frame.rs @@ -1,13 +1,13 @@ use crate::error::Result; use crate::id3::v2::frame::content::verify_encoding; use crate::id3::v2::header::Id3v2Version; -use crate::id3::v2::{FrameFlags, FrameHeader, FrameId}; -use crate::util::text::{TextDecodeOptions, TextEncoding, decode_text, encode_text}; - -use byteorder::ReadBytesExt; +use crate::id3::v2::{FrameFlags, FrameHeader, FrameId, Id3TextEncodingExt}; use std::io::Read; +use aud_io::text::{TextDecodeOptions, TextEncoding, decode_text, encode_text}; +use byteorder::ReadBytesExt; + /// An `ID3v2` key-value frame #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct KeyValueFrame<'a> { diff --git a/lofty/src/id3/v2/items/language_frame.rs b/lofty/src/id3/v2/items/language_frame.rs index 22c283621..031c14361 100644 --- a/lofty/src/id3/v2/items/language_frame.rs +++ b/lofty/src/id3/v2/items/language_frame.rs @@ -1,18 +1,18 @@ use crate::error::{Id3v2Error, Id3v2ErrorKind, Result}; use crate::id3::v2::frame::content::verify_encoding; use crate::id3::v2::header::Id3v2Version; -use crate::id3::v2::{FrameFlags, FrameHeader, FrameId}; -use crate::macros::err; +use crate::id3::v2::{FrameFlags, FrameHeader, FrameId, Id3TextEncodingExt}; use crate::tag::items::Lang; -use crate::util::text::{ - DecodeTextResult, TextDecodeOptions, TextEncoding, decode_text, encode_text, - utf16_decode_terminated_maybe_bom, -}; use std::borrow::Cow; use std::hash::{Hash, Hasher}; use std::io::Read; +use aud_io::err as io_err; +use aud_io::text::{ + DecodeTextResult, TextDecodeOptions, TextEncoding, decode_text, encode_text, + utf16_decode_terminated_maybe_bom, +}; use byteorder::ReadBytesExt; // Generic struct for a text frame that has a language @@ -56,7 +56,7 @@ impl LanguageFrame { endianness = match bom { [0xFF, 0xFE] => u16::from_le_bytes, [0xFE, 0xFF] => u16::from_be_bytes, - _ => err!(TextDecode("UTF-16 string missing a BOM")), + _ => io_err!(TextDecode("UTF-16 string missing a BOM")), }; } diff --git a/lofty/src/id3/v2/items/ownership_frame.rs b/lofty/src/id3/v2/items/ownership_frame.rs index af53707d7..5fcbc613a 100644 --- a/lofty/src/id3/v2/items/ownership_frame.rs +++ b/lofty/src/id3/v2/items/ownership_frame.rs @@ -1,13 +1,12 @@ -use crate::error::{ErrorKind, Id3v2Error, Id3v2ErrorKind, LoftyError, Result}; -use crate::id3::v2::{FrameFlags, FrameHeader, FrameId}; -use crate::util::text::{ - TextDecodeOptions, TextEncoding, decode_text, encode_text, utf8_decode_str, -}; +use crate::error::{Id3v2Error, Id3v2ErrorKind, Result}; +use crate::id3::v2::{FrameFlags, FrameHeader, FrameId, Id3TextEncodingExt}; use std::borrow::Cow; use std::hash::Hash; use std::io::Read; +use aud_io::err as io_err; +use aud_io::text::{TextDecodeOptions, TextEncoding, decode_text, encode_text, utf8_decode_str}; use byteorder::ReadBytesExt; const FRAME_ID: FrameId<'static> = FrameId::Valid(Cow::Borrowed("OWNE")); @@ -82,8 +81,10 @@ impl OwnershipFrame<'_> { return Ok(None); }; - let encoding = TextEncoding::from_u8(encoding_byte) - .ok_or_else(|| LoftyError::new(ErrorKind::TextDecode("Found invalid encoding")))?; + let Some(encoding) = TextEncoding::from_u8(encoding_byte) else { + io_err!(TextDecode("Found invalid encoding")); + }; + let price_paid = decode_text( reader, TextDecodeOptions::new() @@ -138,11 +139,12 @@ impl OwnershipFrame<'_> { #[cfg(test)] mod tests { - use crate::TextEncoding; use crate::id3::v2::{FrameFlags, FrameHeader, FrameId, OwnershipFrame}; use std::borrow::Cow; + use aud_io::text::TextEncoding; + fn expected() -> OwnershipFrame<'static> { OwnershipFrame { header: FrameHeader::new(FrameId::Valid(Cow::Borrowed("OWNE")), FrameFlags::default()), diff --git a/lofty/src/id3/v2/items/popularimeter.rs b/lofty/src/id3/v2/items/popularimeter.rs index 193c5c9f0..06eae7e51 100644 --- a/lofty/src/id3/v2/items/popularimeter.rs +++ b/lofty/src/id3/v2/items/popularimeter.rs @@ -1,12 +1,12 @@ use crate::error::Result; use crate::id3::v2::{FrameFlags, FrameHeader, FrameId}; -use crate::util::alloc::VecFallibleCapacity; -use crate::util::text::{TextDecodeOptions, TextEncoding, decode_text, encode_text}; use std::borrow::Cow; use std::hash::{Hash, Hasher}; use std::io::Read; +use aud_io::alloc::VecFallibleCapacity; +use aud_io::text::{TextDecodeOptions, TextEncoding, decode_text, encode_text}; use byteorder::ReadBytesExt; const FRAME_ID: FrameId<'static> = FrameId::Valid(Cow::Borrowed("POPM")); diff --git a/lofty/src/id3/v2/items/private_frame.rs b/lofty/src/id3/v2/items/private_frame.rs index 73c018eea..f92b05bf6 100644 --- a/lofty/src/id3/v2/items/private_frame.rs +++ b/lofty/src/id3/v2/items/private_frame.rs @@ -1,11 +1,12 @@ use crate::error::Result; use crate::id3::v2::{FrameFlags, FrameHeader, FrameId}; -use crate::util::alloc::VecFallibleCapacity; -use crate::util::text::{TextDecodeOptions, TextEncoding, decode_text, encode_text}; use std::borrow::Cow; use std::io::Read; +use aud_io::alloc::VecFallibleCapacity; +use aud_io::text::{TextDecodeOptions, TextEncoding, decode_text, encode_text}; + const FRAME_ID: FrameId<'static> = FrameId::Valid(Cow::Borrowed("PRIV")); /// An `ID3v2` private frame diff --git a/lofty/src/id3/v2/items/relative_volume_adjustment_frame.rs b/lofty/src/id3/v2/items/relative_volume_adjustment_frame.rs index 7725b07e5..26a29fde8 100644 --- a/lofty/src/id3/v2/items/relative_volume_adjustment_frame.rs +++ b/lofty/src/id3/v2/items/relative_volume_adjustment_frame.rs @@ -1,14 +1,14 @@ use crate::config::ParsingMode; use crate::error::{Id3v2Error, Id3v2ErrorKind, Result}; use crate::id3::v2::{FrameFlags, FrameHeader, FrameId}; -use crate::macros::try_vec; -use crate::util::text::{TextDecodeOptions, TextEncoding, decode_text, encode_text}; use std::borrow::Cow; use std::collections::HashMap; use std::hash::{Hash, Hasher}; use std::io::Read; +use aud_io::text::{TextDecodeOptions, TextEncoding, decode_text, encode_text}; +use aud_io::try_vec; use byteorder::{BigEndian, ReadBytesExt}; const FRAME_ID: FrameId<'static> = FrameId::Valid(Cow::Borrowed("RVA2")); diff --git a/lofty/src/id3/v2/items/sync_text.rs b/lofty/src/id3/v2/items/sync_text.rs index af028bd13..442c6beb2 100644 --- a/lofty/src/id3/v2/items/sync_text.rs +++ b/lofty/src/id3/v2/items/sync_text.rs @@ -1,14 +1,14 @@ -use crate::error::{ErrorKind, Id3v2Error, Id3v2ErrorKind, LoftyError, Result}; +use crate::error::{Id3v2Error, Id3v2ErrorKind, Result}; use crate::id3::v2::{FrameFlags, FrameHeader, FrameId}; -use crate::macros::err; -use crate::util::text::{ - DecodeTextResult, TextDecodeOptions, TextEncoding, decode_text, encode_text, - utf16_decode_terminated_maybe_bom, -}; use std::borrow::Cow; use std::io::{Cursor, Seek, Write}; +use aud_io::err as io_err; +use aud_io::text::{ + DecodeTextResult, TextDecodeOptions, TextEncoding, decode_text, encode_text, + utf16_decode_terminated_maybe_bom, +}; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; const FRAME_ID: FrameId<'static> = FrameId::Valid(Cow::Borrowed("SYLT")); @@ -136,8 +136,10 @@ impl SynchronizedTextFrame<'_> { return Err(Id3v2Error::new(Id3v2ErrorKind::BadFrameLength).into()); } - let encoding = TextEncoding::from_u8(data[0]) - .ok_or_else(|| LoftyError::new(ErrorKind::TextDecode("Found invalid encoding")))?; + let Some(encoding) = TextEncoding::from_u8(data[0]) else { + io_err!(TextDecode("Found invalid encoding")); + }; + let language: [u8; 3] = data[1..4].try_into().unwrap(); if language.iter().any(|c| !c.is_ascii_alphabetic()) { return Err(Id3v2Error::new(Id3v2ErrorKind::BadSyncText).into()); @@ -166,7 +168,7 @@ impl SynchronizedTextFrame<'_> { endianness = match bom { [0xFF, 0xFE] => u16::from_le_bytes, [0xFE, 0xFF] => u16::from_be_bytes, - _ => err!(TextDecode("UTF-16 string missing a BOM")), + _ => io_err!(TextDecode("UTF-16 string missing a BOM")), }; } @@ -247,7 +249,7 @@ impl SynchronizedTextFrame<'_> { } if data.len() as u64 > u64::from(u32::MAX) { - err!(TooMuchData); + io_err!(TooMuchData); } return Ok(data); @@ -262,7 +264,7 @@ mod tests { use crate::id3::v2::{ FrameFlags, FrameHeader, SyncTextContentType, SynchronizedTextFrame, TimestampFormat, }; - use crate::util::text::TextEncoding; + use aud_io::text::TextEncoding; fn expected(encoding: TextEncoding) -> SynchronizedTextFrame<'static> { SynchronizedTextFrame { diff --git a/lofty/src/id3/v2/items/text_information_frame.rs b/lofty/src/id3/v2/items/text_information_frame.rs index b27781969..fdcb9ac63 100644 --- a/lofty/src/id3/v2/items/text_information_frame.rs +++ b/lofty/src/id3/v2/items/text_information_frame.rs @@ -1,14 +1,14 @@ use crate::error::Result; use crate::id3::v2::frame::content::verify_encoding; use crate::id3::v2::header::Id3v2Version; -use crate::id3::v2::{FrameFlags, FrameHeader, FrameId}; -use crate::util::text::{TextDecodeOptions, TextEncoding, decode_text, encode_text}; - -use byteorder::ReadBytesExt; +use crate::id3::v2::{FrameFlags, FrameHeader, FrameId, Id3TextEncodingExt}; use std::hash::Hash; use std::io::Read; +use aud_io::text::{TextDecodeOptions, TextEncoding, decode_text, encode_text}; +use byteorder::ReadBytesExt; + /// An `ID3v2` text frame #[derive(Clone, Debug, Eq)] pub struct TextInformationFrame<'a> { diff --git a/lofty/src/id3/v2/items/timestamp_frame.rs b/lofty/src/id3/v2/items/timestamp_frame.rs index 3f186c775..eb8ddc2f7 100644 --- a/lofty/src/id3/v2/items/timestamp_frame.rs +++ b/lofty/src/id3/v2/items/timestamp_frame.rs @@ -1,12 +1,13 @@ use crate::config::ParsingMode; use crate::error::Result; -use crate::id3::v2::{FrameFlags, FrameHeader, FrameId}; +use crate::id3::v2::{FrameFlags, FrameHeader, FrameId, Id3TextEncodingExt}; use crate::macros::err; use crate::tag::items::Timestamp; -use crate::util::text::{TextDecodeOptions, TextEncoding, decode_text, encode_text}; use std::io::Read; +use aud_io::err as io_err; +use aud_io::text::{TextDecodeOptions, TextEncoding, decode_text, encode_text}; use byteorder::ReadBytesExt; /// An `ID3v2` timestamp frame @@ -78,7 +79,7 @@ impl<'a> TimestampFrame<'a> { }; let Some(encoding) = TextEncoding::from_u8(encoding_byte) else { if parse_mode != ParsingMode::Relaxed { - err!(TextDecode("Found invalid encoding")) + io_err!(TextDecode("Found invalid encoding")) } return Ok(None); }; diff --git a/lofty/src/id3/v2/items/unique_file_identifier.rs b/lofty/src/id3/v2/items/unique_file_identifier.rs index 8b25d42fb..1f541dcdd 100644 --- a/lofty/src/id3/v2/items/unique_file_identifier.rs +++ b/lofty/src/id3/v2/items/unique_file_identifier.rs @@ -2,12 +2,13 @@ use crate::config::ParsingMode; use crate::error::{Id3v2Error, Id3v2ErrorKind, Result}; use crate::id3::v2::{FrameFlags, FrameHeader, FrameId}; use crate::macros::parse_mode_choice; -use crate::util::text::{TextDecodeOptions, TextEncoding, decode_text, encode_text}; use std::borrow::Cow; use std::hash::{Hash, Hasher}; use std::io::Read; +use aud_io::text::{TextDecodeOptions, TextEncoding, decode_text, encode_text}; + const FRAME_ID: FrameId<'static> = FrameId::Valid(Cow::Borrowed("UFID")); /// An `ID3v2` unique file identifier frame (UFID). diff --git a/lofty/src/id3/v2/items/url_link_frame.rs b/lofty/src/id3/v2/items/url_link_frame.rs index 36c251a5f..881583133 100644 --- a/lofty/src/id3/v2/items/url_link_frame.rs +++ b/lofty/src/id3/v2/items/url_link_frame.rs @@ -1,10 +1,11 @@ use crate::error::Result; use crate::id3::v2::{FrameFlags, FrameHeader, FrameId}; -use crate::util::text::{TextDecodeOptions, TextEncoding, decode_text, encode_text}; use std::hash::Hash; use std::io::Read; +use aud_io::text::{TextDecodeOptions, TextEncoding, decode_text, encode_text}; + /// An `ID3v2` URL frame #[derive(Clone, Debug, Eq)] pub struct UrlLinkFrame<'a> { diff --git a/lofty/src/id3/v2/mod.rs b/lofty/src/id3/v2/mod.rs index e8193e61b..836af38e5 100644 --- a/lofty/src/id3/v2/mod.rs +++ b/lofty/src/id3/v2/mod.rs @@ -31,3 +31,26 @@ pub use frame::{Frame, FrameFlags}; pub use restrictions::{ ImageSizeRestrictions, TagRestrictions, TagSizeRestrictions, TextSizeRestrictions, }; + +/// ID3v2 [`TextEncoding`] extensions +pub(crate) trait Id3TextEncodingExt { + /// ID3v2.4 introduced two new text encodings. + /// + /// When writing ID3v2.3, we just substitute with UTF-16. + fn to_id3v23(self) -> Self; +} + +impl Id3TextEncodingExt for aud_io::text::TextEncoding { + fn to_id3v23(self) -> Self { + match self { + Self::UTF8 | Self::UTF16BE => { + log::warn!( + "Text encoding {:?} is not supported in ID3v2.3, substituting with UTF-16", + self + ); + Self::UTF16 + }, + _ => self, + } + } +} diff --git a/lofty/src/id3/v2/tag.rs b/lofty/src/id3/v2/tag.rs index ccc52966a..f38ada1de 100644 --- a/lofty/src/id3/v2/tag.rs +++ b/lofty/src/id3/v2/tag.rs @@ -22,8 +22,6 @@ use crate::tag::companion_tag::CompanionTag; use crate::tag::items::{Lang, Timestamp, UNKNOWN_LANGUAGE}; use crate::tag::{Accessor, ItemKey, ItemValue, MergeTag, SplitTag, Tag, TagExt, TagItem, TagType}; use crate::util::flag_item; -use crate::util::io::{FileLike, Length, Truncate}; -use crate::util::text::{TextDecodeOptions, TextEncoding, decode_text}; use std::borrow::Cow; use std::io::{Cursor, Write}; @@ -31,6 +29,8 @@ use std::iter::Peekable; use std::ops::Deref; use std::str::FromStr; +use aud_io::io::{FileLike, Length, Truncate}; +use aud_io::text::{TextDecodeOptions, TextEncoding, decode_text}; use lofty_attr::tag; const INVOLVED_PEOPLE_LIST_ID: &str = "TIPL"; diff --git a/lofty/src/id3/v2/util/synchsafe.rs b/lofty/src/id3/v2/util/synchsafe.rs index 019208a02..2441390d1 100644 --- a/lofty/src/id3/v2/util/synchsafe.rs +++ b/lofty/src/id3/v2/util/synchsafe.rs @@ -243,7 +243,7 @@ macro_rules! impl_synchsafe { }; if self > MAXIMUM_INTEGER { - crate::macros::err!(TooMuchData); + aud_io::err!(TooMuchData); } let $n = self; diff --git a/lofty/src/id3/v2/write/chunk_file.rs b/lofty/src/id3/v2/write/chunk_file.rs index 3cd76b575..db37b8b07 100644 --- a/lofty/src/id3/v2/write/chunk_file.rs +++ b/lofty/src/id3/v2/write/chunk_file.rs @@ -1,11 +1,11 @@ use crate::config::WriteOptions; use crate::error::{LoftyError, Result}; use crate::iff::chunk::Chunks; -use crate::macros::err; -use crate::util::io::{FileLike, Length, Truncate}; use std::io::{Cursor, Seek, SeekFrom, Write}; +use aud_io::err as io_err; +use aud_io::io::{FileLike, Length, Truncate}; use byteorder::{ByteOrder, WriteBytesExt}; const CHUNK_NAME_UPPER: [u8; 4] = [b'I', b'D', b'3', b' ']; @@ -37,7 +37,7 @@ where file.read_to_end(file_bytes.get_mut())?; if file_bytes.get_ref().len() < (actual_stream_size as usize + RIFF_CHUNK_HEADER_SIZE) { - err!(SizeMismatch); + io_err!(SizeMismatch); } // The first chunk format is RIFF....WAVE @@ -91,7 +91,7 @@ where } let Ok(tag_size): std::result::Result = tag_bytes.get_ref().len().try_into() else { - err!(TooMuchData) + io_err!(TooMuchData) }; let tag_position = actual_stream_size as usize + RIFF_CHUNK_HEADER_SIZE; diff --git a/lofty/src/id3/v2/write/frame.rs b/lofty/src/id3/v2/write/frame.rs index 453c0aeed..3dea19e1f 100644 --- a/lofty/src/id3/v2/write/frame.rs +++ b/lofty/src/id3/v2/write/frame.rs @@ -1,12 +1,12 @@ use crate::error::{Id3v2Error, Id3v2ErrorKind, Result}; use crate::id3::v2::frame::{FrameFlags, FrameRef}; +use crate::id3::v2::tag::GenresIter; use crate::id3::v2::util::synchsafe::SynchsafeInteger; -use crate::id3::v2::{Frame, FrameId, KeyValueFrame, TextInformationFrame}; +use crate::id3::v2::{Frame, FrameId, Id3TextEncodingExt, KeyValueFrame, TextInformationFrame}; use crate::tag::items::Timestamp; use std::io::Write; -use crate::id3::v2::tag::GenresIter; use byteorder::{BigEndian, WriteBytesExt}; pub(in crate::id3::v2) fn create_items( diff --git a/lofty/src/id3/v2/write/mod.rs b/lofty/src/id3/v2/write/mod.rs index b9e169fdd..7aec3a63d 100644 --- a/lofty/src/id3/v2/write/mod.rs +++ b/lofty/src/id3/v2/write/mod.rs @@ -10,14 +10,15 @@ use crate::id3::v2::frame::FrameRef; use crate::id3::v2::tag::Id3v2TagRef; use crate::id3::v2::util::synchsafe::SynchsafeInteger; use crate::id3::{FindId3v2Config, find_id3v2}; -use crate::macros::{err, try_vec}; +use crate::macros::err; use crate::probe::Probe; -use crate::util::io::{FileLike, Length, Truncate}; use std::io::{Cursor, Read, Seek, SeekFrom, Write}; use std::ops::Not; use std::sync::OnceLock; +use aud_io::io::{FileLike, Length, Truncate}; +use aud_io::try_vec; use byteorder::{BigEndian, LittleEndian, WriteBytesExt}; // In the very rare chance someone wants to write a CRC in their extended header diff --git a/lofty/src/iff/aiff/properties.rs b/lofty/src/iff/aiff/properties.rs index 9b1236760..48fcdfb98 100644 --- a/lofty/src/iff/aiff/properties.rs +++ b/lofty/src/iff/aiff/properties.rs @@ -1,14 +1,15 @@ use super::read::CompressionPresent; use crate::error::Result; -use crate::macros::{decode_err, try_vec}; +use crate::io::ReadExt; +use crate::macros::decode_err; use crate::properties::FileProperties; -use crate::util::text::utf8_decode; use std::borrow::Cow; use std::io::Read; use std::time::Duration; -use crate::io::ReadExt; +use aud_io::text::utf8_decode; +use aud_io::try_vec; use byteorder::{BigEndian, ReadBytesExt}; /// The AIFC compression type diff --git a/lofty/src/iff/aiff/tag.rs b/lofty/src/iff/aiff/tag.rs index 8b0c17f1f..c5e051c55 100644 --- a/lofty/src/iff/aiff/tag.rs +++ b/lofty/src/iff/aiff/tag.rs @@ -1,14 +1,14 @@ use crate::config::WriteOptions; use crate::error::{LoftyError, Result}; use crate::iff::chunk::Chunks; -use crate::macros::err; use crate::tag::{Accessor, ItemKey, ItemValue, MergeTag, SplitTag, Tag, TagExt, TagItem, TagType}; -use crate::util::io::{FileLike, Length, Truncate}; use std::borrow::Cow; use std::convert::TryFrom; use std::io::{SeekFrom, Write}; +use aud_io::err as io_err; +use aud_io::io::{FileLike, Length, Truncate}; use byteorder::BigEndian; use lofty_attr::tag; @@ -371,7 +371,7 @@ where let comt_len = comt.text.len(); if comt_len > u16::MAX as usize { - err!(TooMuchData); + io_err!(TooMuchData); } text_chunks.extend((comt_len as u16).to_be_bytes()); @@ -394,7 +394,7 @@ where i += 1; } } else { - err!(TooMuchData); + io_err!(TooMuchData); } if (text_chunks.len() - 4) % 2 != 0 { diff --git a/lofty/src/iff/chunk.rs b/lofty/src/iff/chunk.rs index 9b80ee8a3..03f2bfbca 100644 --- a/lofty/src/iff/chunk.rs +++ b/lofty/src/iff/chunk.rs @@ -1,12 +1,12 @@ use crate::config::ParseOptions; use crate::error::Result; use crate::id3::v2::tag::Id3v2Tag; -use crate::macros::{err, try_vec}; -use crate::util::text::utf8_decode; use std::io::{Read, Seek, SeekFrom}; use std::marker::PhantomData; +use aud_io::text::utf8_decode; +use aud_io::{err as io_err, try_vec}; use byteorder::{ByteOrder, ReadBytesExt}; const RIFF_CHUNK_HEADER_SIZE: u64 = 8; @@ -55,7 +55,7 @@ impl Chunks { let cont = self.content(data)?; self.correct_position(data)?; - utf8_decode(cont) + utf8_decode(cont).map_err(Into::into) } pub fn read_pstring(&mut self, data: &mut R, size: Option) -> Result @@ -72,7 +72,7 @@ impl Chunks { data.seek(SeekFrom::Current(1))?; } - utf8_decode(cont) + utf8_decode(cont).map_err(Into::into) } pub fn content(&mut self, data: &mut R) -> Result> @@ -87,7 +87,7 @@ impl Chunks { R: Read, { if size > self.remaining_size { - err!(SizeMismatch); + io_err!(SizeMismatch); } let mut content = try_vec![0; size as usize]; diff --git a/lofty/src/iff/wav/properties.rs b/lofty/src/iff/wav/properties.rs index ff3114cbd..38deb39ec 100644 --- a/lofty/src/iff/wav/properties.rs +++ b/lofty/src/iff/wav/properties.rs @@ -1,10 +1,10 @@ use crate::error::Result; use crate::macros::decode_err; use crate::properties::{ChannelMask, FileProperties}; -use crate::util::math::RoundedDivision; use std::time::Duration; +use aud_io::math::RoundedDivision; use byteorder::{LittleEndian, ReadBytesExt}; const PCM: u16 = 0x0001; diff --git a/lofty/src/iff/wav/read.rs b/lofty/src/iff/wav/read.rs index 637357e82..d44f77017 100644 --- a/lofty/src/iff/wav/read.rs +++ b/lofty/src/iff/wav/read.rs @@ -5,10 +5,11 @@ use crate::config::ParseOptions; use crate::error::Result; use crate::id3::v2::tag::Id3v2Tag; use crate::iff::chunk::Chunks; -use crate::macros::{decode_err, err}; +use crate::macros::decode_err; use std::io::{Read, Seek, SeekFrom}; +use aud_io::err as io_err; use byteorder::{LittleEndian, ReadBytesExt}; // Verifies that the stream is a WAV file and returns the stream length @@ -91,7 +92,7 @@ where // to avoid the seeks. let end = data.stream_position()? + u64::from(size); if end > file_len { - err!(SizeMismatch); + io_err!(SizeMismatch); } super::tag::read::parse_riff_info( diff --git a/lofty/src/iff/wav/tag/mod.rs b/lofty/src/iff/wav/tag/mod.rs index 93b9b4cb1..868ca5e82 100644 --- a/lofty/src/iff/wav/tag/mod.rs +++ b/lofty/src/iff/wav/tag/mod.rs @@ -6,11 +6,11 @@ use crate::error::{LoftyError, Result}; use crate::tag::{ Accessor, ItemKey, ItemValue, MergeTag, SplitTag, Tag, TagExt, TagItem, TagType, try_parse_year, }; -use crate::util::io::{FileLike, Length, Truncate}; use std::borrow::Cow; use std::io::Write; +use aud_io::io::{FileLike, Length, Truncate}; use lofty_attr::tag; macro_rules! impl_accessor { diff --git a/lofty/src/iff/wav/tag/read.rs b/lofty/src/iff/wav/tag/read.rs index 32dcf853c..7688fe253 100644 --- a/lofty/src/iff/wav/tag/read.rs +++ b/lofty/src/iff/wav/tag/read.rs @@ -3,10 +3,11 @@ use crate::config::ParsingMode; use crate::error::{ErrorKind, Result}; use crate::iff::chunk::Chunks; use crate::macros::decode_err; -use crate::util::text::utf8_decode_str; use std::io::{Read, Seek}; +use aud_io::error::AudioError; +use aud_io::text::utf8_decode_str; use byteorder::LittleEndian; pub(in crate::iff::wav) fn parse_riff_info( @@ -39,7 +40,7 @@ where // RIFF INFO tags have no standard text encoding, so they will occasionally default // to the system encoding, which isn't always UTF-8. In reality, if one item fails // they likely all will, but we'll keep trying. - if matches!(e.kind(), ErrorKind::StringFromUtf8(_)) { + if matches!(e.kind(), ErrorKind::AudioIo(AudioError::StringFromUtf8(_))) { continue; } diff --git a/lofty/src/iff/wav/tag/write.rs b/lofty/src/iff/wav/tag/write.rs index b8c22804e..215b77aab 100644 --- a/lofty/src/iff/wav/tag/write.rs +++ b/lofty/src/iff/wav/tag/write.rs @@ -3,11 +3,11 @@ use crate::config::WriteOptions; use crate::error::{LoftyError, Result}; use crate::iff::chunk::Chunks; use crate::iff::wav::read::verify_wav; -use crate::macros::err; -use crate::util::io::{FileLike, Length, Truncate}; use std::io::{Cursor, Read, Seek, SeekFrom}; +use aud_io::err as io_err; +use aud_io::io::{FileLike, Length, Truncate}; use byteorder::{LittleEndian, WriteBytesExt}; const RIFF_CHUNK_HEADER_SIZE: usize = 8; @@ -34,7 +34,7 @@ where file.read_to_end(file_bytes.get_mut())?; if file_bytes.get_ref().len() < (stream_length as usize + RIFF_CHUNK_HEADER_SIZE) { - err!(SizeMismatch); + io_err!(SizeMismatch); } // The first chunk format is RIFF....WAVE @@ -154,7 +154,7 @@ pub(super) fn create_riff_info( let packet_size = Vec::len(bytes) - 4; if packet_size > u32::MAX as usize { - err!(TooMuchData); + io_err!(TooMuchData); } log::debug!("Created RIFF INFO list, size: {} bytes", packet_size); diff --git a/lofty/src/lib.rs b/lofty/src/lib.rs index 22ef81ca3..4a06f75ae 100644 --- a/lofty/src/lib.rs +++ b/lofty/src/lib.rs @@ -130,12 +130,11 @@ pub mod wavpack; pub use crate::probe::{read_from, read_from_path}; -pub use util::text::TextEncoding; +pub use aud_io::io; +pub use aud_io::text::TextEncoding; pub use lofty_attr::LoftyFile; -pub use util::io; - pub mod prelude { //! A prelude for commonly used items in the library. //! diff --git a/lofty/src/macros.rs b/lofty/src/macros.rs index 4468b7563..87ce48698 100644 --- a/lofty/src/macros.rs +++ b/lofty/src/macros.rs @@ -1,7 +1,3 @@ -macro_rules! try_vec { - ($elem:expr; $size:expr) => {{ $crate::util::alloc::fallible_vec_from_element($elem, $size)? }}; -} - // Shorthand for return Err(LoftyError::new(ErrorKind::Foo)) // // Usage: @@ -93,4 +89,4 @@ macro_rules! parse_mode_choice { }; } -pub(crate) use {decode_err, err, parse_mode_choice, try_vec}; +pub(crate) use {decode_err, err, parse_mode_choice}; diff --git a/lofty/src/mp4/ilst/mod.rs b/lofty/src/mp4/ilst/mod.rs index 7d71d5f7f..8cd66746e 100644 --- a/lofty/src/mp4/ilst/mod.rs +++ b/lofty/src/mp4/ilst/mod.rs @@ -16,7 +16,6 @@ use crate::tag::{ Accessor, ItemKey, ItemValue, MergeTag, SplitTag, Tag, TagExt, TagItem, TagType, try_parse_year, }; use crate::util::flag_item; -use crate::util::io::{FileLike, Length, Truncate}; use advisory_rating::AdvisoryRating; use atom::{Atom, AtomData}; use data_type::DataType; @@ -25,6 +24,8 @@ use std::borrow::Cow; use std::io::Write; use std::ops::Deref; +use aud_io::err as io_err; +use aud_io::io::{FileLike, Length, Truncate}; use lofty_attr::tag; const ARTIST: AtomIdent<'_> = AtomIdent::Fourcc(*b"\xa9ART"); @@ -885,12 +886,54 @@ impl From for Ilst { } } +impl<'a> TryFrom<&'a ItemKey> for AtomIdent<'a> { + type Error = LoftyError; + + fn try_from(value: &'a ItemKey) -> std::result::Result { + if let Some(mapped_key) = value.map_key(TagType::Mp4Ilst) { + if mapped_key.starts_with("----") { + let mut split = mapped_key.split(':'); + + split.next(); + + let mean = split.next(); + let name = split.next(); + + if let (Some(mean), Some(name)) = (mean, name) { + return Ok(AtomIdent::Freeform { + mean: Cow::Borrowed(mean), + name: Cow::Borrowed(name), + }); + } + } else { + let fourcc = mapped_key.chars().map(|c| c as u8).collect::>(); + + if let Ok(fourcc) = TryInto::<[u8; 4]>::try_into(fourcc) { + return Ok(AtomIdent::Fourcc(fourcc)); + } + } + } + + io_err!(TextDecode( + "ItemKey does not map to a freeform or fourcc identifier" + )) + } +} + +impl TryFrom for AtomIdent<'static> { + type Error = LoftyError; + + fn try_from(value: ItemKey) -> std::result::Result { + let ret: AtomIdent<'_> = (&value).try_into()?; + Ok(ret.into_owned()) + } +} + #[cfg(test)] mod tests { use crate::config::{ParseOptions, ParsingMode, WriteOptions}; use crate::mp4::ilst::TITLE; use crate::mp4::ilst::atom::AtomDataStorage; - use crate::mp4::read::AtomReader; use crate::mp4::{AdvisoryRating, Atom, AtomData, AtomIdent, DataType, Ilst, Mp4File}; use crate::picture::{MimeType, Picture, PictureType}; use crate::prelude::*; @@ -900,6 +943,8 @@ mod tests { use std::io::{Cursor, Read as _, Seek as _, Write as _}; + use aud_io::mp4::AtomReader; + fn read_ilst(path: &str, parse_options: ParseOptions) -> Ilst { let tag = std::fs::read(path).unwrap(); read_ilst_raw(&tag, parse_options) diff --git a/lofty/src/mp4/ilst/read.rs b/lofty/src/mp4/ilst/read.rs index 9ef297b88..af1fa9f38 100644 --- a/lofty/src/mp4/ilst/read.rs +++ b/lofty/src/mp4/ilst/read.rs @@ -4,17 +4,18 @@ use super::{Atom, AtomData, AtomIdent, Ilst}; use crate::config::{ParseOptions, ParsingMode}; use crate::error::{LoftyError, Result}; use crate::id3::v1::constants::GENRES; -use crate::macros::{err, try_vec}; -use crate::mp4::atom_info::{ATOM_HEADER_LEN, AtomInfo}; use crate::mp4::ilst::atom::AtomDataStorage; -use crate::mp4::read::{AtomReader, skip_atom}; +use crate::mp4::read::skip_atom; use crate::picture::{MimeType, Picture, PictureType}; use crate::tag::TagExt; -use crate::util::text::{utf8_decode, utf16_decode_bytes}; use std::borrow::Cow; use std::io::{Cursor, Read, Seek, SeekFrom}; +use aud_io::mp4::{ATOM_HEADER_LEN, AtomInfo, AtomReader}; +use aud_io::text::{utf8_decode, utf16_decode_bytes}; +use aud_io::{err as io_err, try_vec}; + pub(in crate::mp4) fn parse_ilst( reader: &mut AtomReader, parse_options: ParseOptions, @@ -181,13 +182,12 @@ where R: Read + Seek, { let handle_error = |err: LoftyError, parsing_mode: ParsingMode| -> Result<()> { - match parsing_mode { - ParsingMode::Strict => Err(err), - ParsingMode::BestAttempt | ParsingMode::Relaxed => { - log::warn!("Skipping atom with invalid content: {}", err); - Ok(()) - }, + if parsing_mode == ParsingMode::Strict { + return Err(err); } + + log::warn!("Skipping atom with invalid content: {err}"); + Ok(()) }; if let Some(mut atom_data) = parse_data_inner(reader, parsing_mode, &atom_info)? { @@ -259,7 +259,7 @@ where next_atom.len ); if parsing_mode == ParsingMode::Strict { - err!(BadAtom("Data atom is too small")) + io_err!(BadAtom("Data atom is too small")) } break; @@ -267,7 +267,7 @@ where if next_atom.ident != DATA_ATOM_IDENT { if parsing_mode == ParsingMode::Strict { - err!(BadAtom("Expected atom \"data\" to follow name")) + io_err!(BadAtom("Expected atom \"data\" to follow name")) } log::warn!( @@ -323,7 +323,7 @@ where let type_set = reader.read_u8()?; if type_set != WELL_KNOWN_TYPE_SET { if parsing_mode == ParsingMode::Strict { - err!(BadAtom("Unknown type set in data atom")) + io_err!(BadAtom("Unknown type set in data atom")) } return Ok(None); @@ -338,7 +338,7 @@ fn parse_uint(bytes: &[u8]) -> Result { 2 => u32::from(u16::from_be_bytes([bytes[0], bytes[1]])), 3 => u32::from_be_bytes([0, bytes[0], bytes[1], bytes[2]]), 4 => u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]), - _ => err!(BadAtom( + _ => io_err!(BadAtom( "Unexpected atom size for type \"BE unsigned integer\"" )), }) @@ -350,7 +350,7 @@ fn parse_int(bytes: &[u8]) -> Result { 2 => i32::from(i16::from_be_bytes([bytes[0], bytes[1]])), 3 => i32::from_be_bytes([0, bytes[0], bytes[1], bytes[2]]), 4 => i32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]), - _ => err!(BadAtom( + _ => io_err!(BadAtom( "Unexpected atom size for type \"BE signed integer\"" )), }) @@ -380,7 +380,7 @@ where DataType::Bmp => Some(MimeType::Bmp), _ => { if parsing_mode == ParsingMode::Strict { - err!(BadAtom("\"covr\" atom has an unknown type")) + io_err!(BadAtom("\"covr\" atom has an unknown type")) } log::warn!( diff --git a/lofty/src/mp4/ilst/ref.rs b/lofty/src/mp4/ilst/ref.rs index 929099b94..699e9e21d 100644 --- a/lofty/src/mp4/ilst/ref.rs +++ b/lofty/src/mp4/ilst/ref.rs @@ -5,10 +5,11 @@ use crate::config::WriteOptions; use crate::error::{LoftyError, Result}; use crate::mp4::{Atom, AtomData, AtomIdent, Ilst}; -use crate::util::io::{FileLike, Length, Truncate}; use std::io::Write; +use aud_io::io::{FileLike, Length, Truncate}; + impl Ilst { pub(crate) fn as_ref(&self) -> IlstRef<'_, impl IntoIterator> { IlstRef { diff --git a/lofty/src/mp4/ilst/write.rs b/lofty/src/mp4/ilst/write.rs index 2dc10b59a..1bc3ca033 100644 --- a/lofty/src/mp4/ilst/write.rs +++ b/lofty/src/mp4/ilst/write.rs @@ -3,18 +3,19 @@ use super::r#ref::IlstRef; use crate::config::{ParseOptions, WriteOptions}; use crate::error::{FileEncodingError, LoftyError, Result}; use crate::file::FileType; -use crate::macros::{decode_err, err, try_vec}; +use crate::macros::decode_err; use crate::mp4::AtomData; -use crate::mp4::atom_info::{ATOM_HEADER_LEN, AtomIdent, AtomInfo, FOURCC_LEN}; use crate::mp4::ilst::r#ref::AtomRef; -use crate::mp4::read::{AtomReader, atom_tree, find_child_atom, meta_is_full, verify_mp4}; +use crate::mp4::read::{atom_tree, find_child_atom, meta_is_full, verify_mp4}; use crate::mp4::write::{AtomWriter, AtomWriterCompanion, ContextualAtom}; use crate::picture::{MimeType, Picture}; -use crate::util::alloc::VecFallibleCapacity; -use crate::util::io::{FileLike, Length, Truncate}; use std::io::{Cursor, Seek, SeekFrom, Write}; +use aud_io::alloc::VecFallibleCapacity; +use aud_io::io::{FileLike, Length, Truncate}; +use aud_io::mp4::{ATOM_HEADER_LEN, AtomIdent, AtomInfo, AtomReader, FOURCC_LEN}; +use aud_io::{err as io_err, try_vec}; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; // A "full" atom is a traditional length + identifier, followed by a version (1) and flags (3) @@ -298,7 +299,7 @@ fn save_to_existing( let remaining_space = available_space - ilst_len; if remaining_space > u64::from(u32::MAX) { - err!(TooMuchData); + io_err!(TooMuchData); } let remaining_space = remaining_space as u32; diff --git a/lofty/src/mp4/mod.rs b/lofty/src/mp4/mod.rs index 0a9617b9c..670802e03 100644 --- a/lofty/src/mp4/mod.rs +++ b/lofty/src/mp4/mod.rs @@ -3,7 +3,6 @@ //! ## File notes //! //! The only supported tag format is [`Ilst`]. -mod atom_info; pub(crate) mod ilst; mod moov; mod properties; @@ -21,15 +20,13 @@ pub mod constants { pub use super::ilst::constants::*; } -pub use crate::mp4::properties::{AudioObjectType, Mp4Codec, Mp4Properties}; -pub use atom_info::AtomIdent; +pub use crate::mp4::properties::{Mp4Codec, Mp4Properties}; +pub use aud_io::mp4::{AtomIdent, AudioObjectType}; pub use ilst::Ilst; pub use ilst::advisory_rating::AdvisoryRating; pub use ilst::atom::{Atom, AtomData}; pub use ilst::data_type::DataType; -pub(crate) use properties::SAMPLE_RATES; - /// An MP4 file #[derive(LoftyFile)] #[lofty(read_fn = "read::read_from")] diff --git a/lofty/src/mp4/moov.rs b/lofty/src/mp4/moov.rs index 6e43fbb64..a17ea3545 100644 --- a/lofty/src/mp4/moov.rs +++ b/lofty/src/mp4/moov.rs @@ -1,13 +1,14 @@ -use super::atom_info::{AtomIdent, AtomInfo}; use super::ilst::Ilst; use super::ilst::read::parse_ilst; -use super::read::{AtomReader, find_child_atom, meta_is_full, skip_atom}; +use super::read::{find_child_atom, meta_is_full, skip_atom}; use crate::config::ParseOptions; use crate::error::Result; use crate::macros::decode_err; use std::io::{Read, Seek}; +use aud_io::mp4::{AtomIdent, AtomInfo, AtomReader}; + pub(crate) struct Moov { // Represents the trak.mdia atom pub(crate) traks: Vec, diff --git a/lofty/src/mp4/properties.rs b/lofty/src/mp4/properties.rs index ded05fb03..bb59518d9 100644 --- a/lofty/src/mp4/properties.rs +++ b/lofty/src/mp4/properties.rs @@ -1,15 +1,16 @@ -use super::atom_info::{AtomIdent, AtomInfo}; -use super::read::{AtomReader, find_child_atom, skip_atom}; +use super::read::{find_child_atom, skip_atom}; use crate::config::ParsingMode; -use crate::error::{LoftyError, Result}; -use crate::macros::{decode_err, err, try_vec}; +use crate::error::Result; +use crate::macros::decode_err; use crate::properties::FileProperties; -use crate::util::alloc::VecFallibleCapacity; -use crate::util::math::RoundedDivision; use std::io::{Cursor, Read, Seek, SeekFrom}; use std::time::Duration; +use aud_io::alloc::VecFallibleCapacity; +use aud_io::math::RoundedDivision; +use aud_io::mp4::{AtomIdent, AtomInfo, AtomReader, AudioObjectType, SAMPLE_RATES}; +use aud_io::{err as io_err, try_vec}; use byteorder::{BigEndian, ReadBytesExt}; /// An MP4 file's audio codec @@ -25,112 +26,6 @@ pub enum Mp4Codec { FLAC, } -#[allow(missing_docs)] -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] -#[rustfmt::skip] -#[non_exhaustive] -pub enum AudioObjectType { - // https://en.wikipedia.org/wiki/MPEG-4_Part_3#MPEG-4_Audio_Object_Types - - #[default] - NULL = 0, - AacMain = 1, // AAC Main Profile - AacLowComplexity = 2, // AAC Low Complexity - AacScalableSampleRate = 3, // AAC Scalable Sample Rate - AacLongTermPrediction = 4, // AAC Long Term Predictor - SpectralBandReplication = 5, // Spectral band Replication - AACScalable = 6, // AAC Scalable - TwinVQ = 7, // Twin VQ - CodeExcitedLinearPrediction = 8, // CELP - HarmonicVectorExcitationCoding = 9, // HVXC - TextToSpeechtInterface = 12, // TTSI - MainSynthetic = 13, // Main Synthetic - WavetableSynthesis = 14, // Wavetable Synthesis - GeneralMIDI = 15, // General MIDI - AlgorithmicSynthesis = 16, // Algorithmic Synthesis - ErrorResilientAacLowComplexity = 17, // ER AAC LC - ErrorResilientAacLongTermPrediction = 19, // ER AAC LTP - ErrorResilientAacScalable = 20, // ER AAC Scalable - ErrorResilientAacTwinVQ = 21, // ER AAC TwinVQ - ErrorResilientAacBitSlicedArithmeticCoding = 22, // ER Bit Sliced Arithmetic Coding - ErrorResilientAacLowDelay = 23, // ER AAC Low Delay - ErrorResilientCodeExcitedLinearPrediction = 24, // ER CELP - ErrorResilientHarmonicVectorExcitationCoding = 25, // ER HVXC - ErrorResilientHarmonicIndividualLinesNoise = 26, // ER HILN - ErrorResilientParametric = 27, // ER Parametric - SinuSoidalCoding = 28, // SSC - ParametricStereo = 29, // PS - MpegSurround = 30, // MPEG Surround - MpegLayer1 = 32, // MPEG Layer 1 - MpegLayer2 = 33, // MPEG Layer 2 - MpegLayer3 = 34, // MPEG Layer 3 - DirectStreamTransfer = 35, // DST Direct Stream Transfer - AudioLosslessCoding = 36, // ALS Audio Lossless Coding - ScalableLosslessCoding = 37, // SLC Scalable Lossless Coding - ScalableLosslessCodingNoneCore = 38, // SLC non-core - ErrorResilientAacEnhancedLowDelay = 39, // ER AAC ELD - SymbolicMusicRepresentationSimple = 40, // SMR Simple - SymbolicMusicRepresentationMain = 41, // SMR Main - UnifiedSpeechAudioCoding = 42, // USAC - SpatialAudioObjectCoding = 43, // SAOC - LowDelayMpegSurround = 44, // LD MPEG Surround - SpatialAudioObjectCodingDialogueEnhancement = 45, // SAOC-DE - AudioSync = 46, // Audio Sync -} - -impl TryFrom for AudioObjectType { - type Error = LoftyError; - - #[rustfmt::skip] - fn try_from(value: u8) -> std::result::Result { - match value { - 1 => Ok(Self::AacMain), - 2 => Ok(Self::AacLowComplexity), - 3 => Ok(Self::AacScalableSampleRate), - 4 => Ok(Self::AacLongTermPrediction), - 5 => Ok(Self::SpectralBandReplication), - 6 => Ok(Self::AACScalable), - 7 => Ok(Self::TwinVQ), - 8 => Ok(Self::CodeExcitedLinearPrediction), - 9 => Ok(Self::HarmonicVectorExcitationCoding), - 12 => Ok(Self::TextToSpeechtInterface), - 13 => Ok(Self::MainSynthetic), - 14 => Ok(Self::WavetableSynthesis), - 15 => Ok(Self::GeneralMIDI), - 16 => Ok(Self::AlgorithmicSynthesis), - 17 => Ok(Self::ErrorResilientAacLowComplexity), - 19 => Ok(Self::ErrorResilientAacLongTermPrediction), - 20 => Ok(Self::ErrorResilientAacScalable), - 21 => Ok(Self::ErrorResilientAacTwinVQ), - 22 => Ok(Self::ErrorResilientAacBitSlicedArithmeticCoding), - 23 => Ok(Self::ErrorResilientAacLowDelay), - 24 => Ok(Self::ErrorResilientCodeExcitedLinearPrediction), - 25 => Ok(Self::ErrorResilientHarmonicVectorExcitationCoding), - 26 => Ok(Self::ErrorResilientHarmonicIndividualLinesNoise), - 27 => Ok(Self::ErrorResilientParametric), - 28 => Ok(Self::SinuSoidalCoding), - 29 => Ok(Self::ParametricStereo), - 30 => Ok(Self::MpegSurround), - 32 => Ok(Self::MpegLayer1), - 33 => Ok(Self::MpegLayer2), - 34 => Ok(Self::MpegLayer3), - 35 => Ok(Self::DirectStreamTransfer), - 36 => Ok(Self::AudioLosslessCoding), - 37 => Ok(Self::ScalableLosslessCoding), - 38 => Ok(Self::ScalableLosslessCodingNoneCore), - 39 => Ok(Self::ErrorResilientAacEnhancedLowDelay), - 40 => Ok(Self::SymbolicMusicRepresentationSimple), - 41 => Ok(Self::SymbolicMusicRepresentationMain), - 42 => Ok(Self::UnifiedSpeechAudioCoding), - 43 => Ok(Self::SpatialAudioObjectCoding), - 44 => Ok(Self::LowDelayMpegSurround), - 45 => Ok(Self::SpatialAudioObjectCodingDialogueEnhancement), - 46 => Ok(Self::AudioSync), - _ => decode_err!(@BAIL Mp4, "Encountered an invalid audio object type"), - } - } -} - /// An MP4 file's audio properties #[derive(Debug, Clone, PartialEq, Eq, Default)] #[non_exhaustive] @@ -285,7 +180,7 @@ where } let Some(mdhd) = mdhd else { - err!(BadAtom("Expected atom \"trak.mdia.mdhd\"")); + io_err!(BadAtom("Expected atom \"trak.mdia.mdhd\"")); }; Ok(AudioTrak { mdhd, minf }) @@ -433,11 +328,11 @@ where for _ in 0..num_sample_entries { let Some(atom) = reader.next()? else { - err!(BadAtom("Expected sample entry atom in `stsd` atom")) + io_err!(BadAtom("Expected sample entry atom in `stsd` atom")) }; let AtomIdent::Fourcc(ref fourcc) = atom.ident else { - err!(BadAtom("Expected fourcc atom in `stsd` atom")) + io_err!(BadAtom("Expected fourcc atom in `stsd` atom")) }; match fourcc { @@ -565,11 +460,6 @@ where Ok(properties) } -// https://wiki.multimedia.cx/index.php?title=MPEG-4_Audio#Sampling_Frequencies -pub(crate) const SAMPLE_RATES: [u32; 15] = [ - 96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350, 0, 0, -]; - fn mp4a_properties(stsd: &mut AtomReader, properties: &mut Mp4Properties) -> Result<()> where R: Read + Seek, @@ -671,8 +561,11 @@ where frequency_index = (byte_b >> 1) & 0x0F; } - properties.extended_audio_object_type = - Some(AudioObjectType::try_from(object_type)?); + let Ok(extended_audio_object_type) = AudioObjectType::try_from(object_type) else { + decode_err!(@BAIL Mp4, "Encountered an invalid audio object type"); + }; + + properties.extended_audio_object_type = Some(extended_audio_object_type); match frequency_index { // 15 means the sample rate is stored in the next 24 bits diff --git a/lofty/src/mp4/read/mod.rs b/lofty/src/mp4/read.rs similarity index 91% rename from lofty/src/mp4/read/mod.rs rename to lofty/src/mp4/read.rs index 396e915f3..abd155004 100644 --- a/lofty/src/mp4/read/mod.rs +++ b/lofty/src/mp4/read.rs @@ -1,21 +1,18 @@ -mod atom_reader; - use super::Mp4File; -use super::atom_info::{AtomIdent, AtomInfo}; use super::moov::Moov; use super::properties::Mp4Properties; use crate::config::{ParseOptions, ParsingMode}; -use crate::error::{ErrorKind, LoftyError, Result}; +use crate::error::Result; use crate::macros::{decode_err, err}; -use crate::util::io::SeekStreamLen; -use crate::util::text::utf8_decode_str; use std::io::{Read, Seek, SeekFrom}; +use aud_io::err as io_err; +use aud_io::io::SeekStreamLen; +use aud_io::mp4::{AtomIdent, AtomInfo, AtomReader}; +use aud_io::text::utf8_decode_str; use byteorder::{BigEndian, ReadBytesExt}; -pub(super) use atom_reader::AtomReader; - pub(in crate::mp4) fn verify_mp4(reader: &mut AtomReader) -> Result where R: Read + Seek, @@ -39,11 +36,9 @@ where reader.seek(SeekFrom::Current((atom.len - 12) as i64))?; - let major_brand = utf8_decode_str(&major_brand) - .map(ToOwned::to_owned) - .map_err(|_| { - LoftyError::new(ErrorKind::BadAtom("Unable to parse \"ftyp\"'s major brand")) - })?; + let Ok(major_brand) = utf8_decode_str(&major_brand).map(ToOwned::to_owned) else { + io_err!(BadAtom("Unable to parse \"ftyp\"'s major brand")); + }; log::debug!("Verified to be an MP4 file. Major brand: {}", major_brand); Ok(major_brand) @@ -107,7 +102,7 @@ where if let (pos, false) = pos.overflowing_add(len - 8) { reader.seek(SeekFrom::Start(pos))?; } else { - err!(TooMuchData); + io_err!(TooMuchData); } Ok(()) diff --git a/lofty/src/mp4/write.rs b/lofty/src/mp4/write.rs index 83251257a..fbc79b86a 100644 --- a/lofty/src/mp4/write.rs +++ b/lofty/src/mp4/write.rs @@ -1,14 +1,14 @@ use crate::config::ParsingMode; use crate::error::{LoftyError, Result}; use crate::io::{FileLike, Length, Truncate}; -use crate::macros::err; -use crate::mp4::atom_info::{AtomIdent, AtomInfo, IDENTIFIER_LEN}; use crate::mp4::read::{meta_is_full, skip_atom}; use std::cell::{RefCell, RefMut}; use std::io::{Cursor, Read, Seek, SeekFrom, Write}; use std::ops::RangeBounds; +use aud_io::err as io_err; +use aud_io::mp4::{AtomIdent, AtomInfo, IDENTIFIER_LEN}; use byteorder::{BigEndian, WriteBytesExt}; /// A wrapper around [`AtomInfo`] that allows us to track all of the children of containers we deem important @@ -75,7 +75,7 @@ impl ContextualAtom { if len != 0 { // TODO: Print the container ident - err!(BadAtom("Unable to read entire container")); + io_err!(BadAtom("Unable to read entire container")); } *reader_len = reader_len.saturating_sub(info.len); diff --git a/lofty/src/mpeg/header.rs b/lofty/src/mpeg/header.rs index 8e82f28f9..e012ed79c 100644 --- a/lofty/src/mpeg/header.rs +++ b/lofty/src/mpeg/header.rs @@ -1,9 +1,8 @@ -use super::constants::{BITRATES, PADDING_SIZES, SAMPLE_RATES, SAMPLES, SIDE_INFORMATION_SIZES}; use crate::error::Result; -use crate::macros::decode_err; use std::io::{Read, Seek, SeekFrom}; +use aud_io::mpeg::FrameHeader; use byteorder::{BigEndian, ReadBytesExt}; pub(crate) fn verify_frame_sync(frame_sync: [u8; 2]) -> bool { @@ -49,7 +48,10 @@ where // Unlike `search_for_frame_sync`, since this has the `Seek` bound, it will seek the reader // back to the start of the header. const REV_FRAME_SEARCH_BOUNDS: u64 = 1024; -pub(super) fn rev_search_for_frame_header(input: &mut R, pos: &mut u64) -> Result> +pub(super) fn rev_search_for_frame_header( + input: &mut R, + pos: &mut u64, +) -> Result> where R: Read + Seek, { @@ -74,24 +76,22 @@ where continue; } - let header = Header::read(u32::from_be_bytes([ + let Ok(header) = FrameHeader::parse(u32::from_be_bytes([ frame_sync[0], frame_sync[1], buf[relative_frame_start + 2], buf[relative_frame_start + 3], - ])); - - // We need to check if the header is actually valid. For - // all we know, we could be in some junk (ex. 0xFF_FF_FF_FF). - if header.is_none() { + ])) else { + // We need to check if the header is actually valid. For + // all we know, we could be in some junk (ex. 0xFF_FF_FF_FF). continue; - } + }; // Seek to the start of the frame sync *pos += relative_frame_start as u64; input.seek(SeekFrom::Start(*pos))?; - return Ok(header); + return Ok(Some(header)); } Ok(None) @@ -157,244 +157,6 @@ where } } -/// MPEG Audio version -#[derive(Default, PartialEq, Eq, Copy, Clone, Debug)] -#[allow(missing_docs)] -pub enum MpegVersion { - #[default] - V1, - V2, - V2_5, - /// Exclusive to AAC - V4, -} - -/// MPEG layer -#[derive(Default, Copy, Clone, Debug, PartialEq, Eq)] -#[allow(missing_docs)] -pub enum Layer { - Layer1 = 1, - Layer2 = 2, - #[default] - Layer3 = 3, -} - -/// Channel mode -#[derive(Default, Copy, Clone, PartialEq, Eq, Debug)] -#[allow(missing_docs)] -pub enum ChannelMode { - #[default] - Stereo = 0, - JointStereo = 1, - /// Two independent mono channels - DualChannel = 2, - SingleChannel = 3, -} - -/// A rarely-used decoder hint that the file must be de-emphasized -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -#[allow(missing_docs, non_camel_case_types)] -pub enum Emphasis { - /// 50/15 ms - MS5015, - Reserved, - /// CCIT J.17 - CCIT_J17, -} - -#[derive(Copy, Clone, Debug)] -pub(crate) struct Header { - pub(crate) sample_rate: u32, - pub(crate) len: u32, - pub(crate) data_start: u32, - pub(crate) samples: u16, - pub(crate) bitrate: u32, - pub(crate) version: MpegVersion, - pub(crate) layer: Layer, - pub(crate) channel_mode: ChannelMode, - pub(crate) mode_extension: Option, - pub(crate) copyright: bool, - pub(crate) original: bool, - pub(crate) emphasis: Option, -} - -impl Header { - pub(super) fn read(data: u32) -> Option { - let version = match (data >> 19) & 0b11 { - 0b00 => MpegVersion::V2_5, - 0b10 => MpegVersion::V2, - 0b11 => MpegVersion::V1, - _ => return None, - }; - - let version_index = if version == MpegVersion::V1 { 0 } else { 1 }; - - let layer = match (data >> 17) & 0b11 { - 0b01 => Layer::Layer3, - 0b10 => Layer::Layer2, - 0b11 => Layer::Layer1, - _ => { - log::debug!("MPEG: Frame header uses a reserved layer"); - return None; - }, - }; - - let mut header = Header { - sample_rate: 0, - len: 0, - data_start: 0, - samples: 0, - bitrate: 0, - version, - layer, - channel_mode: ChannelMode::default(), - mode_extension: None, - copyright: false, - original: false, - emphasis: None, - }; - - let layer_index = (header.layer as usize).saturating_sub(1); - - let bitrate_index = (data >> 12) & 0xF; - header.bitrate = BITRATES[version_index][layer_index][bitrate_index as usize]; - if header.bitrate == 0 { - return None; - } - - // Sample rate index - let sample_rate_index = (data >> 10) & 0b11; - header.sample_rate = match sample_rate_index { - // This is invalid - 0b11 => return None, - _ => SAMPLE_RATES[header.version as usize][sample_rate_index as usize], - }; - - let has_padding = ((data >> 9) & 1) == 1; - let mut padding = 0; - - if has_padding { - padding = u32::from(PADDING_SIZES[layer_index]); - } - - header.channel_mode = match (data >> 6) & 0b11 { - 0b00 => ChannelMode::Stereo, - 0b01 => ChannelMode::JointStereo, - 0b10 => ChannelMode::DualChannel, - 0b11 => ChannelMode::SingleChannel, - _ => unreachable!(), - }; - - if let ChannelMode::JointStereo = header.channel_mode { - header.mode_extension = Some(((data >> 4) & 3) as u8); - } else { - header.mode_extension = None; - } - - header.copyright = ((data >> 3) & 1) == 1; - header.original = ((data >> 2) & 1) == 1; - - header.emphasis = match data & 0b11 { - 0b00 => None, - 0b01 => Some(Emphasis::MS5015), - 0b10 => Some(Emphasis::Reserved), - 0b11 => Some(Emphasis::CCIT_J17), - _ => unreachable!(), - }; - - header.data_start = SIDE_INFORMATION_SIZES[version_index][header.channel_mode as usize] + 4; - header.samples = SAMPLES[layer_index][version_index]; - header.len = - (u32::from(header.samples) * header.bitrate * 125 / header.sample_rate) + padding; - - Some(header) - } - - /// Equivalent of [`cmp_header()`], but for an already constructed `Header`. - pub(super) fn cmp(self, other: &Self) -> bool { - self.version == other.version - && self.layer == other.layer - && self.sample_rate == other.sample_rate - } -} - -#[derive(Copy, Clone)] -pub(super) enum VbrHeaderType { - Xing, - Info, - Vbri, -} - -#[derive(Copy, Clone)] -pub(super) struct VbrHeader { - pub ty: VbrHeaderType, - pub frames: u32, - pub size: u32, -} - -impl VbrHeader { - pub(super) fn read(reader: &mut &[u8]) -> Result> { - let reader_len = reader.len(); - - let mut header = [0; 4]; - reader.read_exact(&mut header)?; - - match &header { - b"Xing" | b"Info" => { - if reader_len < 16 { - decode_err!(@BAIL Mpeg, "Xing header has an invalid size (< 16)"); - } - - let mut flags = [0; 4]; - reader.read_exact(&mut flags)?; - - if flags[3] & 0x03 != 0x03 { - log::debug!( - "MPEG: Xing header doesn't have required flags set (0x0001 and 0x0002)" - ); - return Ok(None); - } - - let frames = reader.read_u32::()?; - let size = reader.read_u32::()?; - - let ty = match &header { - b"Xing" => VbrHeaderType::Xing, - b"Info" => VbrHeaderType::Info, - _ => unreachable!(), - }; - - Ok(Some(Self { ty, frames, size })) - }, - b"VBRI" => { - if reader_len < 32 { - decode_err!(@BAIL Mpeg, "VBRI header has an invalid size (< 32)"); - } - - // Skip 6 bytes - // Version ID (2) - // Delay float (2) - // Quality indicator (2) - let _info = reader.read_uint::(6)?; - - let size = reader.read_u32::()?; - let frames = reader.read_u32::()?; - - Ok(Some(Self { - ty: VbrHeaderType::Vbri, - frames, - size, - })) - }, - _ => Ok(None), - } - } - - pub(super) fn is_valid(&self) -> bool { - self.frames > 0 && self.size > 0 - } -} - #[cfg(test)] mod tests { use crate::tag::utils::test_utils::read_path; diff --git a/lofty/src/mpeg/mod.rs b/lofty/src/mpeg/mod.rs index b36c268d6..66a27be27 100644 --- a/lofty/src/mpeg/mod.rs +++ b/lofty/src/mpeg/mod.rs @@ -1,18 +1,20 @@ //! MP3 specific items -mod constants; pub(crate) mod header; mod properties; mod read; -pub use header::{ChannelMode, Emphasis, Layer, MpegVersion}; -pub use properties::MpegProperties; - use crate::ape::tag::ApeTag; use crate::id3::v1::tag::Id3v1Tag; use crate::id3::v2::tag::Id3v2Tag; use lofty_attr::LoftyFile; +// Exports + +pub use properties::MpegProperties; + +pub use aud_io::mpeg::{ChannelMode, Emphasis, Layer, MpegVersion}; + /// An MPEG file #[derive(LoftyFile, Default)] #[lofty(read_fn = "read::read_from")] diff --git a/lofty/src/mpeg/properties.rs b/lofty/src/mpeg/properties.rs index 12209e6a5..e07920ebf 100644 --- a/lofty/src/mpeg/properties.rs +++ b/lofty/src/mpeg/properties.rs @@ -1,12 +1,15 @@ -use super::header::{ChannelMode, Emphasis, Header, Layer, MpegVersion, VbrHeader, VbrHeaderType}; use crate::error::Result; use crate::mpeg::header::rev_search_for_frame_header; use crate::properties::{ChannelMask, FileProperties}; -use crate::util::math::RoundedDivision; use std::io::{Read, Seek, SeekFrom}; use std::time::Duration; +use aud_io::math::RoundedDivision; +use aud_io::mpeg::{ + ChannelMode, Emphasis, FrameHeader, Layer, MpegVersion, VbrHeader, VbrHeaderType, +}; + /// An MPEG file's audio properties #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] #[non_exhaustive] @@ -123,7 +126,7 @@ impl MpegProperties { pub(super) fn read_properties( properties: &mut MpegProperties, reader: &mut R, - first_frame: (Header, u64), + first_frame: (FrameHeader, u64), mut last_frame_offset: u64, vbr_header: Option, file_length: u64, diff --git a/lofty/src/mpeg/read.rs b/lofty/src/mpeg/read.rs index 7a84065bc..e2ec26a86 100644 --- a/lofty/src/mpeg/read.rs +++ b/lofty/src/mpeg/read.rs @@ -1,4 +1,4 @@ -use super::header::{Header, HeaderCmpResult, VbrHeader, cmp_header, search_for_frame_sync}; +use super::header::{HeaderCmpResult, cmp_header, search_for_frame_sync}; use super::{MpegFile, MpegProperties}; use crate::ape::header::read_ape_header; use crate::config::{ParseOptions, ParsingMode}; @@ -6,12 +6,16 @@ use crate::error::Result; use crate::id3::v2::header::Id3v2Header; use crate::id3::v2::read::parse_id3v2; use crate::id3::{FindId3v2Config, ID3FindResults, find_id3v1, find_lyrics3v2}; -use crate::io::SeekStreamLen; use crate::macros::{decode_err, err}; use crate::mpeg::header::HEADER_MASK; use std::io::{Read, Seek, SeekFrom}; +use aud_io::err as io_err; +use aud_io::error::AudioError; +use aud_io::io::SeekStreamLen; +use aud_io::mpeg::error::VbrHeaderError; +use aud_io::mpeg::{FrameHeader, VbrHeader}; use byteorder::{BigEndian, ReadBytesExt}; pub(super) fn read_from(reader: &mut R, parse_options: ParseOptions) -> Result @@ -164,7 +168,7 @@ where // Seek back to the start of the tag let pos = reader.stream_position()?; let Some(start_of_tag) = pos.checked_sub(u64::from(header.size)) else { - err!(SizeMismatch); + io_err!(SizeMismatch); }; reader.seek(SeekFrom::Start(start_of_tag))?; @@ -197,7 +201,12 @@ where let mut xing_reader = [0; 32]; reader.read_exact(&mut xing_reader)?; - let xing_header = VbrHeader::read(&mut &xing_reader[..])?; + let xing_header; + match VbrHeader::parse(&mut &xing_reader[..]) { + Ok(header) => xing_header = Some(header), + Err(VbrHeaderError::UnknownHeader) => xing_header = None, + Err(e) => return Err(AudioError::from(e).into()), + } let file_length = reader.stream_len_hack()?; @@ -215,7 +224,7 @@ where } // Searches for the next frame, comparing it to the following one -fn find_next_frame(reader: &mut R) -> Result> +fn find_next_frame(reader: &mut R) -> Result> where R: Read + Seek, { @@ -228,7 +237,7 @@ where reader.seek(SeekFrom::Start(first_mp3_frame_start_absolute))?; let first_header_data = reader.read_u32::()?; - if let Some(first_header) = Header::read(first_header_data) { + if let Ok(first_header) = FrameHeader::parse(first_header_data) { match cmp_header(reader, 4, first_header.len, first_header_data, HEADER_MASK) { HeaderCmpResult::Equal => { return Ok(Some((first_header, first_mp3_frame_start_absolute))); diff --git a/lofty/src/musepack/mod.rs b/lofty/src/musepack/mod.rs index 010f7d0fd..d62f04ef2 100644 --- a/lofty/src/musepack/mod.rs +++ b/lofty/src/musepack/mod.rs @@ -1,5 +1,5 @@ //! Musepack specific items -pub mod constants; + mod read; pub mod sv4to6; pub mod sv7; @@ -12,6 +12,10 @@ use crate::properties::FileProperties; use lofty_attr::LoftyFile; +// Exports + +pub use aud_io::musepack::constants; + /// Audio properties of an MPC file /// /// The information available differs between stream versions diff --git a/lofty/src/musepack/read.rs b/lofty/src/musepack/read.rs index fbb8dbcca..0b9a756d4 100644 --- a/lofty/src/musepack/read.rs +++ b/lofty/src/musepack/read.rs @@ -6,11 +6,12 @@ use crate::config::ParseOptions; use crate::error::Result; use crate::id3::v2::read::parse_id3v2; use crate::id3::{FindId3v2Config, ID3FindResults, find_id3v1, find_id3v2, find_lyrics3v2}; -use crate::macros::err; -use crate::util::io::SeekStreamLen; use std::io::{Read, Seek, SeekFrom}; +use aud_io::err as io_err; +use aud_io::io::SeekStreamLen; + pub(super) fn read_from(reader: &mut R, parse_options: ParseOptions) -> Result where R: Read + Seek, @@ -34,7 +35,7 @@ where if let ID3FindResults(Some(header), Some(content)) = find_id3v2(reader, find_id3v2_config)? { let Some(new_stream_length) = stream_length.checked_sub(u64::from(header.full_tag_size())) else { - err!(SizeMismatch); + io_err!(SizeMismatch); }; stream_length = new_stream_length; @@ -54,7 +55,7 @@ where if header.is_some() { file.id3v1_tag = id3v1; let Some(new_stream_length) = stream_length.checked_sub(128) else { - err!(SizeMismatch); + io_err!(SizeMismatch); }; stream_length = new_stream_length; @@ -62,7 +63,7 @@ where let ID3FindResults(_, lyrics3v2_size) = find_lyrics3v2(reader)?; let Some(new_stream_length) = stream_length.checked_sub(u64::from(lyrics3v2_size)) else { - err!(SizeMismatch); + io_err!(SizeMismatch); }; stream_length = new_stream_length; @@ -77,13 +78,13 @@ where let tag_size = u64::from(header.size); let Some(tag_start) = pos.checked_sub(tag_size) else { - err!(SizeMismatch); + io_err!(SizeMismatch); }; reader.seek(SeekFrom::Start(tag_start))?; let Some(new_stream_length) = stream_length.checked_sub(tag_size) else { - err!(SizeMismatch); + io_err!(SizeMismatch); }; stream_length = new_stream_length; } diff --git a/lofty/src/musepack/sv4to6/properties.rs b/lofty/src/musepack/sv4to6/properties.rs index 1dc388685..24ea981ad 100644 --- a/lofty/src/musepack/sv4to6/properties.rs +++ b/lofty/src/musepack/sv4to6/properties.rs @@ -1,13 +1,14 @@ use crate::config::ParsingMode; use crate::error::Result; use crate::macros::decode_err; -use crate::musepack::constants::{MPC_DECODER_SYNTH_DELAY, MPC_FRAME_LENGTH}; use crate::properties::FileProperties; -use crate::util::math::RoundedDivision; use std::io::Read; use std::time::Duration; +use aud_io::math::RoundedDivision; +use aud_io::musepack::constants::{MPC_DECODER_SYNTH_DELAY, MPC_FRAME_LENGTH}; +use aud_io::musepack::sv4to6::StreamHeader; use byteorder::{LittleEndian, ReadBytesExt}; /// MPC stream versions 4-6 audio properties @@ -39,6 +40,19 @@ impl From for FileProperties { } } +impl From for MpcSv4to6Properties { + fn from(header: StreamHeader) -> Self { + Self { + average_bitrate: header.average_bitrate, + mid_side_stereo: header.mid_side_stereo, + stream_version: header.stream_version, + max_band: header.max_band, + frame_count: header.frame_count, + ..Default::default() + } + } +} + impl MpcSv4to6Properties { /// Duration of the audio pub fn duration(&self) -> Duration { @@ -91,40 +105,24 @@ impl MpcSv4to6Properties { let mut header_data = [0u32; 8]; reader.read_u32_into::(&mut header_data)?; - let mut properties = Self::default(); - - properties.average_bitrate = (header_data[0] >> 23) & 0x1FF; - let intensity_stereo = (header_data[0] >> 22) & 0x1 == 1; - properties.mid_side_stereo = (header_data[0] >> 21) & 0x1 == 1; - - properties.stream_version = ((header_data[0] >> 11) & 0x03FF) as u16; - if !(4..=6).contains(&properties.stream_version) { - decode_err!(@BAIL Mpc, "Invalid stream version encountered") - } - - properties.max_band = ((header_data[0] >> 6) & 0x1F) as u8; - let block_size = header_data[0] & 0x3F; - - if properties.stream_version >= 5 { - properties.frame_count = header_data[1]; // 32 bit - } else { - properties.frame_count = header_data[1] >> 16; // 16 bit - } + let header = StreamHeader::parse(header_data)?; if parse_mode == ParsingMode::Strict { - if properties.average_bitrate != 0 { + if header.average_bitrate != 0 { decode_err!(@BAIL Mpc, "Encountered CBR stream") } - if intensity_stereo { + if header.intensity_stereo { decode_err!(@BAIL Mpc, "Stream uses intensity stereo coding") } - if block_size != 1 { + if header.block_size != 1 { decode_err!(@BAIL Mpc, "Stream has an invalid block size (must be 1)") } } + let mut properties: MpcSv4to6Properties = header.into(); + if properties.stream_version < 6 { // Versions before 6 had an invalid last frame properties.frame_count = properties.frame_count.saturating_sub(1); diff --git a/lofty/src/musepack/sv7/properties.rs b/lofty/src/musepack/sv7/properties.rs index 21c733f3c..585105bda 100644 --- a/lofty/src/musepack/sv7/properties.rs +++ b/lofty/src/musepack/sv7/properties.rs @@ -1,103 +1,12 @@ use crate::error::Result; use crate::macros::decode_err; -use crate::musepack::constants::{ - FREQUENCY_TABLE, MPC_DECODER_SYNTH_DELAY, MPC_FRAME_LENGTH, MPC_OLD_GAIN_REF, -}; use crate::properties::FileProperties; use std::io::Read; use std::time::Duration; -use byteorder::{LittleEndian, ReadBytesExt}; - -/// Used profile -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum Profile { - /// No profile - #[default] - None, - /// Unstable/Experimental - Unstable, - /// Profiles 2-4 - Unused, - /// Below Telephone (q= 0.0) - BelowTelephone0, - /// Below Telephone (q= 1.0) - BelowTelephone1, - /// Telephone (q= 2.0) - Telephone, - /// Thumb (q= 3.0) - Thumb, - /// Radio (q= 4.0) - Radio, - /// Standard (q= 5.0) - Standard, - /// Xtreme (q= 6.0) - Xtreme, - /// Insane (q= 7.0) - Insane, - /// BrainDead (q= 8.0) - BrainDead, - /// Above BrainDead (q= 9.0) - AboveBrainDead9, - /// Above BrainDead (q= 10.0) - AboveBrainDead10, -} - -impl Profile { - /// Get a `Profile` from a u8 - /// - /// The mapping is available here: - #[rustfmt::skip] - pub fn from_u8(value: u8) -> Option { - match value { - 0 => Some(Self::None), - 1 => Some(Self::Unstable), - 2 | 3 | 4 => Some(Self::Unused), - 5 => Some(Self::BelowTelephone0), - 6 => Some(Self::BelowTelephone1), - 7 => Some(Self::Telephone), - 8 => Some(Self::Thumb), - 9 => Some(Self::Radio), - 10 => Some(Self::Standard), - 11 => Some(Self::Xtreme), - 12 => Some(Self::Insane), - 13 => Some(Self::BrainDead), - 14 => Some(Self::AboveBrainDead9), - 15 => Some(Self::AboveBrainDead10), - _ => None, - } - } -} - -/// Volume description for the start and end of the title -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum Link { - /// Title starts or ends with a very low level (no live or classical genre titles) - #[default] - VeryLowStartOrEnd, - /// Title ends loudly - LoudEnd, - /// Title starts loudly - LoudStart, - /// Title starts loudly and ends loudly - LoudStartAndEnd, -} - -impl Link { - /// Get a `Link` from a u8 - /// - /// The mapping is available here: - pub fn from_u8(value: u8) -> Option { - match value { - 0 => Some(Self::VeryLowStartOrEnd), - 1 => Some(Self::LoudEnd), - 2 => Some(Self::LoudStart), - 3 => Some(Self::LoudStartAndEnd), - _ => None, - } - } -} +use aud_io::musepack::constants::{MPC_DECODER_SYNTH_DELAY, MPC_FRAME_LENGTH}; +use aud_io::musepack::sv7::{Link, Profile, StreamHeader}; // http://trac.musepack.net/musepack/wiki/SV7Specification @@ -146,6 +55,38 @@ impl From for FileProperties { } } +impl From for MpcSv7Properties { + fn from(input: StreamHeader) -> Self { + Self { + duration: Duration::ZERO, + average_bitrate: 0, + channels: input.channels, + // -- Section 1 -- + frame_count: input.frame_count, + // -- Section 2 -- + intensity_stereo: input.intensity_stereo, + mid_side_stereo: input.mid_side_stereo, + max_band: input.max_band, + profile: input.profile, + link: input.link, + sample_freq: input.sample_frequency, + max_level: input.max_level, + // -- Section 3 -- + title_gain: input.replaygain_title_gain, + title_peak: input.replaygain_title_peak, + // -- Section 4 -- + album_gain: input.replaygain_album_gain, + album_peak: input.replaygain_album_peak, + // -- Section 5 -- + true_gapless: input.true_gapless, + last_frame_length: input.last_frame_length, + fast_seeking_safe: input.fast_seeking_safe, + // -- Section 6 -- + encoder_version: input.encoder_version, + } + } +} + impl MpcSv7Properties { /// Duration of the audio pub fn duration(&self) -> Duration { @@ -266,107 +207,23 @@ impl MpcSv7Properties { where R: Read, { - let version = reader.read_u8()?; - if version & 0x0F != 7 { - decode_err!(@BAIL Mpc, "Expected stream version 7"); - } - - let mut properties = MpcSv7Properties { - channels: 2, // Always 2 channels - ..Self::default() - }; - - // TODO: Make a Bitreader, would be nice crate-wide but especially here - // The SV7 header is split into 6 32-bit sections - - // -- Section 1 -- - properties.frame_count = reader.read_u32::()?; - - // -- Section 2 -- - let chunk = reader.read_u32::()?; - - let byte1 = ((chunk & 0xFF00_0000) >> 24) as u8; - - properties.intensity_stereo = ((byte1 & 0x80) >> 7) == 1; - properties.mid_side_stereo = ((byte1 & 0x40) >> 6) == 1; - properties.max_band = byte1 & 0x3F; - - let byte2 = ((chunk & 0xFF_0000) >> 16) as u8; - - properties.profile = Profile::from_u8((byte2 & 0xF0) >> 4).unwrap(); // Infallible - properties.link = Link::from_u8((byte2 & 0x0C) >> 2).unwrap(); // Infallible - - let sample_freq_index = byte2 & 0x03; - properties.sample_freq = FREQUENCY_TABLE[sample_freq_index as usize]; - - let remaining_bytes = (chunk & 0xFFFF) as u16; - properties.max_level = remaining_bytes; - - // -- Section 3 -- - let title_peak = reader.read_u16::()?; - let title_gain = reader.read_u16::()?; - - // -- Section 4 -- - let album_peak = reader.read_u16::()?; - let album_gain = reader.read_u16::()?; - - // -- Section 5 -- - let chunk = reader.read_u32::()?; - - properties.true_gapless = (chunk >> 31) == 1; - - if properties.true_gapless { - properties.last_frame_length = ((chunk >> 20) & 0x7FF) as u16; - } - - properties.fast_seeking_safe = (chunk >> 19) & 1 == 1; - - // NOTE: Rest of the chunk is zeroed and unused - - // -- Section 6 -- - properties.encoder_version = reader.read_u8()?; - - // -- End of parsing -- - - // Convert ReplayGain values - let set_replay_gain = |gain: u16| -> i16 { - if gain == 0 { - return 0; - } - - let gain = ((MPC_OLD_GAIN_REF - f32::from(gain) / 100.0) * 256.0 + 0.5) as i16; - if !(0..i16::MAX).contains(&gain) { - return 0; - } - gain - }; - let set_replay_peak = |peak: u16| -> u16 { - if peak == 0 { - return 0; - } - - ((f64::from(peak).log10() * 20.0 * 256.0) + 0.5) as u16 - }; - - properties.title_gain = set_replay_gain(title_gain); - properties.title_peak = set_replay_peak(title_peak); - properties.album_gain = set_replay_gain(album_gain); - properties.album_peak = set_replay_peak(album_peak); - - if properties.last_frame_length > MPC_FRAME_LENGTH as u16 { + let header = StreamHeader::parse(reader)?; + if header.last_frame_length > MPC_FRAME_LENGTH as u16 { decode_err!(@BAIL Mpc, "Invalid last frame length"); } - if properties.sample_freq == 0 { + if header.sample_frequency == 0 { log::warn!("Sample rate is 0, unable to calculate duration and bitrate"); - return Ok(properties); + return Ok(header.into()); } - if properties.frame_count == 0 { + if header.frame_count == 0 { log::warn!("Frame count is 0, unable to calculate duration and bitrate"); - return Ok(properties); + return Ok(header.into()); } + let mut properties = MpcSv7Properties::from(header); + let time_per_frame = (MPC_FRAME_LENGTH as f64) / f64::from(properties.sample_freq); let length = (f64::from(properties.frame_count) * time_per_frame) * 1000.0; properties.duration = Duration::from_millis(length as u64); diff --git a/lofty/src/musepack/sv8/properties.rs b/lofty/src/musepack/sv8/properties.rs index c8a4b633b..2d2c4fc76 100644 --- a/lofty/src/musepack/sv8/properties.rs +++ b/lofty/src/musepack/sv8/properties.rs @@ -1,15 +1,13 @@ -use super::read::PacketReader; use crate::config::ParsingMode; use crate::error::Result; use crate::macros::decode_err; -use crate::musepack::constants::FREQUENCY_TABLE; use crate::properties::FileProperties; -use crate::util::math::RoundedDivision; use std::io::Read; use std::time::Duration; -use byteorder::{BigEndian, ReadBytesExt}; +use aud_io::math::RoundedDivision; +use aud_io::musepack::sv8::{EncoderInfo, ReplayGain, StreamHeader}; /// MPC stream version 8 audio properties #[derive(Debug, Clone, PartialEq, Default)] @@ -69,182 +67,6 @@ impl MpcSv8Properties { } } -/// Information from a Stream Header packet -/// -/// This contains the information needed to decode the stream. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct StreamHeader { - /// CRC 32 of the stream header packet - /// - /// The CRC used is here: - pub crc: u32, - /// Bitstream version - pub stream_version: u8, - /// Number of samples in the stream. 0 = unknown - pub sample_count: u64, - /// Number of samples to skip at the beginning of the stream - pub beginning_silence: u64, - /// The sampling frequency - /// - /// NOTE: This is not the index into the frequency table, this is the mapped value. - pub sample_rate: u32, - /// Maximum number of bands used in the file - pub max_used_bands: u8, - /// Number of channels in the stream - pub channels: u8, - /// Whether Mid Side Stereo is enabled - pub ms_used: bool, - /// Number of frames per audio packet - pub audio_block_frames: u16, -} - -impl StreamHeader { - pub(super) fn read(reader: &mut PacketReader) -> Result { - // StreamHeader format: - // - // Field | Size (bits) | Value | Comment - // CRC | 32 | | CRC 32 of the block (this field excluded). 0 = invalid - // Stream version | 8 | 8 | Bitstream version - // Sample count | n*8; 0 < n < 10 | | Number of samples in the stream. 0 = unknown - // Beginning silence | n*8; 0 < n < 10 | | Number of samples to skip at the beginning of the stream - // Sample frequency | 3 | 0..7 | See table below - // Max used bands | 5 | 1..32 | Maximum number of bands used in the file - // Channel count | 4 | 1..16 | Number of channels in the stream - // MS used | 1 | | True if Mid Side Stereo is enabled - // Audio block frames | 3 | 0..7 | Number of frames per audio packet (4value=(1..16384)) - - let crc = reader.read_u32::()?; - let stream_version = reader.read_u8()?; - let (sample_count, _) = PacketReader::read_size(reader)?; - let (beginning_silence, _) = PacketReader::read_size(reader)?; - - // Sample rate and max used bands - let remaining_flags_byte_1 = reader.read_u8()?; - - let sample_rate_index = (remaining_flags_byte_1 & 0xE0) >> 5; - let sample_rate = FREQUENCY_TABLE[sample_rate_index as usize]; - - let max_used_bands = (remaining_flags_byte_1 & 0x1F) + 1; - - // Channel count, MS used, audio block frames - let remaining_flags_byte_2 = reader.read_u8()?; - - let channels = (remaining_flags_byte_2 >> 4) + 1; - let ms_used = remaining_flags_byte_2 & 0x08 == 0x08; - - let audio_block_frames_value = remaining_flags_byte_2 & 0x07; - let audio_block_frames = 4u16.pow(u32::from(audio_block_frames_value)); - - Ok(Self { - crc, - stream_version, - sample_count, - beginning_silence, - sample_rate, - max_used_bands, - channels, - ms_used, - audio_block_frames, - }) - } -} - -/// Information from a ReplayGain packet -/// -/// This contains the necessary data needed to apply ReplayGain on the current stream. -/// -/// The ReplayGain values are stored in dB in Q8.8 format. -/// A value of `0` means that this field has not been computed (no gain must be applied in this case). -/// -/// Examples: -/// -/// * ReplayGain finds that this title has a loudness of 78.56 dB. It will be encoded as $ 78.56 * 256 ~ 20111 = 0x4E8F $ -/// * For 16-bit output (range \[-32767 32768]), the max is 68813 (out of range). It will be encoded as $ 20 * log10(68813) * 256 ~ 24769 = 0x60C1 $ -/// * For float output (range \[-1 1]), the max is 0.96. It will be encoded as $ 20 * log10(0.96 * 215) * 256 ~ 23029 = 0x59F5 $ (for peak values it is suggested to round to nearest higher integer) -#[derive(Debug, Clone, Copy, PartialEq, Default)] -#[allow(missing_docs)] -pub struct ReplayGain { - /// The replay gain version - pub version: u8, - /// The loudness calculated for the title, and not the gain that the player must apply - pub title_gain: u16, - pub title_peak: u16, - /// The loudness calculated for the album - pub album_gain: u16, - pub album_peak: u16, -} - -impl ReplayGain { - pub(super) fn read(reader: &mut PacketReader) -> Result { - // ReplayGain format: - // - // Field | Size (bits) | Value | Comment - // ReplayGain version | 8 | 1 | The replay gain version - // Title gain | 16 | | The loudness calculated for the title, and not the gain that the player must apply - // Title peak | 16 | | - // Album gain | 16 | | The loudness calculated for the album - // Album peak | 16 | | - - let version = reader.read_u8()?; - let title_gain = reader.read_u16::()?; - let title_peak = reader.read_u16::()?; - let album_gain = reader.read_u16::()?; - let album_peak = reader.read_u16::()?; - - Ok(Self { - version, - title_gain, - title_peak, - album_gain, - album_peak, - }) - } -} - -/// Information from an Encoder Info packet -#[derive(Debug, Clone, Copy, PartialEq, Default)] -#[allow(missing_docs)] -pub struct EncoderInfo { - /// Quality in 4.3 format - pub profile: f32, - pub pns_tool: bool, - /// Major version - pub major: u8, - /// Minor version, even numbers for stable version, odd when unstable - pub minor: u8, - /// Build - pub build: u8, -} - -impl EncoderInfo { - pub(super) fn read(reader: &mut PacketReader) -> Result { - // EncoderInfo format: - // - // Field | Size (bits) | Value - // Profile | 7 | 0..15.875 - // PNS tool | 1 | True if enabled - // Major | 8 | 1 - // Minor | 8 | 17 - // Build | 8 | 3 - - let byte1 = reader.read_u8()?; - let profile = f32::from((byte1 & 0xFE) >> 1) / 8.0; - let pns_tool = byte1 & 0x01 == 1; - - let major = reader.read_u8()?; - let minor = reader.read_u8()?; - let build = reader.read_u8()?; - - Ok(Self { - profile, - pns_tool, - major, - minor, - build, - }) - } -} - pub(super) fn read( stream_length: u64, stream_header: StreamHeader, diff --git a/lofty/src/musepack/sv8/read.rs b/lofty/src/musepack/sv8/read.rs index 10e8452f4..f9a876081 100644 --- a/lofty/src/musepack/sv8/read.rs +++ b/lofty/src/musepack/sv8/read.rs @@ -1,19 +1,13 @@ -use super::properties::{EncoderInfo, MpcSv8Properties, ReplayGain, StreamHeader}; +use super::properties::MpcSv8Properties; use crate::config::ParsingMode; -use crate::error::{ErrorKind, LoftyError, Result}; +use crate::error::Result; use crate::macros::{decode_err, parse_mode_choice}; use std::io::Read; -use byteorder::ReadBytesExt; +use aud_io::musepack::sv8::{EncoderInfo, PacketKey, PacketReader, ReplayGain, StreamHeader}; // TODO: Support chapter packets? -const STREAM_HEADER_KEY: [u8; 2] = *b"SH"; -const REPLAYGAIN_KEY: [u8; 2] = *b"RG"; -const ENCODER_INFO_KEY: [u8; 2] = *b"EI"; -#[allow(dead_code)] -const AUDIO_PACKET_KEY: [u8; 2] = *b"AP"; -const STREAM_END_KEY: [u8; 2] = *b"SE"; pub(crate) fn read_from(data: &mut R, parse_mode: ParsingMode) -> Result where @@ -31,11 +25,17 @@ where while let Ok((packet_id, packet_length)) = packet_reader.next() { stream_length += packet_length; - match packet_id { - STREAM_HEADER_KEY => stream_header = Some(StreamHeader::read(&mut packet_reader)?), - REPLAYGAIN_KEY => replay_gain = Some(ReplayGain::read(&mut packet_reader)?), - ENCODER_INFO_KEY => encoder_info = Some(EncoderInfo::read(&mut packet_reader)?), - STREAM_END_KEY => { + let Ok(packet_key) = PacketKey::try_from(packet_id) else { + continue; + }; + + match packet_key { + PacketKey::StreamHeader => { + stream_header = Some(StreamHeader::parse(&mut packet_reader)?) + }, + PacketKey::ReplayGain => replay_gain = Some(ReplayGain::parse(&mut packet_reader)?), + PacketKey::EncoderInfo => encoder_info = Some(EncoderInfo::parse(&mut packet_reader)?), + PacketKey::StreamEnd => { found_stream_end = true; break; }, @@ -80,88 +80,3 @@ where Ok(properties) } - -pub struct PacketReader { - reader: R, - capacity: u64, -} - -impl PacketReader { - fn new(reader: R) -> Self { - Self { - reader, - capacity: 0, - } - } - - /// Move the reader to the next packet, returning the next packet key and size - fn next(&mut self) -> Result<([u8; 2], u64)> { - // Discard the rest of the current packet - std::io::copy( - &mut self.reader.by_ref().take(self.capacity), - &mut std::io::sink(), - )?; - - // Packet format: - // - // Field | Size (bits) | Value - // Key | 16 | "EX" - // Size | n*8; 0 < n < 10 | 0x1A - // Payload | Size * 8 | "example" - - let mut key = [0; 2]; - self.reader.read_exact(&mut key)?; - - if !key[0].is_ascii_uppercase() || !key[1].is_ascii_uppercase() { - decode_err!(@BAIL Mpc, "Packet key contains characters that are out of the allowed range") - } - - let (packet_size, packet_size_byte_count) = Self::read_size(&mut self.reader)?; - - // The packet size contains the key (2) and the size (?, variable length <= 9) - self.capacity = packet_size.saturating_sub(u64::from(2 + packet_size_byte_count)); - - Ok((key, self.capacity)) - } - - /// Read the variable-length packet size - /// - /// This takes a reader since we need to both use it for packet reading *and* setting up the reader itself in `PacketReader::next` - pub fn read_size(reader: &mut R) -> Result<(u64, u8)> { - let mut current; - let mut size = 0u64; - - // bits, big-endian - // 0xxx xxxx - value 0 to 2^7-1 - // 1xxx xxxx 0xxx xxxx - value 0 to 2^14-1 - // 1xxx xxxx 1xxx xxxx 0xxx xxxx - value 0 to 2^21-1 - // 1xxx xxxx 1xxx xxxx 1xxx xxxx 0xxx xxxx - value 0 to 2^28-1 - // ... - - let mut bytes_read = 0; - loop { - current = reader.read_u8()?; - bytes_read += 1; - - // Sizes cannot go above 9 bytes - if bytes_read > 9 { - return Err(LoftyError::new(ErrorKind::TooMuchData)); - } - - size = (size << 7) | u64::from(current & 0x7F); - if current & 0x80 == 0 { - break; - } - } - - Ok((size, bytes_read)) - } -} - -impl Read for PacketReader { - fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - let bytes_read = self.reader.by_ref().take(self.capacity).read(buf)?; - self.capacity = self.capacity.saturating_sub(bytes_read as u64); - Ok(bytes_read) - } -} diff --git a/lofty/src/ogg/opus/properties.rs b/lofty/src/ogg/opus/properties.rs index 4475c761c..2e7a6b5ab 100644 --- a/lofty/src/ogg/opus/properties.rs +++ b/lofty/src/ogg/opus/properties.rs @@ -2,11 +2,11 @@ use super::find_last_page; use crate::error::Result; use crate::macros::decode_err; use crate::properties::{ChannelMask, FileProperties}; -use crate::util::math::RoundedDivision; use std::io::{Read, Seek, SeekFrom}; use std::time::Duration; +use aud_io::math::RoundedDivision; use byteorder::{LittleEndian, ReadBytesExt}; use ogg_pager::{Packets, PageHeader}; diff --git a/lofty/src/ogg/read.rs b/lofty/src/ogg/read.rs index c265bc239..5edf6a2e2 100644 --- a/lofty/src/ogg/read.rs +++ b/lofty/src/ogg/read.rs @@ -2,14 +2,16 @@ use super::tag::VorbisComments; use super::verify_signature; use crate::config::{ParseOptions, ParsingMode}; use crate::error::{ErrorKind, LoftyError, Result}; -use crate::macros::{decode_err, err, parse_mode_choice}; +use crate::macros::{decode_err, parse_mode_choice}; use crate::picture::{MimeType, Picture, PictureInformation, PictureType}; use crate::tag::Accessor; -use crate::util::text::{utf8_decode, utf8_decode_str, utf16_decode}; use std::borrow::Cow; use std::io::{Read, Seek, SeekFrom}; +use aud_io::err as io_err; +use aud_io::error::AudioError; +use aud_io::text::{utf8_decode, utf8_decode_str, utf16_decode}; use byteorder::{LittleEndian, ReadBytesExt}; use data_encoding::BASE64; use ogg_pager::{Packets, PageHeader}; @@ -24,13 +26,13 @@ pub(crate) fn read_comments( where R: Read, { - use crate::macros::try_vec; + use aud_io::try_vec; let parse_mode = parse_options.parsing_mode; let vendor_len = data.read_u32::()?; if u64::from(vendor_len) > len { - err!(SizeMismatch); + io_err!(SizeMismatch); } let mut vendor_bytes = try_vec![0; vendor_len as usize]; @@ -45,7 +47,7 @@ where // The actions following this are not spec-compliant in the slightest, so // we need to short circuit if strict. if parse_mode == ParsingMode::Strict { - return Err(e); + return Err(e.into()); } log::warn!("Possibly corrupt vendor string, attempting to recover"); @@ -53,12 +55,10 @@ where // Some vendor strings have invalid mixed UTF-8 and UTF-16 encodings. // This seems to work, while preserving the string opposed to using // the replacement character - let LoftyError { - kind: ErrorKind::StringFromUtf8(e), - } = e - else { - return Err(e); + let AudioError::StringFromUtf8(e) = e else { + return Err(e.into()); }; + let s = e .as_bytes() .iter() @@ -77,7 +77,7 @@ where let number_of_items = data.read_u32::()?; if number_of_items > (len >> 2) as u32 { - err!(SizeMismatch); + io_err!(SizeMismatch); } let mut tag = VorbisComments { @@ -89,7 +89,7 @@ where for _ in 0..number_of_items { let comment_len = data.read_u32::()?; if u64::from(comment_len) > len { - err!(SizeMismatch); + io_err!(SizeMismatch); } let mut comment_bytes = try_vec![0; comment_len as usize]; @@ -220,7 +220,7 @@ where }, Err(e) => { if parse_mode == ParsingMode::Strict { - return Err(e); + return Err(e.into()); } log::warn!("Non UTF-8 value found, discarding field {key:?}"); @@ -237,7 +237,7 @@ where Ok(value) => tag.items.push((key, value.to_owned())), Err(e) => { if parse_mode == ParsingMode::Strict { - return Err(e); + return Err(e.into()); } log::warn!("Non UTF-8 value found, discarding field {key:?}"); diff --git a/lofty/src/ogg/speex/properties.rs b/lofty/src/ogg/speex/properties.rs index 9f7189691..a39d99172 100644 --- a/lofty/src/ogg/speex/properties.rs +++ b/lofty/src/ogg/speex/properties.rs @@ -2,11 +2,11 @@ use crate::error::Result; use crate::macros::decode_err; use crate::ogg::find_last_page; use crate::properties::FileProperties; -use crate::util::math::RoundedDivision; use std::io::{Read, Seek, SeekFrom}; use std::time::Duration; +use aud_io::math::RoundedDivision; use byteorder::{LittleEndian, ReadBytesExt}; use ogg_pager::{Packets, PageHeader}; diff --git a/lofty/src/ogg/tag.rs b/lofty/src/ogg/tag.rs index b2ec22ea1..08222de66 100644 --- a/lofty/src/ogg/tag.rs +++ b/lofty/src/ogg/tag.rs @@ -10,12 +10,12 @@ use crate::tag::{ Accessor, ItemKey, ItemValue, MergeTag, SplitTag, Tag, TagExt, TagItem, TagType, try_parse_year, }; use crate::util::flag_item; -use crate::util::io::{FileLike, Length, Truncate}; use std::borrow::Cow; use std::io::Write; use std::ops::Deref; +use aud_io::io::{FileLike, Length, Truncate}; use lofty_attr::tag; macro_rules! impl_accessor { diff --git a/lofty/src/ogg/vorbis/properties.rs b/lofty/src/ogg/vorbis/properties.rs index caa69abec..01f190a8f 100644 --- a/lofty/src/ogg/vorbis/properties.rs +++ b/lofty/src/ogg/vorbis/properties.rs @@ -1,11 +1,11 @@ use super::find_last_page; use crate::error::Result; use crate::properties::FileProperties; -use crate::util::math::RoundedDivision; use std::io::{Read, Seek, SeekFrom}; use std::time::Duration; +use aud_io::math::RoundedDivision; use byteorder::{LittleEndian, ReadBytesExt}; use ogg_pager::{Packets, PageHeader}; diff --git a/lofty/src/ogg/write.rs b/lofty/src/ogg/write.rs index b90ba0dfb..85175df4e 100644 --- a/lofty/src/ogg/write.rs +++ b/lofty/src/ogg/write.rs @@ -2,16 +2,17 @@ use super::verify_signature; use crate::config::WriteOptions; use crate::error::{LoftyError, Result}; use crate::file::FileType; -use crate::macros::{decode_err, err, try_vec}; +use crate::macros::{decode_err, err}; use crate::ogg::constants::{OPUSTAGS, VORBIS_COMMENT_HEAD}; use crate::ogg::tag::{VorbisCommentsRef, create_vorbis_comments_ref}; use crate::picture::{Picture, PictureInformation}; use crate::tag::{Tag, TagType}; -use crate::util::io::{FileLike, Length, Truncate}; use std::borrow::Cow; use std::io::{Cursor, Read, Seek, SeekFrom, Write}; +use aud_io::io::{FileLike, Length, Truncate}; +use aud_io::{err as io_err, try_vec}; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use ogg_pager::{CONTAINS_FIRST_PAGE_OF_BITSTREAM, Packets, Page, PageHeader}; @@ -213,7 +214,7 @@ pub(crate) fn create_comments( let comment_bytes = comment.as_bytes(); let Ok(bytes_len) = u32::try_from(comment_bytes.len()) else { - err!(TooMuchData); + io_err!(TooMuchData); }; *count += 1; @@ -236,7 +237,7 @@ fn create_pictures( let picture = pic.as_flac_bytes(info, true); let Ok(bytes_len) = u32::try_from(picture.len() + PICTURE_KEY.len()) else { - err!(TooMuchData); + io_err!(TooMuchData); }; *count += 1; diff --git a/lofty/src/picture.rs b/lofty/src/picture.rs index d7f3a4da3..01c752970 100644 --- a/lofty/src/picture.rs +++ b/lofty/src/picture.rs @@ -3,12 +3,13 @@ use crate::config::ParsingMode; use crate::error::{ErrorKind, LoftyError, Result}; use crate::macros::err; -use crate::util::text::utf8_decode_str; use std::borrow::Cow; use std::fmt::{Debug, Display, Formatter}; use std::io::{Cursor, Read, Seek, SeekFrom}; +use aud_io::err as io_err; +use aud_io::text::utf8_decode_str; use byteorder::{BigEndian, ReadBytesExt as _}; use data_encoding::BASE64; @@ -778,7 +779,7 @@ impl Picture { content: &[u8], parse_mode: ParsingMode, ) -> Result<(Self, PictureInformation)> { - use crate::macros::try_vec; + use aud_io::try_vec; let mut size = content.len(); let mut reader = Cursor::new(content); @@ -801,7 +802,7 @@ impl Picture { size -= 4; if mime_len > size { - err!(SizeMismatch); + io_err!(SizeMismatch); } let mime_type_str = utf8_decode_str(&content[8..8 + mime_len])?; diff --git a/lofty/src/properties/tests.rs b/lofty/src/properties/tests.rs index af65b4345..ee8562cf1 100644 --- a/lofty/src/properties/tests.rs +++ b/lofty/src/properties/tests.rs @@ -6,10 +6,10 @@ use crate::flac::{FlacFile, FlacProperties}; use crate::iff::aiff::{AiffFile, AiffProperties}; use crate::iff::wav::{WavFile, WavFormat, WavProperties}; use crate::mp4::{AudioObjectType, Mp4Codec, Mp4File, Mp4Properties}; -use crate::mpeg::{ChannelMode, Layer, MpegFile, MpegProperties, MpegVersion}; +use crate::mpeg::{ChannelMode, Layer, MpegFile, MpegProperties}; use crate::musepack::sv4to6::MpcSv4to6Properties; -use crate::musepack::sv7::{Link, MpcSv7Properties, Profile}; -use crate::musepack::sv8::{EncoderInfo, MpcSv8Properties, ReplayGain, StreamHeader}; +use crate::musepack::sv7::MpcSv7Properties; +use crate::musepack::sv8::MpcSv8Properties; use crate::musepack::{MpcFile, MpcProperties}; use crate::ogg::{ OpusFile, OpusProperties, SpeexFile, SpeexProperties, VorbisFile, VorbisProperties, @@ -20,6 +20,10 @@ use crate::wavpack::{WavPackFile, WavPackProperties}; use std::fs::File; use std::time::Duration; +use aud_io::mpeg::MpegVersion; +use aud_io::musepack::sv7::{Link, Profile}; +use aud_io::musepack::sv8::{EncoderInfo, ReplayGain, StreamHeader}; + // These values are taken from FFmpeg's ffprobe // There is a chance they will be +/- 1, anything greater (for real world files) // is an issue. diff --git a/lofty/src/tag/mod.rs b/lofty/src/tag/mod.rs index c946d0387..fe05b0141 100644 --- a/lofty/src/tag/mod.rs +++ b/lofty/src/tag/mod.rs @@ -14,12 +14,13 @@ use crate::error::{LoftyError, Result}; use crate::macros::err; use crate::picture::{Picture, PictureType}; use crate::probe::Probe; -use crate::util::io::{FileLike, Length, Truncate}; use std::borrow::Cow; use std::io::Write; use std::path::Path; +use aud_io::io::{FileLike, Length, Truncate}; + // Exports pub use accessor::Accessor; pub use item::{ItemKey, ItemValue, TagItem}; diff --git a/lofty/src/tag/utils.rs b/lofty/src/tag/utils.rs index 5065aae0d..17707378f 100644 --- a/lofty/src/tag/utils.rs +++ b/lofty/src/tag/utils.rs @@ -3,7 +3,6 @@ use crate::error::{LoftyError, Result}; use crate::file::FileType; use crate::macros::err; use crate::tag::{Tag, TagType}; -use crate::util::io::{FileLike, Length, Truncate}; use crate::{aac, ape, flac, iff, mpeg, musepack, wavpack}; use crate::id3::v1::tag::Id3v1TagRef; @@ -18,6 +17,8 @@ use iff::wav::tag::RIFFInfoListRef; use std::borrow::Cow; use std::io::Write; +use aud_io::io::{FileLike, Length, Truncate}; + #[allow(unreachable_patterns)] pub(crate) fn write_tag( tag: &Tag, diff --git a/lofty/src/util/mod.rs b/lofty/src/util/mod.rs index 99aeb9064..5733b5ba9 100644 --- a/lofty/src/util/mod.rs +++ b/lofty/src/util/mod.rs @@ -1,8 +1,3 @@ -pub(crate) mod alloc; -pub mod io; -pub(crate) mod math; -pub(crate) mod text; - pub(crate) fn flag_item(item: &str) -> Option { match item { "1" | "true" => Some(true), diff --git a/lofty/src/wavpack/properties.rs b/lofty/src/wavpack/properties.rs index 72aa55384..43c47a8c5 100644 --- a/lofty/src/wavpack/properties.rs +++ b/lofty/src/wavpack/properties.rs @@ -1,11 +1,12 @@ use crate::config::ParsingMode; use crate::error::Result; -use crate::macros::{decode_err, err, parse_mode_choice, try_vec}; +use crate::macros::{decode_err, err, parse_mode_choice}; use crate::properties::{ChannelMask, FileProperties}; use std::io::{Read, Seek, SeekFrom}; use std::time::Duration; +use aud_io::{err as io_err, try_vec}; use byteorder::{LittleEndian, ReadBytesExt}; /// A WavPack file's audio properties @@ -324,7 +325,7 @@ fn get_extended_meta_info( } if (size as usize) > reader.len() { - err!(SizeMismatch); + io_err!(SizeMismatch); } if id & ID_FLAG_ODD_SIZE > 0 { @@ -415,7 +416,7 @@ fn get_extended_meta_info( // Skip over any remaining block size if (size as usize) > reader.len() { - err!(SizeMismatch); + io_err!(SizeMismatch); } let (_, rem) = reader.split_at(size as usize); diff --git a/lofty/src/wavpack/read.rs b/lofty/src/wavpack/read.rs index 0dcb2b724..6e96f22bb 100644 --- a/lofty/src/wavpack/read.rs +++ b/lofty/src/wavpack/read.rs @@ -4,9 +4,10 @@ use crate::config::ParseOptions; use crate::error::Result; use crate::id3::{ID3FindResults, find_id3v1, find_lyrics3v2}; -use crate::macros::err; use std::io::{Read, Seek, SeekFrom}; +use aud_io::err as io_err; + pub(super) fn read_from(reader: &mut R, parse_options: ParseOptions) -> Result where R: Read + Seek, @@ -23,7 +24,7 @@ where if id3v1_header.is_some() { id3v1_tag = id3v1; let Some(new_stream_length) = stream_length.checked_sub(128) else { - err!(SizeMismatch); + io_err!(SizeMismatch); }; stream_length = new_stream_length; @@ -32,7 +33,7 @@ where // Next, check for a Lyrics3v2 tag, and skip over it, as it's no use to us let ID3FindResults(_, lyrics3v2_size) = find_lyrics3v2(reader)?; let Some(new_stream_length) = stream_length.checked_sub(u64::from(lyrics3v2_size)) else { - err!(SizeMismatch); + io_err!(SizeMismatch); }; stream_length = new_stream_length; diff --git a/lofty/tests/io.rs b/lofty/tests/io.rs new file mode 100644 index 000000000..e38a7f462 --- /dev/null +++ b/lofty/tests/io.rs @@ -0,0 +1,122 @@ +#![allow(missing_docs)] + +use std::io::{Cursor, Read, Seek, Write}; + +use lofty::config::{ParseOptions, WriteOptions}; +use lofty::file::AudioFile; +use lofty::mpeg::MpegFile; +use lofty::tag::Accessor; + +const TEST_ASSET: &str = "tests/files/assets/minimal/full_test.mp3"; + +fn test_asset_contents() -> Vec { + std::fs::read(TEST_ASSET).unwrap() +} + +fn file() -> MpegFile { + let file_contents = test_asset_contents(); + let mut reader = Cursor::new(file_contents); + MpegFile::read_from(&mut reader, ParseOptions::new()).unwrap() +} + +fn alter_tag(file: &mut MpegFile) { + let tag = file.id3v2_mut().unwrap(); + tag.set_artist(String::from("Bar artist")); +} + +fn revert_tag(file: &mut MpegFile) { + let tag = file.id3v2_mut().unwrap(); + tag.set_artist(String::from("Foo artist")); +} + +#[test_log::test] +fn io_save_to_file() { + // Read the file and change the artist + let mut file = file(); + alter_tag(&mut file); + + let mut temp_file = tempfile::tempfile().unwrap(); + let file_content = std::fs::read(TEST_ASSET).unwrap(); + temp_file.write_all(&file_content).unwrap(); + temp_file.rewind().unwrap(); + + // Save the new artist + file.save_to(&mut temp_file, WriteOptions::new().preferred_padding(0)) + .expect("Failed to save to file"); + + // Read the file again and change the artist back + temp_file.rewind().unwrap(); + let mut file = MpegFile::read_from(&mut temp_file, ParseOptions::new()).unwrap(); + revert_tag(&mut file); + + temp_file.rewind().unwrap(); + file.save_to(&mut temp_file, WriteOptions::new().preferred_padding(0)) + .expect("Failed to save to file"); + + // The contents should be the same as the original file + temp_file.rewind().unwrap(); + let mut current_file_contents = Vec::new(); + temp_file.read_to_end(&mut current_file_contents).unwrap(); + + assert_eq!(current_file_contents, test_asset_contents()); +} + +#[test_log::test] +fn io_save_to_vec() { + // Same test as above, but using a Cursor> instead of a file + let mut file = file(); + alter_tag(&mut file); + + let file_content = std::fs::read(TEST_ASSET).unwrap(); + + let mut reader = Cursor::new(file_content); + file.save_to(&mut reader, WriteOptions::new().preferred_padding(0)) + .expect("Failed to save to vec"); + + reader.rewind().unwrap(); + let mut file = MpegFile::read_from(&mut reader, ParseOptions::new()).unwrap(); + revert_tag(&mut file); + + reader.rewind().unwrap(); + file.save_to(&mut reader, WriteOptions::new().preferred_padding(0)) + .expect("Failed to save to vec"); + + let current_file_contents = reader.into_inner(); + assert_eq!(current_file_contents, test_asset_contents()); +} + +#[test_log::test] +fn io_save_using_references() { + struct File { + buf: Vec, + } + + let mut f = File { + buf: std::fs::read(TEST_ASSET).unwrap(), + }; + + // Same test as above, but using references instead of owned values + let mut file = file(); + alter_tag(&mut file); + + { + let mut reader = Cursor::new(&mut f.buf); + file.save_to(&mut reader, WriteOptions::new().preferred_padding(0)) + .expect("Failed to save to vec"); + } + + { + let mut reader = Cursor::new(&f.buf[..]); + file = MpegFile::read_from(&mut reader, ParseOptions::new()).unwrap(); + revert_tag(&mut file); + } + + { + let mut reader = Cursor::new(&mut f.buf); + file.save_to(&mut reader, WriteOptions::new().preferred_padding(0)) + .expect("Failed to save to vec"); + } + + let current_file_contents = f.buf; + assert_eq!(current_file_contents, test_asset_contents()); +} diff --git a/lofty_attr/Cargo.toml b/lofty_attr/Cargo.toml index 5e94271f5..13a2094b7 100644 --- a/lofty_attr/Cargo.toml +++ b/lofty_attr/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "lofty_attr" version = "0.11.1" -authors = ["Serial <69764315+Serial-ATA@users.noreply.github.com>"] description = "Macros for Lofty" readme = "README.md" include = ["src", "Cargo.toml", "../LICENSE-*"] +authors.workspace = true edition.workspace = true rust-version.workspace = true repository.workspace = true diff --git a/ogg_pager/Cargo.toml b/ogg_pager/Cargo.toml index c1aba9a4f..0a8b880e4 100644 --- a/ogg_pager/Cargo.toml +++ b/ogg_pager/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "ogg_pager" version = "0.7.0" -authors = ["Serial <69764315+Serial-ATA@users.noreply.github.com>"] description = "A simple OGG page reader" keywords = ["ogg", "xiph"] categories = ["multimedia", "multimedia::audio", "parser-implementations"] include = ["src", "Cargo.toml", "../LICENSE-*"] +authors.workspace = true edition.workspace = true rust-version.workspace = true repository.workspace = true