diff --git a/lofty/src/aac/properties.rs b/lofty/src/aac/properties.rs index 40c0b6e4c..11f7d2c9a 100644 --- a/lofty/src/aac/properties.rs +++ b/lofty/src/aac/properties.rs @@ -94,7 +94,8 @@ impl From for FileProperties { bit_depth: None, channels: Some(input.channels), channel_mask: input.channel_mask, - } + bitrate_mode: None, +} } } diff --git a/lofty/src/ape/properties.rs b/lofty/src/ape/properties.rs index fcaf47cb6..fbdc2e81b 100644 --- a/lofty/src/ape/properties.rs +++ b/lofty/src/ape/properties.rs @@ -31,7 +31,8 @@ impl From for FileProperties { bit_depth: Some(input.bit_depth), channels: Some(input.channels), channel_mask: None, - } + bitrate_mode: Some(crate::properties::BitrateMode::Vbr), +} } } diff --git a/lofty/src/flac/properties.rs b/lofty/src/flac/properties.rs index 106246720..0c111108e 100644 --- a/lofty/src/flac/properties.rs +++ b/lofty/src/flac/properties.rs @@ -29,7 +29,8 @@ impl From for FileProperties { bit_depth: Some(input.bit_depth), channels: Some(input.channels), channel_mask: None, - } + bitrate_mode: Some(crate::properties::BitrateMode::Vbr), +} } } diff --git a/lofty/src/iff/aiff/properties.rs b/lofty/src/iff/aiff/properties.rs index 9b1236760..67782bb59 100644 --- a/lofty/src/iff/aiff/properties.rs +++ b/lofty/src/iff/aiff/properties.rs @@ -111,7 +111,8 @@ impl From for FileProperties { bit_depth: Some(value.sample_size as u8), channels: Some(value.channels as u8), channel_mask: None, - } + bitrate_mode: Some(crate::properties::BitrateMode::Cbr), +} } } diff --git a/lofty/src/iff/wav/properties.rs b/lofty/src/iff/wav/properties.rs index 31c5b64cd..aa6e38654 100644 --- a/lofty/src/iff/wav/properties.rs +++ b/lofty/src/iff/wav/properties.rs @@ -63,7 +63,8 @@ impl From for FileProperties { bit_depth: Some(bit_depth), channels: Some(channels), channel_mask, - } + bitrate_mode: Some(crate::properties::BitrateMode::Cbr), +} } } diff --git a/lofty/src/mp4/ilst/write.rs b/lofty/src/mp4/ilst/write.rs index faba3dcea..cb639e68b 100644 --- a/lofty/src/mp4/ilst/write.rs +++ b/lofty/src/mp4/ilst/write.rs @@ -230,17 +230,7 @@ fn save_to_existing( ParseOptions::DEFAULT_PARSING_MODE, )?; - if tree.is_empty() { - // Nothing to do - if remove_tag { - return Ok(()); - } - - let meta_end = (meta.start + meta.len) as usize; - - replacement = ilst; - range = meta_end..meta_end; - } else { + if let Some(ilst_idx) = ilst_idx { let existing_ilst = &tree[ilst_idx]; let existing_ilst_size = existing_ilst.len; @@ -315,6 +305,16 @@ fn save_to_existing( replacement = ilst; range = range_start as usize..range_end as usize; } + } else { + // Nothing to do + if remove_tag { + return Ok(()); + } + + let meta_end = (meta.start + meta.len) as usize; + + replacement = ilst; + range = meta_end..meta_end; } drop(write_handle); diff --git a/lofty/src/mp4/properties.rs b/lofty/src/mp4/properties.rs index 144fb42f6..3b7cbf711 100644 --- a/lofty/src/mp4/properties.rs +++ b/lofty/src/mp4/properties.rs @@ -204,7 +204,8 @@ impl From for FileProperties { bit_depth: input.bit_depth, channels: Some(input.channels), channel_mask: None, - } + bitrate_mode: None, +} } } diff --git a/lofty/src/mp4/read/mod.rs b/lofty/src/mp4/read/mod.rs index f55bbb60a..3ff9fec56 100644 --- a/lofty/src/mp4/read/mod.rs +++ b/lofty/src/mp4/read/mod.rs @@ -156,11 +156,11 @@ pub(super) fn atom_tree( mut len: u64, up_to: &[u8], parse_mode: ParsingMode, -) -> Result<(usize, Vec)> +) -> Result<(Option, Vec)> where R: Read + Seek, { - let mut found_idx: usize = 0; + let mut found_idx: Option = None; let mut buf = Vec::new(); let mut i = 0; @@ -174,18 +174,15 @@ where len = len.saturating_sub(atom.len); if let AtomIdent::Fourcc(ref fourcc) = atom.ident { - i += 1; - if fourcc == up_to { - found_idx = i; + found_idx = Some(i); } + i += 1; buf.push(atom); } } - found_idx = found_idx.saturating_sub(1); - Ok((found_idx, buf)) } diff --git a/lofty/src/mpeg/properties.rs b/lofty/src/mpeg/properties.rs index 288c79098..3763d3cd2 100644 --- a/lofty/src/mpeg/properties.rs +++ b/lofty/src/mpeg/properties.rs @@ -23,6 +23,7 @@ pub struct MpegProperties { pub(crate) copyright: bool, pub(crate) original: bool, pub(crate) emphasis: Option, + pub(crate) bitrate_mode: Option, } impl From for FileProperties { @@ -40,6 +41,7 @@ impl From for FileProperties { emphasis: _, mode_extension: _, original: _, + bitrate_mode, } = input; let channel_mask = match channel_mode { ChannelMode::SingleChannel => Some(ChannelMask::mono()), @@ -54,6 +56,7 @@ impl From for FileProperties { bit_depth: None, channels: Some(channels), channel_mask, + bitrate_mode, } } } @@ -118,6 +121,11 @@ impl MpegProperties { pub fn emphasis(&self) -> Option { self.emphasis } + + /// Bitrate mode of the audio + pub fn bitrate_mode(&self) -> Option { + self.bitrate_mode + } } pub(super) fn read_properties( @@ -136,6 +144,11 @@ where properties.version = first_frame_header.version; properties.layer = first_frame_header.layer; + properties.bitrate_mode = match vbr_header.as_ref().map(|h| h.ty) { + Some(VbrHeaderType::Info) => Some(crate::properties::BitrateMode::Cbr), + Some(VbrHeaderType::Xing | VbrHeaderType::Vbri) => Some(crate::properties::BitrateMode::Vbr), + None => Some(crate::properties::BitrateMode::Cbr), + }; properties.channel_mode = first_frame_header.channel_mode; properties.mode_extension = first_frame_header.mode_extension; properties.copyright = first_frame_header.copyright; diff --git a/lofty/src/musepack/sv4to6/properties.rs b/lofty/src/musepack/sv4to6/properties.rs index 1dc388685..7a21154fd 100644 --- a/lofty/src/musepack/sv4to6/properties.rs +++ b/lofty/src/musepack/sv4to6/properties.rs @@ -35,7 +35,8 @@ impl From for FileProperties { bit_depth: None, channels: Some(input.channels), channel_mask: None, - } + bitrate_mode: Some(crate::properties::BitrateMode::Vbr), +} } } diff --git a/lofty/src/musepack/sv7/properties.rs b/lofty/src/musepack/sv7/properties.rs index 21c733f3c..6c6b30fd8 100644 --- a/lofty/src/musepack/sv7/properties.rs +++ b/lofty/src/musepack/sv7/properties.rs @@ -142,7 +142,8 @@ impl From for FileProperties { bit_depth: None, channels: Some(input.channels), channel_mask: None, - } + bitrate_mode: Some(crate::properties::BitrateMode::Vbr), +} } } diff --git a/lofty/src/musepack/sv8/properties.rs b/lofty/src/musepack/sv8/properties.rs index 4ddc09765..48823d899 100644 --- a/lofty/src/musepack/sv8/properties.rs +++ b/lofty/src/musepack/sv8/properties.rs @@ -34,7 +34,8 @@ impl From for FileProperties { bit_depth: None, channels: Some(input.stream_header.channels), channel_mask: None, - } + bitrate_mode: Some(crate::properties::BitrateMode::Vbr), +} } } diff --git a/lofty/src/ogg/opus/properties.rs b/lofty/src/ogg/opus/properties.rs index 4475c761c..229996014 100644 --- a/lofty/src/ogg/opus/properties.rs +++ b/lofty/src/ogg/opus/properties.rs @@ -37,6 +37,7 @@ impl From for FileProperties { } else { Some(input.channel_mask) }, + bitrate_mode: Some(crate::properties::BitrateMode::Vbr), } } } diff --git a/lofty/src/ogg/speex/properties.rs b/lofty/src/ogg/speex/properties.rs index 9f7189691..445fb90cd 100644 --- a/lofty/src/ogg/speex/properties.rs +++ b/lofty/src/ogg/speex/properties.rs @@ -35,7 +35,8 @@ impl From for FileProperties { bit_depth: None, channels: Some(input.channels), channel_mask: None, - } + bitrate_mode: Some(if input.vbr { crate::properties::BitrateMode::Vbr } else { crate::properties::BitrateMode::Cbr }), +} } } diff --git a/lofty/src/ogg/vorbis/properties.rs b/lofty/src/ogg/vorbis/properties.rs index 1d00dce0d..1a1c4a8bf 100644 --- a/lofty/src/ogg/vorbis/properties.rs +++ b/lofty/src/ogg/vorbis/properties.rs @@ -34,7 +34,8 @@ impl From for FileProperties { bit_depth: None, channels: Some(input.channels), channel_mask: None, - } + bitrate_mode: Some(crate::properties::BitrateMode::Vbr), +} } } diff --git a/lofty/src/properties/file_properties.rs b/lofty/src/properties/file_properties.rs index 9d34d8e4c..515d475ac 100644 --- a/lofty/src/properties/file_properties.rs +++ b/lofty/src/properties/file_properties.rs @@ -1,6 +1,16 @@ use super::channel_mask::ChannelMask; use std::time::Duration; +/// Bitrate mode of an audio file +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[non_exhaustive] +pub enum BitrateMode { + /// Constant Bitrate (CBR) + Cbr, + /// Variable Bitrate (VBR) + Vbr, +} + /// Various *immutable* audio properties #[derive(Debug, PartialEq, Eq, Clone)] #[non_exhaustive] @@ -12,6 +22,7 @@ pub struct FileProperties { pub(crate) bit_depth: Option, pub(crate) channels: Option, pub(crate) channel_mask: Option, + pub(crate) bitrate_mode: Option, } impl Default for FileProperties { @@ -24,6 +35,7 @@ impl Default for FileProperties { bit_depth: None, channels: None, channel_mask: None, + bitrate_mode: None, } } } @@ -39,6 +51,7 @@ impl FileProperties { bit_depth: Option, channels: Option, channel_mask: Option, + bitrate_mode: Option, ) -> Self { Self { duration, @@ -48,6 +61,7 @@ impl FileProperties { bit_depth, channels, channel_mask, + bitrate_mode, } } @@ -86,6 +100,11 @@ impl FileProperties { self.channel_mask } + /// Bitrate mode of the audio + pub fn bitrate_mode(&self) -> Option { + self.bitrate_mode + } + /// Used for tests #[doc(hidden)] pub fn is_empty(&self) -> bool { @@ -99,6 +118,7 @@ impl FileProperties { bit_depth: None | Some(0), channels: None | Some(0), channel_mask: None, + bitrate_mode: None, } ) } diff --git a/lofty/src/properties/mod.rs b/lofty/src/properties/mod.rs index 84cb263a3..2a623292e 100644 --- a/lofty/src/properties/mod.rs +++ b/lofty/src/properties/mod.rs @@ -11,4 +11,4 @@ mod file_properties; mod tests; pub use channel_mask::ChannelMask; -pub use file_properties::FileProperties; +pub use file_properties::{BitrateMode, FileProperties}; diff --git a/lofty/src/wavpack/properties.rs b/lofty/src/wavpack/properties.rs index 72aa55384..2a73dec23 100644 --- a/lofty/src/wavpack/properties.rs +++ b/lofty/src/wavpack/properties.rs @@ -37,6 +37,7 @@ impl From for FileProperties { } else { Some(input.channel_mask) }, + bitrate_mode: Some(crate::properties::BitrateMode::Vbr), } } } diff --git a/lofty/tests/files/mp4.rs b/lofty/tests/files/mp4.rs index 08f7a8ad0..7d41602b6 100644 --- a/lofty/tests/files/mp4.rs +++ b/lofty/tests/files/mp4.rs @@ -73,3 +73,33 @@ fn read_no_properties() { fn read_no_tags() { crate::util::no_tag_test("tests/files/assets/minimal/m4a_codec_aac.m4a", None); } + +#[test_log::test] +fn hdlr_preservation_on_tag_recreation() { + use lofty::tag::Tag; + use std::fs; + + let src = "tests/files/assets/minimal/m4a_codec_aac.m4a"; + let temp_path = "tests/files/assets/minimal/temp_hdlr_test.m4a"; + let _ = fs::remove_file(temp_path); + fs::copy(src, temp_path).unwrap(); + + // 1. Remove all tags + TagType::Mp4Ilst.remove_from_path(temp_path).unwrap(); + + // 2. Open the file and write a brand new tag + let mut tagged_file = Probe::open(temp_path).unwrap().read().unwrap(); + let tag = Tag::new(TagType::Mp4Ilst); + tagged_file.insert_tag(tag); + tagged_file + .save_to_path(temp_path, lofty::config::WriteOptions::default()) + .unwrap(); + + // 3. Read the file bytes and verify that "hdlr" is present + let bytes = fs::read(temp_path).unwrap(); + let has_hdlr = bytes.windows(4).any(|w| w == b"hdlr"); + + let _ = fs::remove_file(temp_path); + + assert!(has_hdlr, "Output M4A is missing the hdlr atom!"); +}