Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

APE: Fix property reading on old stream versions #245

Merged
merged 1 commit into from
Jul 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **WavPack**: Custom sample rates will no longer be overwritten
- When a custom sample rate (or multiplier) was encountered, it would accidentally be overwritten with 0, causing
incorrect duration and bitrate values.
- **APE**: Reading properties on older files will no longer error
- Older APE stream versions were not properly handled, leading to incorrect properties and errors.

## [0.15.0] - 2023-07-11

Expand Down
147 changes: 66 additions & 81 deletions src/ape/properties.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::error::Result;
use crate::macros::decode_err;
use crate::probe::ParsingMode;
use crate::properties::FileProperties;

use std::convert::TryInto;
Expand Down Expand Up @@ -76,6 +77,7 @@ pub(super) fn read_properties<R>(
data: &mut R,
stream_len: u64,
file_length: u64,
parse_mode: ParsingMode,
) -> Result<ApeProperties>
where
R: Read + Seek,
Expand All @@ -86,9 +88,9 @@ where

// Property reading differs between versions
if version >= 3980 {
properties_gt_3980(data, version, stream_len, file_length)
properties_gt_3980(data, version, stream_len, file_length, parse_mode)
} else {
properties_lt_3980(data, version, stream_len, file_length)
properties_lt_3980(data, version, stream_len, file_length, parse_mode)
}
}

Expand All @@ -97,6 +99,7 @@ fn properties_gt_3980<R>(
version: u16,
stream_len: u64,
file_length: u64,
parse_mode: ParsingMode,
) -> Result<ApeProperties>
where
R: Read + Seek,
Expand Down Expand Up @@ -126,6 +129,9 @@ where
data.read_exact(&mut header)
.map_err(|_| decode_err!(Ape, "Not enough data left in reader to finish MAC header"))?;

let mut properties = ApeProperties::default();
properties.version = version;

// Skip the first 4 bytes of the header
// Compression type (2)
// Format flags (2)
Expand All @@ -135,72 +141,57 @@ where
let final_frame_blocks = header_read.read_u32::<LittleEndian>()?;
let total_frames = header_read.read_u32::<LittleEndian>()?;

if total_frames == 0 {
decode_err!(@BAIL Ape, "File contains no frames");
}

let bits_per_sample = header_read.read_u16::<LittleEndian>()?;
properties.bit_depth = header_read.read_u16::<LittleEndian>()? as u8;
properties.channels = header_read.read_u16::<LittleEndian>()? as u8;
properties.sample_rate = header_read.read_u32::<LittleEndian>()?;

let channels = header_read.read_u16::<LittleEndian>()?;

