From d89250eef7ff8c2c957f7d3cc8ae22a9af3fac6a Mon Sep 17 00:00:00 2001 From: Serial <69764315+Serial-ATA@users.noreply.github.com> Date: Sun, 21 May 2023 12:35:13 -0400 Subject: [PATCH] musepack: Support SV7 property reading --- src/musepack/constants.rs | 2 + src/musepack/read.rs | 67 +++--- src/musepack/sv7/properties.rs | 305 +++++++++++++++++++++++-- src/probe.rs | 2 +- src/properties.rs | 34 ++- tests/files/assets/minimal/mpc_sv7.mpc | Bin 0 -> 15688 bytes tests/files/mpc.rs | 126 +++++----- 7 files changed, 432 insertions(+), 104 deletions(-) create mode 100644 tests/files/assets/minimal/mpc_sv7.mpc 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 0000000000000000000000000000000000000000..250f7149208bfffc77c3e515b8a5fa4077b88dc7 GIT binary patch literal 15688 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 J|NpfD{|jXaHKhOm literal 0 HcmV?d00001 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");