mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-12-13 14:12:31 +00:00
Musepack: Improve audio properties
This commit is contained in:
parent
da91e43d02
commit
d2d4b81fe0
11 changed files with 222 additions and 147 deletions
|
@ -54,6 +54,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- **MPEG**: Durations estimated by bitrate are more accurate ([PR](https://github.com/Serial-ATA/lofty-rs/pull/395))
|
- **MPEG**: Durations estimated by bitrate are more accurate ([PR](https://github.com/Serial-ATA/lofty-rs/pull/395))
|
||||||
- **MP4**: Bitrate calculation is now more accurate ([PR](https://github.com/Serial-ATA/lofty-rs/pull/398))
|
- **MP4**: Bitrate calculation is now more accurate ([PR](https://github.com/Serial-ATA/lofty-rs/pull/398))
|
||||||
- **WAV**: Bitrate calculation is now more accurate ([PR](https://github.com/Serial-ATA/lofty-rs/pull/399))
|
- **WAV**: Bitrate calculation is now more accurate ([PR](https://github.com/Serial-ATA/lofty-rs/pull/399))
|
||||||
|
- **MusePack**: Overall improved audio properties ([PR](https://github.com/Serial-ATA/lofty-rs/pull/402))
|
||||||
|
|
||||||
## [0.19.2] - 2024-04-26
|
## [0.19.2] - 2024-04-26
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,7 @@ pub struct Id3v2TagFlags {
|
||||||
pub(crate) struct Id3v2Header {
|
pub(crate) struct Id3v2Header {
|
||||||
pub version: Id3v2Version,
|
pub version: Id3v2Version,
|
||||||
pub flags: Id3v2TagFlags,
|
pub flags: Id3v2TagFlags,
|
||||||
|
/// The size of the tag contents (**DOES NOT INCLUDE THE HEADER/FOOTER**)
|
||||||
pub size: u32,
|
pub size: u32,
|
||||||
pub extended_size: u32,
|
pub extended_size: u32,
|
||||||
}
|
}
|
||||||
|
@ -140,4 +141,9 @@ impl Id3v2Header {
|
||||||
extended_size,
|
extended_size,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The total size of the tag, including the header, footer, and extended header
|
||||||
|
pub(crate) fn full_tag_size(&self) -> u32 {
|
||||||
|
self.size + 10 + self.extended_size + if self.flags.footer { 10 } else { 0 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,5 +13,5 @@ pub(super) const FREQUENCY_TABLE: [u32; 8] = [44100, 48000, 37800, 32000, 0, 0,
|
||||||
/// This is the gain reference used in old ReplayGain
|
/// This is the gain reference used in old ReplayGain
|
||||||
pub const MPC_OLD_GAIN_REF: f32 = 64.82;
|
pub const MPC_OLD_GAIN_REF: f32 = 64.82;
|
||||||
|
|
||||||
pub(super) const MPC_DECODER_SYNTH_DELAY: u32 = 481;
|
pub(super) const MPC_DECODER_SYNTH_DELAY: u64 = 481;
|
||||||
pub(super) const MPC_FRAME_LENGTH: u32 = 36 * 32; // Samples per mpc frame
|
pub(super) const MPC_FRAME_LENGTH: u64 = 36 * 32; // Samples per mpc frame
|
||||||
|
|
|
@ -32,12 +32,7 @@ where
|
||||||
let id3v2 = parse_id3v2(reader, header, parse_options.parsing_mode)?;
|
let id3v2 = parse_id3v2(reader, header, parse_options.parsing_mode)?;
|
||||||
file.id3v2_tag = Some(id3v2);
|
file.id3v2_tag = Some(id3v2);
|
||||||
|
|
||||||
let mut size = header.size;
|
stream_length -= u64::from(header.full_tag_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
|
// Save the current position, so we can go back and read the properties after the tags
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
use crate::config::ParsingMode;
|
use crate::config::ParsingMode;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::macros::{decode_err, parse_mode_choice};
|
use crate::macros::decode_err;
|
||||||
use crate::musepack::constants::{MPC_DECODER_SYNTH_DELAY, MPC_FRAME_LENGTH};
|
use crate::musepack::constants::{MPC_DECODER_SYNTH_DELAY, MPC_FRAME_LENGTH};
|
||||||
use crate::properties::FileProperties;
|
use crate::properties::FileProperties;
|
||||||
|
use crate::util::math::RoundedDivision;
|
||||||
|
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
@ -17,7 +18,7 @@ pub struct MpcSv4to6Properties {
|
||||||
pub(crate) sample_rate: u32, // NOTE: always 44100
|
pub(crate) sample_rate: u32, // NOTE: always 44100
|
||||||
|
|
||||||
// Fields actually contained in the header
|
// Fields actually contained in the header
|
||||||
pub(crate) audio_bitrate: u32,
|
pub(crate) average_bitrate: u32,
|
||||||
pub(crate) mid_side_stereo: bool,
|
pub(crate) mid_side_stereo: bool,
|
||||||
pub(crate) stream_version: u16,
|
pub(crate) stream_version: u16,
|
||||||
pub(crate) max_band: u8,
|
pub(crate) max_band: u8,
|
||||||
|
@ -28,8 +29,8 @@ impl From<MpcSv4to6Properties> for FileProperties {
|
||||||
fn from(input: MpcSv4to6Properties) -> Self {
|
fn from(input: MpcSv4to6Properties) -> Self {
|
||||||
Self {
|
Self {
|
||||||
duration: input.duration,
|
duration: input.duration,
|
||||||
overall_bitrate: Some(input.audio_bitrate),
|
overall_bitrate: Some(input.average_bitrate),
|
||||||
audio_bitrate: Some(input.audio_bitrate),
|
audio_bitrate: Some(input.average_bitrate),
|
||||||
sample_rate: Some(input.sample_rate),
|
sample_rate: Some(input.sample_rate),
|
||||||
bit_depth: None,
|
bit_depth: None,
|
||||||
channels: Some(input.channels),
|
channels: Some(input.channels),
|
||||||
|
@ -54,9 +55,9 @@ impl MpcSv4to6Properties {
|
||||||
self.sample_rate
|
self.sample_rate
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Audio bitrate (kbps)
|
/// Average bitrate (kbps)
|
||||||
pub fn audio_bitrate(&self) -> u32 {
|
pub fn average_bitrate(&self) -> u32 {
|
||||||
self.audio_bitrate
|
self.average_bitrate
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether MidSideStereo is used
|
/// Whether MidSideStereo is used
|
||||||
|
@ -92,7 +93,7 @@ impl MpcSv4to6Properties {
|
||||||
|
|
||||||
let mut properties = Self::default();
|
let mut properties = Self::default();
|
||||||
|
|
||||||
properties.audio_bitrate = (header_data[0] >> 23) & 0x1FF;
|
properties.average_bitrate = (header_data[0] >> 23) & 0x1FF;
|
||||||
let intensity_stereo = (header_data[0] >> 22) & 0x1 == 1;
|
let intensity_stereo = (header_data[0] >> 22) & 0x1 == 1;
|
||||||
properties.mid_side_stereo = (header_data[0] >> 21) & 0x1 == 1;
|
properties.mid_side_stereo = (header_data[0] >> 21) & 0x1 == 1;
|
||||||
|
|
||||||
|
@ -110,22 +111,19 @@ impl MpcSv4to6Properties {
|
||||||
properties.frame_count = header_data[1] >> 16; // 16 bit
|
properties.frame_count = header_data[1] >> 16; // 16 bit
|
||||||
}
|
}
|
||||||
|
|
||||||
parse_mode_choice!(
|
if parse_mode == ParsingMode::Strict {
|
||||||
parse_mode,
|
if properties.average_bitrate != 0 {
|
||||||
STRICT: {
|
decode_err!(@BAIL Mpc, "Encountered CBR stream")
|
||||||
if properties.audio_bitrate != 0 {
|
}
|
||||||
decode_err!(@BAIL Mpc, "Encountered CBR stream")
|
|
||||||
}
|
|
||||||
|
|
||||||
if intensity_stereo {
|
if intensity_stereo {
|
||||||
decode_err!(@BAIL Mpc, "Stream uses intensity stereo coding")
|
decode_err!(@BAIL Mpc, "Stream uses intensity stereo coding")
|
||||||
}
|
}
|
||||||
|
|
||||||
if block_size != 1 {
|
if block_size != 1 {
|
||||||
decode_err!(@BAIL Mpc, "Stream has an invalid block size (must be 1)")
|
decode_err!(@BAIL Mpc, "Stream has an invalid block size (must be 1)")
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
|
||||||
|
|
||||||
if properties.stream_version < 6 {
|
if properties.stream_version < 6 {
|
||||||
// Versions before 6 had an invalid last frame
|
// Versions before 6 had an invalid last frame
|
||||||
|
@ -135,18 +133,28 @@ impl MpcSv4to6Properties {
|
||||||
properties.sample_rate = 44100;
|
properties.sample_rate = 44100;
|
||||||
properties.channels = 2;
|
properties.channels = 2;
|
||||||
|
|
||||||
if properties.frame_count > 0 {
|
// Nothing more we can do
|
||||||
let samples =
|
if properties.frame_count == 0 {
|
||||||
(properties.frame_count * MPC_FRAME_LENGTH).saturating_sub(MPC_DECODER_SYNTH_DELAY);
|
return Ok(properties);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let samples = (u64::from(properties.frame_count) * MPC_FRAME_LENGTH)
|
||||||
|
.saturating_sub(MPC_DECODER_SYNTH_DELAY);
|
||||||
|
let length = (samples * 1000).div_round(u64::from(properties.sample_rate));
|
||||||
|
properties.duration = Duration::from_millis(length);
|
||||||
|
|
||||||
|
// 576 is a magic number from the reference decoder
|
||||||
|
//
|
||||||
|
// Quote from the reference source (libmpcdec/trunk/src/streaminfo.c:248 @rev 153):
|
||||||
|
// "estimation, exact value needs too much time"
|
||||||
|
let pcm_frames = (MPC_FRAME_LENGTH * u64::from(properties.frame_count)).saturating_sub(576);
|
||||||
|
|
||||||
|
// Is this accurate? If not, it really doesn't matter.
|
||||||
|
properties.average_bitrate = ((stream_length as f64
|
||||||
|
* 8.0 * f64::from(properties.sample_rate))
|
||||||
|
/ (pcm_frames as f64)
|
||||||
|
/ (MPC_FRAME_LENGTH as f64)) as u32;
|
||||||
|
|
||||||
Ok(properties)
|
Ok(properties)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::macros::decode_err;
|
use crate::macros::decode_err;
|
||||||
use crate::musepack::constants::{FREQUENCY_TABLE, MPC_OLD_GAIN_REF};
|
use crate::musepack::constants::{
|
||||||
|
FREQUENCY_TABLE, MPC_DECODER_SYNTH_DELAY, MPC_FRAME_LENGTH, MPC_OLD_GAIN_REF,
|
||||||
|
};
|
||||||
use crate::properties::FileProperties;
|
use crate::properties::FileProperties;
|
||||||
|
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use byteorder::{BigEndian, LittleEndian, ReadBytesExt};
|
use byteorder::{LittleEndian, ReadBytesExt};
|
||||||
|
|
||||||
/// Used profile
|
/// Used profile
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
@ -104,8 +106,7 @@ impl Link {
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
pub struct MpcSv7Properties {
|
pub struct MpcSv7Properties {
|
||||||
pub(crate) duration: Duration,
|
pub(crate) duration: Duration,
|
||||||
pub(crate) overall_bitrate: u32,
|
pub(crate) average_bitrate: u32,
|
||||||
pub(crate) audio_bitrate: u32,
|
|
||||||
pub(crate) channels: u8, // NOTE: always 2
|
pub(crate) channels: u8, // NOTE: always 2
|
||||||
// -- Section 1 --
|
// -- Section 1 --
|
||||||
pub(crate) frame_count: u32,
|
pub(crate) frame_count: u32,
|
||||||
|
@ -135,8 +136,8 @@ impl From<MpcSv7Properties> for FileProperties {
|
||||||
fn from(input: MpcSv7Properties) -> Self {
|
fn from(input: MpcSv7Properties) -> Self {
|
||||||
Self {
|
Self {
|
||||||
duration: input.duration,
|
duration: input.duration,
|
||||||
overall_bitrate: Some(input.overall_bitrate),
|
overall_bitrate: Some(input.average_bitrate),
|
||||||
audio_bitrate: Some(input.audio_bitrate),
|
audio_bitrate: Some(input.average_bitrate),
|
||||||
sample_rate: Some(input.sample_freq),
|
sample_rate: Some(input.sample_freq),
|
||||||
bit_depth: None,
|
bit_depth: None,
|
||||||
channels: Some(input.channels),
|
channels: Some(input.channels),
|
||||||
|
@ -151,14 +152,9 @@ impl MpcSv7Properties {
|
||||||
self.duration
|
self.duration
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Overall bitrate (kbps)
|
/// Average bitrate (kbps)
|
||||||
pub fn overall_bitrate(&self) -> u32 {
|
pub fn average_bitrate(&self) -> u32 {
|
||||||
self.overall_bitrate
|
self.average_bitrate
|
||||||
}
|
|
||||||
|
|
||||||
/// Audio bitrate (kbps)
|
|
||||||
pub fn audio_bitrate(&self) -> u32 {
|
|
||||||
self.audio_bitrate
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sample rate (Hz)
|
/// Sample rate (Hz)
|
||||||
|
@ -208,7 +204,7 @@ impl MpcSv7Properties {
|
||||||
|
|
||||||
/// Change in the replay level
|
/// Change in the replay level
|
||||||
///
|
///
|
||||||
/// The value is a signed 16 bit integer, with the level being attenuated by that many mB
|
/// The value is a signed 16-bit integer, with the level being attenuated by that many mB
|
||||||
pub fn title_gain(&self) -> i16 {
|
pub fn title_gain(&self) -> i16 {
|
||||||
self.title_gain
|
self.title_gain
|
||||||
}
|
}
|
||||||
|
@ -224,7 +220,7 @@ impl MpcSv7Properties {
|
||||||
|
|
||||||
/// Change in the replay level if the whole CD is supposed to be played with the same level change
|
/// 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
|
/// The value is a signed 16-bit integer, with the level being attenuated by that many mB
|
||||||
pub fn album_gain(&self) -> i16 {
|
pub fn album_gain(&self) -> i16 {
|
||||||
self.album_gain
|
self.album_gain
|
||||||
}
|
}
|
||||||
|
@ -280,6 +276,9 @@ impl MpcSv7Properties {
|
||||||
..Self::default()
|
..Self::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: Make a Bitreader, would be nice crate-wide but especially here
|
||||||
|
// The SV7 header is split into 6 32-bit sections
|
||||||
|
|
||||||
// -- Section 1 --
|
// -- Section 1 --
|
||||||
properties.frame_count = reader.read_u32::<LittleEndian>()?;
|
properties.frame_count = reader.read_u32::<LittleEndian>()?;
|
||||||
|
|
||||||
|
@ -294,11 +293,8 @@ impl MpcSv7Properties {
|
||||||
|
|
||||||
let byte2 = ((chunk & 0xFF_0000) >> 16) as u8;
|
let byte2 = ((chunk & 0xFF_0000) >> 16) as u8;
|
||||||
|
|
||||||
let profile_index = (byte2 & 0xF0) >> 4;
|
properties.profile = Profile::from_u8((byte2 & 0xF0) >> 4).unwrap(); // Infallible
|
||||||
properties.profile = Profile::from_u8(profile_index).unwrap(); // Infallible
|
properties.link = Link::from_u8((byte2 & 0x0C) >> 2).unwrap(); // Infallible
|
||||||
|
|
||||||
let link_index = (byte2 & 0x0C) >> 2;
|
|
||||||
properties.link = Link::from_u8(link_index).unwrap(); // Infallible
|
|
||||||
|
|
||||||
let sample_freq_index = byte2 & 0x03;
|
let sample_freq_index = byte2 & 0x03;
|
||||||
properties.sample_freq = FREQUENCY_TABLE[sample_freq_index as usize];
|
properties.sample_freq = FREQUENCY_TABLE[sample_freq_index as usize];
|
||||||
|
@ -307,27 +303,24 @@ impl MpcSv7Properties {
|
||||||
properties.max_level = remaining_bytes;
|
properties.max_level = remaining_bytes;
|
||||||
|
|
||||||
// -- Section 3 --
|
// -- Section 3 --
|
||||||
let title_gain = reader.read_i16::<BigEndian>()?;
|
let title_peak = reader.read_u16::<LittleEndian>()?;
|
||||||
let title_peak = reader.read_u16::<BigEndian>()?;
|
let title_gain = reader.read_u16::<LittleEndian>()?;
|
||||||
|
|
||||||
// -- Section 4 --
|
// -- Section 4 --
|
||||||
let album_gain = reader.read_i16::<BigEndian>()?;
|
let album_peak = reader.read_u16::<LittleEndian>()?;
|
||||||
let album_peak = reader.read_u16::<BigEndian>()?;
|
let album_gain = reader.read_u16::<LittleEndian>()?;
|
||||||
|
|
||||||
// -- Section 5 --
|
// -- Section 5 --
|
||||||
let chunk = reader.read_u32::<LittleEndian>()?;
|
let chunk = reader.read_u32::<LittleEndian>()?;
|
||||||
|
|
||||||
let byte1 = ((chunk & 0xFF00_0000) >> 24) as u8;
|
properties.true_gapless = (chunk >> 31) == 1;
|
||||||
|
|
||||||
properties.true_gapless = ((byte1 & 0x80) >> 7) == 1;
|
|
||||||
|
|
||||||
let byte2 = ((chunk & 0xFF_0000) >> 16) as u8;
|
|
||||||
|
|
||||||
if properties.true_gapless {
|
if properties.true_gapless {
|
||||||
properties.last_frame_length =
|
properties.last_frame_length = ((chunk >> 20) & 0x7FF) as u16;
|
||||||
(u16::from(byte1 & 0x7F) << 4) | u16::from((byte2 & 0xF0) >> 4);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
properties.fast_seeking_safe = (chunk >> 19) & 1 == 1;
|
||||||
|
|
||||||
// NOTE: Rest of the chunk is zeroed and unused
|
// NOTE: Rest of the chunk is zeroed and unused
|
||||||
|
|
||||||
// -- Section 6 --
|
// -- Section 6 --
|
||||||
|
@ -336,19 +329,23 @@ impl MpcSv7Properties {
|
||||||
// -- End of parsing --
|
// -- End of parsing --
|
||||||
|
|
||||||
// Convert ReplayGain values
|
// Convert ReplayGain values
|
||||||
let set_replay_gain = |gain: i16| -> i16 {
|
let set_replay_gain = |gain: u16| -> i16 {
|
||||||
let mut gain = (MPC_OLD_GAIN_REF - f32::from(gain) / 100.0) * 256.0 + 0.5;
|
if gain == 0 {
|
||||||
if gain >= ((1 << 16) as f32) || gain < 0.0 {
|
return 0;
|
||||||
gain = 0.0
|
|
||||||
}
|
}
|
||||||
gain as i16
|
|
||||||
|
let gain = ((MPC_OLD_GAIN_REF - f32::from(gain) / 100.0) * 256.0 + 0.5) as i16;
|
||||||
|
if !(0..i16::MAX).contains(&gain) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
gain
|
||||||
};
|
};
|
||||||
let set_replay_peak = |peak: u16| -> u16 {
|
let set_replay_peak = |peak: u16| -> u16 {
|
||||||
if peak == 0 {
|
if peak == 0 {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
((peak.ilog10() * 20 * 256) as f32 + 0.5) as u16
|
((f64::from(peak).log10() * 20.0 * 256.0) + 0.5) as u16
|
||||||
};
|
};
|
||||||
|
|
||||||
properties.title_gain = set_replay_gain(title_gain);
|
properties.title_gain = set_replay_gain(title_gain);
|
||||||
|
@ -356,22 +353,36 @@ impl MpcSv7Properties {
|
||||||
properties.album_gain = set_replay_gain(album_gain);
|
properties.album_gain = set_replay_gain(album_gain);
|
||||||
properties.album_peak = set_replay_peak(album_peak);
|
properties.album_peak = set_replay_peak(album_peak);
|
||||||
|
|
||||||
let total_samples;
|
if properties.last_frame_length > MPC_FRAME_LENGTH as u16 {
|
||||||
if properties.true_gapless {
|
decode_err!(@BAIL Mpc, "Invalid last frame length");
|
||||||
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 {
|
if properties.sample_freq == 0 {
|
||||||
let length =
|
log::warn!("Sample rate is 0, unable to calculate duration and bitrate");
|
||||||
(f64::from(total_samples) * 1000.0 / f64::from(properties.sample_freq)).ceil();
|
return Ok(properties);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if properties.frame_count == 0 {
|
||||||
|
log::warn!("Frame count is 0, unable to calculate duration and bitrate");
|
||||||
|
return Ok(properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
let time_per_frame = (MPC_FRAME_LENGTH as f64) / f64::from(properties.sample_freq);
|
||||||
|
let length = (f64::from(properties.frame_count) * time_per_frame) * 1000.0;
|
||||||
|
properties.duration = Duration::from_millis(length as u64);
|
||||||
|
|
||||||
|
let total_samples;
|
||||||
|
if properties.true_gapless {
|
||||||
|
total_samples = (u64::from(properties.frame_count) * MPC_FRAME_LENGTH)
|
||||||
|
- (MPC_FRAME_LENGTH - u64::from(properties.last_frame_length));
|
||||||
|
} else {
|
||||||
|
total_samples =
|
||||||
|
(u64::from(properties.frame_count) * MPC_FRAME_LENGTH) - MPC_DECODER_SYNTH_DELAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
properties.average_bitrate = ((stream_length * 8 * u64::from(properties.sample_freq))
|
||||||
|
/ (total_samples * 1000)) as u32;
|
||||||
|
|
||||||
Ok(properties)
|
Ok(properties)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
use super::read::PacketReader;
|
use super::read::PacketReader;
|
||||||
use crate::config::ParsingMode;
|
use crate::config::ParsingMode;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
|
use crate::macros::decode_err;
|
||||||
use crate::musepack::constants::FREQUENCY_TABLE;
|
use crate::musepack::constants::FREQUENCY_TABLE;
|
||||||
use crate::properties::FileProperties;
|
use crate::properties::FileProperties;
|
||||||
|
use crate::util::math::RoundedDivision;
|
||||||
|
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
@ -13,8 +15,7 @@ use byteorder::{BigEndian, ReadBytesExt};
|
||||||
#[derive(Debug, Clone, PartialEq, Default)]
|
#[derive(Debug, Clone, PartialEq, Default)]
|
||||||
pub struct MpcSv8Properties {
|
pub struct MpcSv8Properties {
|
||||||
pub(crate) duration: Duration,
|
pub(crate) duration: Duration,
|
||||||
pub(crate) overall_bitrate: u32,
|
pub(crate) average_bitrate: u32,
|
||||||
pub(crate) audio_bitrate: u32,
|
|
||||||
/// Mandatory Stream Header packet
|
/// Mandatory Stream Header packet
|
||||||
pub stream_header: StreamHeader,
|
pub stream_header: StreamHeader,
|
||||||
/// Mandatory ReplayGain packet
|
/// Mandatory ReplayGain packet
|
||||||
|
@ -27,8 +28,8 @@ impl From<MpcSv8Properties> for FileProperties {
|
||||||
fn from(input: MpcSv8Properties) -> Self {
|
fn from(input: MpcSv8Properties) -> Self {
|
||||||
Self {
|
Self {
|
||||||
duration: input.duration,
|
duration: input.duration,
|
||||||
overall_bitrate: Some(input.overall_bitrate),
|
overall_bitrate: Some(input.average_bitrate),
|
||||||
audio_bitrate: Some(input.audio_bitrate),
|
audio_bitrate: Some(input.average_bitrate),
|
||||||
sample_rate: Some(input.stream_header.sample_rate),
|
sample_rate: Some(input.stream_header.sample_rate),
|
||||||
bit_depth: None,
|
bit_depth: None,
|
||||||
channels: Some(input.stream_header.channels),
|
channels: Some(input.stream_header.channels),
|
||||||
|
@ -43,14 +44,9 @@ impl MpcSv8Properties {
|
||||||
self.duration
|
self.duration
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Overall bitrate (kbps)
|
/// Average bitrate (kbps)
|
||||||
pub fn overall_bitrate(&self) -> u32 {
|
pub fn average_bitrate(&self) -> u32 {
|
||||||
self.overall_bitrate
|
self.average_bitrate
|
||||||
}
|
|
||||||
|
|
||||||
/// Audio bitrate (kbps)
|
|
||||||
pub fn audio_bitrate(&self) -> u32 {
|
|
||||||
self.audio_bitrate
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sample rate (Hz)
|
/// Sample rate (Hz)
|
||||||
|
@ -248,3 +244,45 @@ impl EncoderInfo {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn read(
|
||||||
|
stream_length: u64,
|
||||||
|
stream_header: StreamHeader,
|
||||||
|
replay_gain: ReplayGain,
|
||||||
|
encoder_info: Option<EncoderInfo>,
|
||||||
|
) -> Result<MpcSv8Properties> {
|
||||||
|
let mut properties = MpcSv8Properties {
|
||||||
|
duration: Duration::ZERO,
|
||||||
|
average_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 beginning_silence > sample_count {
|
||||||
|
decode_err!(@BAIL Mpc, "Beginning silence is greater than the total sample count");
|
||||||
|
}
|
||||||
|
|
||||||
|
if sample_rate == 0 {
|
||||||
|
log::warn!("Sample rate is 0, unable to calculate duration and bitrate");
|
||||||
|
return Ok(properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
if sample_count == 0 {
|
||||||
|
log::warn!("Sample count is 0, unable to calculate duration and bitrate");
|
||||||
|
return Ok(properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
let total_samples = sample_count - beginning_silence;
|
||||||
|
let length = (total_samples * 1000).div_round(u64::from(sample_rate));
|
||||||
|
|
||||||
|
properties.duration = Duration::from_millis(length);
|
||||||
|
properties.average_bitrate =
|
||||||
|
((stream_length * 8 * u64::from(sample_rate)) / (total_samples * 1000)) as u32;
|
||||||
|
|
||||||
|
Ok(properties)
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ use crate::error::{ErrorKind, LoftyError, Result};
|
||||||
use crate::macros::{decode_err, parse_mode_choice};
|
use crate::macros::{decode_err, parse_mode_choice};
|
||||||
|
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use byteorder::ReadBytesExt;
|
use byteorder::ReadBytesExt;
|
||||||
|
|
||||||
|
@ -12,6 +11,7 @@ use byteorder::ReadBytesExt;
|
||||||
const STREAM_HEADER_KEY: [u8; 2] = *b"SH";
|
const STREAM_HEADER_KEY: [u8; 2] = *b"SH";
|
||||||
const REPLAYGAIN_KEY: [u8; 2] = *b"RG";
|
const REPLAYGAIN_KEY: [u8; 2] = *b"RG";
|
||||||
const ENCODER_INFO_KEY: [u8; 2] = *b"EI";
|
const ENCODER_INFO_KEY: [u8; 2] = *b"EI";
|
||||||
|
#[allow(dead_code)]
|
||||||
const AUDIO_PACKET_KEY: [u8; 2] = *b"AP";
|
const AUDIO_PACKET_KEY: [u8; 2] = *b"AP";
|
||||||
const STREAM_END_KEY: [u8; 2] = *b"SE";
|
const STREAM_END_KEY: [u8; 2] = *b"SE";
|
||||||
|
|
||||||
|
@ -29,13 +29,12 @@ where
|
||||||
let mut found_stream_end = false;
|
let mut found_stream_end = false;
|
||||||
|
|
||||||
while let Ok((packet_id, packet_length)) = packet_reader.next() {
|
while let Ok((packet_id, packet_length)) = packet_reader.next() {
|
||||||
|
stream_length += packet_length;
|
||||||
|
|
||||||
match packet_id {
|
match packet_id {
|
||||||
STREAM_HEADER_KEY => stream_header = Some(StreamHeader::read(&mut packet_reader)?),
|
STREAM_HEADER_KEY => stream_header = Some(StreamHeader::read(&mut packet_reader)?),
|
||||||
REPLAYGAIN_KEY => replay_gain = Some(ReplayGain::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)?),
|
ENCODER_INFO_KEY => encoder_info = Some(EncoderInfo::read(&mut packet_reader)?),
|
||||||
AUDIO_PACKET_KEY => {
|
|
||||||
stream_length += packet_length;
|
|
||||||
},
|
|
||||||
STREAM_END_KEY => {
|
STREAM_END_KEY => {
|
||||||
found_stream_end = true;
|
found_stream_end = true;
|
||||||
break;
|
break;
|
||||||
|
@ -68,41 +67,16 @@ where
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if stream_length == 0 {
|
if stream_length == 0 && parse_mode == ParsingMode::Strict {
|
||||||
parse_mode_choice!(
|
decode_err!(@BAIL Mpc, "File is missing an Audio packet");
|
||||||
parse_mode,
|
|
||||||
STRICT: decode_err!(@BAIL Mpc, "File is missing an Audio packet"),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found_stream_end {
|
if !found_stream_end && parse_mode == ParsingMode::Strict {
|
||||||
parse_mode_choice!(
|
decode_err!(@BAIL Mpc, "File is missing a Stream End packet");
|
||||||
parse_mode,
|
|
||||||
STRICT: decode_err!(@BAIL Mpc, "File is missing a Stream End packet"),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut properties = MpcSv8Properties {
|
let properties =
|
||||||
duration: Duration::ZERO,
|
super::properties::read(stream_length, stream_header, replay_gain, encoder_info)?;
|
||||||
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)
|
Ok(properties)
|
||||||
}
|
}
|
||||||
|
|
|
@ -160,9 +160,10 @@ const MP4_FLAC_PROPERTIES: Mp4Properties = Mp4Properties {
|
||||||
drm_protected: false,
|
drm_protected: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Properties verified with libmpcdec 1.2.2
|
||||||
const MPC_SV5_PROPERTIES: MpcSv4to6Properties = MpcSv4to6Properties {
|
const MPC_SV5_PROPERTIES: MpcSv4to6Properties = MpcSv4to6Properties {
|
||||||
duration: Duration::from_millis(27),
|
duration: Duration::from_millis(26347),
|
||||||
audio_bitrate: 41,
|
average_bitrate: 119,
|
||||||
channels: 2,
|
channels: 2,
|
||||||
frame_count: 1009,
|
frame_count: 1009,
|
||||||
mid_side_stereo: true,
|
mid_side_stereo: true,
|
||||||
|
@ -172,9 +173,8 @@ const MPC_SV5_PROPERTIES: MpcSv4to6Properties = MpcSv4to6Properties {
|
||||||
};
|
};
|
||||||
|
|
||||||
const MPC_SV7_PROPERTIES: MpcSv7Properties = MpcSv7Properties {
|
const MPC_SV7_PROPERTIES: MpcSv7Properties = MpcSv7Properties {
|
||||||
duration: Duration::from_millis(1428),
|
duration: Duration::from_millis(1440),
|
||||||
overall_bitrate: 86,
|
average_bitrate: 86,
|
||||||
audio_bitrate: 86,
|
|
||||||
channels: 2,
|
channels: 2,
|
||||||
frame_count: 60,
|
frame_count: 60,
|
||||||
intensity_stereo: false,
|
intensity_stereo: false,
|
||||||
|
@ -184,9 +184,9 @@ const MPC_SV7_PROPERTIES: MpcSv7Properties = MpcSv7Properties {
|
||||||
link: Link::VeryLowStartOrEnd,
|
link: Link::VeryLowStartOrEnd,
|
||||||
sample_freq: 48000,
|
sample_freq: 48000,
|
||||||
max_level: 0,
|
max_level: 0,
|
||||||
title_gain: 16594,
|
title_gain: 0,
|
||||||
title_peak: 0,
|
title_peak: 0,
|
||||||
album_gain: 16594,
|
album_gain: 0,
|
||||||
album_peak: 0,
|
album_peak: 0,
|
||||||
true_gapless: true,
|
true_gapless: true,
|
||||||
last_frame_length: 578,
|
last_frame_length: 578,
|
||||||
|
@ -196,8 +196,7 @@ const MPC_SV7_PROPERTIES: MpcSv7Properties = MpcSv7Properties {
|
||||||
|
|
||||||
const MPC_SV8_PROPERTIES: MpcSv8Properties = MpcSv8Properties {
|
const MPC_SV8_PROPERTIES: MpcSv8Properties = MpcSv8Properties {
|
||||||
duration: Duration::from_millis(1428),
|
duration: Duration::from_millis(1428),
|
||||||
overall_bitrate: 82,
|
average_bitrate: 82,
|
||||||
audio_bitrate: 82,
|
|
||||||
stream_header: StreamHeader {
|
stream_header: StreamHeader {
|
||||||
crc: 4_252_559_415,
|
crc: 4_252_559_415,
|
||||||
stream_version: 8,
|
stream_version: 8,
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
/// Perform a rounded division.
|
||||||
|
///
|
||||||
|
/// This is implemented for all unsigned integers.
|
||||||
|
///
|
||||||
|
/// NOTE: If the result is less than 1, it will be rounded up to 1.
|
||||||
pub(crate) trait RoundedDivision<Rhs = Self> {
|
pub(crate) trait RoundedDivision<Rhs = Self> {
|
||||||
type Output;
|
type Output;
|
||||||
|
|
||||||
|
@ -18,4 +23,42 @@ macro_rules! unsigned_rounded_division {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
unsigned_rounded_division!(u8, u16, u32, u64, u128, usize);
|
unsigned_rounded_division!(u8, u16, u32, u64, usize);
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_div_round() {
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct TestEntry {
|
||||||
|
lhs: u32,
|
||||||
|
rhs: u32,
|
||||||
|
result: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
let tests = [
|
||||||
|
TestEntry { lhs: 1, rhs: 1, result: 1 },
|
||||||
|
TestEntry { lhs: 1, rhs: 2, result: 1 },
|
||||||
|
TestEntry { lhs: 2, rhs: 2, result: 1 },
|
||||||
|
TestEntry { lhs: 3, rhs: 2, result: 2 },
|
||||||
|
TestEntry { lhs: 4, rhs: 2, result: 2 },
|
||||||
|
TestEntry { lhs: 5, rhs: 2, result: 3 },
|
||||||
|
|
||||||
|
// Should be rounded up to 1
|
||||||
|
TestEntry { lhs: 800, rhs: 1500, result: 1 },
|
||||||
|
TestEntry { lhs: 1500, rhs: 3000, result: 1 },
|
||||||
|
|
||||||
|
// Shouldn't be rounded
|
||||||
|
TestEntry { lhs: 0, rhs: 4000, result: 0 },
|
||||||
|
TestEntry { lhs: 1500, rhs: 4000, result: 0 },
|
||||||
|
];
|
||||||
|
|
||||||
|
for test in &tests {
|
||||||
|
let result = test.lhs.div_round(test.rhs);
|
||||||
|
assert_eq!(result, test.result, "{}.div_round({})", test.lhs, test.rhs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Binary file not shown.
Loading…
Reference in a new issue