From fda2eb2c90a879dfa774bbdab40eb33f5f253436 Mon Sep 17 00:00:00 2001 From: axel10 Date: Sun, 24 May 2026 20:16:06 +0800 Subject: [PATCH 1/6] fix(mp4): prevent overwriting `hdlr` atom when writing new tags to files with empty `ilst` --- lofty/src/mp4/ilst/write.rs | 3 ++- lofty/src/mp4/read/mod.rs | 11 ++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/lofty/src/mp4/ilst/write.rs b/lofty/src/mp4/ilst/write.rs index faba3dcea..90b58c147 100644 --- a/lofty/src/mp4/ilst/write.rs +++ b/lofty/src/mp4/ilst/write.rs @@ -230,7 +230,7 @@ fn save_to_existing( ParseOptions::DEFAULT_PARSING_MODE, )?; - if tree.is_empty() { + if ilst_idx.is_none() { // Nothing to do if remove_tag { return Ok(()); @@ -241,6 +241,7 @@ fn save_to_existing( replacement = ilst; range = meta_end..meta_end; } else { + let ilst_idx = ilst_idx.unwrap(); let existing_ilst = &tree[ilst_idx]; let existing_ilst_size = existing_ilst.len; 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)) } From ab5b0173babed95ca6cc2bb77a3cbd9c083536c6 Mon Sep 17 00:00:00 2001 From: axel10 Date: Sun, 24 May 2026 20:44:23 +0800 Subject: [PATCH 2/6] test: add unit tests for MP4 file reading, writing, removal, and atom preservation --- lofty/tests/files/mp4.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/lofty/tests/files/mp4.rs b/lofty/tests/files/mp4.rs index 08f7a8ad0..0db728090 100644 --- a/lofty/tests/files/mp4.rs +++ b/lofty/tests/files/mp4.rs @@ -73,3 +73,32 @@ 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 std::fs; + use lofty::tag::Tag; + + 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!"); +} + From ac04da5f8c86d685e7260026befc7138e2e89f9a Mon Sep 17 00:00:00 2001 From: axel10 Date: Sun, 24 May 2026 21:00:14 +0800 Subject: [PATCH 3/6] fix(mp4): resolve clippy unnecessary-unwrap warning in ilst write --- lofty/src/mp4/ilst/write.rs | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/lofty/src/mp4/ilst/write.rs b/lofty/src/mp4/ilst/write.rs index 90b58c147..cb639e68b 100644 --- a/lofty/src/mp4/ilst/write.rs +++ b/lofty/src/mp4/ilst/write.rs @@ -230,18 +230,7 @@ fn save_to_existing( ParseOptions::DEFAULT_PARSING_MODE, )?; - if ilst_idx.is_none() { - // 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 { - let ilst_idx = ilst_idx.unwrap(); + if let Some(ilst_idx) = ilst_idx { let existing_ilst = &tree[ilst_idx]; let existing_ilst_size = existing_ilst.len; @@ -316,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); From 8b994f9f528a22015e180075913b9cf4dd92bb26 Mon Sep 17 00:00:00 2001 From: axel10 Date: Mon, 25 May 2026 06:58:46 +0800 Subject: [PATCH 4/6] fix(fmt): normalize mp4 test formatting --- lofty/tests/files/mp4.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lofty/tests/files/mp4.rs b/lofty/tests/files/mp4.rs index 0db728090..7d41602b6 100644 --- a/lofty/tests/files/mp4.rs +++ b/lofty/tests/files/mp4.rs @@ -76,8 +76,8 @@ fn read_no_tags() { #[test_log::test] fn hdlr_preservation_on_tag_recreation() { - use std::fs; 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"; @@ -91,7 +91,9 @@ fn hdlr_preservation_on_tag_recreation() { 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(); + 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(); @@ -101,4 +103,3 @@ fn hdlr_preservation_on_tag_recreation() { assert!(has_hdlr, "Output M4A is missing the hdlr atom!"); } - From 6370589b182298f77cb865ae31deffb1633150ae Mon Sep 17 00:00:00 2001 From: axel10 Date: Mon, 25 May 2026 16:09:38 +0800 Subject: [PATCH 5/6] =?UTF-8?q?feat(properties):=20add=20bitrate=5Fmode=20?= =?UTF-8?q?to=20various=20audio=20properties=20=E9=9F=B3=E9=A2=91=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E6=B7=BB=E5=8A=A0=E6=AF=94=E7=89=B9=E7=8E=87=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lofty/src/aac/properties.rs | 3 ++- lofty/src/ape/properties.rs | 3 ++- lofty/src/flac/properties.rs | 3 ++- lofty/src/iff/aiff/properties.rs | 3 ++- lofty/src/iff/wav/properties.rs | 3 ++- lofty/src/mp4/properties.rs | 3 ++- lofty/src/mpeg/properties.rs | 13 +++++++++++++ lofty/src/musepack/sv4to6/properties.rs | 3 ++- lofty/src/musepack/sv7/properties.rs | 3 ++- lofty/src/musepack/sv8/properties.rs | 3 ++- lofty/src/ogg/opus/properties.rs | 1 + lofty/src/ogg/speex/properties.rs | 3 ++- lofty/src/ogg/vorbis/properties.rs | 3 ++- lofty/src/properties/file_properties.rs | 20 ++++++++++++++++++++ lofty/src/properties/mod.rs | 2 +- lofty/src/wavpack/properties.rs | 1 + 16 files changed, 58 insertions(+), 12 deletions(-) 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/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/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..5bf765bee 100644 --- a/lofty/src/properties/file_properties.rs +++ b/lofty/src/properties/file_properties.rs @@ -1,5 +1,15 @@ 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)] @@ -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), } } } From b35c60319a580c2ac2ed44f97e053305145b0e8c Mon Sep 17 00:00:00 2001 From: axel10 Date: Mon, 25 May 2026 16:43:40 +0800 Subject: [PATCH 6/6] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E6=8A=A5=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lofty/src/properties/file_properties.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lofty/src/properties/file_properties.rs b/lofty/src/properties/file_properties.rs index 5bf765bee..515d475ac 100644 --- a/lofty/src/properties/file_properties.rs +++ b/lofty/src/properties/file_properties.rs @@ -1,6 +1,6 @@ use super::channel_mask::ChannelMask; use std::time::Duration; -音频信息添加比特率模式 + /// Bitrate mode of an audio file #[derive(Debug, PartialEq, Eq, Clone, Copy)] #[non_exhaustive]