diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fb95073..61de6952 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) - **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)) +- **MusePack**: Overall improved audio properties ([PR](https://github.com/Serial-ATA/lofty-rs/pull/402)) ## [0.19.2] - 2024-04-26 diff --git a/lofty/src/id3/v2/header.rs b/lofty/src/id3/v2/header.rs index 554df890..c7bf5dd9 100644 --- a/lofty/src/id3/v2/header.rs +++ b/lofty/src/id3/v2/header.rs @@ -44,6 +44,7 @@ pub struct Id3v2TagFlags { pub(crate) struct Id3v2Header { pub version: Id3v2Version, pub flags: Id3v2TagFlags, + /// The size of the tag contents (**DOES NOT INCLUDE THE HEADER/FOOTER**) pub size: u32, pub extended_size: u32, } @@ -140,4 +141,9 @@ impl Id3v2Header { 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 } + } } diff --git a/lofty/src/musepack/constants.rs b/lofty/src/musepack/constants.rs index 96feb14b..5b3aedff 100644 --- a/lofty/src/musepack/constants.rs +++ b/lofty/src/musepack/constants.rs @@ -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 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 +pub(super) const MPC_DECODER_SYNTH_DELAY: u64 = 481; +pub(super) const MPC_FRAME_LENGTH: u64 = 36 * 32; // Samples per mpc frame diff --git a/lofty/src/musepack/read.rs b/lofty/src/musepack/read.rs index 38fed80a..c82535f8 100644 --- a/lofty/src/musepack/read.rs +++ b/lofty/src/musepack/read.rs @@ -32,12 +32,7 @@ where 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); + stream_length -= u64::from(header.full_tag_size()); } // Save the current position, so we can go back and read the properties after the tags diff --git a/lofty/src/musepack/sv4to6/properties.rs b/lofty/src/musepack/sv4to6/properties.rs index 9f5b1c78..1dc38868 100644 --- a/lofty/src/musepack/sv4to6/properties.rs +++ b/lofty/src/musepack/sv4to6/properties.rs @@ -1,8 +1,9 @@ use crate::config::ParsingMode; 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::properties::FileProperties; +use crate::util::math::RoundedDivision; use std::io::Read; use std::time::Duration; @@ -17,7 +18,7 @@ pub struct MpcSv4to6Properties { pub(crate) sample_rate: u32, // NOTE: always 44100 // Fields actually contained in the header - pub(crate) audio_bitrate: u32, + pub(crate) average_bitrate: u32, pub(crate) mid_side_stereo: bool, pub(crate) stream_version: u16, pub(crate) max_band: u8, @@ -28,8 +29,8 @@ 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), + overall_bitrate: Some(input.average_bitrate), + audio_bitrate: Some(input.average_bitrate), sample_rate: Some(input.sample_rate), bit_depth: None, channels: Some(input.channels), @@ -54,9 +55,9 @@ impl MpcSv4to6Properties { self.sample_rate } - /// Audio bitrate (kbps) - pub fn audio_bitrate(&self) -> u32 { - self.audio_bitrate + /// Average bitrate (kbps) + pub fn average_bitrate(&self) -> u32 { + self.average_bitrate } /// Whether MidSideStereo is used @@ -92,7 +93,7 @@ impl MpcSv4to6Properties { 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; 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 } - parse_mode_choice!( - parse_mode, - STRICT: { - if properties.audio_bitrate != 0 { - decode_err!(@BAIL Mpc, "Encountered CBR stream") - } + if parse_mode == ParsingMode::Strict { + if properties.average_bitrate != 0 { + decode_err!(@BAIL Mpc, "Encountered CBR stream") + } - if intensity_stereo { - decode_err!(@BAIL Mpc, "Stream uses intensity stereo coding") - } + 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 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 @@ -135,18 +133,28 @@ impl MpcSv4to6Properties { 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; + // Nothing more we can do + if properties.frame_count == 0 { + return Ok(properties); } + 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) } } diff --git a/lofty/src/musepack/sv7/properties.rs b/lofty/src/musepack/sv7/properties.rs index c2f3a8c5..21c733f3 100644 --- a/lofty/src/musepack/sv7/properties.rs +++ b/lofty/src/musepack/sv7/properties.rs @@ -1,12 +1,14 @@ use crate::error::Result; 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 std::io::Read; use std::time::Duration; -use byteorder::{BigEndian, LittleEndian, ReadBytesExt}; +use byteorder::{LittleEndian, ReadBytesExt}; /// Used profile #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] @@ -104,8 +106,7 @@ impl Link { #[allow(clippy::struct_excessive_bools)] pub struct MpcSv7Properties { pub(crate) duration: Duration, - pub(crate) overall_bitrate: u32, - pub(crate) audio_bitrate: u32, + pub(crate) average_bitrate: u32, pub(crate) channels: u8, // NOTE: always 2 // -- Section 1 -- pub(crate) frame_count: u32, @@ -135,8 +136,8 @@ 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), + overall_bitrate: Some(input.average_bitrate), + audio_bitrate: Some(input.average_bitrate), sample_rate: Some(input.sample_freq), bit_depth: None, channels: Some(input.channels), @@ -151,14 +152,9 @@ impl MpcSv7Properties { 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 + /// Average bitrate (kbps) + pub fn average_bitrate(&self) -> u32 { + self.average_bitrate } /// Sample rate (Hz) @@ -208,7 +204,7 @@ impl MpcSv7Properties { /// 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 { 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 /// - /// 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 { self.album_gain } @@ -280,6 +276,9 @@ impl MpcSv7Properties { ..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 -- properties.frame_count = reader.read_u32::()?; @@ -294,11 +293,8 @@ impl MpcSv7Properties { 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 + properties.profile = Profile::from_u8((byte2 & 0xF0) >> 4).unwrap(); // Infallible + properties.link = Link::from_u8((byte2 & 0x0C) >> 2).unwrap(); // Infallible let sample_freq_index = byte2 & 0x03; properties.sample_freq = FREQUENCY_TABLE[sample_freq_index as usize]; @@ -307,27 +303,24 @@ impl MpcSv7Properties { properties.max_level = remaining_bytes; // -- Section 3 -- - let title_gain = reader.read_i16::()?; - let title_peak = reader.read_u16::()?; + let title_peak = reader.read_u16::()?; + let title_gain = reader.read_u16::()?; // -- Section 4 -- - let album_gain = reader.read_i16::()?; - let album_peak = reader.read_u16::()?; + let album_peak = reader.read_u16::()?; + let album_gain = 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; + properties.true_gapless = (chunk >> 31) == 1; if properties.true_gapless { - properties.last_frame_length = - (u16::from(byte1 & 0x7F) << 4) | u16::from((byte2 & 0xF0) >> 4); + properties.last_frame_length = ((chunk >> 20) & 0x7FF) as u16; } + properties.fast_seeking_safe = (chunk >> 19) & 1 == 1; + // NOTE: Rest of the chunk is zeroed and unused // -- Section 6 -- @@ -336,19 +329,23 @@ impl MpcSv7Properties { // -- 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 + let set_replay_gain = |gain: u16| -> i16 { + if gain == 0 { + return 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 { if peak == 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); @@ -356,22 +353,36 @@ impl MpcSv7Properties { 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 properties.last_frame_length > MPC_FRAME_LENGTH as u16 { + decode_err!(@BAIL Mpc, "Invalid last frame length"); } - 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; + if properties.sample_freq == 0 { + log::warn!("Sample rate is 0, unable to calculate duration and bitrate"); + return Ok(properties); } + 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) } } diff --git a/lofty/src/musepack/sv8/properties.rs b/lofty/src/musepack/sv8/properties.rs index 4ad1fa18..bf8639b8 100644 --- a/lofty/src/musepack/sv8/properties.rs +++ b/lofty/src/musepack/sv8/properties.rs @@ -1,8 +1,10 @@ use super::read::PacketReader; use crate::config::ParsingMode; use crate::error::Result; +use crate::macros::decode_err; use crate::musepack::constants::FREQUENCY_TABLE; use crate::properties::FileProperties; +use crate::util::math::RoundedDivision; use std::io::Read; use std::time::Duration; @@ -13,8 +15,7 @@ use byteorder::{BigEndian, ReadBytesExt}; #[derive(Debug, Clone, PartialEq, Default)] pub struct MpcSv8Properties { pub(crate) duration: Duration, - pub(crate) overall_bitrate: u32, - pub(crate) audio_bitrate: u32, + pub(crate) average_bitrate: u32, /// Mandatory Stream Header packet pub stream_header: StreamHeader, /// Mandatory ReplayGain packet @@ -27,8 +28,8 @@ 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), + overall_bitrate: Some(input.average_bitrate), + audio_bitrate: Some(input.average_bitrate), sample_rate: Some(input.stream_header.sample_rate), bit_depth: None, channels: Some(input.stream_header.channels), @@ -43,14 +44,9 @@ impl MpcSv8Properties { 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 + /// Average bitrate (kbps) + pub fn average_bitrate(&self) -> u32 { + self.average_bitrate } /// 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, +) -> Result { + 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) +} diff --git a/lofty/src/musepack/sv8/read.rs b/lofty/src/musepack/sv8/read.rs index 93c745b0..10e8452f 100644 --- a/lofty/src/musepack/sv8/read.rs +++ b/lofty/src/musepack/sv8/read.rs @@ -4,7 +4,6 @@ use crate::error::{ErrorKind, LoftyError, Result}; use crate::macros::{decode_err, parse_mode_choice}; use std::io::Read; -use std::time::Duration; use byteorder::ReadBytesExt; @@ -12,6 +11,7 @@ use byteorder::ReadBytesExt; const STREAM_HEADER_KEY: [u8; 2] = *b"SH"; const REPLAYGAIN_KEY: [u8; 2] = *b"RG"; const ENCODER_INFO_KEY: [u8; 2] = *b"EI"; +#[allow(dead_code)] const AUDIO_PACKET_KEY: [u8; 2] = *b"AP"; const STREAM_END_KEY: [u8; 2] = *b"SE"; @@ -29,13 +29,12 @@ where let mut found_stream_end = false; while let Ok((packet_id, packet_length)) = packet_reader.next() { + stream_length += packet_length; + 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; @@ -68,41 +67,16 @@ where }, }; - if stream_length == 0 { - parse_mode_choice!( - parse_mode, - STRICT: decode_err!(@BAIL Mpc, "File is missing an Audio packet"), - ) + if stream_length == 0 && parse_mode == ParsingMode::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"), - ) + if !found_stream_end && parse_mode == ParsingMode::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; - } + let properties = + super::properties::read(stream_length, stream_header, replay_gain, encoder_info)?; Ok(properties) } diff --git a/lofty/src/properties/tests.rs b/lofty/src/properties/tests.rs index 8f26af86..1940d224 100644 --- a/lofty/src/properties/tests.rs +++ b/lofty/src/properties/tests.rs @@ -160,9 +160,10 @@ const MP4_FLAC_PROPERTIES: Mp4Properties = Mp4Properties { drm_protected: false, }; +// Properties verified with libmpcdec 1.2.2 const MPC_SV5_PROPERTIES: MpcSv4to6Properties = MpcSv4to6Properties { - duration: Duration::from_millis(27), - audio_bitrate: 41, + duration: Duration::from_millis(26347), + average_bitrate: 119, channels: 2, frame_count: 1009, mid_side_stereo: true, @@ -172,9 +173,8 @@ const MPC_SV5_PROPERTIES: MpcSv4to6Properties = MpcSv4to6Properties { }; const MPC_SV7_PROPERTIES: MpcSv7Properties = MpcSv7Properties { - duration: Duration::from_millis(1428), - overall_bitrate: 86, - audio_bitrate: 86, + duration: Duration::from_millis(1440), + average_bitrate: 86, channels: 2, frame_count: 60, intensity_stereo: false, @@ -184,9 +184,9 @@ const MPC_SV7_PROPERTIES: MpcSv7Properties = MpcSv7Properties { link: Link::VeryLowStartOrEnd, sample_freq: 48000, max_level: 0, - title_gain: 16594, + title_gain: 0, title_peak: 0, - album_gain: 16594, + album_gain: 0, album_peak: 0, true_gapless: true, last_frame_length: 578, @@ -196,8 +196,7 @@ const MPC_SV7_PROPERTIES: MpcSv7Properties = MpcSv7Properties { const MPC_SV8_PROPERTIES: MpcSv8Properties = MpcSv8Properties { duration: Duration::from_millis(1428), - overall_bitrate: 82, - audio_bitrate: 82, + average_bitrate: 82, stream_header: StreamHeader { crc: 4_252_559_415, stream_version: 8, diff --git a/lofty/src/util/math.rs b/lofty/src/util/math.rs index 1326db98..2b535848 100644 --- a/lofty/src/util/math.rs +++ b/lofty/src/util/math.rs @@ -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 { 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); + } + } +} diff --git a/lofty/tests/files/assets/minimal/mpc_sv5.mpc b/lofty/tests/files/assets/minimal/mpc_sv5.mpc index d94a9027..0978d4f0 100644 Binary files a/lofty/tests/files/assets/minimal/mpc_sv5.mpc and b/lofty/tests/files/assets/minimal/mpc_sv5.mpc differ