musepack: Support SV7 property reading

This commit is contained in:
Serial 2023-05-21 12:35:13 -04:00 committed by Alex
parent 6888c17bad
commit d89250eef7
7 changed files with 432 additions and 104 deletions

View file

@ -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:

View file

@ -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)
}

View file

@ -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: <http://trac.musepack.net/musepack/wiki/SV7Specification>
#[rustfmt::skip]
pub fn from_u8(value: u8) -> Option<Self> {
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: <http://trac.musepack.net/musepack/wiki/SV7Specification>
pub fn from_u8(value: u8) -> Option<Self> {
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<MpcSv7Properties> for FileProperties {
@ -102,10 +146,233 @@ impl From<MpcSv7Properties> for FileProperties {
}
impl MpcSv7Properties {
pub(crate) fn read<R>(_reader: &mut R, _parse_mode: ParsingMode) -> Result<Self>
/// 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<R>(reader: &mut R, stream_length: u64) -> Result<Self>
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::<LittleEndian>()?;
// -- Section 2 --
let chunk = reader.read_u32::<LittleEndian>()?;
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::<BigEndian>()?;
let title_peak = reader.read_u16::<BigEndian>()?;
// -- Section 4 --
let album_gain = reader.read_i16::<BigEndian>()?;
let album_peak = reader.read_u16::<BigEndian>()?;
// -- Section 5 --
let chunk = reader.read_u32::<LittleEndian>()?;
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)
}
}

View file

@ -476,7 +476,7 @@ impl<R: Read + Seek> Probe<R> {
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

View file

@ -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::<MpcFile>("tests/files/assets/minimal/mpc_sv7.mpc"),
MpcProperties::Sv7(MPC_SV7_PROPERTIES)
)
}
#[test]
fn mpc_sv8_properties() {
assert_eq!(

Binary file not shown.

View file

@ -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 [<read_ $stream_version>]() {
// 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 [<write_ $stream_version>]() {
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 [<remove_id3v2_ $stream_version>]() {
crate::remove_tag!($path, TagType::Id3v2);
}
#[test]
fn [<remove_id3v1_ $stream_version>]() {
crate::remove_tag!($path, TagType::Id3v1);
}
#[test]
fn [<remove_ape_ $stream_version>]() {
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");