diff --git a/CHANGELOG.md b/CHANGELOG.md index b597a7e5e..eeaaa1801 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - **ParsingMode**: A new variant, `BestAttempt` will attempt to fill holes in otherwise valid tag items ([PR](https://github.com/Serial-ATA/lofty-rs/pull/205)) +- **🎉 Support for Musepack files** ([issue](https://github.com/Serial-ATA/lofty-rs/issues/199)) ([PR](https://github.com/Serial-ATA/lofty-rs/pull/200)) ### Changed - **Probe**: The default `ParsingMode` is now `ParsingMode::BestAttempt` (It was previously `ParsingMode::Strict`) diff --git a/Cargo.toml b/Cargo.toml index 45de4a409..00238dcf5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ byteorder = "1.4.3" # ID3 compressed frames flate2 = { version = "1.0.26", optional = true } # Proc macros -lofty_attr = "0.7.0" +lofty_attr = { path = "lofty_attr" } # Debug logging log = "0.4.17" # OGG Vorbis/Opus diff --git a/SUPPORTED_FORMATS.md b/SUPPORTED_FORMATS.md index 59dafc77c..659e3592c 100644 --- a/SUPPORTED_FORMATS.md +++ b/SUPPORTED_FORMATS.md @@ -1,15 +1,16 @@ -| File Format | Metadata Format(s) | -|-------------|--------------------------------------| -| AAC (ADTS) | `ID3v2`, `ID3v1` | -| Ape | `APEv2`, `APEv1`, `ID3v2`\*, `ID3v1` | -| AIFF | `ID3v2`, `Text Chunks` | -| FLAC | `Vorbis Comments`, `ID3v2`\* | -| MP3 | `ID3v2`, `ID3v1`, `APEv2`, `APEv1` | -| MP4 | `iTunes-style ilst` | -| Opus | `Vorbis Comments` | -| Ogg Vorbis | `Vorbis Comments` | -| Speex | `Vorbis Comments` | -| WAV | `ID3v2`, `RIFF INFO` | -| WavPack | `APEv2`, `APEv1`, `ID3v1` | +| File Format | Metadata Format(s) | +|-------------|------------------------------| +| AAC (ADTS) | `ID3v2`, `ID3v1` | +| Ape | `APE`, `ID3v2`\*, `ID3v1` | +| AIFF | `ID3v2`, `Text Chunks` | +| FLAC | `Vorbis Comments`, `ID3v2`\* | +| MP3 | `ID3v2`, `ID3v1`, `APE` | +| MP4 | `iTunes-style ilst` | +| MPC | `APE`, `ID3v2`\*, `ID3v1`\* | +| Opus | `Vorbis Comments` | +| Ogg Vorbis | `Vorbis Comments` | +| Speex | `Vorbis Comments` | +| WAV | `ID3v2`, `RIFF INFO` | +| WavPack | `APE`, `ID3v1` | \* The tag will be **read only**, due to lack of official support diff --git a/benches/read_file.rs b/benches/read_file.rs index eb740d914..93b91465a 100644 --- a/benches/read_file.rs +++ b/benches/read_file.rs @@ -36,6 +36,7 @@ fn content_infer_read(c: &mut Criterion) { (FLAC, "../tests/files/assets/minimal/full_test.flac"), (MP4, "../tests/files/assets/minimal/m4a_codec_aac.m4a"), (MP3, "../tests/files/assets/minimal/full_test.mp3"), + (MPC, "../tests/files/assets/minimal/mpc_sv8.mpc"), (OPUS, "../tests/files/assets/minimal/full_test.opus"), (RIFF, "../tests/files/assets/minimal/wav_format_pcm.wav"), (SPEEX, "../tests/files/assets/minimal/full_test.spx"), diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index c82c493c1..df8fb2749 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -22,6 +22,10 @@ members = ["."] name = "filetype_from_buffer" path = "fuzz_targets/filetype_from_buffer.rs" +[[bin]] +name = "mpcfile_read_from" +path = "fuzz_targets/mpcfile_read_from.rs" + [[bin]] name = "mpegfile_read_from" path = "fuzz_targets/mpegfile_read_from.rs" diff --git a/fuzz/fuzz_targets/mpcfile_read_from.rs b/fuzz/fuzz_targets/mpcfile_read_from.rs new file mode 100644 index 000000000..47b883f16 --- /dev/null +++ b/fuzz/fuzz_targets/mpcfile_read_from.rs @@ -0,0 +1,10 @@ +#![no_main] + +use std::io::Cursor; + +use libfuzzer_sys::fuzz_target; +use lofty::{AudioFile, ParseOptions}; + +fuzz_target!(|data: Vec| { + let _ = lofty::musepack::MpcFile::read_from(&mut Cursor::new(data), ParseOptions::new()); +}); diff --git a/lofty_attr/src/internal.rs b/lofty_attr/src/internal.rs index ea33c1fb6..b4c3355cb 100644 --- a/lofty_attr/src/internal.rs +++ b/lofty_attr/src/internal.rs @@ -9,8 +9,9 @@ use quote::quote; pub(crate) fn opt_internal_file_type( struct_name: String, ) -> Option<(proc_macro2::TokenStream, bool)> { - const LOFTY_FILE_TYPES: [&str; 11] = [ - "Aac", "Aiff", "Ape", "Flac", "Mpeg", "Mp4", "Opus", "Vorbis", "Speex", "Wav", "WavPack", + const LOFTY_FILE_TYPES: [&str; 12] = [ + "Aac", "Aiff", "Ape", "Flac", "Mpeg", "Mp4", "Mpc", "Opus", "Vorbis", "Speex", "Wav", + "WavPack", ]; const ID3V2_STRIPPABLE: [&str; 2] = ["Flac", "Ape"]; diff --git a/src/ape/read.rs b/src/ape/read.rs index dd8cb25e9..2279abda1 100644 --- a/src/ape/read.rs +++ b/src/ape/read.rs @@ -1,8 +1,7 @@ -use super::constants::APE_PREAMBLE; use super::header::read_ape_header; -use super::tag::read::read_ape_tag; use super::tag::ApeTag; use super::{ApeFile, ApeProperties}; +use crate::ape::tag::read::{read_ape_tag, read_ape_tag_with_header}; use crate::error::Result; use crate::id3::v1::tag::Id3v1Tag; use crate::id3::v2::read::parse_id3v2; @@ -76,7 +75,7 @@ where let ape_header = read_ape_header(data, false)?; stream_len -= u64::from(ape_header.size); - let ape = read_ape_tag(data, ape_header)?; + let ape = read_ape_tag_with_header(data, ape_header)?; ape_tag = Some(ape); }, _ => { @@ -111,15 +110,9 @@ where // Strongly recommended to be at the end of the file data.seek(SeekFrom::Current(-32))?; - let mut ape_preamble = [0; 8]; - data.read_exact(&mut ape_preamble)?; - - if &ape_preamble == APE_PREAMBLE { - let ape_header = read_ape_header(data, true)?; - stream_len -= u64::from(ape_header.size); - - let ape = read_ape_tag(data, ape_header)?; - ape_tag = Some(ape); + if let Some((tag, header)) = read_ape_tag(data, true)? { + stream_len -= u64::from(header.size); + ape_tag = Some(tag); } let file_length = data.stream_position()?; diff --git a/src/ape/tag/mod.rs b/src/ape/tag/mod.rs index 42e3eafb3..9d9714876 100644 --- a/src/ape/tag/mod.rs +++ b/src/ape/tag/mod.rs @@ -67,7 +67,10 @@ macro_rules! impl_accessor { /// When converting pictures, any of type [`PictureType::Undefined`](crate::PictureType::Undefined) will be discarded. /// For items, see [`ApeItem::new`]. #[derive(Default, Debug, PartialEq, Eq, Clone)] -#[tag(description = "An `APE` tag", supported_formats(Ape, Mpeg, WavPack))] +#[tag( + description = "An `APE` tag", + supported_formats(Ape, Mpeg, Mpc, WavPack) +)] pub struct ApeTag { /// Whether or not to mark the tag as read only pub read_only: bool, @@ -485,11 +488,10 @@ pub(crate) fn tagitems_into_ape<'a>( #[cfg(test)] mod tests { - use crate::ape::header::read_ape_header; use crate::ape::{ApeItem, ApeTag}; use crate::{ItemValue, Tag, TagExt, TagType}; - use std::io::{Cursor, Seek, SeekFrom}; + use std::io::Cursor; #[test] fn parse_ape() { @@ -542,11 +544,9 @@ mod tests { let tag = crate::tag::utils::test_utils::read_path("tests/tags/assets/test.apev2"); let mut reader = Cursor::new(tag); - // Remove the APE preamble - reader.seek(SeekFrom::Current(8)).unwrap(); - - let header = read_ape_header(&mut reader, false).unwrap(); - let parsed_tag = crate::ape::tag::read::read_ape_tag(&mut reader, header).unwrap(); + let (parsed_tag, _) = crate::ape::tag::read::read_ape_tag(&mut reader, false) + .unwrap() + .unwrap(); assert_eq!(expected_tag.len(), parsed_tag.len()); @@ -560,23 +560,18 @@ mod tests { let tag_bytes = crate::tag::utils::test_utils::read_path("tests/tags/assets/test.apev2"); let mut reader = Cursor::new(tag_bytes); - // Remove the APE preamble - reader.seek(SeekFrom::Current(8)).unwrap(); - - let header = read_ape_header(&mut reader, false).unwrap(); - let parsed_tag = crate::ape::tag::read::read_ape_tag(&mut reader, header).unwrap(); + let (parsed_tag, _) = crate::ape::tag::read::read_ape_tag(&mut reader, false) + .unwrap() + .unwrap(); let mut writer = Vec::new(); parsed_tag.dump_to(&mut writer).unwrap(); let mut temp_reader = Cursor::new(writer); - // Remove the APE preamble - temp_reader.seek(SeekFrom::Current(8)).unwrap(); - - let temp_header = read_ape_header(&mut temp_reader, false).unwrap(); - let temp_parsed_tag = - crate::ape::tag::read::read_ape_tag(&mut temp_reader, temp_header).unwrap(); + let (temp_parsed_tag, _) = crate::ape::tag::read::read_ape_tag(&mut temp_reader, false) + .unwrap() + .unwrap(); assert_eq!(parsed_tag, temp_parsed_tag); } @@ -586,11 +581,9 @@ mod tests { let tag_bytes = crate::tag::utils::test_utils::read_path("tests/tags/assets/test.apev2"); let mut reader = Cursor::new(tag_bytes); - // Remove the APE preamble - reader.seek(SeekFrom::Current(8)).unwrap(); - - let header = read_ape_header(&mut reader, false).unwrap(); - let ape = crate::ape::tag::read::read_ape_tag(&mut reader, header).unwrap(); + let (ape, _) = crate::ape::tag::read::read_ape_tag(&mut reader, false) + .unwrap() + .unwrap(); let tag: Tag = ape.into(); diff --git a/src/ape/tag/read.rs b/src/ape/tag/read.rs index 67768d580..e4e492e25 100644 --- a/src/ape/tag/read.rs +++ b/src/ape/tag/read.rs @@ -1,7 +1,7 @@ use super::item::ApeItem; use super::ApeTag; -use crate::ape::constants::INVALID_KEYS; -use crate::ape::header::ApeHeader; +use crate::ape::constants::{APE_PREAMBLE, INVALID_KEYS}; +use crate::ape::header::{self, ApeHeader}; use crate::error::Result; use crate::macros::{decode_err, err, try_vec}; use crate::tag::item::ItemValue; @@ -10,7 +10,7 @@ use std::io::{Read, Seek, SeekFrom}; use byteorder::{LittleEndian, ReadBytesExt}; -pub(crate) fn read_ape_tag(data: &mut R, header: ApeHeader) -> Result +pub(crate) fn read_ape_tag_with_header(data: &mut R, header: ApeHeader) -> Result where R: Read + Seek, { @@ -81,3 +81,20 @@ where Ok(tag) } + +pub(crate) fn read_ape_tag( + reader: &mut R, + footer: bool, +) -> Result> { + let mut ape_preamble = [0; 8]; + reader.read_exact(&mut ape_preamble)?; + + if &ape_preamble == APE_PREAMBLE { + let ape_header = header::read_ape_header(reader, footer)?; + + let ape = read_ape_tag_with_header(reader, ape_header)?; + return Ok(Some((ape, ape_header))); + } + + Ok(None) +} diff --git a/src/ape/tag/write.rs b/src/ape/tag/write.rs index 8cf59758f..b2f7f87f6 100644 --- a/src/ape/tag/write.rs +++ b/src/ape/tag/write.rs @@ -1,8 +1,7 @@ use super::item::ApeItemRef; -use super::read::read_ape_tag; use super::ApeTagRef; use crate::ape::constants::APE_PREAMBLE; -use crate::ape::header::read_ape_header; +use crate::ape::tag::read; use crate::error::Result; use crate::id3::{find_id3v1, find_id3v2, find_lyrics3v2}; use crate::macros::{decode_err, err}; @@ -41,26 +40,21 @@ where // If one is found, it'll be removed and rewritten at the bottom, where it should be let mut header_ape_tag = (false, (0, 0)); - if &ape_preamble == APE_PREAMBLE { - let start = data.seek(SeekFrom::Current(-8))?; - - data.seek(SeekFrom::Current(8))?; - - let header = read_ape_header(data, false)?; - let size = header.size; - - let mut existing = read_ape_tag(data, header)?; - - // Only keep metadata around that's marked read only - existing.items.retain(|i| i.read_only); - - if !existing.items.is_empty() { - read_only = Some(existing) - } - - header_ape_tag = (true, (start, start + u64::from(size))) - } else { - data.seek(SeekFrom::Current(-8))?; + let start = data.stream_position()?; + match read::read_ape_tag(data, false)? { + Some((mut existing_tag, header)) => { + // Only keep metadata around that's marked read only + existing_tag.items.retain(|i| i.read_only); + + if !existing_tag.items.is_empty() { + read_only = Some(existing_tag) + } + + header_ape_tag = (true, (start, start + u64::from(header.size))) + }, + None => { + data.seek(SeekFrom::Current(-8))?; + }, } // Skip over ID3v1 and Lyrics3v2 tags @@ -73,23 +67,17 @@ where // Now search for an APE tag at the end data.seek(SeekFrom::Current(-32))?; - data.read_exact(&mut ape_preamble)?; - let mut ape_tag_location = None; // Also check this tag for any read only items - if &ape_preamble == APE_PREAMBLE { - let start = data.stream_position()? as usize + 24; - - let header = read_ape_header(data, true)?; + let start = data.stream_position()? as usize + 32; + if let Some((mut existing_tag, header)) = read::read_ape_tag(data, true)? { let size = header.size; - let mut existing = read_ape_tag(data, header)?; - - existing.items.retain(|i| i.read_only); + existing_tag.items.retain(|i| i.read_only); - if !existing.items.is_empty() { - read_only = Some(existing) + if !existing_tag.items.is_empty() { + read_only = Some(existing_tag) } // Since the "start" was really at the end of the tag, this sanity check seems necessary diff --git a/src/file.rs b/src/file.rs index 7ecfc8d56..233722853 100644 --- a/src/file.rs +++ b/src/file.rs @@ -720,6 +720,7 @@ pub enum FileType { Flac, Mpeg, Mp4, + Mpc, Opus, Vorbis, Speex, @@ -753,7 +754,7 @@ impl FileType { pub fn primary_tag_type(&self) -> TagType { match self { FileType::Aiff | FileType::Mpeg | FileType::Wav | FileType::Aac => TagType::Id3v2, - FileType::Ape | FileType::WavPack => TagType::Ape, + FileType::Ape | FileType::Mpc | FileType::WavPack => TagType::Ape, FileType::Flac | FileType::Opus | FileType::Vorbis | FileType::Speex => { TagType::VorbisComments }, @@ -784,29 +785,19 @@ impl FileType { /// assert!(file_type.supports_tag_type(TagType::Id3v2)); /// ``` pub fn supports_tag_type(&self, tag_type: TagType) -> bool { - match self { - FileType::Aiff | FileType::Ape | FileType::Mpeg | FileType::Wav | FileType::Aac - if tag_type == TagType::Id3v2 => - { - true - }, - FileType::Aiff if tag_type == TagType::AiffText => true, - FileType::Ape | FileType::Mpeg | FileType::WavPack | FileType::Aac - if tag_type == TagType::Id3v1 => - { - true - }, - FileType::Ape | FileType::Mpeg | FileType::WavPack if tag_type == TagType::Ape => true, - FileType::Opus | FileType::Flac | FileType::Vorbis | FileType::Speex => { - tag_type == TagType::VorbisComments - }, - FileType::Mp4 => tag_type == TagType::Mp4Ilst, - FileType::Wav => tag_type == TagType::RiffInfo, - FileType::Custom(c) => { - let resolver = crate::resolve::lookup_resolver(c); - resolver.supported_tag_types().contains(&tag_type) - }, - _ => false, + if let FileType::Custom(c) = self { + let resolver = crate::resolve::lookup_resolver(c); + return resolver.supported_tag_types().contains(&tag_type); + } + + match tag_type { + TagType::Ape => crate::ape::ApeTag::SUPPORTED_FORMATS.contains(self), + TagType::Id3v1 => crate::id3::v1::Id3v1Tag::SUPPORTED_FORMATS.contains(self), + TagType::Id3v2 => crate::id3::v2::Id3v2Tag::SUPPORTED_FORMATS.contains(self), + TagType::Mp4Ilst => crate::mp4::Ilst::SUPPORTED_FORMATS.contains(self), + TagType::VorbisComments => crate::ogg::VorbisComments::SUPPORTED_FORMATS.contains(self), + TagType::RiffInfo => crate::iff::wav::RIFFInfoList::SUPPORTED_FORMATS.contains(self), + TagType::AiffText => crate::iff::aiff::AIFFTextChunks::SUPPORTED_FORMATS.contains(self), } } @@ -837,6 +828,7 @@ impl FileType { "flac" => Some(Self::Flac), "ogg" => Some(Self::Vorbis), "mp4" | "m4a" | "m4b" | "m4p" | "m4r" | "m4v" | "3gp" => Some(Self::Mp4), + "mpc" | "mp+" | "mpp" => Some(Self::Mpc), "spx" => Some(Self::Speex), e => { if let Some((ty, _)) = CUSTOM_RESOLVERS @@ -1011,6 +1003,7 @@ impl FileType { }, 119 if buf.len() >= 4 && &buf[..4] == b"wvpk" => Some(Self::WavPack), _ if buf.len() >= 8 && &buf[4..8] == b"ftyp" => Some(Self::Mp4), + _ if buf.starts_with(b"MPCK") || buf.starts_with(b"MP+") => Some(Self::Mpc), _ => None, } } diff --git a/src/id3/v1/tag.rs b/src/id3/v1/tag.rs index 81bbf051b..f2cc9179f 100644 --- a/src/id3/v1/tag.rs +++ b/src/id3/v1/tag.rs @@ -54,7 +54,7 @@ macro_rules! impl_accessor { #[derive(Default, Debug, PartialEq, Eq, Clone)] #[tag( description = "An ID3v1 tag", - supported_formats(Aac, Ape, Mpeg, WavPack) + supported_formats(Aac, Ape, Mpeg, WavPack, read_only(Mpc)) )] pub struct Id3v1Tag { /// Track title, 30 bytes max diff --git a/src/id3/v2/tag.rs b/src/id3/v2/tag.rs index 9f4afb44e..a848c49bb 100644 --- a/src/id3/v2/tag.rs +++ b/src/id3/v2/tag.rs @@ -96,7 +96,7 @@ macro_rules! impl_accessor { #[derive(PartialEq, Eq, Debug, Clone)] #[tag( description = "An `ID3v2` tag", - supported_formats(Aac, Aiff, Mpeg, Wav, read_only(Flac, Ape)) + supported_formats(Aac, Aiff, Mpeg, Wav, read_only(Ape, Flac, Mpc)) )] pub struct Id3v2Tag { flags: Id3v2TagFlags, diff --git a/src/id3/v2/util/synchsafe.rs b/src/id3/v2/util/synchsafe.rs index 483cbad20..fedfe7489 100644 --- a/src/id3/v2/util/synchsafe.rs +++ b/src/id3/v2/util/synchsafe.rs @@ -2,7 +2,8 @@ //! //! See [`FrameFlags::unsynchronisation`](crate::id3::v2::FrameFlags::unsynchronisation) for an explanation. -use crate::error::{Id3v2Error, Id3v2ErrorKind, Result}; +use crate::error::Result; + use std::io::Read; /// A reader for unsynchronized content diff --git a/src/lib.rs b/src/lib.rs index 4822f3da6..9ed168aad 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -135,6 +135,7 @@ clippy::uninlined_format_args, /* This should be changed for any normal "{}", but I'm not a fan of it for any debug or width specific formatting */ clippy::manual_let_else, clippy::let_underscore_untyped, + clippy::field_reassign_with_default, )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] @@ -152,6 +153,7 @@ pub mod iff; pub(crate) mod macros; pub mod mp4; pub mod mpeg; +pub mod musepack; pub mod ogg; pub(crate) mod picture; mod probe; diff --git a/src/mpeg/read.rs b/src/mpeg/read.rs index 24b58c321..608d287d0 100644 --- a/src/mpeg/read.rs +++ b/src/mpeg/read.rs @@ -1,8 +1,6 @@ use super::header::{cmp_header, search_for_frame_sync, Header, HeaderCmpResult, XingHeader}; use super::{MpegFile, MpegProperties}; -use crate::ape::constants::APE_PREAMBLE; use crate::ape::header::read_ape_header; -use crate::ape::tag::read::read_ape_tag; use crate::error::Result; use crate::id3::v2::read::parse_id3v2; use crate::id3::v2::read_id3v2_header; @@ -66,7 +64,9 @@ where if &header_remaining == b"AGEX" { let ape_header = read_ape_header(reader, false)?; - file.ape_tag = Some(crate::ape::tag::read::read_ape_tag(reader, ape_header)?); + file.ape_tag = Some(crate::ape::tag::read::read_ape_tag_with_header( + reader, ape_header, + )?); continue; } @@ -101,23 +101,16 @@ where reader.seek(SeekFrom::Current(-32))?; - let mut ape_preamble = [0; 8]; - reader.read_exact(&mut ape_preamble)?; - - match &ape_preamble { - APE_PREAMBLE => { - let ape_header = read_ape_header(reader, true)?; - let size = ape_header.size; - - let ape = read_ape_tag(reader, ape_header)?; - file.ape_tag = Some(ape); + match crate::ape::tag::read::read_ape_tag(reader, true)? { + Some((tag, header)) => { + file.ape_tag = Some(tag); // Seek back to the start of the tag let pos = reader.stream_position()?; - reader.seek(SeekFrom::Start(pos - u64::from(size)))?; + reader.seek(SeekFrom::Start(pos - u64::from(header.size)))?; }, - // Correct the position (APE header - Preamble) - _ => { + None => { + // Correct the position (APE header - Preamble) reader.seek(SeekFrom::Current(24))?; }, } diff --git a/src/musepack/constants.rs b/src/musepack/constants.rs new file mode 100644 index 000000000..96feb14ba --- /dev/null +++ b/src/musepack/constants.rs @@ -0,0 +1,17 @@ +//! MusePack constants + +// There are only 4 frequencies defined in the spec, but there are 8 possible indices in the header. +// +// The reference decoder defines the table as: +// +// 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]; + +// 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: u32 = 481; +pub(super) const MPC_FRAME_LENGTH: u32 = 36 * 32; // Samples per mpc frame diff --git a/src/musepack/mod.rs b/src/musepack/mod.rs new file mode 100644 index 000000000..010f7d0fd --- /dev/null +++ b/src/musepack/mod.rs @@ -0,0 +1,81 @@ +//! Musepack specific items +pub mod constants; +mod read; +pub mod sv4to6; +pub mod sv7; +pub mod sv8; + +use crate::ape::tag::ApeTag; +use crate::id3::v1::tag::Id3v1Tag; +use crate::id3::v2::tag::Id3v2Tag; +use crate::properties::FileProperties; + +use lofty_attr::LoftyFile; + +/// Audio properties of an MPC file +/// +/// The information available differs between stream versions +#[derive(Debug, Clone, PartialEq)] +pub enum MpcProperties { + /// MPC stream version 8 properties + Sv8(sv8::MpcSv8Properties), + /// MPC stream version 7 properties + Sv7(sv7::MpcSv7Properties), + /// MPC stream version 4-6 properties + Sv4to6(sv4to6::MpcSv4to6Properties), +} + +impl Default for MpcProperties { + fn default() -> Self { + Self::Sv8(sv8::MpcSv8Properties::default()) + } +} + +impl From for FileProperties { + fn from(input: MpcProperties) -> Self { + match input { + MpcProperties::Sv8(sv8prop) => sv8prop.into(), + MpcProperties::Sv7(sv7prop) => sv7prop.into(), + MpcProperties::Sv4to6(sv4to6prop) => sv4to6prop.into(), + } + } +} + +/// The version of the MPC stream +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum MpcStreamVersion { + /// Stream version 8 + #[default] + Sv8, + /// Stream version 7 + Sv7, + /// Stream version 4 to 6 + Sv4to6, +} + +/// An MPC file +#[derive(LoftyFile, Default)] +#[lofty(read_fn = "read::read_from")] +#[lofty(internal_write_module_do_not_use_anywhere_else)] +pub struct MpcFile { + /// The stream version + pub(crate) stream_version: MpcStreamVersion, + /// An ID3v2 tag (Not officially supported) + #[lofty(tag_type = "Id3v2")] + pub(crate) id3v2_tag: Option, + /// An ID3v1 tag + #[lofty(tag_type = "Id3v1")] + pub(crate) id3v1_tag: Option, + /// An APEv1/v2 tag + #[lofty(tag_type = "Ape")] + pub(crate) ape_tag: Option, + /// The file's audio properties + pub(crate) properties: MpcProperties, +} + +impl MpcFile { + /// The version of the MPC stream + pub fn stream_version(&self) -> MpcStreamVersion { + self.stream_version + } +} diff --git a/src/musepack/read.rs b/src/musepack/read.rs new file mode 100644 index 000000000..6dccf4fea --- /dev/null +++ b/src/musepack/read.rs @@ -0,0 +1,107 @@ +use super::sv4to6::MpcSv4to6Properties; +use super::sv7::MpcSv7Properties; +use super::sv8::MpcSv8Properties; +use super::{MpcFile, MpcProperties, MpcStreamVersion}; +use crate::error::Result; +use crate::id3::v2::read::parse_id3v2; +use crate::id3::{find_id3v1, find_id3v2, find_lyrics3v2, ID3FindResults}; +use crate::probe::ParseOptions; +use crate::traits::SeekStreamLen; + +use std::io::{Read, Seek, SeekFrom}; + +pub(super) fn read_from(reader: &mut R, parse_options: ParseOptions) -> Result +where + R: Read + Seek, +{ + // Sv4to6 is the default, as it doesn't have a marker like Sv8's b'MPCK' or Sv7's b'MP+' + let mut version = MpcStreamVersion::Sv4to6; + let mut file = MpcFile::default(); + + #[allow(unstable_name_collisions)] + let mut stream_length = reader.stream_len()?; + + // ID3v2 tags are unsupported in MPC files, but still possible + #[allow(unused_variables)] + if let ID3FindResults(Some(header), Some(content)) = find_id3v2(reader, true)? { + let reader = &mut &*content; + + let id3v2 = parse_id3v2(reader, header, parse_options.parsing_mode)?; + file.id3v2_tag = Some(id3v2); + + let mut size = header.size; + if header.flags.footer { + size += 10; + } + + stream_length -= u64::from(size); + } + + // Save the current position, so we can go back and read the properties after the tags + let pos_past_id3v2 = reader.stream_position()?; + + #[allow(unused_variables)] + let ID3FindResults(header, id3v1) = find_id3v1(reader, true)?; + + if header.is_some() { + file.id3v1_tag = id3v1; + stream_length -= 128; + } + + let ID3FindResults(_, lyrics3v2_size) = find_lyrics3v2(reader)?; + stream_length -= u64::from(lyrics3v2_size); + + reader.seek(SeekFrom::Current(-32))?; + + if let Some((tag, header)) = crate::ape::tag::read::read_ape_tag(reader, true)? { + file.ape_tag = Some(tag); + + // Seek back to the start of the tag + let pos = reader.stream_position()?; + reader.seek(SeekFrom::Start(pos - u64::from(header.size)))?; + + stream_length -= u64::from(header.size); + } + + // Restore the position of the magic signature + reader.seek(SeekFrom::Start(pos_past_id3v2))?; + + let mut header = [0; 4]; + reader.read_exact(&mut header)?; + + match &header { + b"MPCK" => { + version = MpcStreamVersion::Sv8; + }, + [b'M', b'P', b'+', ..] => { + // Seek back the extra byte we read + reader.seek(SeekFrom::Current(-1))?; + version = MpcStreamVersion::Sv7; + }, + _ => { + // We should be reading into the actual content now, seek back + reader.seek(SeekFrom::Current(-4))?; + }, + } + + if parse_options.read_properties { + match version { + MpcStreamVersion::Sv8 => { + file.properties = + MpcProperties::Sv8(MpcSv8Properties::read(reader, parse_options.parsing_mode)?) + }, + MpcStreamVersion::Sv7 => { + file.properties = MpcProperties::Sv7(MpcSv7Properties::read(reader, stream_length)?) + }, + MpcStreamVersion::Sv4to6 => { + file.properties = MpcProperties::Sv4to6(MpcSv4to6Properties::read( + reader, + parse_options.parsing_mode, + stream_length, + )?) + }, + } + } + + Ok(file) +} diff --git a/src/musepack/sv4to6/mod.rs b/src/musepack/sv4to6/mod.rs new file mode 100644 index 000000000..75aebb395 --- /dev/null +++ b/src/musepack/sv4to6/mod.rs @@ -0,0 +1,7 @@ +//! Musepack stream versions 4-6 + +mod properties; + +// Exports + +pub use properties::*; diff --git a/src/musepack/sv4to6/properties.rs b/src/musepack/sv4to6/properties.rs new file mode 100644 index 000000000..18f60f66c --- /dev/null +++ b/src/musepack/sv4to6/properties.rs @@ -0,0 +1,152 @@ +use crate::error::Result; +use crate::macros::{decode_err, parse_mode_choice}; +use crate::musepack::constants::{MPC_DECODER_SYNTH_DELAY, MPC_FRAME_LENGTH}; +use crate::probe::ParsingMode; +use crate::properties::FileProperties; + +use std::io::Read; +use std::time::Duration; + +use byteorder::{LittleEndian, ReadBytesExt}; + +/// MPC stream versions 4-6 audio properties +#[derive(Debug, Clone, PartialEq, Default)] +pub struct MpcSv4to6Properties { + pub(crate) duration: Duration, + pub(crate) channels: u8, // NOTE: always 2 + pub(crate) sample_rate: u32, // NOTE: always 44100 + + // Fields actually contained in the header + pub(crate) audio_bitrate: u32, + pub(crate) mid_side_stereo: bool, + pub(crate) stream_version: u16, + pub(crate) max_band: u8, + pub(crate) frame_count: u32, +} + +impl From for FileProperties { + fn from(input: MpcSv4to6Properties) -> Self { + Self { + duration: input.duration, + overall_bitrate: Some(input.audio_bitrate), + audio_bitrate: Some(input.audio_bitrate), + sample_rate: Some(input.sample_rate), + bit_depth: None, + channels: Some(input.channels), + channel_mask: None, + } + } +} + +impl MpcSv4to6Properties { + /// Duration of the audio + pub fn duration(&self) -> Duration { + self.duration + } + + /// Channel count + pub fn channels(&self) -> u8 { + self.channels + } + + /// Sample rate (Hz) + pub fn sample_rate(&self) -> u32 { + self.sample_rate + } + + /// Audio bitrate (kbps) + pub fn audio_bitrate(&self) -> u32 { + self.audio_bitrate + } + + /// Whether MidSideStereo is used + pub fn mid_side_stereo(&self) -> bool { + self.mid_side_stereo + } + + /// The MPC stream version (4-6) + pub fn stream_version(&self) -> u16 { + self.stream_version + } + + /// Last subband used in the whole file + pub fn max_band(&self) -> u8 { + self.max_band + } + + /// Total number of audio frames + pub fn frame_count(&self) -> u32 { + self.frame_count + } + + pub(crate) fn read( + reader: &mut R, + parse_mode: ParsingMode, + stream_length: u64, + ) -> Result + where + R: Read, + { + let mut header_data = [0u32; 8]; + reader.read_u32_into::(&mut header_data)?; + + let mut properties = Self::default(); + + properties.audio_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 + } + + parse_mode_choice!( + parse_mode, + STRICT: { + if properties.audio_bitrate != 0 { + decode_err!(@BAIL Mpc, "Encountered CBR stream") + } + + if intensity_stereo { + decode_err!(@BAIL Mpc, "Stream uses intensity stereo coding") + } + + if block_size != 1 { + decode_err!(@BAIL Mpc, "Stream has an invalid block size (must be 1)") + } + }, + ); + + if properties.stream_version < 6 { + // Versions before 6 had an invalid last frame + properties.frame_count = properties.frame_count.saturating_sub(1); + } + + properties.sample_rate = 44100; + properties.channels = 2; + + if properties.frame_count > 0 { + let samples = + (properties.frame_count * MPC_FRAME_LENGTH).saturating_sub(MPC_DECODER_SYNTH_DELAY); + let length = f64::from(samples) / f64::from(properties.sample_rate); + properties.duration = Duration::from_millis(length.ceil() as u64); + + let pcm_frames = 1152 * u64::from(properties.frame_count) - 576; + properties.audio_bitrate = ((stream_length as f64 + * 8.0 * f64::from(properties.sample_rate)) + / pcm_frames as f64) as u32; + } + + Ok(properties) + } +} diff --git a/src/musepack/sv7/mod.rs b/src/musepack/sv7/mod.rs new file mode 100644 index 000000000..8c63d73f6 --- /dev/null +++ b/src/musepack/sv7/mod.rs @@ -0,0 +1,7 @@ +//! Musepack stream version 7 + +mod properties; + +// Exports + +pub use properties::*; diff --git a/src/musepack/sv7/properties.rs b/src/musepack/sv7/properties.rs new file mode 100644 index 000000000..c2f3a8c50 --- /dev/null +++ b/src/musepack/sv7/properties.rs @@ -0,0 +1,377 @@ +use crate::error::Result; +use crate::macros::decode_err; +use crate::musepack::constants::{FREQUENCY_TABLE, MPC_OLD_GAIN_REF}; +use crate::properties::FileProperties; + +use std::io::Read; +use std::time::Duration; + +use byteorder::{BigEndian, 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, + } + } +} + +// http://trac.musepack.net/musepack/wiki/SV7Specification + +/// MPC stream version 7 audio properties +#[derive(Debug, Clone, PartialEq, Default)] +#[allow(clippy::struct_excessive_bools)] +pub struct MpcSv7Properties { + pub(crate) duration: Duration, + pub(crate) overall_bitrate: u32, + pub(crate) audio_bitrate: u32, + pub(crate) channels: u8, // NOTE: always 2 + // -- Section 1 -- + pub(crate) frame_count: u32, + // -- Section 2 -- + pub(crate) intensity_stereo: bool, + pub(crate) mid_side_stereo: bool, + pub(crate) max_band: u8, + pub(crate) profile: Profile, + pub(crate) link: Link, + pub(crate) sample_freq: u32, + pub(crate) max_level: u16, + // -- Section 3 -- + pub(crate) title_gain: i16, + pub(crate) title_peak: u16, + // -- Section 4 -- + pub(crate) album_gain: i16, + pub(crate) album_peak: u16, + // -- Section 5 -- + pub(crate) true_gapless: bool, + pub(crate) last_frame_length: u16, + pub(crate) fast_seeking_safe: bool, + // -- Section 6 -- + pub(crate) encoder_version: u8, +} + +impl From for FileProperties { + fn from(input: MpcSv7Properties) -> Self { + Self { + duration: input.duration, + overall_bitrate: Some(input.overall_bitrate), + audio_bitrate: Some(input.audio_bitrate), + sample_rate: Some(input.sample_freq), + bit_depth: None, + channels: Some(input.channels), + channel_mask: None, + } + } +} + +impl MpcSv7Properties { + /// Duration of the audio + pub fn duration(&self) -> Duration { + self.duration + } + + /// Overall bitrate (kbps) + pub fn overall_bitrate(&self) -> u32 { + self.overall_bitrate + } + + /// Audio bitrate (kbps) + pub fn audio_bitrate(&self) -> u32 { + self.audio_bitrate + } + + /// Sample rate (Hz) + pub fn sample_rate(&self) -> u32 { + self.sample_freq + } + + /// Channel count + pub fn channels(&self) -> u8 { + self.channels + } + + /// Total number of audio frames + pub fn frame_count(&self) -> u32 { + self.frame_count + } + + /// Whether intensity stereo coding (IS) is used + pub fn intensity_stereo(&self) -> bool { + self.intensity_stereo + } + + /// Whether MidSideStereo is used + pub fn mid_side_stereo(&self) -> bool { + self.mid_side_stereo + } + + /// Last subband used in the whole file + pub fn max_band(&self) -> u8 { + self.max_band + } + + /// Profile used + pub fn profile(&self) -> Profile { + self.profile + } + + /// Volume description of the start and end + pub fn link(&self) -> Link { + self.link + } + + /// Maximum level of the coded PCM input signal + pub fn max_level(&self) -> u16 { + self.max_level + } + + /// Change in the replay level + /// + /// The value is a signed 16 bit integer, with the level being attenuated by that many mB + pub fn title_gain(&self) -> i16 { + self.title_gain + } + + /// Maximum level of the decoded title + /// + /// * 16422: -6 dB + /// * 32767: 0 dB + /// * 65379: +6 dB + pub fn title_peak(&self) -> u16 { + self.title_peak + } + + /// Change in the replay level if the whole CD is supposed to be played with the same level change + /// + /// The value is a signed 16 bit integer, with the level being attenuated by that many mB + pub fn album_gain(&self) -> i16 { + self.album_gain + } + + /// Maximum level of the whole decoded CD + /// + /// * 16422: -6 dB + /// * 32767: 0 dB + /// * 65379: +6 dB + pub fn album_peak(&self) -> u16 { + self.album_peak + } + + /// Whether true gapless is used + pub fn true_gapless(&self) -> bool { + self.true_gapless + } + + /// Used samples of the last frame + /// + /// * TrueGapless = 0: always 0 + /// * TrueGapless = 1: 1...1152 + pub fn last_frame_length(&self) -> u16 { + self.last_frame_length + } + + /// Whether fast seeking can be used safely + pub fn fast_seeking_safe(&self) -> bool { + self.fast_seeking_safe + } + + /// Encoder version + /// + /// * Encoder version * 100 (106 = 1.06) + /// * EncoderVersion % 10 == 0 Release (1.0) + /// * EncoderVersion % 2 == 0 Beta (1.06) + /// * EncoderVersion % 2 == 1 Alpha (1.05a...z) + pub fn encoder_version(&self) -> u8 { + self.encoder_version + } + + pub(crate) fn read(reader: &mut R, stream_length: u64) -> Result + 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() + }; + + // -- 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; + + let profile_index = (byte2 & 0xF0) >> 4; + properties.profile = Profile::from_u8(profile_index).unwrap(); // Infallible + + let link_index = (byte2 & 0x0C) >> 2; + properties.link = Link::from_u8(link_index).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_gain = reader.read_i16::()?; + let title_peak = reader.read_u16::()?; + + // -- Section 4 -- + let album_gain = reader.read_i16::()?; + let album_peak = reader.read_u16::()?; + + // -- Section 5 -- + let chunk = reader.read_u32::()?; + + let byte1 = ((chunk & 0xFF00_0000) >> 24) as u8; + + properties.true_gapless = ((byte1 & 0x80) >> 7) == 1; + + let byte2 = ((chunk & 0xFF_0000) >> 16) as u8; + + if properties.true_gapless { + properties.last_frame_length = + (u16::from(byte1 & 0x7F) << 4) | u16::from((byte2 & 0xF0) >> 4); + } + + // 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: i16| -> i16 { + let mut gain = (MPC_OLD_GAIN_REF - f32::from(gain) / 100.0) * 256.0 + 0.5; + if gain >= ((1 << 16) as f32) || gain < 0.0 { + gain = 0.0 + } + gain as i16 + }; + let set_replay_peak = |peak: u16| -> u16 { + if peak == 0 { + return 0; + } + + ((peak.ilog10() * 20 * 256) as f32 + 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); + + let total_samples; + if properties.true_gapless { + total_samples = + (properties.frame_count * 1152) - u32::from(properties.last_frame_length); + } else { + total_samples = (properties.frame_count * 1152) - 576; + } + + if total_samples > 0 && properties.sample_freq > 0 { + let length = + (f64::from(total_samples) * 1000.0 / f64::from(properties.sample_freq)).ceil(); + properties.duration = Duration::from_millis(length as u64); + properties.audio_bitrate = (stream_length * 8 / length as u64) as u32; + properties.overall_bitrate = properties.audio_bitrate; + } + + Ok(properties) + } +} diff --git a/src/musepack/sv8/mod.rs b/src/musepack/sv8/mod.rs new file mode 100644 index 000000000..673832a49 --- /dev/null +++ b/src/musepack/sv8/mod.rs @@ -0,0 +1,8 @@ +//! Musepack stream version 8 + +mod properties; +mod read; + +// Exports + +pub use properties::*; diff --git a/src/musepack/sv8/properties.rs b/src/musepack/sv8/properties.rs new file mode 100644 index 000000000..bc26fcb59 --- /dev/null +++ b/src/musepack/sv8/properties.rs @@ -0,0 +1,250 @@ +use super::read::PacketReader; +use crate::error::Result; +use crate::musepack::constants::FREQUENCY_TABLE; +use crate::probe::ParsingMode; +use crate::properties::FileProperties; + +use std::io::Read; +use std::time::Duration; + +use byteorder::{BigEndian, ReadBytesExt}; + +/// MPC stream version 8 audio properties +#[derive(Debug, Clone, PartialEq, Default)] +pub struct MpcSv8Properties { + pub(crate) duration: Duration, + pub(crate) overall_bitrate: u32, + pub(crate) audio_bitrate: u32, + /// Mandatory Stream Header packet + pub stream_header: StreamHeader, + /// Mandatory ReplayGain packet + pub replay_gain: ReplayGain, + /// Optional encoder information + pub encoder_info: Option, +} + +impl From for FileProperties { + fn from(input: MpcSv8Properties) -> Self { + Self { + duration: input.duration, + overall_bitrate: Some(input.overall_bitrate), + audio_bitrate: Some(input.audio_bitrate), + sample_rate: Some(input.stream_header.sample_rate), + bit_depth: None, + channels: Some(input.stream_header.channels), + channel_mask: None, + } + } +} + +impl MpcSv8Properties { + /// Duration of the audio + pub fn duration(&self) -> Duration { + self.duration + } + + /// Overall bitrate (kbps) + pub fn overall_bitrate(&self) -> u32 { + self.overall_bitrate + } + + /// Audio bitrate (kbps) + pub fn audio_bitrate(&self) -> u32 { + self.audio_bitrate + } + + /// Sample rate (Hz) + pub fn sample_rate(&self) -> u32 { + self.stream_header.sample_rate + } + + /// Channel count + pub fn channels(&self) -> u8 { + self.stream_header.channels + } + + /// MusePack stream version + pub fn version(&self) -> u8 { + self.stream_header.stream_version + } + + pub(crate) fn read(reader: &mut R, parse_mode: ParsingMode) -> Result { + super::read::read_from(reader, parse_mode) + } +} + +/// 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, + }) + } +} diff --git a/src/musepack/sv8/read.rs b/src/musepack/sv8/read.rs new file mode 100644 index 000000000..913b89580 --- /dev/null +++ b/src/musepack/sv8/read.rs @@ -0,0 +1,193 @@ +use super::properties::{EncoderInfo, MpcSv8Properties, ReplayGain, StreamHeader}; +use crate::error::{ErrorKind, LoftyError, Result}; +use crate::macros::{decode_err, parse_mode_choice}; +use crate::probe::ParsingMode; + +use std::io::Read; +use std::time::Duration; + +use byteorder::ReadBytesExt; + +// 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"; +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 + R: Read, +{ + let mut packet_reader = PacketReader::new(data); + + let mut stream_header = None; + let mut replay_gain = None; + let mut encoder_info = None; + + let mut stream_length = 0; + let mut found_stream_end = false; + + while let Ok((packet_id, packet_length)) = packet_reader.next() { + 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)?), + AUDIO_PACKET_KEY => { + stream_length += packet_length; + }, + STREAM_END_KEY => { + found_stream_end = true; + break; + }, + _ => {}, + } + } + + // Check mandatory packets + + let stream_header = match stream_header { + Some(stream_header) => stream_header, + None => { + parse_mode_choice!( + parse_mode, + STRICT: decode_err!(@BAIL Mpc, "File is missing a Stream Header packet"), + DEFAULT: StreamHeader::default() + ) + }, + }; + + let replay_gain = match replay_gain { + Some(replay_gain) => replay_gain, + None => { + parse_mode_choice!( + parse_mode, + STRICT: decode_err!(@BAIL Mpc, "File is missing a ReplayGain packet"), + DEFAULT: ReplayGain::default() + ) + }, + }; + + if stream_length == 0 { + parse_mode_choice!( + parse_mode, + STRICT: decode_err!(@BAIL Mpc, "File is missing an Audio packet"), + ) + } + + if !found_stream_end { + parse_mode_choice!( + parse_mode, + STRICT: decode_err!(@BAIL Mpc, "File is missing a Stream End packet"), + ) + } + + let mut properties = MpcSv8Properties { + duration: Duration::ZERO, + overall_bitrate: 0, + audio_bitrate: 0, + stream_header, + replay_gain, + encoder_info, + }; + + let sample_count = stream_header.sample_count; + let beginning_silence = stream_header.beginning_silence; + let sample_rate = stream_header.sample_rate; + + if sample_count > 0 && beginning_silence <= sample_count && sample_rate > 0 { + let total_samples = sample_count - beginning_silence; + let length = (total_samples as f64 * 1000.0) / f64::from(sample_rate); + + properties.duration = Duration::from_millis(length as u64); + properties.audio_bitrate = ((stream_length * 8) / length as u64) as u32; + properties.overall_bitrate = properties.audio_bitrate; + } + + 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/src/probe.rs b/src/probe.rs index 0bc3563d0..f84403810 100644 --- a/src/probe.rs +++ b/src/probe.rs @@ -9,6 +9,7 @@ use crate::macros::err; use crate::mp4::Mp4File; use crate::mpeg::header::search_for_frame_sync; use crate::mpeg::MpegFile; +use crate::musepack::MpcFile; use crate::ogg::opus::OpusFile; use crate::ogg::speex::SpeexFile; use crate::ogg::vorbis::VorbisFile; @@ -475,6 +476,7 @@ impl Probe { let file_type_after_id3_block = match &ident { [b'M', b'A', b'C', ..] => Ok(Some(FileType::Ape)), b"fLaC" => Ok(Some(FileType::Flac)), + b"MPCK" | [b'M', b'P', b'+', ..] => Ok(Some(FileType::Mpc)), // Search for a frame sync, which may be preceded by junk _ if search_for_frame_sync(&mut self.inner)?.is_some() => { // Seek back to the start of the frame sync to check if we are dealing with @@ -558,6 +560,7 @@ impl Probe { FileType::Vorbis => VorbisFile::read_from(reader, options)?.into(), FileType::Wav => WavFile::read_from(reader, options)?.into(), FileType::Mp4 => Mp4File::read_from(reader, options)?.into(), + FileType::Mpc => MpcFile::read_from(reader, options)?.into(), FileType::Speex => SpeexFile::read_from(reader, options)?.into(), FileType::WavPack => WavPackFile::read_from(reader, options)?.into(), FileType::Custom(c) => { diff --git a/src/properties.rs b/src/properties.rs index 3f783549a..5bf7784ef 100644 --- a/src/properties.rs +++ b/src/properties.rs @@ -127,6 +127,10 @@ mod tests { use crate::iff::wav::{WavFile, WavFormat, WavProperties}; use crate::mp4::{AudioObjectType, Mp4Codec, Mp4File, Mp4Properties}; use crate::mpeg::{ChannelMode, Emphasis, Layer, MpegFile, MpegProperties, MpegVersion}; + use crate::musepack::sv4to6::MpcSv4to6Properties; + use crate::musepack::sv7::{Link, MpcSv7Properties, Profile}; + use crate::musepack::sv8::{EncoderInfo, MpcSv8Properties, ReplayGain, StreamHeader}; + use crate::musepack::{MpcFile, MpcProperties}; use crate::ogg::{ OpusFile, OpusProperties, SpeexFile, SpeexProperties, VorbisFile, VorbisProperties, }; @@ -272,6 +276,71 @@ mod tests { channels: 2, }; + const MPC_SV5_PROPERTIES: MpcSv4to6Properties = MpcSv4to6Properties { + duration: Duration::from_millis(27), + audio_bitrate: 41, + channels: 2, + frame_count: 1009, + mid_side_stereo: true, + stream_version: 5, + max_band: 31, + sample_rate: 44100, + }; + + const MPC_SV7_PROPERTIES: MpcSv7Properties = MpcSv7Properties { + duration: Duration::from_millis(1428), + overall_bitrate: 86, + audio_bitrate: 86, + channels: 2, + frame_count: 60, + intensity_stereo: false, + mid_side_stereo: true, + max_band: 26, + profile: Profile::Standard, + link: Link::VeryLowStartOrEnd, + sample_freq: 48000, + max_level: 0, + title_gain: 16594, + title_peak: 0, + album_gain: 16594, + album_peak: 0, + true_gapless: true, + last_frame_length: 578, + fast_seeking_safe: false, + encoder_version: 192, + }; + + const MPC_SV8_PROPERTIES: MpcSv8Properties = MpcSv8Properties { + duration: Duration::from_millis(1428), + overall_bitrate: 82, + audio_bitrate: 82, + stream_header: StreamHeader { + crc: 4_252_559_415, + stream_version: 8, + sample_count: 68546, + beginning_silence: 0, + sample_rate: 48000, + max_used_bands: 26, + channels: 2, + ms_used: true, + audio_block_frames: 64, + }, + replay_gain: ReplayGain { + version: 1, + title_gain: 16655, + title_peak: 21475, + album_gain: 16655, + album_peak: 21475, + }, + encoder_info: Some(EncoderInfo { + profile: 10.0, + pns_tool: false, + major: 1, + minor: 30, + build: 1, + }), + }; + const OPUS_PROPERTIES: OpusProperties = OpusProperties { duration: Duration::from_millis(1428), overall_bitrate: 120, @@ -427,6 +496,30 @@ mod tests { ) } + #[test] + fn mpc_sv5_properties() { + assert_eq!( + get_properties::("tests/files/assets/minimal/mpc_sv5.mpc"), + MpcProperties::Sv4to6(MPC_SV5_PROPERTIES) + ) + } + + #[test] + fn mpc_sv7_properties() { + assert_eq!( + get_properties::("tests/files/assets/minimal/mpc_sv7.mpc"), + MpcProperties::Sv7(MPC_SV7_PROPERTIES) + ) + } + + #[test] + fn mpc_sv8_properties() { + assert_eq!( + get_properties::("tests/files/assets/minimal/mpc_sv8.mpc"), + MpcProperties::Sv8(MPC_SV8_PROPERTIES) + ) + } + #[test] fn opus_properties() { assert_eq!( diff --git a/src/tag/mod.rs b/src/tag/mod.rs index ee5019947..12179fc08 100644 --- a/src/tag/mod.rs +++ b/src/tag/mod.rs @@ -651,8 +651,12 @@ impl TagType { None => err!(UnknownFormat), }; - let special_exceptions = - (file_type == FileType::Ape || file_type == FileType::Flac) && *self == TagType::Id3v2; + // TODO: This should not have to be manually updated + let special_exceptions = ((file_type == FileType::Ape + || file_type == FileType::Mpc + || file_type == FileType::Flac) + && *self == TagType::Id3v2) + || file_type == FileType::Mpc && *self == TagType::Id3v1; if !special_exceptions && !file_type.supports_tag_type(*self) { err!(UnsupportedTag); diff --git a/src/tag/utils.rs b/src/tag/utils.rs index ac347f995..15f63d6e5 100644 --- a/src/tag/utils.rs +++ b/src/tag/utils.rs @@ -2,7 +2,7 @@ use crate::error::Result; use crate::file::FileType; use crate::macros::err; use crate::tag::{Tag, TagType}; -use crate::{aac, ape, flac, iff, mpeg, wavpack}; +use crate::{aac, ape, flac, iff, mpeg, musepack, wavpack}; use crate::id3::v1::tag::Id3v1TagRef; use crate::id3::v2::tag::Id3v2TagRef; @@ -26,6 +26,7 @@ pub(crate) fn write_tag(tag: &Tag, file: &mut File, file_type: FileType) -> Resu FileType::Opus | FileType::Speex | FileType::Vorbis => { crate::ogg::write::write_to(file, tag, file_type) }, + FileType::Mpc => musepack::write::write_to(file, tag), FileType::Mpeg => mpeg::write::write_to(file, tag), FileType::Mp4 => { crate::mp4::ilst::write::write_to(file, &mut Into::::into(tag.clone()).as_ref()) diff --git a/src/wavpack/read.rs b/src/wavpack/read.rs index da4707436..2d402fe20 100644 --- a/src/wavpack/read.rs +++ b/src/wavpack/read.rs @@ -1,8 +1,5 @@ use super::properties::WavPackProperties; use super::WavPackFile; -use crate::ape::constants::APE_PREAMBLE; -use crate::ape::header::read_ape_header; -use crate::ape::tag::read::read_ape_tag; use crate::error::Result; use crate::id3::{find_id3v1, find_lyrics3v2, ID3FindResults}; use crate::probe::ParseOptions; @@ -41,15 +38,9 @@ where // Strongly recommended to be at the end of the file reader.seek(SeekFrom::Current(-32))?; - let mut ape_preamble = [0; 8]; - reader.read_exact(&mut ape_preamble)?; - - if &ape_preamble == APE_PREAMBLE { - let ape_header = read_ape_header(reader, true)?; - stream_length -= u64::from(ape_header.size); - - let ape = read_ape_tag(reader, ape_header)?; - ape_tag = Some(ape); + if let Some((tag, header)) = crate::ape::tag::read::read_ape_tag(reader, true)? { + stream_length -= u64::from(header.size); + ape_tag = Some(tag); } Ok(WavPackFile { diff --git a/tests/files/assets/minimal/mpc_sv5.mpc b/tests/files/assets/minimal/mpc_sv5.mpc new file mode 100644 index 000000000..d94a9027d Binary files /dev/null and b/tests/files/assets/minimal/mpc_sv5.mpc differ diff --git a/tests/files/assets/minimal/mpc_sv7.mpc b/tests/files/assets/minimal/mpc_sv7.mpc new file mode 100644 index 000000000..250f71492 Binary files /dev/null and b/tests/files/assets/minimal/mpc_sv7.mpc differ diff --git a/tests/files/assets/minimal/mpc_sv8.mpc b/tests/files/assets/minimal/mpc_sv8.mpc new file mode 100644 index 000000000..4b322e006 Binary files /dev/null and b/tests/files/assets/minimal/mpc_sv8.mpc differ diff --git a/tests/files/main.rs b/tests/files/main.rs index 8f7b58d15..bcbeaf5c4 100644 --- a/tests/files/main.rs +++ b/tests/files/main.rs @@ -2,6 +2,7 @@ mod aac; mod aiff; mod ape; mod mp4; +mod mpc; mod mpeg; mod ogg; pub(crate) mod util; diff --git a/tests/files/mpc.rs b/tests/files/mpc.rs new file mode 100644 index 000000000..36484fa16 --- /dev/null +++ b/tests/files/mpc.rs @@ -0,0 +1,108 @@ +use crate::{set_artist, temp_file, verify_artist}; +use lofty::musepack::MpcFile; +use lofty::{ + AudioFile, FileType, ItemKey, ItemValue, ParseOptions, Probe, TagExt, TagItem, TagType, + TaggedFile, TaggedFileExt, +}; +use std::io::{Seek, Write}; + +// Marker test so IntelliJ Rust recognizes this as a test module +#[test] +fn fake() {} + +macro_rules! generate_tests { + ($stream_version:ident, $path:literal) => { + paste::paste! { + #[test] + fn []() { + // Here we have an MPC file with an ID3v2, ID3v1, and an APEv2 tag + let file = Probe::open($path) + .unwrap() + .options(ParseOptions::new().read_properties(false)) + .read() + .unwrap(); + + assert_eq!(file.file_type(), FileType::Mpc); + + // Verify the APE tag first + crate::verify_artist!(file, primary_tag, "Foo artist", 1); + + // Now verify ID3v1 (read only) + crate::verify_artist!(file, tag, TagType::Id3v1, "Bar artist", 1); + + // Finally, verify ID3v2 (read only) + crate::verify_artist!(file, tag, TagType::Id3v2, "Baz artist", 1); + } + + + #[test] + fn []() { + let mut file = temp_file!($path); + + let mut tagged_file = Probe::new(&mut file) + .options(ParseOptions::new().read_properties(false)) + .guess_file_type() + .unwrap() + .read() + .unwrap(); + + assert_eq!(tagged_file.file_type(), FileType::Mpc); + + // APE + crate::set_artist!(tagged_file, primary_tag_mut, "Foo artist", 1 => file, "Bar artist"); + + // Now reread the file + file.rewind().unwrap(); + let mut tagged_file = Probe::new(&mut file) + .options(ParseOptions::new().read_properties(false)) + .guess_file_type() + .unwrap() + .read() + .unwrap(); + + crate::set_artist!(tagged_file, primary_tag_mut, "Bar artist", 1 => file, "Foo artist"); + } + + #[test] + fn []() { + crate::remove_tag!($path, TagType::Id3v2); + } + + #[test] + fn []() { + crate::remove_tag!($path, TagType::Id3v1); + } + + #[test] + fn []() { + crate::remove_tag!($path, TagType::Ape); + } + } + }; +} + +generate_tests!(sv8, "tests/files/assets/minimal/mpc_sv8.mpc"); +generate_tests!(sv7, "tests/files/assets/minimal/mpc_sv7.mpc"); + +// We have to use `MpcFile::read_from` for stream versions <= 6 + +#[test] +fn read_sv5() { + let mut file = temp_file!("tests/files/assets/minimal/mpc_sv5.mpc"); + + // Here we have an MPC file with an ID3v2, ID3v1, and an APEv2 tag + let file: TaggedFile = MpcFile::read_from(&mut file, ParseOptions::new()) + .unwrap() + .into(); + + assert_eq!(file.file_type(), FileType::Mpc); + + // Verify the APE tag first + crate::verify_artist!(file, primary_tag, "Foo artist", 1); + + // Now verify ID3v1 (read only) + crate::verify_artist!(file, tag, TagType::Id3v1, "Bar artist", 1); + + // Finally, verify ID3v2 (read only) + crate::verify_artist!(file, tag, TagType::Id3v2, "Baz artist", 1); +}