if !(1..=32).contains(&channels) {
decode_err!(@BAIL Ape, "File has an invalid channel count (must be between 1 and 32 inclusive)");
match verify(total_frames, properties.channels) {
Err(e) if parse_mode == ParsingMode::Strict => return Err(e),
Err(_) => return Ok(properties),
_ => {},
}

let sample_rate = header_read.read_u32::<LittleEndian>()?;

let (duration, overall_bitrate, audio_bitrate) = get_duration_bitrate(
get_duration_bitrate(
&mut properties,
file_length,
total_frames,
final_frame_blocks,
blocks_per_frame,
sample_rate,
stream_len,
);

Ok(ApeProperties {
version,
duration,
overall_bitrate,
audio_bitrate,
sample_rate,
bit_depth: bits_per_sample as u8,
channels: channels as u8,
})
Ok(properties)
}

fn properties_lt_3980<R>(
data: &mut R,
version: u16,
stream_len: u64,
file_length: u64,
parse_mode: ParsingMode,
) -> Result<ApeProperties>
where
R: Read + Seek,
R: Read,
{
// Versions < 3980 don't have a descriptor
let mut header = [0; 26];
data.read_exact(&mut header)
.map_err(|_| decode_err!(Ape, "Not enough data left in reader to finish MAC header"))?;

// We don't need all the header data, so just make 2 slices
let header_first = &mut &header[..8];
let mut properties = ApeProperties::default();
properties.version = version;

// Skipping 8 bytes
// WAV header length (4)
// WAV tail length (4)
let header_second = &mut &header[18..];
let header_reader = &mut &header[..];

let compression_level = header_first.read_u16::<LittleEndian>()?;

let format_flags = header_first.read_u16::<LittleEndian>()?;
// https://github.com/fernandotcl/monkeys-audio/blob/5fe956c7e67c13daa80518a4cc7001e9fa185297/src/MACLib/MACLib.h#L74
let bit_depth = if (format_flags & 0b1) == 1 {
8
} else if (format_flags & 0b100) == 4 {
24
let compression_level = header_reader.read_u16::<LittleEndian>()?;
let format_flags = header_reader.read_u16::<LittleEndian>()?;
if (format_flags & 0b1) == 1 {
properties.bit_depth = 8
} else if (format_flags & 0b1000) == 8 {
properties.bit_depth = 24
} else {
16
properties.bit_depth = 16
};

let blocks_per_frame = match version {
Expand All @@ -209,74 +200,68 @@ where
_ => 9216,
};

let channels = header_first.read_u16::<LittleEndian>()?;

if !(1..=32).contains(&channels) {
decode_err!(@BAIL Ape, "File has an invalid channel count (must be between 1 and 32 inclusive)");
}
properties.channels = header_reader.read_u16::<LittleEndian>()? as u8;
properties.sample_rate = header_reader.read_u32::<LittleEndian>()?;

let sample_rate = header_first.read_u32::<LittleEndian>()?;
// Skipping 8 bytes
// WAV header length (4)
// WAV tail length (4)
let mut _skip = [0; 8];
header_reader.read_exact(&mut _skip)?;

// Move on the second part of header
let total_frames = header_second.read_u32::<LittleEndian>()?;
let total_frames = header_reader.read_u32::<LittleEndian>()?;
let final_frame_blocks = header_reader.read_u32::<LittleEndian>()?;

if total_frames == 0 {
decode_err!(@BAIL Ape, "File contains no frames");
match verify(total_frames, properties.channels) {
Err(e) if parse_mode == ParsingMode::Strict => return Err(e),
Err(_) => return Ok(properties),
_ => {},
}

let final_frame_blocks = data.read_u32::<LittleEndian>()?;

let (duration, overall_bitrate, audio_bitrate) = get_duration_bitrate(
get_duration_bitrate(
&mut properties,
file_length,
total_frames,
final_frame_blocks,
blocks_per_frame,
sample_rate,
stream_len,
);

Ok(ApeProperties {
version,
duration,
overall_bitrate,
audio_bitrate,
sample_rate,
bit_depth,
channels: channels as u8,
})
Ok(properties)
}

/// Verifies the channel count falls within the bounds of the spec, and we have some audio frames to work with.
fn verify(total_frames: u32, channels: u8) -> Result<()> {
if !(1..=32).contains(&channels) {
decode_err!(@BAIL Ape, "File has an invalid channel count (must be between 1 and 32 inclusive)");
}

if total_frames == 0 {
decode_err!(@BAIL Ape, "File contains no frames");
}

Ok(())
}

fn get_duration_bitrate(
properties: &mut ApeProperties,
file_length: u64,
total_frames: u32,
final_frame_blocks: u32,
blocks_per_frame: u32,
sample_rate: u32,
stream_len: u64,
) -> (Duration, u32, u32) {
) {
let mut total_samples = u64::from(final_frame_blocks);

if total_samples > 1 {
total_samples += u64::from(blocks_per_frame) * u64::from(total_frames - 1)
}

let mut overall_bitrate = 0;
let mut audio_bitrate = 0;

if sample_rate > 0 {
let length = (total_samples * 1000) / u64::from(sample_rate);
if properties.sample_rate > 0 {
let length = (total_samples as f64 * 1000.0) / f64::from(properties.sample_rate);

if length > 0 {
overall_bitrate = crate::div_ceil(file_length * 8, length) as u32;
audio_bitrate = crate::div_ceil(stream_len * 8, length) as u32;
}

(
Duration::from_millis(length),
overall_bitrate,
audio_bitrate,
)
} else {
(Duration::ZERO, overall_bitrate, audio_bitrate)
properties.duration = Duration::from_millis((length + 0.5) as u64);
properties.audio_bitrate = ((stream_len as f64) * 8.0 / length + 0.5) as u32;
properties.overall_bitrate = ((file_length as f64) * 8.0 / length + 0.5) as u32;
}
}
7 changes: 6 additions & 1 deletion src/ape/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,12 @@ where
id3v2_tag,
ape_tag,
properties: if parse_options.read_properties {
super::properties::read_properties(data, stream_len, file_length)?
super::properties::read_properties(
data,
stream_len,
file_length,
parse_options.parsing_mode,
)?
} else {
ApeProperties::default()
},
Expand Down
16 changes: 3 additions & 13 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,9 @@
clippy::field_reassign_with_default,
clippy::manual_range_patterns, /* This is not at all clearer as it suggests */
clippy::explicit_iter_loop,
clippy::from_iter_instead_of_collect
clippy::from_iter_instead_of_collect,
clippy::no_effect_underscore_binding,
clippy::used_underscore_binding,
)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]

Expand Down Expand Up @@ -183,15 +185,3 @@ pub use crate::traits::{Accessor, MergeTag, SplitTag, TagExt};
pub use picture::PictureInformation;

pub use lofty_attr::LoftyFile;

// TODO: https://github.com/rust-lang/rust/issues/88581
#[inline]
pub(crate) const fn div_ceil(dividend: u64, divisor: u64) -> u64 {
let d = dividend / divisor;
let r = dividend % divisor;
if r > 0 && divisor > 0 {
d + 1
} else {
d
}
}
2 changes: 1 addition & 1 deletion src/properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ mod tests {
version: 3990,
duration: Duration::from_millis(1428),
overall_bitrate: 361,
audio_bitrate: 361,
audio_bitrate: 360,
sample_rate: 48000,
bit_depth: 16,
channels: 2,
Expand Down
8 changes: 5 additions & 3 deletions tests/files/zero_sized.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ use lofty::iff::aiff::AiffFile;
use lofty::iff::wav::WavFile;
use lofty::mp4::Mp4File;
use lofty::mpeg::MpegFile;
use lofty::{AudioFile, ParseOptions};
use lofty::{AudioFile, ParseOptions, ParsingMode};

fn read_file_with_properties<A: AudioFile>(path: &str) -> bool {
let res =
<A as AudioFile>::read_from(&mut std::fs::File::open(path).unwrap(), ParseOptions::new());
let res = <A as AudioFile>::read_from(
&mut std::fs::File::open(path).unwrap(),
ParseOptions::new().parsing_mode(ParsingMode::Strict),
);
res.is_ok()
}

Expand Down