diff --git a/src/musepack/constants.rs b/src/musepack/constants.rs index 344a0152..35bd6293 100644 --- a/src/musepack/constants.rs +++ b/src/musepack/constants.rs @@ -1,3 +1,5 @@ +//! 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: diff --git a/src/musepack/read.rs b/src/musepack/read.rs index a4e2ac2a..320d021a 100644 --- a/src/musepack/read.rs +++ b/src/musepack/read.rs @@ -6,6 +6,7 @@ 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}; @@ -17,6 +18,8 @@ where let mut version = MpcStreamVersion::Sv4to6; let mut file = MpcFile::default(); + 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)? { @@ -24,8 +27,44 @@ where let id3v2 = parse_id3v2(reader, header)?; file.id3v2_tag = Some(id3v2); + + let mut size = header.size; + if header.flags.footer { + size += 10; + } + + stream_length -= size as u64; } + // 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 -= lyrics3v2_size as u64; + + 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 -= header.size as u64; + } + + // Restore the position of the magic signature + reader.seek(SeekFrom::Start(pos_past_id3v2))?; + let mut header = [0; 4]; reader.read_exact(&mut header)?; @@ -51,8 +90,7 @@ where MpcProperties::Sv8(MpcSv8Properties::read(reader, parse_options.parsing_mode)?) }, MpcStreamVersion::Sv7 => { - file.properties = - MpcProperties::Sv7(MpcSv7Properties::read(reader, parse_options.parsing_mode)?) + file.properties = MpcProperties::Sv7(MpcSv7Properties::read(reader, stream_length)?) }, MpcStreamVersion::Sv4to6 => { file.properties = MpcProperties::Sv4to6(MpcSv4to6Properties::read( @@ -63,30 +101,5 @@ where } } - #[allow(unused_variables)] - let ID3FindResults(header, id3v1) = find_id3v1(reader, true)?; - - if header.is_some() { - file.id3v1_tag = id3v1; - } - - let _ = find_lyrics3v2(reader)?; - - reader.seek(SeekFrom::Current(-32))?; - - 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(header.size)))?; - }, - None => { - // Correct the position (APE header - Preamble) - reader.seek(SeekFrom::Current(24))?; - }, - } - Ok(file) } diff --git a/src/musepack/sv7/properties.rs b/src/musepack/sv7/properties.rs index 45798fe0..27cc8cce 100644 --- a/src/musepack/sv7/properties.rs +++ b/src/musepack/sv7/properties.rs @@ -1,10 +1,13 @@ use crate::error::Result; -use crate::probe::ParsingMode; +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 { @@ -39,6 +42,32 @@ pub enum Profile { 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 { @@ -53,6 +82,21 @@ pub enum Link { 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 @@ -64,27 +108,27 @@ pub struct MpcSv7Properties { pub(crate) audio_bitrate: u32, pub(crate) channels: u8, // NOTE: always 2 // -- Section 1 -- - frame_count: u32, + pub(crate) frame_count: u32, // -- Section 2 -- - intensity_stereo: bool, - mid_side_stereo: bool, - max_band: u8, - profile: Profile, - link: Link, - sample_freq: u32, - max_level: u16, + 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 -- - title_gain: i16, - title_peak: u16, + pub(crate) title_gain: i16, + pub(crate) title_peak: u16, // -- Section 4 -- - album_gain: i16, - album_peak: u16, + pub(crate) album_gain: i16, + pub(crate) album_peak: u16, // -- Section 5 -- - true_gapless: bool, - last_frame_length: u16, - fast_seeking_safe: bool, + pub(crate) true_gapless: bool, + pub(crate) last_frame_length: u16, + pub(crate) fast_seeking_safe: bool, // -- Section 6 -- - encoder_version: u8, + pub(crate) encoder_version: u8, } impl From for FileProperties { @@ -102,10 +146,233 @@ impl From for FileProperties { } impl MpcSv7Properties { - pub(crate) fn read(_reader: &mut R, _parse_mode: ParsingMode) -> Result + /// 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 + } + + #[allow(clippy::field_reassign_with_default)] + pub(crate) fn read(reader: &mut R, stream_length: u64) -> Result where R: Read, { - todo!() + 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/probe.rs b/src/probe.rs index c1c9e2d6..f8440381 100644 --- a/src/probe.rs +++ b/src/probe.rs @@ -476,7 +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)), + 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 diff --git a/src/properties.rs b/src/properties.rs index 5db324ae..dc71bae0 100644 --- a/src/properties.rs +++ b/src/properties.rs @@ -127,6 +127,7 @@ 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::sv7::{Link, MpcSv7Properties, Profile}; use crate::musepack::sv8::{EncoderInfo, MpcSv8Properties, ReplayGain, StreamHeader}; use crate::musepack::{MpcFile, MpcProperties}; use crate::ogg::{ @@ -274,9 +275,32 @@ mod tests { channels: 2, }; + 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, // TODO: Reference decoder reports 84 + overall_bitrate: 82, audio_bitrate: 82, stream_header: StreamHeader { crc: 4_252_559_415, @@ -460,6 +484,14 @@ mod tests { ) } + #[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!( diff --git a/tests/files/assets/minimal/mpc_sv7.mpc b/tests/files/assets/minimal/mpc_sv7.mpc new file mode 100644 index 00000000..250f7149 Binary files /dev/null and b/tests/files/assets/minimal/mpc_sv7.mpc differ diff --git a/tests/files/mpc.rs b/tests/files/mpc.rs index d7fba798..8c6ce4f8 100644 --- a/tests/files/mpc.rs +++ b/tests/files/mpc.rs @@ -4,66 +4,80 @@ use lofty::{ }; use std::io::{Seek, Write}; +// Marker test so IntelliJ Rust recognizes this as a test module #[test] -fn read() { - // Here we have an MPC file with an ID3v2, ID3v1, and an APEv2 tag - let file = Probe::open("tests/files/assets/minimal/mpc_sv8.mpc") - .unwrap() - .options(ParseOptions::new().read_properties(false)) - .read() - .unwrap(); +fn fake() {} - assert_eq!(file.file_type(), FileType::Mpc); +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(); - // Verify the APE tag first - crate::verify_artist!(file, primary_tag, "Foo artist", 1); + assert_eq!(file.file_type(), FileType::Mpc); - // Now verify ID3v1 (read only) - crate::verify_artist!(file, tag, TagType::Id3v1, "Bar artist", 1); + // Verify the APE tag first + crate::verify_artist!(file, primary_tag, "Foo artist", 1); - // Finally, verify ID3v2 (read only) - crate::verify_artist!(file, tag, TagType::Id3v2, "Baz 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); + } + } + }; } -#[test] -fn write() { - let mut file = temp_file!("tests/files/assets/minimal/mpc_sv8.mpc"); - - 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 remove_id3v2() { - crate::remove_tag!("tests/files/assets/minimal/mpc_sv8.mpc", TagType::Id3v2); -} - -#[test] -fn remove_id3v1() { - crate::remove_tag!("tests/files/assets/minimal/mpc_sv8.mpc", TagType::Id3v1); -} - -#[test] -fn remove_ape() { - crate::remove_tag!("tests/files/assets/minimal/mpc_sv8.mpc", TagType::Ape); -} +generate_tests!(sv8, "tests/files/assets/minimal/mpc_sv8.mpc"); +generate_tests!(sv7, "tests/files/assets/minimal/mpc_sv7.mpc");