use crate::id3::specs::{FrameFlags, FrameHeader, FrameProcessor, ID3TimeStamp, TextEncoding};
use crate::id3::util::JunkFrameRecovery;
use crate::{AudexError, Result};
use std::any::Any;
use std::fmt;
pub const ID3V1_GENRES: &[&str] = &[
"Blues",
"Classic Rock",
"Country",
"Dance",
"Disco",
"Funk",
"Grunge",
"Hip-Hop",
"Jazz",
"Metal",
"New Age",
"Oldies",
"Other",
"Pop",
"R&B",
"Rap",
"Reggae",
"Rock",
"Techno",
"Industrial",
"Alternative",
"Ska",
"Death Metal",
"Pranks",
"Soundtrack",
"Euro-Techno",
"Ambient",
"Trip-Hop",
"Vocal",
"Jazz+Funk",
"Fusion",
"Trance",
"Classical",
"Instrumental",
"Acid",
"House",
"Game",
"Sound Clip",
"Gospel",
"Noise",
"Alt. Rock",
"Bass",
"Soul",
"Punk",
"Space",
"Meditative",
"Instrumental Pop",
"Instrumental Rock",
"Ethnic",
"Gothic",
"Darkwave",
"Techno-Industrial",
"Electronic",
"Pop-Folk",
"Eurodance",
"Dream",
"Southern Rock",
"Comedy",
"Cult",
"Gangsta Rap",
"Top 40",
"Christian Rap",
"Pop/Funk",
"Jungle",
"Native American",
"Cabaret",
"New Wave",
"Psychedelic",
"Rave",
"Showtunes",
"Trailer",
"Lo-Fi",
"Tribal",
"Acid Punk",
"Acid Jazz",
"Polka",
"Retro",
"Musical",
"Rock & Roll",
"Hard Rock",
"Folk",
"Folk-Rock",
"National Folk",
"Swing",
"Fast-Fusion",
"Bebop",
"Latin",
"Revival",
"Celtic",
"Bluegrass",
"Avantgarde",
"Gothic Rock",
"Progressive Rock",
"Psychedelic Rock",
"Symphonic Rock",
"Slow Rock",
"Big Band",
"Chorus",
"Easy Listening",
"Acoustic",
"Humour",
"Speech",
"Chanson",
"Opera",
"Chamber Music",
"Sonata",
"Symphony",
"Booty Bass",
"Primus",
"Porn Groove",
"Satire",
"Slow Jam",
"Club",
"Tango",
"Samba",
"Folklore",
"Ballad",
"Power Ballad",
"Rhythmic Soul",
"Freestyle",
"Duet",
"Punk Rock",
"Drum Solo",
"A Cappella",
"Euro-House",
"Dance Hall",
"Goa",
"Drum & Bass",
"Club-House",
"Hardcore",
"Terror",
"Indie",
"BritPop",
"Afro-Punk",
"Polsk Punk",
"Beat",
"Christian Gangsta Rap",
"Heavy Metal",
"Black Metal",
"Crossover",
"Contemporary Christian",
"Christian Rock",
"Merengue",
"Salsa",
"Thrash Metal",
"Anime",
"JPop",
"Synthpop",
"Abstract",
"Art Rock",
"Baroque",
"Bhangra",
"Big Beat",
"Breakbeat",
"Chillout",
"Downtempo",
"Dub",
"EBM",
"Eclectic",
"Electro",
"Electroclash",
"Emo",
"Experimental",
"Garage",
"Global",
"IDM",
"Illbient",
"Industro-Goth",
"Jam Band",
"Krautrock",
"Leftfield",
"Lounge",
"Math Rock",
"New Romantic",
"Nu-Breakz",
"Post-Punk",
"Post-Rock",
"Psytrance",
"Shoegaze",
"Space Rock",
"Trop Rock",
"World Music",
"Neoclassical",
"Audiobook",
"Audio Theatre",
"Neue Deutsche Welle",
"Podcast",
"Indie Rock",
"G-Funk",
"Dubstep",
"Garage Rock",
"Psybient",
];
pub const ID3V22_UPGRADE_MAP: &[(&str, &str)] = &[
("TT1", "TIT1"), ("TT2", "TIT2"), ("TT3", "TIT3"), ("TP1", "TPE1"), ("TP2", "TPE2"), ("TP3", "TPE3"), ("TP4", "TPE4"), ("TCM", "TCOM"), ("TXT", "TEXT"), ("TLA", "TLAN"), ("TCO", "TCON"), ("TAL", "TALB"), ("TPA", "TPOS"), ("TRK", "TRCK"), ("TRC", "TSRC"), ("TYE", "TYER"), ("TDA", "TDAT"), ("TIM", "TIME"), ("TRD", "TRDA"), ("TMT", "TMED"), ("TFT", "TFLT"), ("TBP", "TBPM"), ("TCR", "TCOP"), ("TPB", "TPUB"), ("TEN", "TENC"), ("TSS", "TSSE"), ("TOF", "TOFN"), ("TLE", "TLEN"), ("TSI", "TSIZ"), ("TDY", "TDLY"), ("TKE", "TKEY"), ("TOT", "TOAL"), ("TOA", "TOPE"), ("TOL", "TOLY"), ("TOR", "TORY"), ("WAF", "WOAF"), ("WAR", "WOAR"), ("WAS", "WOAS"), ("WCM", "WCOM"), ("WCP", "WCOP"), ("WPB", "WPUB"), ("COM", "COMM"), ("ULT", "USLT"), ("UFI", "UFID"), ("MCI", "MCDI"), ("ETC", "ETCO"), ("MLL", "MLLT"), ("STC", "SYTC"), ("SLT", "SYLT"), ("RVA", "RVAD"), ("EQU", "EQUA"), ("REV", "RVRB"), ("PIC", "APIC"), ("GEO", "GEOB"), ("CNT", "PCNT"), ("POP", "POPM"), ("BUF", "RBUF"), ("CRA", "AENC"), ("LNK", "LINK"), ];
pub const ID3V22_TO_V24_UPGRADES: &[(&str, &str)] = &[
("TDA", "TDRC"), ("TIM", "TDRC"), ("TRD", "TDRC"), ("TYE", "TDRC"), ("TOR", "TDOR"), ("IPL", "TIPL"), ("RVA", "RVA2"), ("EQU", "EQU2"), ];
pub mod flags_v23 {
pub const ALTER_TAG: u16 = 0x8000;
pub const ALTER_FILE: u16 = 0x4000;
pub const READ_ONLY: u16 = 0x2000;
pub const COMPRESS: u16 = 0x0080;
pub const ENCRYPT: u16 = 0x0040;
pub const GROUP: u16 = 0x0020;
}
pub mod flags_v24 {
pub const ALTER_TAG: u16 = 0x4000;
pub const ALTER_FILE: u16 = 0x2000;
pub const READ_ONLY: u16 = 0x1000;
pub const GROUP_ID: u16 = 0x0040;
pub const COMPRESS: u16 = 0x0008;
pub const ENCRYPT: u16 = 0x0004;
pub const UNSYNCH: u16 = 0x0002;
pub const DATA_LEN: u16 = 0x0001;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(u8)]
pub enum PictureType {
Other = 0x00,
FileIcon = 0x01,
OtherFileIcon = 0x02,
CoverFront = 0x03,
CoverBack = 0x04,
LeafletPage = 0x05,
Media = 0x06,
LeadArtist = 0x07,
Artist = 0x08,
Conductor = 0x09,
Band = 0x0A,
Composer = 0x0B,
Lyricist = 0x0C,
RecordingLocation = 0x0D,
DuringRecording = 0x0E,
DuringPerformance = 0x0F,
VideoScreenCapture = 0x10,
BrightColoredFish = 0x11,
Illustration = 0x12,
BandLogo = 0x13,
PublisherLogo = 0x14,
}
impl From<u8> for PictureType {
fn from(value: u8) -> Self {
match value {
0x01 => PictureType::FileIcon,
0x02 => PictureType::OtherFileIcon,
0x03 => PictureType::CoverFront,
0x04 => PictureType::CoverBack,
0x05 => PictureType::LeafletPage,
0x06 => PictureType::Media,
0x07 => PictureType::LeadArtist,
0x08 => PictureType::Artist,
0x09 => PictureType::Conductor,
0x0A => PictureType::Band,
0x0B => PictureType::Composer,
0x0C => PictureType::Lyricist,
0x0D => PictureType::RecordingLocation,
0x0E => PictureType::DuringRecording,
0x0F => PictureType::DuringPerformance,
0x10 => PictureType::VideoScreenCapture,
0x11 => PictureType::BrightColoredFish,
0x12 => PictureType::Illustration,
0x13 => PictureType::BandLogo,
0x14 => PictureType::PublisherLogo,
_ => PictureType::Other,
}
}
}
impl fmt::Display for PictureType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = match self {
PictureType::Other => "Other",
PictureType::FileIcon => "32x32 pixels 'file icon' (PNG only)",
PictureType::OtherFileIcon => "Other file icon",
PictureType::CoverFront => "Cover (front)",
PictureType::CoverBack => "Cover (back)",
PictureType::LeafletPage => "Leaflet page",
PictureType::Media => "Media (e.g. label side of CD)",
PictureType::LeadArtist => "Lead artist/lead performer/soloist",
PictureType::Artist => "Artist/performer",
PictureType::Conductor => "Conductor",
PictureType::Band => "Band/Orchestra",
PictureType::Composer => "Composer",
PictureType::Lyricist => "Lyricist/text writer",
PictureType::RecordingLocation => "Recording Location",
PictureType::DuringRecording => "During recording",
PictureType::DuringPerformance => "During performance",
PictureType::VideoScreenCapture => "Movie/video screen capture",
PictureType::BrightColoredFish => "A bright coloured fish",
PictureType::Illustration => "Illustration",
PictureType::BandLogo => "Band/artist logotype",
PictureType::PublisherLogo => "Publisher/Studio logotype",
};
write!(f, "{}", name)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum ChannelType {
Other = 0,
MasterVolume = 1,
FrontRight = 2,
FrontLeft = 3,
BackRight = 4,
BackLeft = 5,
FrontCentre = 6,
BackCentre = 7,
Subwoofer = 8,
}
impl From<u8> for ChannelType {
fn from(value: u8) -> Self {
match value {
1 => ChannelType::MasterVolume,
2 => ChannelType::FrontRight,
3 => ChannelType::FrontLeft,
4 => ChannelType::BackRight,
5 => ChannelType::BackLeft,
6 => ChannelType::FrontCentre,
7 => ChannelType::BackCentre,
8 => ChannelType::Subwoofer,
_ => ChannelType::Other,
}
}
}
const CHANNEL_NAMES: [&str; 9] = [
"Other",
"Master volume",
"Front right",
"Front left",
"Back right",
"Back left",
"Front centre",
"Back centre",
"Subwoofer",
];
impl fmt::Display for ChannelType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = CHANNEL_NAMES.get(*self as usize).unwrap_or(&"Unknown");
write!(f, "{}", name)
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum FrameData {
Text {
id: String,
encoding: TextEncoding,
text: Vec<String>,
},
UserText {
encoding: TextEncoding,
description: String,
text: Vec<String>,
},
Url { id: String, url: String },
UserUrl {
encoding: TextEncoding,
description: String,
url: String,
},
Comment {
encoding: TextEncoding,
language: [u8; 3],
description: String,
text: String,
},
UnsyncLyrics {
encoding: TextEncoding,
language: [u8; 3],
description: String,
lyrics: String,
},
AttachedPicture {
encoding: TextEncoding,
mime_type: String,
picture_type: PictureType,
description: String,
data: Vec<u8>,
},
GeneralObject {
encoding: TextEncoding,
mime_type: String,
filename: String,
description: String,
data: Vec<u8>,
},
PlayCounter { count: u64 },
Popularimeter {
email: String,
rating: u8,
count: u64,
},
Private { owner: String, data: Vec<u8> },
UniqueFileId { owner: String, identifier: Vec<u8> },
TermsOfUse {
encoding: TextEncoding,
language: [u8; 3],
text: String,
},
Ownership {
encoding: TextEncoding,
price: String,
date: String,
seller: String,
},
Commercial {
encoding: TextEncoding,
price: String,
valid_until: String,
contact_url: String,
received_as: u8,
seller: String,
description: String,
picture_mime: String,
picture: Vec<u8>,
},
EncryptionMethod {
owner: String,
method_symbol: u8,
encryption_data: Vec<u8>,
},
GroupIdentification {
owner: String,
group_symbol: u8,
group_data: Vec<u8>,
},
LinkedInfo {
frame_id: String,
url: String,
id_data: Vec<u8>,
},
MusicCdId { cd_toc: Vec<u8> },
EventTiming {
format: TimeStampFormat,
events: Vec<(u8, u32)>, },
MpegLocationLookup {
frames_between_reference: u16,
bytes_between_reference: u32,
milliseconds_between_reference: u32,
bits_for_bytes: u8,
bits_for_milliseconds: u8,
references: Vec<(u32, u32)>, },
SyncTempo {
format: TimeStampFormat,
tempo_data: Vec<u8>,
},
SyncLyrics {
encoding: TextEncoding,
language: [u8; 3],
format: TimeStampFormat,
content_type: u8,
description: String,
lyrics: Vec<(String, u32)>, },
RelativeVolumeAdjustment {
identification: String,
channels: Vec<(ChannelType, i16, u8)>, },
Equalisation {
method: u8,
identification: String,
adjustments: Vec<(u16, i16)>, },
Reverb {
reverb_left: u16,
reverb_right: u16,
reverb_bounces_left: u8,
reverb_bounces_right: u8,
reverb_feedback_left_to_left: u8,
reverb_feedback_left_to_right: u8,
reverb_feedback_right_to_right: u8,
reverb_feedback_right_to_left: u8,
premix_left_to_right: u8,
premix_right_to_left: u8,
},
RecommendedBufferSize {
buffer_size: u32,
embedded_info_flag: bool,
offset_to_next_tag: u32,
},
AudioEncryption {
owner: String,
preview_start: u16,
preview_length: u16,
encryption_info: Vec<u8>,
},
PositionSync {
format: TimeStampFormat,
position: u32,
},
Signature {
group_symbol: u8,
signature: Vec<u8>,
},
Seek { minimum_offset: u32 },
Chapter {
element_id: String,
start_time: u32,
end_time: u32,
start_offset: u32,
end_offset: u32,
sub_frames: Vec<FrameData>,
},
TableOfContents {
element_id: String,
flags: u8,
child_elements: Vec<String>,
sub_frames: Vec<FrameData>,
},
Genre {
encoding: TextEncoding,
genres: Vec<String>,
},
PairedText {
id: String,
encoding: TextEncoding,
people: Vec<(String, String)>,
},
NumericText {
id: String,
encoding: TextEncoding,
text: Vec<String>,
value: Option<u64>,
},
NumericPartText {
id: String,
encoding: TextEncoding,
text: Vec<String>,
value: Option<u64>,
},
TimeStampText {
id: String,
encoding: TextEncoding,
timestamps: Vec<ID3TimeStamp>,
},
Unknown { id: String, data: Vec<u8> },
}
pub fn strip_trailing_null<'a>(data: &'a [u8], encoding: &TextEncoding) -> &'a [u8] {
let term = encoding.null_terminator();
if data.len() >= term.len() && &data[data.len() - term.len()..] == term {
&data[..data.len() - term.len()]
} else {
data
}
}
impl FrameData {
pub fn from_bytes(header: &FrameHeader, mut data: Vec<u8>) -> Result<Self> {
data = FrameProcessor::process_read(header, data)?;
if JunkFrameRecovery::is_frame_corrupted(
&data,
&header.frame_id,
header.flags.to_raw(header.version),
) {
data = JunkFrameRecovery::reconstruct_frame(&data, &header.frame_id, header.size)?;
}
match header.frame_id.as_str() {
"TCON" => Self::parse_genre_frame(&data),
"TIPL" | "TMCL" => Self::parse_paired_text_frame(header.frame_id.as_str(), &data),
"TBPM" | "TLEN" | "TDAT" | "TDLY" | "TORY" | "TYER" | "TSIZ" | "TCMP" => {
Self::parse_numeric_text_frame(header.frame_id.as_str(), &data)
}
"TRCK" | "TPOS" => Self::parse_numeric_part_text_frame(header.frame_id.as_str(), &data),
"TDRC" | "TDRL" | "TDTG" | "TDOR" | "TDEN" => {
Self::parse_timestamp_text_frame(header.frame_id.as_str(), &data)
}
id if id.starts_with('T') && id != "TXXX" => Self::parse_text_frame(id, &data),
"TXXX" => Self::parse_user_text_frame(&data),
id if id.starts_with('W') && id != "WXXX" => Self::parse_url_frame(id, &data),
"WXXX" => Self::parse_user_url_frame(&data),
"COMM" => Self::parse_comment_frame(&data),
"USLT" => Self::parse_unsync_lyrics_frame(&data),
"SYLT" => Self::parse_sync_lyrics_frame(&data),
"APIC" => Self::parse_attached_picture_frame(&data),
"GEOB" => Self::parse_general_object_frame(&data),
"PCNT" => Self::parse_play_counter_frame(&data),
"POPM" => Self::parse_popularimeter_frame(&data),
"PRIV" => Self::parse_private_frame(&data),
"UFID" => Self::parse_unique_file_id_frame(&data),
"USER" => Self::parse_terms_of_use_frame(&data),
"OWNE" => Self::parse_ownership_frame(&data),
"COMR" => Self::parse_commercial_frame(&data),
"ENCR" => Self::parse_encryption_method_frame(&data),
"GRID" => Self::parse_group_id_frame(&data),
"LINK" => Self::parse_linked_info_frame(&data),
"MCDI" => Self::parse_music_cd_id_frame(&data),
"ETCO" => Self::parse_event_timing_frame(&data),
"MLLT" => Self::parse_mpeg_location_lookup_frame(&data),
"SYTC" => Self::parse_sync_tempo_frame(&data),
"RVA2" | "RVAD" => Self::parse_relative_volume_frame(&data),
"EQU2" | "EQUA" => Self::parse_equalisation_frame(&data),
"RVRB" => Self::parse_reverb_frame(&data),
"RBUF" => Self::parse_recommended_buffer_size_frame(&data),
"AENC" => Self::parse_audio_encryption_frame(&data),
"POSS" => Self::parse_position_sync_frame(&data),
"SIGN" => Self::parse_signature_frame(&data),
"SEEK" => Self::parse_seek_frame(&data),
"CHAP" => Self::parse_chapter_frame_depth(&data, header.version, 0),
"CTOC" => Self::parse_table_of_contents_frame_depth(&data, header.version, 0),
_ => Ok(FrameData::Unknown {
id: header.frame_id.clone(),
data,
}),
}
}
pub fn to_bytes(&self, version: (u8, u8)) -> Result<Vec<u8>> {
match self {
FrameData::Text { encoding, text, .. } => {
Self::write_text_frame(*encoding, text, version)
}
FrameData::UserText {
encoding,
description,
text,
} => Self::write_user_text_frame(*encoding, description, text, version),
FrameData::Url { url, .. } => Ok(url.as_bytes().to_vec()),
FrameData::UserUrl {
encoding,
description,
url,
} => Self::write_user_url_frame(*encoding, description, url, version),
FrameData::Comment {
encoding,
language,
description,
text,
} => Self::write_comment_frame(*encoding, *language, description, text, version),
FrameData::UnsyncLyrics {
encoding,
language,
description,
lyrics,
} => {
Self::write_unsync_lyrics_frame(*encoding, *language, description, lyrics, version)
}
FrameData::AttachedPicture {
encoding,
mime_type,
picture_type,
description,
data,
} => Self::write_attached_picture_frame(
*encoding,
mime_type,
*picture_type,
description,
data,
version,
),
FrameData::GeneralObject {
encoding,
mime_type,
filename,
description,
data,
} => Self::write_general_object_frame(
*encoding,
mime_type,
filename,
description,
data,
version,
),
FrameData::PlayCounter { count } => Self::write_play_counter_frame(*count),
FrameData::Popularimeter {
email,
rating,
count,
} => Self::write_popularimeter_frame(email, *rating, *count),
FrameData::Private { owner, data } => Self::write_private_frame(owner, data),
FrameData::UniqueFileId { owner, identifier } => {
Self::write_unique_file_id_frame(owner, identifier)
}
FrameData::TermsOfUse {
encoding,
language,
text,
} => Self::write_terms_of_use_frame(*encoding, *language, text, version),
FrameData::Ownership {
encoding,
price,
date,
seller,
} => Self::write_ownership_frame(*encoding, price, date, seller, version),
FrameData::Commercial {
encoding,
price,
valid_until,
contact_url,
received_as,
seller,
description,
picture_mime,
picture,
} => {
let params = CommercialFrameParams {
encoding: *encoding,
price,
valid_until,
contact_url,
received_as: *received_as,
seller,
description,
picture_mime,
picture,
_version: version,
};
Self::write_commercial_frame(¶ms)
}
FrameData::EncryptionMethod {
owner,
method_symbol,
encryption_data,
} => Self::write_encryption_method_frame(owner, *method_symbol, encryption_data),
FrameData::GroupIdentification {
owner,
group_symbol,
group_data,
} => Self::write_group_id_frame(owner, *group_symbol, group_data),
FrameData::LinkedInfo {
frame_id,
url,
id_data,
} => Self::write_linked_info_frame(frame_id, url, id_data),
FrameData::MusicCdId { cd_toc } => Ok(cd_toc.clone()),
FrameData::EventTiming { format, events } => {
Self::write_event_timing_frame(*format, events)
}
FrameData::MpegLocationLookup {
frames_between_reference,
bytes_between_reference,
milliseconds_between_reference,
bits_for_bytes,
bits_for_milliseconds,
references,
} => Self::write_mpeg_location_lookup_frame(
*frames_between_reference,
*bytes_between_reference,
*milliseconds_between_reference,
*bits_for_bytes,
*bits_for_milliseconds,
references,
),
FrameData::SyncTempo { format, tempo_data } => {
Self::write_sync_tempo_frame(*format, tempo_data)
}
FrameData::SyncLyrics {
encoding,
language,
format,
content_type,
description,
lyrics,
} => Self::write_sync_lyrics_frame(
*encoding,
*language,
*format,
*content_type,
description,
lyrics,
version,
),
FrameData::RelativeVolumeAdjustment {
identification,
channels,
} => Self::write_relative_volume_frame(identification, channels),
FrameData::Equalisation {
method,
identification,
adjustments,
} => Self::write_equalisation_frame(*method, identification, adjustments),
FrameData::Reverb {
reverb_left,
reverb_right,
reverb_bounces_left,
reverb_bounces_right,
reverb_feedback_left_to_left,
reverb_feedback_left_to_right,
reverb_feedback_right_to_right,
reverb_feedback_right_to_left,
premix_left_to_right,
premix_right_to_left,
} => {
let params = ReverbFrameParams {
reverb_left: *reverb_left,
reverb_right: *reverb_right,
reverb_bounces_left: *reverb_bounces_left,
reverb_bounces_right: *reverb_bounces_right,
reverb_feedback_left_to_left: *reverb_feedback_left_to_left,
reverb_feedback_left_to_right: *reverb_feedback_left_to_right,
reverb_feedback_right_to_right: *reverb_feedback_right_to_right,
reverb_feedback_right_to_left: *reverb_feedback_right_to_left,
premix_left_to_right: *premix_left_to_right,
premix_right_to_left: *premix_right_to_left,
};
Self::write_reverb_frame(¶ms)
}
FrameData::RecommendedBufferSize {
buffer_size,
embedded_info_flag,
offset_to_next_tag,
} => Self::write_recommended_buffer_size_frame(
*buffer_size,
*embedded_info_flag,
*offset_to_next_tag,
),
FrameData::AudioEncryption {
owner,
preview_start,
preview_length,
encryption_info,
} => Self::write_audio_encryption_frame(
owner,
*preview_start,
*preview_length,
encryption_info,
),
FrameData::PositionSync { format, position } => {
Self::write_position_sync_frame(*format, *position)
}
FrameData::Signature {
group_symbol,
signature,
} => Self::write_signature_frame(*group_symbol, signature),
FrameData::Seek { minimum_offset } => Self::write_seek_frame(*minimum_offset),
FrameData::Chapter {
element_id,
start_time,
end_time,
start_offset,
end_offset,
sub_frames,
} => Self::write_chapter_frame(
element_id,
*start_time,
*end_time,
*start_offset,
*end_offset,
sub_frames,
version,
),
FrameData::TableOfContents {
element_id,
flags,
child_elements,
sub_frames,
} => Self::write_table_of_contents_frame(
element_id,
*flags,
child_elements,
sub_frames,
version,
),
FrameData::Genre { encoding, genres } => {
Self::write_text_frame(*encoding, genres, version)
}
FrameData::PairedText {
encoding, people, ..
} => {
let text_parts: Vec<String> = people
.iter()
.flat_map(|(key, value)| [key.clone(), value.clone()])
.collect();
Self::write_text_frame(*encoding, &text_parts, version)
}
FrameData::NumericText { encoding, text, .. } => {
Self::write_text_frame(*encoding, text, version)
}
FrameData::NumericPartText { encoding, text, .. } => {
Self::write_text_frame(*encoding, text, version)
}
FrameData::TimeStampText {
encoding,
timestamps,
..
} => {
let text: Vec<String> = timestamps.iter().map(|ts| ts.text.clone()).collect();
Self::write_text_frame(*encoding, &text, version)
}
FrameData::Unknown { data, .. } => Ok(data.clone()),
}
}
pub fn id(&self) -> &str {
match self {
FrameData::Text { id, .. } => id,
FrameData::UserText { .. } => "TXXX",
FrameData::Url { id, .. } => id,
FrameData::UserUrl { .. } => "WXXX",
FrameData::Comment { .. } => "COMM",
FrameData::UnsyncLyrics { .. } => "USLT",
FrameData::AttachedPicture { .. } => "APIC",
FrameData::GeneralObject { .. } => "GEOB",
FrameData::PlayCounter { .. } => "PCNT",
FrameData::Popularimeter { .. } => "POPM",
FrameData::Private { .. } => "PRIV",
FrameData::UniqueFileId { .. } => "UFID",
FrameData::TermsOfUse { .. } => "USER",
FrameData::Ownership { .. } => "OWNE",
FrameData::Commercial { .. } => "COMR",
FrameData::EncryptionMethod { .. } => "ENCR",
FrameData::GroupIdentification { .. } => "GRID",
FrameData::LinkedInfo { .. } => "LINK",
FrameData::MusicCdId { .. } => "MCDI",
FrameData::EventTiming { .. } => "ETCO",
FrameData::MpegLocationLookup { .. } => "MLLT",
FrameData::SyncTempo { .. } => "SYTC",
FrameData::SyncLyrics { .. } => "SYLT",
FrameData::RelativeVolumeAdjustment { .. } => "RVA2",
FrameData::Equalisation { .. } => "EQU2",
FrameData::Reverb { .. } => "RVRB",
FrameData::RecommendedBufferSize { .. } => "RBUF",
FrameData::AudioEncryption { .. } => "AENC",
FrameData::PositionSync { .. } => "POSS",
FrameData::Signature { .. } => "SIGN",
FrameData::Seek { .. } => "SEEK",
FrameData::Chapter { .. } => "CHAP",
FrameData::TableOfContents { .. } => "CTOC",
FrameData::Genre { .. } => "TCON",
FrameData::PairedText { id, .. } => id,
FrameData::NumericText { id, .. } => id,
FrameData::NumericPartText { id, .. } => id,
FrameData::TimeStampText { id, .. } => id,
FrameData::Unknown { id, .. } => id,
}
}
fn parse_text_frame(frame_id: &str, data: &[u8]) -> Result<Self> {
if data.is_empty() {
return Ok(FrameData::Text {
id: frame_id.to_string(),
encoding: TextEncoding::Latin1,
text: vec![],
});
}
let encoding = TextEncoding::from_byte(data[0])?;
let text_data = &data[1..];
let decoded = encoding.decode_text(text_data)?;
let text_parts = decoded
.split('\u{0}')
.filter(|part| !part.is_empty())
.map(|part| part.trim_start_matches('\u{feff}').to_string())
.collect::<Vec<_>>();
Ok(FrameData::Text {
id: frame_id.to_string(),
encoding,
text: text_parts,
})
}
fn parse_user_text_frame(data: &[u8]) -> Result<Self> {
if data.len() < 2 {
warn_event!(frame = "TXXX", len = data.len(), "frame data too short");
return Err(AudexError::InvalidData("TXXX frame too short".to_string()));
}
let encoding = TextEncoding::from_byte(data[0])?;
let text_data = &data[1..];
let null_term = encoding.null_terminator();
let desc_end = if null_term.len() == 1 {
text_data
.iter()
.position(|&b| b == null_term[0])
.unwrap_or(text_data.len())
} else {
(0..(text_data.len() & !1).saturating_sub(1))
.step_by(2)
.find(|&i| text_data[i] == 0 && text_data[i + 1] == 0)
.unwrap_or(text_data.len())
};
let description = encoding.decode_text(&text_data[..desc_end])?;
let text_start = desc_end + null_term.len();
let text_parts = if text_start < text_data.len() {
let remaining = &text_data[text_start..];
if null_term.len() == 1 {
remaining
.split(|&b| b == null_term[0])
.filter(|part| !part.is_empty())
.map(|part| encoding.decode_text(part))
.collect::<Result<Vec<_>>>()?
} else {
vec![encoding.decode_text(remaining)?]
}
} else {
vec![]
};
Ok(FrameData::UserText {
encoding,
description,
text: text_parts,
})
}
fn parse_url_frame(frame_id: &str, data: &[u8]) -> Result<Self> {
let url = String::from_utf8_lossy(data)
.trim_end_matches('\0')
.to_string();
Ok(FrameData::Url {
id: frame_id.to_string(),
url,
})
}
fn parse_user_url_frame(data: &[u8]) -> Result<Self> {
if data.is_empty() {
return Err(AudexError::InvalidData("WXXX frame too short".to_string()));
}
let encoding = TextEncoding::from_byte(data[0])?;
let text_data = &data[1..];
let null_term = encoding.null_terminator();
let desc_end = if null_term.len() == 1 {
text_data
.iter()
.position(|&b| b == null_term[0])
.unwrap_or(text_data.len())
} else {
(0..(text_data.len() & !1).saturating_sub(1))
.step_by(2)
.find(|&i| text_data[i] == 0 && text_data[i + 1] == 0)
.unwrap_or(text_data.len())
};
let description = encoding.decode_text(&text_data[..desc_end])?;
let url_start = desc_end + null_term.len();
let url = if url_start < text_data.len() {
String::from_utf8_lossy(&text_data[url_start..])
.trim_end_matches('\0')
.to_string()
} else {
String::new()
};
Ok(FrameData::UserUrl {
encoding,
description,
url,
})
}
fn parse_comment_frame(data: &[u8]) -> Result<Self> {
if data.len() < 5 {
warn_event!(frame = "COMM", len = data.len(), "frame data too short");
return Err(AudexError::ID3FrameTooShort {
expected: 5,
actual: data.len(),
});
}
let encoding = TextEncoding::from_byte(data[0])?;
let language = [data[1], data[2], data[3]];
let text_data = &data[4..];
let null_term = encoding.null_terminator();
let desc_end = if null_term.len() == 1 {
text_data
.iter()
.position(|&b| b == null_term[0])
.unwrap_or(text_data.len())
} else {
(0..(text_data.len() & !1).saturating_sub(1))
.step_by(2)
.find(|&i| text_data[i] == 0 && text_data[i + 1] == 0)
.unwrap_or(text_data.len())
};
let description = encoding.decode_text(&text_data[..desc_end])?;
let comment_start = desc_end + null_term.len();
let comment_text = if comment_start < text_data.len() {
let raw = &text_data[comment_start..];
let raw = strip_trailing_null(raw, &encoding);
encoding.decode_text(raw)?
} else {
String::new()
};
Ok(FrameData::Comment {
encoding,
language,
description,
text: comment_text,
})
}
fn parse_unsync_lyrics_frame(data: &[u8]) -> Result<Self> {
if data.len() < 5 {
warn_event!(frame = "USLT", len = data.len(), "frame data too short");
return Err(AudexError::InvalidData("USLT frame too short".to_string()));
}
let encoding = TextEncoding::from_byte(data[0])?;
let language = [data[1], data[2], data[3]];
let text_data = &data[4..];
let null_term = encoding.null_terminator();
let desc_end = if null_term.len() == 1 {
text_data
.iter()
.position(|&b| b == null_term[0])
.unwrap_or(text_data.len())
} else {
(0..(text_data.len() & !1).saturating_sub(1))
.step_by(2)
.find(|&i| text_data[i] == 0 && text_data[i + 1] == 0)
.unwrap_or(text_data.len())
};
let description = encoding.decode_text(&text_data[..desc_end])?;
let lyrics_start = desc_end + null_term.len();
let lyrics = if lyrics_start < text_data.len() {
let raw = &text_data[lyrics_start..];
let raw = strip_trailing_null(raw, &encoding);
encoding.decode_text(raw)?
} else {
String::new()
};
Ok(FrameData::UnsyncLyrics {
encoding,
language,
description,
lyrics,
})
}
fn parse_attached_picture_frame(data: &[u8]) -> Result<Self> {
if data.len() < 5 {
warn_event!(frame = "APIC", len = data.len(), "frame data too short");
return Err(AudexError::ID3FrameTooShort {
expected: 5,
actual: data.len(),
});
}
let encoding = TextEncoding::from_byte(data[0])?;
let mut offset = 1;
let mime_end = data[offset..].iter().position(|&b| b == 0).ok_or_else(|| {
AudexError::InvalidData("No MIME type terminator in APIC frame".to_string())
})? + offset;
let mime_type = String::from_utf8_lossy(&data[offset..mime_end]).into_owned();
offset = mime_end + 1;
if offset >= data.len() {
return Err(AudexError::InvalidData(
"APIC frame missing picture type".to_string(),
));
}
let picture_type = PictureType::from(data[offset]);
offset += 1;
let remaining_data = &data[offset..];
let null_term = encoding.null_terminator();
let desc_end = if null_term.len() == 1 {
remaining_data
.iter()
.position(|&b| b == null_term[0])
.unwrap_or(remaining_data.len())
} else {
(0..(remaining_data.len() & !1).saturating_sub(1))
.step_by(2)
.find(|&i| remaining_data[i] == 0 && remaining_data[i + 1] == 0)
.unwrap_or(remaining_data.len())
};
let description = encoding.decode_text(&remaining_data[..desc_end])?;
if desc_end == remaining_data.len() {
return Err(AudexError::InvalidData(
"APIC frame description is not null-terminated".to_string(),
));
}
let picture_start = desc_end + null_term.len();
crate::limits::ParseLimits::default().check_image_size(
remaining_data[picture_start..].len() as u64,
"ID3 APIC image",
)?;
let picture_data = if picture_start < remaining_data.len() {
remaining_data[picture_start..].to_vec()
} else {
Vec::new()
};
Ok(FrameData::AttachedPicture {
encoding,
mime_type,
picture_type,
description,
data: picture_data,
})
}
fn parse_play_counter_frame(data: &[u8]) -> Result<Self> {
if data.is_empty() {
return Ok(FrameData::PlayCounter { count: 0 });
}
if data.len() > 8 {
return Err(crate::AudexError::InvalidData(format!(
"PCNT play counter too large: {} bytes exceeds the 8-byte u64 maximum",
data.len(),
)));
}
let mut count = 0u64;
for &byte in data.iter() {
count = (count << 8) | byte as u64;
}
Ok(FrameData::PlayCounter { count })
}
fn parse_popularimeter_frame(data: &[u8]) -> Result<Self> {
if data.is_empty() {
warn_event!(frame = "POPM", "frame data is empty");
return Err(AudexError::InvalidData("POPM frame too short".to_string()));
}
let email_end = data.iter().position(|&b| b == 0).ok_or_else(|| {
AudexError::InvalidData("No email terminator in POPM frame".to_string())
})?;
let email = String::from_utf8_lossy(&data[..email_end]).into_owned();
if email_end + 1 >= data.len() {
return Err(AudexError::InvalidData(
"POPM frame missing rating".to_string(),
));
}
let rating = data[email_end + 1];
let count_data = &data[email_end + 2..];
if count_data.len() > 8 {
return Err(AudexError::InvalidData(format!(
"POPM play counter too large: {} bytes exceeds the 8-byte u64 maximum",
count_data.len(),
)));
}
let mut count = 0u64;
for &byte in count_data.iter() {
count = (count << 8) | byte as u64;
}
Ok(FrameData::Popularimeter {
email,
rating,
count,
})
}
fn parse_private_frame(data: &[u8]) -> Result<Self> {
if data.is_empty() {
return Err(AudexError::InvalidData("PRIV frame too short".to_string()));
}
let owner_end = data.iter().position(|&b| b == 0).ok_or_else(|| {
AudexError::InvalidData("No owner terminator in PRIV frame".to_string())
})?;
let owner = String::from_utf8_lossy(&data[..owner_end]).into_owned();
let private_data = data[owner_end + 1..].to_vec();
Ok(FrameData::Private {
owner,
data: private_data,
})
}
fn parse_unique_file_id_frame(data: &[u8]) -> Result<Self> {
if data.is_empty() {
return Err(AudexError::InvalidData("UFID frame too short".to_string()));
}
let owner_end = data.iter().position(|&b| b == 0).ok_or_else(|| {
AudexError::InvalidData("No owner terminator in UFID frame".to_string())
})?;
let owner = String::from_utf8_lossy(&data[..owner_end]).into_owned();
let identifier = data[owner_end + 1..].to_vec();
Ok(FrameData::UniqueFileId { owner, identifier })
}
fn parse_sync_lyrics_frame(data: &[u8]) -> Result<Self> {
if data.len() < 6 {
return Err(AudexError::InvalidData("SYLT frame too short".to_string()));
}
let encoding = TextEncoding::from_byte(data[0])?;
let language = [data[1], data[2], data[3]];
let format = TimeStampFormat::from(data[4]);
let content_type = data[5];
let text_data = &data[6..];
let null_term = encoding.null_terminator();
let desc_end = if null_term.len() == 1 {
text_data
.iter()
.position(|&b| b == null_term[0])
.unwrap_or(text_data.len())
} else {
(0..(text_data.len() & !1).saturating_sub(1))
.step_by(2)
.find(|&i| text_data[i] == 0 && text_data[i + 1] == 0)
.unwrap_or(text_data.len())
};
let description = encoding.decode_text(&text_data[..desc_end])?;
let sync_data_start = desc_end + null_term.len();
let mut lyrics = Vec::new();
if sync_data_start < text_data.len() {
let sync_data = &text_data[sync_data_start..];
let mut pos = 0;
const MAX_LYRICS_ENTRIES: usize = 50_000;
while pos + 4 < sync_data.len() && lyrics.len() < MAX_LYRICS_ENTRIES {
let prev_pos = pos;
let text_end = if null_term.len() == 1 {
sync_data[pos..]
.iter()
.position(|&b| b == null_term[0])
.unwrap_or(sync_data.len() - pos)
+ pos
} else {
(pos..(sync_data.len() & !1).saturating_sub(1))
.step_by(2)
.find(|&i| sync_data[i] == 0 && sync_data[i + 1] == 0)
.unwrap_or(sync_data.len())
};
if text_end + null_term.len() + 4 > sync_data.len() {
break;
}
let text = encoding.decode_text(&sync_data[pos..text_end])?;
let timestamp_pos = text_end + null_term.len();
let timestamp = u32::from_be_bytes([
sync_data[timestamp_pos],
sync_data[timestamp_pos + 1],
sync_data[timestamp_pos + 2],
sync_data[timestamp_pos + 3],
]);
lyrics.push((text, timestamp));
pos = timestamp_pos + 4;
if pos <= prev_pos {
break;
}
}
}
Ok(FrameData::SyncLyrics {
encoding,
language,
format,
content_type,
description,
lyrics,
})
}
fn parse_general_object_frame(data: &[u8]) -> Result<Self> {
if data.len() < 2 {
return Err(AudexError::InvalidData("GEOB frame too short".to_string()));
}
let encoding = TextEncoding::from_byte(data[0])?;
let mut offset = 1;
let mime_end = data[offset..].iter().position(|&b| b == 0).ok_or_else(|| {
AudexError::InvalidData("No MIME type terminator in GEOB frame".to_string())
})? + offset;
let mime_type = String::from_utf8_lossy(&data[offset..mime_end]).into_owned();
offset = mime_end + 1;
let remaining_data = &data[offset..];
let null_term = encoding.null_terminator();
let filename_end = if null_term.len() == 1 {
remaining_data
.iter()
.position(|&b| b == null_term[0])
.unwrap_or(remaining_data.len())
} else {
(0..(remaining_data.len() & !1).saturating_sub(1))
.step_by(2)
.find(|&i| remaining_data[i] == 0 && remaining_data[i + 1] == 0)
.unwrap_or(remaining_data.len())
};
let filename = encoding.decode_text(&remaining_data[..filename_end])?;
let desc_start = filename_end + null_term.len();
let mut description = String::new();
let mut data_start = desc_start;
if desc_start < remaining_data.len() {
let desc_end = if null_term.len() == 1 {
remaining_data[desc_start..]
.iter()
.position(|&b| b == null_term[0])
.map(|pos| pos + desc_start)
.unwrap_or(remaining_data.len())
} else {
(desc_start..(remaining_data.len() & !1).saturating_sub(1))
.step_by(2)
.find(|&i| remaining_data[i] == 0 && remaining_data[i + 1] == 0)
.unwrap_or(remaining_data.len())
};
description = encoding.decode_text(&remaining_data[desc_start..desc_end])?;
data_start = desc_end + null_term.len();
}
let object_data = if data_start < remaining_data.len() {
remaining_data[data_start..].to_vec()
} else {
Vec::new()
};
Ok(FrameData::GeneralObject {
encoding,
mime_type,
filename,
description,
data: object_data,
})
}
fn parse_terms_of_use_frame(data: &[u8]) -> Result<Self> {
if data.len() < 5 {
return Err(AudexError::InvalidData("USER frame too short".to_string()));
}
let encoding = TextEncoding::from_byte(data[0])?;
let language = [data[1], data[2], data[3]];
let text = encoding.decode_text(&data[4..])?;
Ok(FrameData::TermsOfUse {
encoding,
language,
text,
})
}
fn parse_ownership_frame(data: &[u8]) -> Result<Self> {
if data.len() < 2 {
return Err(AudexError::InvalidData("OWNE frame too short".to_string()));
}
let encoding = TextEncoding::from_byte(data[0])?;
let text_data = &data[1..];
let price_end = text_data
.iter()
.position(|&b| b == 0)
.unwrap_or(text_data.len());
let price = TextEncoding::Latin1.decode_text(&text_data[..price_end])?;
let date_start = price_end.saturating_add(1);
let mut date = String::new();
let mut seller_start = date_start;
if date_start < text_data.len() {
let available = text_data.len() - date_start;
if available < 8 {
return Err(AudexError::InvalidData(format!(
"OWNE date field too short: {} bytes (expected 8)",
available
)));
}
let date_end = date_start + 8;
date = String::from_utf8_lossy(&text_data[date_start..date_end]).into_owned();
seller_start = date_end;
}
let seller = if seller_start < text_data.len() {
encoding.decode_text(&text_data[seller_start..])?
} else {
String::new()
};
Ok(FrameData::Ownership {
encoding,
price,
date,
seller,
})
}
fn parse_commercial_frame(data: &[u8]) -> Result<Self> {
if data.len() < 2 {
return Err(AudexError::InvalidData("COMR frame too short".to_string()));
}
let encoding = TextEncoding::from_byte(data[0])?;
let mut offset = 1;
let price_end = data[offset..].iter().position(|&b| b == 0).ok_or_else(|| {
AudexError::InvalidData("No price terminator in COMR frame".to_string())
})? + offset;
let price = String::from_utf8_lossy(&data[offset..price_end]).into_owned();
offset = price_end + 1;
if offset + 8 > data.len() {
return Err(AudexError::InvalidData(
"COMR frame missing valid until date".to_string(),
));
}
let valid_until = String::from_utf8_lossy(&data[offset..offset + 8]).into_owned();
offset += 8;
if offset >= data.len() {
return Err(AudexError::InvalidData(
"COMR frame missing contact URL".to_string(),
));
}
let contact_end = data[offset..].iter().position(|&b| b == 0).ok_or_else(|| {
AudexError::InvalidData("No contact URL terminator in COMR frame".to_string())
})? + offset;
let contact_url = String::from_utf8_lossy(&data[offset..contact_end]).into_owned();
offset = contact_end + 1;
if offset >= data.len() {
return Err(AudexError::InvalidData(
"COMR frame missing received as byte".to_string(),
));
}
let received_as = data[offset];
offset += 1;
let remaining_data = &data[offset..];
let null_term = encoding.null_terminator();
let seller_end = if null_term.len() == 1 {
remaining_data
.iter()
.position(|&b| b == null_term[0])
.unwrap_or(remaining_data.len())
} else {
(0..(remaining_data.len() & !1).saturating_sub(1))
.step_by(2)
.find(|&i| remaining_data[i] == 0 && remaining_data[i + 1] == 0)
.unwrap_or(remaining_data.len())
};
let seller = encoding.decode_text(&remaining_data[..seller_end])?;
let desc_start = seller_end + null_term.len();
let mut description = String::new();
let mut picture_start = desc_start;
if desc_start < remaining_data.len() {
let desc_end = if null_term.len() == 1 {
remaining_data[desc_start..]
.iter()
.position(|&b| b == null_term[0])
.map(|pos| pos + desc_start)
.unwrap_or(remaining_data.len())
} else {
(desc_start..(remaining_data.len() & !1).saturating_sub(1))
.step_by(2)
.find(|&i| remaining_data[i] == 0 && remaining_data[i + 1] == 0)
.unwrap_or(remaining_data.len())
};
description = encoding.decode_text(&remaining_data[desc_start..desc_end])?;
picture_start = desc_end + null_term.len();
}
let mut picture_mime = String::new();
let mut picture_data_start = picture_start;
if picture_start < remaining_data.len() {
let mime_end = remaining_data[picture_start..]
.iter()
.position(|&b| b == 0)
.map(|pos| pos + picture_start)
.unwrap_or(remaining_data.len());
picture_mime =
String::from_utf8_lossy(&remaining_data[picture_start..mime_end]).into_owned();
picture_data_start = mime_end + 1;
}
let picture = if picture_data_start < remaining_data.len() {
remaining_data[picture_data_start..].to_vec()
} else {
Vec::new()
};
Ok(FrameData::Commercial {
encoding,
price,
valid_until,
contact_url,
received_as,
seller,
description,
picture_mime,
picture,
})
}
fn parse_encryption_method_frame(data: &[u8]) -> Result<Self> {
if data.len() < 3 {
return Err(AudexError::InvalidData("ENCR frame too short".to_string()));
}
let owner_end = data.iter().position(|&b| b == 0).ok_or_else(|| {
AudexError::InvalidData("No owner terminator in ENCR frame".to_string())
})?;
let owner = String::from_utf8_lossy(&data[..owner_end]).into_owned();
if owner_end + 2 > data.len() {
return Err(AudexError::InvalidData(
"ENCR frame missing method symbol".to_string(),
));
}
let method_symbol = data[owner_end + 1];
let encryption_data = data[owner_end + 2..].to_vec();
Ok(FrameData::EncryptionMethod {
owner,
method_symbol,
encryption_data,
})
}
fn parse_group_id_frame(data: &[u8]) -> Result<Self> {
if data.len() < 3 {
return Err(AudexError::InvalidData("GRID frame too short".to_string()));
}
let owner_end = data.iter().position(|&b| b == 0).ok_or_else(|| {
AudexError::InvalidData("No owner terminator in GRID frame".to_string())
})?;
let owner = String::from_utf8_lossy(&data[..owner_end]).into_owned();
if owner_end + 2 > data.len() {
return Err(AudexError::InvalidData(
"GRID frame missing group symbol".to_string(),
));
}
let group_symbol = data[owner_end + 1];
let group_data = data[owner_end + 2..].to_vec();
Ok(FrameData::GroupIdentification {
owner,
group_symbol,
group_data,
})
}
fn parse_linked_info_frame(data: &[u8]) -> Result<Self> {
if data.len() < 5 {
return Err(AudexError::InvalidData("LINK frame too short".to_string()));
}
let frame_id = String::from_utf8_lossy(&data[..4]).into_owned();
let mut offset = 4;
let url_end = data[offset..].iter().position(|&b| b == 0).ok_or_else(|| {
AudexError::InvalidData("No URL terminator in LINK frame".to_string())
})? + offset;
let url = String::from_utf8_lossy(&data[offset..url_end]).into_owned();
offset = url_end + 1;
let id_data = if offset < data.len() {
data[offset..].to_vec()
} else {
Vec::new()
};
Ok(FrameData::LinkedInfo {
frame_id,
url,
id_data,
})
}
fn parse_music_cd_id_frame(data: &[u8]) -> Result<Self> {
Ok(FrameData::MusicCdId {
cd_toc: data.to_vec(),
})
}
fn parse_event_timing_frame(data: &[u8]) -> Result<Self> {
if data.is_empty() {
return Err(AudexError::InvalidData("ETCO frame too short".to_string()));
}
let format = TimeStampFormat::from(data[0]);
let mut events = Vec::new();
let mut pos = 1;
while pos + 4 < data.len() {
let event_type = data[pos];
let timestamp =
u32::from_be_bytes([data[pos + 1], data[pos + 2], data[pos + 3], data[pos + 4]]);
events.push((event_type, timestamp));
pos += 5;
}
Ok(FrameData::EventTiming { format, events })
}
fn parse_mpeg_location_lookup_frame(data: &[u8]) -> Result<Self> {
if data.len() < 10 {
return Err(AudexError::InvalidData("MLLT frame too short".to_string()));
}
let frames_between_reference = u16::from_be_bytes([data[0], data[1]]);
let bytes_between_reference = u32::from_be_bytes([0, data[2], data[3], data[4]]);
let milliseconds_between_reference = u32::from_be_bytes([0, data[5], data[6], data[7]]);
let bits_for_bytes = data[8];
let bits_for_milliseconds = data[9];
let mut references = Vec::new();
let mut pos = 10;
let total_bits = bits_for_bytes as u16 + bits_for_milliseconds as u16;
let bytes_per_entry = total_bits.div_ceil(8) as u8;
if bytes_per_entry == 0 {
return Ok(FrameData::MpegLocationLookup {
frames_between_reference,
bytes_between_reference,
milliseconds_between_reference,
bits_for_bytes,
bits_for_milliseconds,
references,
});
}
while pos + bytes_per_entry as usize <= data.len() {
let mut bytes_deviation = 0u32;
let mut milliseconds_deviation = 0u32;
let byte_bytes = bits_for_bytes.div_ceil(8);
let ms_bytes = bits_for_milliseconds.div_ceil(8);
if pos + byte_bytes as usize <= data.len() {
for i in 0..byte_bytes as usize {
bytes_deviation = (bytes_deviation << 8) | data[pos + i] as u32;
}
pos += byte_bytes as usize;
}
if pos + ms_bytes as usize <= data.len() {
for i in 0..ms_bytes as usize {
milliseconds_deviation = (milliseconds_deviation << 8) | data[pos + i] as u32;
}
pos += ms_bytes as usize;
}
references.push((bytes_deviation, milliseconds_deviation));
}
Ok(FrameData::MpegLocationLookup {
frames_between_reference,
bytes_between_reference,
milliseconds_between_reference,
bits_for_bytes,
bits_for_milliseconds,
references,
})
}
fn parse_sync_tempo_frame(data: &[u8]) -> Result<Self> {
if data.is_empty() {
return Err(AudexError::InvalidData("SYTC frame too short".to_string()));
}
let format = TimeStampFormat::from(data[0]);
let tempo_data = data[1..].to_vec();
Ok(FrameData::SyncTempo { format, tempo_data })
}
fn parse_relative_volume_frame(data: &[u8]) -> Result<Self> {
if data.is_empty() {
return Err(AudexError::InvalidData(
"RVA2/RVAD frame too short".to_string(),
));
}
let id_end = data.iter().position(|&b| b == 0).ok_or_else(|| {
AudexError::InvalidData("No identification terminator in RVA2/RVAD frame".to_string())
})?;
let identification = TextEncoding::Latin1.decode_text(&data[..id_end])?;
let mut channels = Vec::new();
let mut pos = id_end + 1;
while pos + 4 <= data.len() {
let channel_type = ChannelType::from(data[pos]);
let adjustment = i16::from_be_bytes([data[pos + 1], data[pos + 2]]);
let peak_bits = data[pos + 3];
let peak_bytes = peak_bits.div_ceil(8) as usize;
if pos + 4 + peak_bytes > data.len() {
return Err(AudexError::InvalidData(format!(
"RVA2 channel peak data ({} bytes for {} bits) extends \
beyond frame boundary at offset {}",
peak_bytes, peak_bits, pos
)));
}
channels.push((channel_type, adjustment, peak_bits));
pos += 4 + peak_bytes;
}
Ok(FrameData::RelativeVolumeAdjustment {
identification,
channels,
})
}
fn parse_equalisation_frame(data: &[u8]) -> Result<Self> {
if data.len() < 2 {
return Err(AudexError::InvalidData(
"EQU2/EQUA frame too short".to_string(),
));
}
let method = data[0];
let id_end = data[1..].iter().position(|&b| b == 0).ok_or_else(|| {
AudexError::InvalidData("No identification terminator in EQU2/EQUA frame".to_string())
})? + 1;
let identification = String::from_utf8_lossy(&data[1..id_end]).into_owned();
let mut adjustments = Vec::new();
let mut pos = id_end + 1;
while pos + 4 <= data.len() {
let frequency = u16::from_be_bytes([data[pos], data[pos + 1]]);
let adjustment = i16::from_be_bytes([data[pos + 2], data[pos + 3]]);
adjustments.push((frequency, adjustment));
pos += 4;
}
Ok(FrameData::Equalisation {
method,
identification,
adjustments,
})
}
fn parse_reverb_frame(data: &[u8]) -> Result<Self> {
if data.len() < 12 {
return Err(AudexError::InvalidData("RVRB frame too short".to_string()));
}
let reverb_left = u16::from_be_bytes([data[0], data[1]]);
let reverb_right = u16::from_be_bytes([data[2], data[3]]);
let reverb_bounces_left = data[4];
let reverb_bounces_right = data[5];
let reverb_feedback_left_to_left = data[6];
let reverb_feedback_left_to_right = data[7];
let reverb_feedback_right_to_right = data[8];
let reverb_feedback_right_to_left = data[9];
let premix_left_to_right = data[10];
let premix_right_to_left = data[11];
Ok(FrameData::Reverb {
reverb_left,
reverb_right,
reverb_bounces_left,
reverb_bounces_right,
reverb_feedback_left_to_left,
reverb_feedback_left_to_right,
reverb_feedback_right_to_right,
reverb_feedback_right_to_left,
premix_left_to_right,
premix_right_to_left,
})
}
fn parse_recommended_buffer_size_frame(data: &[u8]) -> Result<Self> {
if data.len() < 4 {
return Err(AudexError::InvalidData("RBUF frame too short".to_string()));
}
let buffer_size = u32::from_be_bytes([0, data[0], data[1], data[2]]);
let embedded_info_flag = data.len() > 3 && data[3] & 0x01 != 0;
let offset_to_next_tag = if data.len() >= 8 {
u32::from_be_bytes([data[4], data[5], data[6], data[7]])
} else {
0
};
Ok(FrameData::RecommendedBufferSize {
buffer_size,
embedded_info_flag,
offset_to_next_tag,
})
}
fn parse_audio_encryption_frame(data: &[u8]) -> Result<Self> {
if data.len() < 5 {
return Err(AudexError::InvalidData("AENC frame too short".to_string()));
}
let owner_end = data.iter().position(|&b| b == 0).ok_or_else(|| {
AudexError::InvalidData("No owner terminator in AENC frame".to_string())
})?;
let owner = String::from_utf8_lossy(&data[..owner_end]).into_owned();
if owner_end + 5 > data.len() {
return Err(AudexError::InvalidData(
"AENC frame missing preview data".to_string(),
));
}
let preview_start = u16::from_be_bytes([data[owner_end + 1], data[owner_end + 2]]);
let preview_length = u16::from_be_bytes([data[owner_end + 3], data[owner_end + 4]]);
let encryption_info = data[owner_end + 5..].to_vec();
Ok(FrameData::AudioEncryption {
owner,
preview_start,
preview_length,
encryption_info,
})
}
fn parse_position_sync_frame(data: &[u8]) -> Result<Self> {
if data.len() < 5 {
return Err(AudexError::InvalidData("POSS frame too short".to_string()));
}
let format = TimeStampFormat::from(data[0]);
let position = u32::from_be_bytes([data[1], data[2], data[3], data[4]]);
Ok(FrameData::PositionSync { format, position })
}
fn parse_signature_frame(data: &[u8]) -> Result<Self> {
if data.len() < 2 {
return Err(AudexError::InvalidData("SIGN frame too short".to_string()));
}
let group_symbol = data[0];
let signature = data[1..].to_vec();
Ok(FrameData::Signature {
group_symbol,
signature,
})
}
fn parse_seek_frame(data: &[u8]) -> Result<Self> {
if data.len() < 4 {
return Err(AudexError::InvalidData("SEEK frame too short".to_string()));
}
let minimum_offset = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
Ok(FrameData::Seek { minimum_offset })
}
const MAX_SUBFRAME_DEPTH: usize = 4;
fn parse_chapter_frame_depth(data: &[u8], version: (u8, u8), depth: usize) -> Result<Self> {
if data.len() < 17 {
return Err(AudexError::InvalidData("CHAP frame too short".to_string()));
}
let id_end = data.iter().position(|&b| b == 0).ok_or_else(|| {
AudexError::InvalidData("No element ID terminator in CHAP frame".to_string())
})?;
let element_id = String::from_utf8_lossy(&data[..id_end]).into_owned();
if id_end + 17 > data.len() {
return Err(AudexError::InvalidData(
"CHAP frame missing timing data".to_string(),
));
}
let mut offset = id_end + 1;
let start_time = u32::from_be_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]);
offset += 4;
let end_time = u32::from_be_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]);
offset += 4;
let start_offset = u32::from_be_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]);
offset += 4;
let end_offset = u32::from_be_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]);
offset += 4;
let sub_frames = Self::parse_sub_frames_depth(&data[offset..], version, depth)?;
Ok(FrameData::Chapter {
element_id,
start_time,
end_time,
start_offset,
end_offset,
sub_frames,
})
}
fn parse_table_of_contents_frame_depth(
data: &[u8],
version: (u8, u8),
depth: usize,
) -> Result<Self> {
if data.len() < 3 {
return Err(AudexError::InvalidData("CTOC frame too short".to_string()));
}
let id_end = data.iter().position(|&b| b == 0).ok_or_else(|| {
AudexError::InvalidData("No element ID terminator in CTOC frame".to_string())
})?;
let element_id = String::from_utf8_lossy(&data[..id_end]).into_owned();
if id_end + 3 > data.len() {
return Err(AudexError::InvalidData(
"CTOC frame missing flags and count".to_string(),
));
}
let mut offset = id_end + 1;
let flags = data[offset];
offset += 1;
let entry_count = data[offset];
offset += 1;
let mut child_elements = Vec::new();
for _ in 0..entry_count {
if offset >= data.len() {
break;
}
let child_end = data[offset..]
.iter()
.position(|&b| b == 0)
.map(|pos| pos + offset)
.unwrap_or(data.len());
let child_id = String::from_utf8_lossy(&data[offset..child_end]).into_owned();
child_elements.push(child_id);
offset = child_end + 1;
}
let sub_frames = Self::parse_sub_frames_depth(&data[offset..], version, depth)?;
Ok(FrameData::TableOfContents {
element_id,
flags,
child_elements,
sub_frames,
})
}
fn parse_sub_frames_depth(
data: &[u8],
version: (u8, u8),
current_depth: usize,
) -> Result<Vec<FrameData>> {
let next_depth = current_depth + 1;
if next_depth > Self::MAX_SUBFRAME_DEPTH {
return Ok(Vec::new());
}
let mut sub_frames = Vec::new();
let mut offset = 0;
while offset < data.len() {
if offset + 10 > data.len() {
break;
}
let frame_id = String::from_utf8_lossy(&data[offset..offset + 4]).into_owned();
if !crate::id3::util::is_valid_frame_id(&frame_id) {
break;
}
let frame_size = if version.1 == 4 {
super::util::decode_synchsafe_int_checked(&data[offset + 4..offset + 8])? as usize
} else {
u32::from_be_bytes([
data[offset + 4],
data[offset + 5],
data[offset + 6],
data[offset + 7],
]) as usize
};
let flags = u16::from_be_bytes([data[offset + 8], data[offset + 9]]);
offset += 10;
let end = offset
.checked_add(frame_size)
.ok_or_else(|| AudexError::InvalidData("Sub-frame offset overflow".to_string()))?;
if end > data.len() {
break;
}
let frame_data = data[offset..end].to_vec();
offset = end;
let header = FrameHeader::new(frame_id.clone(), frame_size as u32, flags, version);
let parse_result = match frame_id.as_str() {
"CHAP" => Self::parse_chapter_frame_depth(&frame_data, version, next_depth),
"CTOC" => {
Self::parse_table_of_contents_frame_depth(&frame_data, version, next_depth)
}
_ => {
Self::from_bytes(&header, frame_data)
}
};
if let Ok(sub_frame) = parse_result {
sub_frames.push(sub_frame);
}
}
Ok(sub_frames)
}
fn write_text_frame(
encoding: TextEncoding,
text: &[String],
version: (u8, u8),
) -> Result<Vec<u8>> {
let encoding = if !encoding.is_valid_for_version(version) {
TextEncoding::Utf16 } else {
encoding
};
let mut data = vec![encoding.to_byte()];
let separator = if version == (2, 3) { "/" } else { "\0" };
let text_str = text.join(separator);
data.extend(encoding.encode_text(&text_str)?);
Ok(data)
}
fn write_user_text_frame(
encoding: TextEncoding,
description: &str,
text: &[String],
version: (u8, u8),
) -> Result<Vec<u8>> {
let encoding = if !encoding.is_valid_for_version(version) {
TextEncoding::Utf16
} else {
encoding
};
let mut data = vec![encoding.to_byte()];
data.extend(encoding.encode_text(description)?);
data.extend(encoding.null_terminator());
let separator = if version == (2, 3) { "/" } else { "\0" };
let text_str = text.join(separator);
data.extend(encoding.encode_text(&text_str)?);
Ok(data)
}
fn write_comment_frame(
encoding: TextEncoding,
language: [u8; 3],
description: &str,
text: &str,
version: (u8, u8),
) -> Result<Vec<u8>> {
let encoding = if !encoding.is_valid_for_version(version) {
TextEncoding::Utf16
} else {
encoding
};
let mut data = vec![encoding.to_byte()];
data.extend_from_slice(&language);
data.extend(encoding.encode_text(description)?);
data.extend(encoding.null_terminator());
data.extend(encoding.encode_text(text)?);
data.extend(encoding.null_terminator());
Ok(data)
}
fn write_attached_picture_frame(
encoding: TextEncoding,
mime_type: &str,
picture_type: PictureType,
description: &str,
picture_data: &[u8],
version: (u8, u8),
) -> Result<Vec<u8>> {
let encoding = if !encoding.is_valid_for_version(version) {
TextEncoding::Utf16
} else {
encoding
};
let mut data = vec![encoding.to_byte()];
data.extend_from_slice(mime_type.as_bytes());
data.push(0); data.push(picture_type as u8);
data.extend(encoding.encode_text(description)?);
data.extend(encoding.null_terminator());
data.extend_from_slice(picture_data);
Ok(data)
}
fn write_play_counter_frame(count: u64) -> Result<Vec<u8>> {
let mut data = Vec::new();
let mut remaining = count;
if remaining == 0 {
data.push(0);
} else {
let mut bytes = Vec::new();
while remaining > 0 {
bytes.push((remaining & 0xFF) as u8);
remaining >>= 8;
}
bytes.reverse();
data.extend(bytes);
}
Ok(data)
}
fn write_popularimeter_frame(email: &str, rating: u8, count: u64) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.extend_from_slice(email.as_bytes());
data.push(0); data.push(rating);
let mut remaining = count;
if remaining == 0 {
data.push(0);
} else {
let mut bytes = Vec::new();
while remaining > 0 {
bytes.push((remaining & 0xFF) as u8);
remaining >>= 8;
}
bytes.reverse();
data.extend(bytes);
}
Ok(data)
}
fn write_user_url_frame(
encoding: TextEncoding,
description: &str,
url: &str,
version: (u8, u8),
) -> Result<Vec<u8>> {
let encoding = if !encoding.is_valid_for_version(version) {
TextEncoding::Utf16
} else {
encoding
};
let mut data = vec![encoding.to_byte()];
data.extend(encoding.encode_text(description)?);
data.extend(encoding.null_terminator());
data.extend_from_slice(url.as_bytes());
Ok(data)
}
fn write_unsync_lyrics_frame(
encoding: TextEncoding,
language: [u8; 3],
description: &str,
lyrics: &str,
version: (u8, u8),
) -> Result<Vec<u8>> {
let encoding = if !encoding.is_valid_for_version(version) {
TextEncoding::Utf16
} else {
encoding
};
let mut data = vec![encoding.to_byte()];
data.extend_from_slice(&language);
data.extend(encoding.encode_text(description)?);
data.extend(encoding.null_terminator());
data.extend(encoding.encode_text(lyrics)?);
data.extend(encoding.null_terminator());
Ok(data)
}
fn write_general_object_frame(
encoding: TextEncoding,
mime_type: &str,
filename: &str,
description: &str,
object_data: &[u8],
version: (u8, u8),
) -> Result<Vec<u8>> {
let encoding = if !encoding.is_valid_for_version(version) {
TextEncoding::Utf16
} else {
encoding
};
let mut data = vec![encoding.to_byte()];
data.extend_from_slice(mime_type.as_bytes());
data.push(0); data.extend(encoding.encode_text(filename)?);
data.extend(encoding.null_terminator());
data.extend(encoding.encode_text(description)?);
data.extend(encoding.null_terminator());
data.extend_from_slice(object_data);
Ok(data)
}
fn write_private_frame(owner: &str, private_data: &[u8]) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.extend_from_slice(owner.as_bytes());
data.push(0); data.extend_from_slice(private_data);
Ok(data)
}
fn write_unique_file_id_frame(owner: &str, identifier: &[u8]) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.extend_from_slice(owner.as_bytes());
data.push(0); data.extend_from_slice(identifier);
Ok(data)
}
fn write_terms_of_use_frame(
encoding: TextEncoding,
language: [u8; 3],
text: &str,
version: (u8, u8),
) -> Result<Vec<u8>> {
let encoding = if !encoding.is_valid_for_version(version) {
TextEncoding::Utf16
} else {
encoding
};
let mut data = vec![encoding.to_byte()];
data.extend_from_slice(&language);
data.extend(encoding.encode_text(text)?);
Ok(data)
}
fn write_ownership_frame(
encoding: TextEncoding,
price: &str,
date: &str,
seller: &str,
version: (u8, u8),
) -> Result<Vec<u8>> {
let encoding = if !encoding.is_valid_for_version(version) {
TextEncoding::Utf16
} else {
encoding
};
let mut data = vec![encoding.to_byte()];
data.extend(encoding.encode_text(price)?);
data.extend(encoding.null_terminator());
data.extend_from_slice(date.as_bytes());
data.extend(encoding.encode_text(seller)?);
Ok(data)
}
fn write_commercial_frame(params: &CommercialFrameParams) -> Result<Vec<u8>> {
let mut data = vec![params.encoding.to_byte()];
data.extend_from_slice(params.price.as_bytes());
data.push(0);
data.extend_from_slice(params.valid_until.as_bytes());
data.extend_from_slice(params.contact_url.as_bytes());
data.push(0);
data.push(params.received_as);
data.extend(params.encoding.encode_text(params.seller)?);
data.extend(params.encoding.null_terminator());
data.extend(params.encoding.encode_text(params.description)?);
data.extend(params.encoding.null_terminator());
data.extend_from_slice(params.picture_mime.as_bytes());
data.push(0);
data.extend_from_slice(params.picture);
Ok(data)
}
fn write_encryption_method_frame(
owner: &str,
method_symbol: u8,
encryption_data: &[u8],
) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.extend_from_slice(owner.as_bytes());
data.push(0);
data.push(method_symbol);
data.extend_from_slice(encryption_data);
Ok(data)
}
fn write_group_id_frame(owner: &str, group_symbol: u8, group_data: &[u8]) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.extend_from_slice(owner.as_bytes());
data.push(0);
data.push(group_symbol);
data.extend_from_slice(group_data);
Ok(data)
}
fn write_linked_info_frame(frame_id: &str, url: &str, id_data: &[u8]) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.extend_from_slice(frame_id.as_bytes());
data.extend_from_slice(url.as_bytes());
data.push(0);
data.extend_from_slice(id_data);
Ok(data)
}
fn write_event_timing_frame(format: TimeStampFormat, events: &[(u8, u32)]) -> Result<Vec<u8>> {
let mut data = vec![format as u8];
for &(event_type, timestamp) in events {
data.push(event_type);
data.extend_from_slice(×tamp.to_be_bytes());
}
Ok(data)
}
fn write_mpeg_location_lookup_frame(
frames_between_reference: u16,
bytes_between_reference: u32,
milliseconds_between_reference: u32,
bits_for_bytes: u8,
bits_for_milliseconds: u8,
references: &[(u32, u32)],
) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.extend_from_slice(&frames_between_reference.to_be_bytes());
data.extend_from_slice(&bytes_between_reference.to_be_bytes()[1..]);
data.extend_from_slice(&milliseconds_between_reference.to_be_bytes()[1..]);
data.push(bits_for_bytes);
data.push(bits_for_milliseconds);
for &(bytes_deviation, milliseconds_deviation) in references {
let byte_bytes = bits_for_bytes.div_ceil(8);
let ms_bytes = bits_for_milliseconds.div_ceil(8);
for i in 0..byte_bytes {
let shift = (byte_bytes - 1 - i) * 8;
data.push((bytes_deviation >> shift) as u8);
}
for i in 0..ms_bytes {
let shift = (ms_bytes - 1 - i) * 8;
data.push((milliseconds_deviation >> shift) as u8);
}
}
Ok(data)
}
fn write_sync_tempo_frame(format: TimeStampFormat, tempo_data: &[u8]) -> Result<Vec<u8>> {
let mut data = vec![format as u8];
data.extend_from_slice(tempo_data);
Ok(data)
}
fn write_sync_lyrics_frame(
encoding: TextEncoding,
language: [u8; 3],
format: TimeStampFormat,
content_type: u8,
description: &str,
lyrics: &[(String, u32)],
_version: (u8, u8),
) -> Result<Vec<u8>> {
let mut data = vec![encoding.to_byte()];
data.extend_from_slice(&language);
data.push(format as u8);
data.push(content_type);
data.extend(encoding.encode_text(description)?);
data.extend(encoding.null_terminator());
for (text, timestamp) in lyrics {
data.extend(encoding.encode_text(text)?);
data.extend(encoding.null_terminator());
data.extend_from_slice(×tamp.to_be_bytes());
}
Ok(data)
}
fn write_relative_volume_frame(
identification: &str,
channels: &[(ChannelType, i16, u8)],
) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.extend_from_slice(identification.as_bytes());
data.push(0);
for &(channel_type, adjustment, peak_bits) in channels {
data.push(channel_type as u8);
data.extend_from_slice(&adjustment.to_be_bytes());
data.push(peak_bits);
let peak_bytes = peak_bits.div_ceil(8);
data.extend(std::iter::repeat_n(0, peak_bytes as usize));
}
Ok(data)
}
fn write_equalisation_frame(
method: u8,
identification: &str,
adjustments: &[(u16, i16)],
) -> Result<Vec<u8>> {
let mut data = vec![method];
data.extend_from_slice(identification.as_bytes());
data.push(0);
for &(frequency, adjustment) in adjustments {
data.extend_from_slice(&frequency.to_be_bytes());
data.extend_from_slice(&adjustment.to_be_bytes());
}
Ok(data)
}
fn write_reverb_frame(params: &ReverbFrameParams) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.extend_from_slice(¶ms.reverb_left.to_be_bytes());
data.extend_from_slice(¶ms.reverb_right.to_be_bytes());
data.push(params.reverb_bounces_left);
data.push(params.reverb_bounces_right);
data.push(params.reverb_feedback_left_to_left);
data.push(params.reverb_feedback_left_to_right);
data.push(params.reverb_feedback_right_to_right);
data.push(params.reverb_feedback_right_to_left);
data.push(params.premix_left_to_right);
data.push(params.premix_right_to_left);
Ok(data)
}
fn write_recommended_buffer_size_frame(
buffer_size: u32,
embedded_info_flag: bool,
offset_to_next_tag: u32,
) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.extend_from_slice(&buffer_size.to_be_bytes()[1..]);
data.push(if embedded_info_flag { 0x01 } else { 0x00 });
data.extend_from_slice(&offset_to_next_tag.to_be_bytes());
Ok(data)
}
fn write_audio_encryption_frame(
owner: &str,
preview_start: u16,
preview_length: u16,
encryption_info: &[u8],
) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.extend_from_slice(owner.as_bytes());
data.push(0);
data.extend_from_slice(&preview_start.to_be_bytes());
data.extend_from_slice(&preview_length.to_be_bytes());
data.extend_from_slice(encryption_info);
Ok(data)
}
fn write_position_sync_frame(format: TimeStampFormat, position: u32) -> Result<Vec<u8>> {
let mut data = vec![format as u8];
data.extend_from_slice(&position.to_be_bytes());
Ok(data)
}
fn write_signature_frame(group_symbol: u8, signature: &[u8]) -> Result<Vec<u8>> {
let mut data = vec![group_symbol];
data.extend_from_slice(signature);
Ok(data)
}
fn write_seek_frame(minimum_offset: u32) -> Result<Vec<u8>> {
Ok(minimum_offset.to_be_bytes().to_vec())
}
fn write_chapter_frame(
element_id: &str,
start_time: u32,
end_time: u32,
start_offset: u32,
end_offset: u32,
sub_frames: &[FrameData],
version: (u8, u8),
) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.extend_from_slice(element_id.as_bytes());
data.push(0);
data.extend_from_slice(&start_time.to_be_bytes());
data.extend_from_slice(&end_time.to_be_bytes());
data.extend_from_slice(&start_offset.to_be_bytes());
data.extend_from_slice(&end_offset.to_be_bytes());
for sub_frame in sub_frames {
let sub_frame_data = sub_frame.to_bytes(version)?;
append_embedded_frame(&mut data, sub_frame.id(), &sub_frame_data, version)?;
}
Ok(data)
}
fn write_table_of_contents_frame(
element_id: &str,
flags: u8,
child_elements: &[String],
sub_frames: &[FrameData],
version: (u8, u8),
) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.extend_from_slice(element_id.as_bytes());
data.push(0);
data.push(flags);
data.push(child_elements.len() as u8);
for child in child_elements {
data.extend_from_slice(child.as_bytes());
data.push(0);
}
for sub_frame in sub_frames {
let sub_frame_data = sub_frame.to_bytes(version)?;
append_embedded_frame(&mut data, sub_frame.id(), &sub_frame_data, version)?;
}
Ok(data)
}
fn parse_genre_frame(data: &[u8]) -> Result<Self> {
if data.is_empty() {
return Ok(FrameData::Genre {
encoding: TextEncoding::Latin1,
genres: vec![],
});
}
let encoding = TextEncoding::from_byte(data[0])?;
let text_data = &data[1..];
let text = encoding.decode_text(text_data)?;
let genres = Self::parse_genre_text(&text);
Ok(FrameData::Genre { encoding, genres })
}
pub fn parse_genre_text(text: &str) -> Vec<String> {
let mut all_genres = Vec::new();
let genre_parts: Vec<&str> = text.split('\0').filter(|s| !s.is_empty()).collect();
for part in genre_parts {
let mut genres = Vec::new();
if part.chars().all(|c| c.is_ascii_digit()) {
if let Ok(genre_id) = part.parse::<usize>() {
if let Some(genre) = ID3V1_GENRES.get(genre_id) {
genres.push(genre.to_string());
} else {
genres.push("Unknown".to_string());
}
all_genres.extend(genres);
continue;
}
}
if part == "CR" {
all_genres.push("Cover".to_string());
continue;
}
if part == "RX" {
all_genres.push("Remix".to_string());
continue;
}
let mut remaining = part;
while remaining.starts_with('(') {
if let Some(end_paren) = remaining.find(')') {
let genre_id_str = &remaining[1..end_paren];
remaining = &remaining[end_paren + 1..];
if let Ok(genre_id) = genre_id_str.parse::<usize>() {
if let Some(genre) = ID3V1_GENRES.get(genre_id) {
genres.push(genre.to_string());
} else {
genres.push("Unknown".to_string());
}
} else if genre_id_str == "CR" {
genres.push("Cover".to_string());
} else if genre_id_str == "RX" {
genres.push("Remix".to_string());
} else {
genres.push("Unknown".to_string());
}
} else {
break;
}
}
if !remaining.is_empty() {
let mut name = remaining.to_string();
if name.starts_with("((") {
name = name[1..].to_string();
}
if !genres.contains(&name) {
genres.push(name);
}
}
if genres.is_empty() && !part.is_empty() {
genres.push(part.to_string());
}
all_genres.extend(genres);
}
if all_genres.is_empty() && !text.is_empty() {
all_genres.push(text.to_string());
}
all_genres
}
pub fn parse_paired_text_frame(frame_id: &str, data: &[u8]) -> Result<Self> {
if data.is_empty() {
return Ok(FrameData::PairedText {
id: frame_id.to_string(),
encoding: TextEncoding::Latin1,
people: vec![],
});
}
let encoding = TextEncoding::from_byte(data[0])?;
let text_data = &data[1..];
let null_term = encoding.null_terminator();
let mut people = Vec::new();
if null_term.len() == 1 {
let parts: Vec<_> = text_data
.split(|&b| b == null_term[0])
.filter(|part| !part.is_empty())
.map(|part| encoding.decode_text(part))
.collect::<Result<Vec<_>>>()?;
for pair in parts.chunks(2) {
if pair.len() == 2 {
people.push((pair[0].clone(), pair[1].clone()));
}
}
} else {
let mut parts = Vec::new();
let mut start = 0;
while start < text_data.len() {
let mut end = start;
while end + 1 < text_data.len() {
if text_data[end] == 0 && text_data[end + 1] == 0 {
break;
}
end += 2;
}
if end > start {
let part = encoding.decode_text(&text_data[start..end])?;
if !part.is_empty() {
parts.push(part);
}
}
start = end + 2;
}
for pair in parts.chunks(2) {
if pair.len() == 2 {
people.push((pair[0].clone(), pair[1].clone()));
}
}
}
Ok(FrameData::PairedText {
id: frame_id.to_string(),
encoding,
people,
})
}
pub fn parse_numeric_text_frame(frame_id: &str, data: &[u8]) -> Result<Self> {
let text_frame = Self::parse_text_frame(frame_id, data)?;
if let FrameData::Text { id, encoding, text } = text_frame {
let value = text.first().and_then(|s| s.parse::<u64>().ok());
Ok(FrameData::NumericText {
id,
encoding,
text,
value,
})
} else {
Err(AudexError::InvalidData(
"Failed to parse as text frame".to_string(),
))
}
}
pub fn parse_numeric_part_text_frame(frame_id: &str, data: &[u8]) -> Result<Self> {
let text_frame = Self::parse_text_frame(frame_id, data)?;
if let FrameData::Text { id, encoding, text } = text_frame {
let value = text
.first()
.and_then(|s| s.split('/').next())
.and_then(|s| s.parse::<u64>().ok());
Ok(FrameData::NumericPartText {
id,
encoding,
text,
value,
})
} else {
Err(AudexError::InvalidData(
"Failed to parse as text frame".to_string(),
))
}
}
pub fn parse_timestamp_text_frame(frame_id: &str, data: &[u8]) -> Result<Self> {
if data.is_empty() {
return Ok(FrameData::TimeStampText {
id: frame_id.to_string(),
encoding: TextEncoding::Latin1,
timestamps: vec![],
});
}
let encoding = TextEncoding::from_byte(data[0])?;
let text_data = &data[1..];
let text = encoding.decode_text(text_data)?;
let timestamps: Vec<_> = text
.split(',')
.filter(|s| !s.trim().is_empty())
.map(|s| ID3TimeStamp::parse(s.trim()))
.collect();
Ok(FrameData::TimeStampText {
id: frame_id.to_string(),
encoding,
timestamps,
})
}
pub fn upgrade_v22_frame_id(v22_id: &str, target_version: (u8, u8)) -> String {
if target_version == (2, 4) {
for &(old_id, new_id) in ID3V22_TO_V24_UPGRADES {
if old_id == v22_id {
return new_id.to_string();
}
}
}
for &(old_id, new_id) in ID3V22_UPGRADE_MAP {
if old_id == v22_id {
return new_id.to_string();
}
}
v22_id.to_string()
}
pub fn get_v23_frame(&self) -> Result<FrameData> {
match self {
FrameData::TimeStampText {
id,
encoding,
timestamps,
} => {
match id.as_str() {
"TDRC" => {
if let Some(first_timestamp) = timestamps.first() {
if let Some(year) = first_timestamp.year {
Ok(FrameData::Text {
id: "TYER".to_string(),
encoding: *encoding,
text: vec![year.to_string()],
})
} else {
Ok(self.clone())
}
} else {
Ok(self.clone())
}
}
"TDOR" => {
if let Some(first_timestamp) = timestamps.first() {
if let Some(year) = first_timestamp.year {
Ok(FrameData::Text {
id: "TORY".to_string(),
encoding: *encoding,
text: vec![year.to_string()],
})
} else {
Ok(self.clone())
}
} else {
Ok(self.clone())
}
}
_ => Ok(self.clone()),
}
}
_ => Ok(self.clone()),
}
}
pub fn merge_frame(&mut self, other: FrameData) -> Result<()> {
match (self, other) {
(
FrameData::Text {
text: self_text, ..
},
FrameData::Text {
text: other_text, ..
},
) => {
for text in other_text {
if !self_text.contains(&text) {
self_text.push(text);
}
}
Ok(())
}
(
FrameData::Genre {
genres: self_genres,
..
},
FrameData::Genre {
genres: other_genres,
..
},
) => {
for genre in other_genres {
if !self_genres.contains(&genre) {
self_genres.push(genre);
}
}
Ok(())
}
(
FrameData::PairedText {
people: self_people,
..
},
FrameData::PairedText {
people: other_people,
..
},
) => {
for person in other_people {
if !self_people.contains(&person) {
self_people.push(person);
}
}
Ok(())
}
(
FrameData::NumericText {
text: self_text, ..
},
FrameData::NumericText {
text: other_text, ..
},
) => {
for text in other_text {
if !self_text.contains(&text) {
self_text.push(text);
}
}
Ok(())
}
(
FrameData::NumericPartText {
text: self_text, ..
},
FrameData::NumericPartText {
text: other_text, ..
},
) => {
for text in other_text {
if !self_text.contains(&text) {
self_text.push(text);
}
}
Ok(())
}
(
FrameData::TimeStampText {
timestamps: self_timestamps,
..
},
FrameData::TimeStampText {
timestamps: other_timestamps,
..
},
) => {
for timestamp in other_timestamps {
if !self_timestamps.iter().any(|ts| ts.text == timestamp.text) {
self_timestamps.push(timestamp);
}
}
Ok(())
}
_ => Err(AudexError::Unsupported(
"Frame merging not supported for this frame type combination".to_string(),
)),
}
}
pub fn hash_key(&self) -> String {
match self {
FrameData::Text { id, .. } => id.clone(),
FrameData::UserText { description, .. } => format!("TXXX:{}", description),
FrameData::Url { id, .. } => id.clone(),
FrameData::UserUrl { description, .. } => format!("WXXX:{}", description),
FrameData::Comment {
description,
language,
..
} => {
let lang_str = std::str::from_utf8(language).unwrap_or("unknown");
format!("COMM:{}:{}", lang_str, description)
}
FrameData::UnsyncLyrics {
description,
language,
..
} => {
let lang_str = std::str::from_utf8(language).unwrap_or("unknown");
format!("USLT:{}:{}", lang_str, description)
}
FrameData::AttachedPicture {
picture_type,
description,
..
} => {
format!("APIC:{}:{}", *picture_type as u8, description)
}
FrameData::GeneralObject { description, .. } => format!("GEOB:{}", description),
FrameData::PlayCounter { .. } => "PCNT".to_string(),
FrameData::Popularimeter { email, .. } => format!("POPM:{}", email),
FrameData::Private { owner, .. } => format!("PRIV:{}", owner),
FrameData::UniqueFileId { owner, .. } => format!("UFID:{}", owner),
FrameData::TermsOfUse { language, .. } => {
let lang_str = std::str::from_utf8(language).unwrap_or("unknown");
format!("USER:{}", lang_str)
}
FrameData::Genre { .. } => "TCON".to_string(),
FrameData::PairedText { id, .. } => id.clone(),
FrameData::NumericText { id, .. } => id.clone(),
FrameData::NumericPartText { id, .. } => id.clone(),
FrameData::TimeStampText { id, .. } => id.clone(),
_ => self.id().to_string(),
}
}
}
pub trait Frame: fmt::Debug + Send + Sync + Any {
fn frame_id(&self) -> &str;
fn to_data(&self) -> Result<Vec<u8>>;
fn description(&self) -> String;
fn text_values(&self) -> Option<Vec<String>> {
None
}
fn hash_key(&self) -> String {
self.frame_id().to_string()
}
fn merge_frame(&mut self, _other: Box<dyn Frame>) -> Result<()> {
Err(AudexError::Unsupported(
"Frame merging not supported for this frame type".to_string(),
))
}
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn convert_encoding_for_version(&mut self, _version: (u8, u8)) {
}
fn frame_flags(&self) -> FrameFlags {
FrameFlags::new()
}
fn set_frame_flags(&mut self, _flags: FrameFlags) {
}
}
pub(crate) fn serialize_frame_for_version(frame: &dyn Frame, version: (u8, u8)) -> Result<Vec<u8>> {
if let Some(chap) = frame.as_any().downcast_ref::<CHAP>() {
return chap.to_data_for_version(version);
}
if let Some(ctoc) = frame.as_any().downcast_ref::<CTOC>() {
return ctoc.to_data_for_version(version);
}
frame.to_data()
}
fn append_embedded_frame(
data: &mut Vec<u8>,
frame_id: &str,
frame_data: &[u8],
version: (u8, u8),
) -> Result<()> {
let size = u32::try_from(frame_data.len()).map_err(|_| {
AudexError::InvalidData(format!(
"Embedded frame '{}' too large: {} bytes",
frame_id,
frame_data.len()
))
})?;
data.extend_from_slice(frame_id.as_bytes());
if version.1 == 4 {
data.extend_from_slice(&super::util::encode_synchsafe_int(size)?);
} else {
data.extend_from_slice(&size.to_be_bytes());
}
data.extend_from_slice(&0u16.to_be_bytes());
data.extend_from_slice(frame_data);
Ok(())
}
pub trait HasEncoding {
fn get_encoding(&self) -> TextEncoding;
fn set_encoding(&mut self, encoding: TextEncoding);
fn convert_encoding_for_version(&mut self, version: (u8, u8)) {
let current = self.get_encoding();
if !current.is_valid_for_version(version) {
let new_encoding = match current {
TextEncoding::Utf8 | TextEncoding::Utf16Be => TextEncoding::Utf16,
enc => enc,
};
self.set_encoding(new_encoding);
}
}
}
#[derive(Debug, Clone)]
struct CommercialFrameParams<'a> {
encoding: TextEncoding,
price: &'a str,
valid_until: &'a str,
contact_url: &'a str,
received_as: u8,
seller: &'a str,
description: &'a str,
picture_mime: &'a str,
picture: &'a [u8],
_version: (u8, u8),
}
#[derive(Debug, Clone)]
struct ReverbFrameParams {
reverb_left: u16,
reverb_right: u16,
reverb_bounces_left: u8,
reverb_bounces_right: u8,
reverb_feedback_left_to_left: u8,
reverb_feedback_left_to_right: u8,
reverb_feedback_right_to_right: u8,
reverb_feedback_right_to_left: u8,
premix_left_to_right: u8,
premix_right_to_left: u8,
}
#[derive(Debug, Clone, Default)]
pub struct TextFrame {
pub frame_id: String,
pub encoding: TextEncoding,
pub text: Vec<String>,
pub flags: FrameFlags,
}
impl TextFrame {
pub fn new(frame_id: String, text: Vec<String>) -> Self {
Self {
frame_id,
encoding: TextEncoding::Utf16, text,
flags: FrameFlags::new(),
}
}
pub fn with_encoding(frame_id: String, encoding: TextEncoding, text: Vec<String>) -> Self {
Self {
frame_id,
encoding,
text,
flags: FrameFlags::new(),
}
}
pub fn with_flags(frame_id: String, text: Vec<String>, flags: FrameFlags) -> Self {
Self {
frame_id,
encoding: TextEncoding::Utf16,
text,
flags,
}
}
pub fn single(frame_id: String, text: String) -> Self {
Self::new(frame_id, vec![text])
}
}
impl TextFrame {
pub fn get_text(&self) -> String {
self.text.join("\u{0000}")
}
pub fn set_text(&mut self, text: String) {
self.text = vec![text];
}
}
impl HasEncoding for TextFrame {
fn get_encoding(&self) -> TextEncoding {
self.encoding
}
fn set_encoding(&mut self, encoding: TextEncoding) {
self.encoding = encoding;
}
fn convert_encoding_for_version(&mut self, version: (u8, u8)) {
let current = self.get_encoding();
if !current.is_valid_for_version(version) {
self.set_encoding(TextEncoding::Utf16);
}
}
}
impl Frame for TextFrame {
fn frame_id(&self) -> &str {
&self.frame_id
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut data = vec![self.encoding.to_byte()];
if matches!(self.encoding, TextEncoding::Utf16) && self.text.len() > 1 {
for (i, val) in self.text.iter().enumerate() {
if i > 0 {
data.extend(self.encoding.null_terminator());
}
data.extend(self.encoding.encode_text(val)?);
}
} else {
let text_str = self.text.join("\0");
data.extend(self.encoding.encode_text(&text_str)?);
}
data.extend(self.encoding.null_terminator());
Ok(data)
}
fn description(&self) -> String {
format!("{}: {}", self.frame_id, self.text.join("; "))
}
fn text_values(&self) -> Option<Vec<String>> {
Some(self.text.clone())
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn convert_encoding_for_version(&mut self, version: (u8, u8)) {
HasEncoding::convert_encoding_for_version(self, version);
}
fn frame_flags(&self) -> FrameFlags {
self.flags.clone()
}
fn set_frame_flags(&mut self, flags: FrameFlags) {
self.flags = flags;
}
}
#[derive(Debug, Clone, Default)]
pub struct TXXX {
pub encoding: TextEncoding,
pub description: String,
pub text: Vec<String>,
}
impl TXXX {
pub fn new(encoding: TextEncoding, desc: String, text: Vec<String>) -> Self {
Self {
encoding,
description: desc,
text,
}
}
pub fn single(encoding: TextEncoding, desc: String, text: String) -> Self {
Self::new(encoding, desc, vec![text])
}
}
impl HasEncoding for TXXX {
fn get_encoding(&self) -> TextEncoding {
self.encoding
}
fn set_encoding(&mut self, encoding: TextEncoding) {
self.encoding = encoding;
}
fn convert_encoding_for_version(&mut self, version: (u8, u8)) {
let current = self.get_encoding();
if !current.is_valid_for_version(version) {
self.set_encoding(TextEncoding::Utf16);
}
}
}
impl Frame for TXXX {
fn frame_id(&self) -> &str {
"TXXX"
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut data = vec![self.encoding.to_byte()];
data.extend(self.encoding.encode_text(&self.description)?);
data.extend(self.encoding.null_terminator());
let text_str = self.text.join("\0");
data.extend(self.encoding.encode_text(&text_str)?);
data.extend(self.encoding.null_terminator());
Ok(data)
}
fn description(&self) -> String {
format!("TXXX: {} = {}", self.description, self.text.join("; "))
}
fn text_values(&self) -> Option<Vec<String>> {
Some(self.text.clone())
}
fn hash_key(&self) -> String {
format!("TXXX:{}", self.description)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn convert_encoding_for_version(&mut self, version: (u8, u8)) {
HasEncoding::convert_encoding_for_version(self, version);
}
}
#[derive(Debug, Clone)]
pub struct COMM {
pub encoding: TextEncoding,
pub language: [u8; 3],
pub description: String,
pub text: String,
}
impl COMM {
pub fn new(encoding: TextEncoding, lang: [u8; 3], desc: String, text: String) -> Self {
Self {
encoding,
language: lang,
description: desc,
text,
}
}
}
impl HasEncoding for COMM {
fn get_encoding(&self) -> TextEncoding {
self.encoding
}
fn set_encoding(&mut self, encoding: TextEncoding) {
self.encoding = encoding;
}
fn convert_encoding_for_version(&mut self, version: (u8, u8)) {
let current = self.get_encoding();
if !current.is_valid_for_version(version) {
self.set_encoding(TextEncoding::Utf16);
}
}
}
impl Frame for COMM {
fn frame_id(&self) -> &str {
"COMM"
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut data = vec![self.encoding.to_byte()];
data.extend_from_slice(&self.language);
data.extend(self.encoding.encode_text(&self.description)?);
data.extend(self.encoding.null_terminator());
data.extend(self.encoding.encode_text(&self.text)?);
data.extend(self.encoding.null_terminator());
Ok(data)
}
fn description(&self) -> String {
format!("COMM: {} ({})", self.text, self.description)
}
fn text_values(&self) -> Option<Vec<String>> {
Some(vec![self.text.clone()])
}
fn hash_key(&self) -> String {
let lang_str = std::str::from_utf8(&self.language).unwrap_or("unknown");
if self.description.is_empty() {
format!("COMM::{}", lang_str)
} else {
format!("COMM:{}:{}", self.description, lang_str)
}
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn convert_encoding_for_version(&mut self, version: (u8, u8)) {
HasEncoding::convert_encoding_for_version(self, version);
}
}
impl Default for COMM {
fn default() -> Self {
Self {
encoding: TextEncoding::default(), language: *b"XXX", description: String::new(),
text: String::new(),
}
}
}
pub type TCON = TextFrame;
pub type TALB = TextFrame; pub type TBPM = TextFrame; pub type TCOM = TextFrame; pub type TCOP = TextFrame; pub type TCMP = TextFrame; pub type TDAT = TextFrame; pub type TDEN = TextFrame; pub type TDES = TextFrame; pub type TDLY = TextFrame; pub type TDOR = TextFrame; pub type TDRC = TextFrame; pub type TDRL = TextFrame; pub type TDTG = TextFrame; pub type TENC = TextFrame; pub type TEXT = TextFrame; pub type TFLT = TextFrame; pub type TGID = TextFrame; pub type TIME = TextFrame; pub type TIT1 = TextFrame; pub type TIT2 = TextFrame; pub type TIT3 = TextFrame; pub type TKEY = TextFrame; pub type TKWD = TextFrame; pub type TLAN = TextFrame; pub type TLEN = TextFrame; pub type TMED = TextFrame; pub type TMOO = TextFrame; pub type TOAL = TextFrame; pub type TOFN = TextFrame; pub type TOLY = TextFrame; pub type TOPE = TextFrame; pub type TORY = TextFrame; pub type TOWN = TextFrame; pub type TPE1 = TextFrame; pub type TPE2 = TextFrame; pub type TPE3 = TextFrame; pub type TPE4 = TextFrame; pub type TPOS = TextFrame; pub type TPRO = TextFrame; pub type TPUB = TextFrame; pub type TRCK = TextFrame; pub type TRDA = TextFrame; pub type TRSN = TextFrame; pub type TRSO = TextFrame; pub type TSIZ = TextFrame; pub type TSOA = TextFrame; pub type TSOC = TextFrame; pub type TSOP = TextFrame; pub type TSOT = TextFrame; pub type TSRC = TextFrame; pub type TSSE = TextFrame; pub type TSST = TextFrame; pub type TYER = TextFrame; pub type TSO2 = TextFrame; pub type TCAT = TextFrame; pub type MVNM = TextFrame; pub type MVIN = TextFrame; pub type GRP1 = TextFrame;
pub type WCOM = TextFrame; pub type WCOP = TextFrame; pub type WFED = TextFrame; pub type WOAF = TextFrame; pub type WOAR = TextFrame; pub type WOAS = TextFrame; pub type WORS = TextFrame; pub type WPAY = TextFrame; pub type WPUB = TextFrame;
#[derive(Debug, Clone)]
pub struct EQU2 {
pub interpolation_method: u8,
pub identification: String,
pub adjustments: Vec<(u16, i16)>,
}
impl EQU2 {
pub fn new(method: u8, id: String, adjustments: Vec<(u16, i16)>) -> Self {
Self {
interpolation_method: method,
identification: id,
adjustments,
}
}
}
impl Frame for EQU2 {
fn frame_id(&self) -> &str {
"EQU2"
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut data = vec![self.interpolation_method];
data.extend_from_slice(self.identification.as_bytes());
data.push(0); for (freq, adj) in &self.adjustments {
data.extend_from_slice(&freq.to_be_bytes());
data.extend_from_slice(&adj.to_be_bytes());
}
Ok(data)
}
fn description(&self) -> String {
format!(
"EQU2: {} ({} adjustments)",
self.identification,
self.adjustments.len()
)
}
fn hash_key(&self) -> String {
format!("EQU2:{}", self.identification)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
#[derive(Debug, Clone)]
pub struct MLLT {
pub frames_between_reference: u16,
pub bytes_between_reference: u32, pub millis_between_reference: u32, pub bits_for_bytes_deviation: u8,
pub bits_for_millis_deviation: u8,
pub data: Vec<u8>,
}
impl MLLT {
pub fn new(
frames: u16,
bytes: u32,
millis: u32,
bits_bytes: u8,
bits_millis: u8,
data: Vec<u8>,
) -> Self {
Self {
frames_between_reference: frames,
bytes_between_reference: bytes & 0xFFFFFF, millis_between_reference: millis & 0xFFFFFF, bits_for_bytes_deviation: bits_bytes,
bits_for_millis_deviation: bits_millis,
data,
}
}
}
impl Frame for MLLT {
fn frame_id(&self) -> &str {
"MLLT"
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.extend_from_slice(&self.frames_between_reference.to_be_bytes());
data.extend_from_slice(&[
(self.bytes_between_reference >> 16) as u8,
(self.bytes_between_reference >> 8) as u8,
self.bytes_between_reference as u8,
]);
data.extend_from_slice(&[
(self.millis_between_reference >> 16) as u8,
(self.millis_between_reference >> 8) as u8,
self.millis_between_reference as u8,
]);
data.push(self.bits_for_bytes_deviation);
data.push(self.bits_for_millis_deviation);
data.extend_from_slice(&self.data);
Ok(data)
}
fn description(&self) -> String {
format!(
"MLLT: {} frames between references",
self.frames_between_reference
)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
#[derive(Debug, Clone)]
pub struct USER {
pub encoding: TextEncoding,
pub language: [u8; 3],
pub text: String,
}
impl USER {
pub fn new(encoding: TextEncoding, lang: [u8; 3], text: String) -> Self {
Self {
encoding,
language: lang,
text,
}
}
}
impl HasEncoding for USER {
fn get_encoding(&self) -> TextEncoding {
self.encoding
}
fn set_encoding(&mut self, encoding: TextEncoding) {
self.encoding = encoding;
}
}
impl Frame for USER {
fn frame_id(&self) -> &str {
"USER"
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut data = vec![self.encoding.to_byte()];
data.extend_from_slice(&self.language);
data.extend(self.encoding.encode_text(&self.text)?);
Ok(data)
}
fn description(&self) -> String {
let lang_str = std::str::from_utf8(&self.language).unwrap_or("unknown");
format!("USER: {} (lang: {})", self.text, lang_str)
}
fn text_values(&self) -> Option<Vec<String>> {
Some(vec![self.text.clone()])
}
fn hash_key(&self) -> String {
let lang_str = std::str::from_utf8(&self.language).unwrap_or("unknown");
format!("USER:{}", lang_str)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn convert_encoding_for_version(&mut self, version: (u8, u8)) {
HasEncoding::convert_encoding_for_version(self, version);
}
}
#[derive(Debug, Clone)]
pub struct TIPL {
pub encoding: TextEncoding,
pub people: Vec<(String, String)>,
}
impl TIPL {
pub fn new(encoding: TextEncoding, people: Vec<(String, String)>) -> Self {
Self { encoding, people }
}
}
impl HasEncoding for TIPL {
fn get_encoding(&self) -> TextEncoding {
self.encoding
}
fn set_encoding(&mut self, encoding: TextEncoding) {
self.encoding = encoding;
}
}
impl Frame for TIPL {
fn frame_id(&self) -> &str {
"TIPL"
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut data = vec![self.encoding.to_byte()];
for (role, person) in &self.people {
data.extend(self.encoding.encode_text(role)?);
data.extend(self.encoding.null_terminator());
data.extend(self.encoding.encode_text(person)?);
data.extend(self.encoding.null_terminator());
}
Ok(data)
}
fn description(&self) -> String {
let roles: Vec<String> = self
.people
.iter()
.map(|(role, person)| format!("{}: {}", role, person))
.collect();
format!("TIPL: {}", roles.join(", "))
}
fn text_values(&self) -> Option<Vec<String>> {
Some(
self.people
.iter()
.map(|(role, person)| format!("{}={}", role, person))
.collect(),
)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn convert_encoding_for_version(&mut self, version: (u8, u8)) {
HasEncoding::convert_encoding_for_version(self, version);
}
}
#[derive(Debug, Clone)]
pub struct TMCL {
pub encoding: TextEncoding,
pub credits: Vec<(String, String)>,
}
impl TMCL {
pub fn new(encoding: TextEncoding, credits: Vec<(String, String)>) -> Self {
Self { encoding, credits }
}
}
impl HasEncoding for TMCL {
fn get_encoding(&self) -> TextEncoding {
self.encoding
}
fn set_encoding(&mut self, encoding: TextEncoding) {
self.encoding = encoding;
}
}
impl Frame for TMCL {
fn frame_id(&self) -> &str {
"TMCL"
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut data = vec![self.encoding.to_byte()];
for (instrument, musician) in &self.credits {
data.extend(self.encoding.encode_text(instrument)?);
data.extend(self.encoding.null_terminator());
data.extend(self.encoding.encode_text(musician)?);
data.extend(self.encoding.null_terminator());
}
Ok(data)
}
fn description(&self) -> String {
let credits: Vec<String> = self
.credits
.iter()
.map(|(inst, musician)| format!("{}: {}", inst, musician))
.collect();
format!("TMCL: {}", credits.join(", "))
}
fn text_values(&self) -> Option<Vec<String>> {
Some(
self.credits
.iter()
.map(|(inst, musician)| format!("{}={}", inst, musician))
.collect(),
)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn convert_encoding_for_version(&mut self, version: (u8, u8)) {
HasEncoding::convert_encoding_for_version(self, version);
}
}
pub type RVRB = TextFrame; pub type PCST = TextFrame;
pub type TAL = TextFrame; pub type TBP = TextFrame; pub type TCM = TextFrame; pub type TCO = TextFrame; pub type TCR = TextFrame; pub type TDA = TextFrame; pub type TDY = TextFrame; pub type TEN = TextFrame; pub type TFT = TextFrame; pub type TIM = TextFrame; pub type TT1 = TextFrame; pub type TT2 = TextFrame; pub type TT3 = TextFrame; pub type TKE = TextFrame; pub type TLA = TextFrame; pub type TLE = TextFrame; pub type TMT = TextFrame; pub type TOA = TextFrame; pub type TOF = TextFrame; pub type TOL = TextFrame; pub type TOR = TextFrame; pub type TOT = TextFrame; pub type TP1 = TextFrame; pub type TP2 = TextFrame; pub type TP3 = TextFrame; pub type TP4 = TextFrame; pub type TPA = TextFrame; pub type TPB = TextFrame; pub type TRC = TextFrame; pub type TRD = TextFrame; pub type TRK = TextFrame; pub type TSI = TextFrame; pub type TSS = TextFrame; pub type TXX = TXXX; pub type TYE = TextFrame;
pub type BUF = RBUF; pub type CNT = PCNT; pub type COM = COMM; pub type CRA = AENC; pub type CRM = TextFrame; pub type ETC = ETCO; pub type EQU = TextFrame; pub type GEO = GEOB; pub type IPL = TextFrame; pub type LNK = LINK; pub type MCI = MCDI; pub type MLL = TextFrame; pub type PIC = APIC; pub type POP = POPM; pub type REV = TextFrame; pub type RVA = RVAD; pub type SLT = SYLT; pub type STC = SYTC; pub type UFI = UFID; pub type ULT = USLT;
pub type WAF = TextFrame; pub type WAR = TextFrame; pub type WAS = TextFrame; pub type WCM = TextFrame; pub type WCP = TextFrame; pub type WPB = TextFrame; pub type WXX = WXXX;
#[derive(Debug)]
pub struct FrameRegistry;
impl Default for FrameRegistry {
fn default() -> Self {
Self::new()
}
}
impl FrameRegistry {
pub fn new() -> Self {
Self
}
pub fn create_frame(frame_id: &str, data: &[u8]) -> Result<Box<dyn Frame>> {
match frame_id {
"COMM" => {
if data.len() < 5 {
return Err(AudexError::InvalidData("COMM frame too short".to_string()));
}
let encoding = TextEncoding::from_byte(data[0])?;
let language = [data[1], data[2], data[3]];
let text_data = &data[4..];
let null_term = encoding.null_terminator();
let desc_end = if null_term.len() == 1 {
text_data
.iter()
.position(|&b| b == null_term[0])
.unwrap_or(text_data.len())
} else {
(0..(text_data.len() & !1).saturating_sub(1))
.step_by(2)
.find(|&i| text_data[i] == 0 && text_data[i + 1] == 0)
.unwrap_or(text_data.len())
};
let description = encoding.decode_text(&text_data[..desc_end])?;
let comment_start = desc_end + null_term.len();
let text = if comment_start < text_data.len() {
encoding
.decode_text(&text_data[comment_start..])?
.trim_end_matches('\0')
.to_string()
} else {
String::new()
};
Ok(Box::new(COMM::new(encoding, language, description, text)))
}
"APIC" => {
if data.len() < 3 {
return Err(AudexError::InvalidData("APIC frame too short".to_string()));
}
let encoding = TextEncoding::from_byte(data[0])?;
let mime_end = data[1..]
.iter()
.position(|&b| b == 0)
.unwrap_or(data.len() - 1)
+ 1;
let mime_type = String::from_utf8_lossy(&data[1..mime_end]).into_owned();
if mime_end + 2 >= data.len() {
return Err(AudexError::InvalidData("APIC frame corrupted".to_string()));
}
let picture_type = PictureType::from(data[mime_end + 1]);
let desc_start = mime_end + 2;
let remaining_data = &data[desc_start..];
let null_term = encoding.null_terminator();
let desc_end = if null_term.len() == 1 {
remaining_data
.iter()
.position(|&b| b == null_term[0])
.unwrap_or(remaining_data.len())
} else {
(0..(remaining_data.len() & !1).saturating_sub(1))
.step_by(2)
.find(|&i| remaining_data[i] == 0 && remaining_data[i + 1] == 0)
.unwrap_or(remaining_data.len())
} + desc_start;
let description = encoding.decode_text(&data[desc_start..desc_end])?;
if desc_end == data.len() {
return Err(AudexError::InvalidData(
"APIC frame description is not null-terminated".to_string(),
));
}
let image_start = desc_end + encoding.null_terminator().len();
crate::limits::ParseLimits::default()
.check_image_size(data[image_start..].len() as u64, "ID3 APIC image")?;
let image_data = if image_start < data.len() {
data[image_start..].to_vec()
} else {
Vec::new()
};
Ok(Box::new(APIC::new(
TextEncoding::Utf8,
mime_type,
picture_type,
description,
image_data,
)))
}
"POPM" => {
if data.is_empty() {
return Err(AudexError::InvalidData("POPM frame is empty".to_string()));
}
let email_end = data.iter().position(|&b| b == 0).unwrap_or(data.len());
let email = String::from_utf8_lossy(&data[..email_end]).into_owned();
if email_end + 1 >= data.len() {
return Err(AudexError::InvalidData(
"POPM frame missing rating".to_string(),
));
}
let rating = data[email_end + 1];
let count = if email_end + 2 < data.len() {
let count_bytes = &data[email_end + 2..];
match count_bytes.len() {
4 => Some(u32::from_be_bytes([
count_bytes[0],
count_bytes[1],
count_bytes[2],
count_bytes[3],
])),
_ => {
let mut padded = [0u8; 4];
let copy_len = count_bytes.len().min(4);
padded[4 - copy_len..].copy_from_slice(&count_bytes[..copy_len]);
Some(u32::from_be_bytes(padded))
}
}
} else {
None
};
Ok(Box::new(POPM::new(email, rating, count)))
}
"TXXX" => {
if data.is_empty() {
return Err(AudexError::InvalidData("TXXX frame is empty".to_string()));
}
let encoding = TextEncoding::from_byte(data[0])?;
let null_term = encoding.null_terminator();
let desc_end = if null_term.len() == 1 {
data[1..]
.iter()
.position(|&b| b == null_term[0])
.unwrap_or(data.len() - 1)
+ 1
} else {
(0..(data.len() & !1).saturating_sub(2))
.skip(1)
.step_by(2)
.find(|&i| data[i] == 0 && data[i + 1] == 0)
.unwrap_or(data.len())
};
let description = encoding.decode_text(&data[1..desc_end])?;
let text_start = desc_end + null_term.len();
let text_data = if text_start < data.len() {
&data[text_start..]
} else {
&[]
};
let text = if text_data.is_empty() {
vec![]
} else {
let text_str = encoding.decode_text(text_data)?;
text_str
.split('\0')
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect()
};
Ok(Box::new(TXXX::new(encoding, description, text)))
}
"USLT" => {
if data.len() < 5 {
return Err(AudexError::InvalidData("USLT frame too short".to_string()));
}
let encoding = TextEncoding::from_byte(data[0])?;
let language = [data[1], data[2], data[3]];
let desc_end = data[4..]
.iter()
.position(|&b| b == 0)
.unwrap_or(data.len() - 4)
+ 4;
let description = encoding.decode_text(&data[4..desc_end])?;
let text_start = desc_end + encoding.null_terminator().len();
let text = if text_start < data.len() {
encoding
.decode_text(&data[text_start..])?
.trim_end_matches('\0')
.to_string()
} else {
String::new()
};
Ok(Box::new(USLT::new(encoding, language, description, text)))
}
"RVA2" | "RVAD" => {
if data.is_empty() {
return Err(AudexError::InvalidData("RVA2 frame is empty".to_string()));
}
let id_end = data.iter().position(|&b| b == 0).ok_or_else(|| {
AudexError::InvalidData(
"No identification terminator in RVA2 frame".to_string(),
)
})?;
let identification = TextEncoding::Latin1.decode_text(&data[..id_end])?;
let mut channels = Vec::new();
let mut pos = id_end + 1;
while pos + 4 <= data.len() {
let channel_type = ChannelType::from(data[pos]);
let volume_adjustment = i16::from_be_bytes([data[pos + 1], data[pos + 2]]);
let gain_db = volume_adjustment as f32 / 512.0;
let peak_bits = data[pos + 3];
let peak_bytes = (peak_bits as usize).div_ceil(8);
if pos + 4 + peak_bytes > data.len() {
break;
}
let peak = if peak_bytes > 0 && peak_bits > 0 {
let mut peak_value = 0u64;
for i in 0..peak_bytes.min(8) {
peak_value = (peak_value << 8) | data[pos + 4 + i] as u64;
}
let clamped_bits = peak_bits.min(63);
peak_value as f32 / (1u64 << (clamped_bits - 1)).max(1) as f32
} else {
0.0
};
channels.push((channel_type, gain_db, peak));
pos += 4 + peak_bytes;
}
Ok(Box::new(RVA2::new(identification, channels)))
}
_ => {
if frame_id.starts_with('T') {
let encoding = if data.is_empty() {
TextEncoding::Utf8
} else {
TextEncoding::from_byte(data[0])?
};
let text_data = if data.is_empty() {
Vec::new()
} else {
data[1..].to_vec()
};
let text = if text_data.is_empty() {
vec![]
} else {
let decoded = encoding.decode_text(&text_data)?;
decoded
.split('\u{0}')
.filter(|part| !part.is_empty())
.map(|part| part.trim_start_matches('\u{feff}').to_string())
.collect()
};
let mut frame = TextFrame::new(frame_id.to_string(), text);
frame.encoding = encoding;
Ok(Box::new(frame))
} else {
Err(AudexError::UnsupportedFormat(format!(
"Unknown frame type: {}",
frame_id
)))
}
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum TimeStampFormat {
Mpeg = 1,
Milliseconds = 2,
}
impl From<u8> for TimeStampFormat {
fn from(value: u8) -> Self {
match value {
2 => TimeStampFormat::Milliseconds,
_ => TimeStampFormat::Mpeg,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum EventType {
Padding = 0x00,
EndOfInitialSilence = 0x01,
IntroStart = 0x02,
MainpartStart = 0x03,
OutroStart = 0x04,
OutroEnd = 0x05,
VerseStart = 0x06,
RefrainStart = 0x07,
InterludeStart = 0x08,
ThemeStart = 0x09,
VariationStart = 0x0A,
KeyChange = 0x0B,
TimeChange = 0x0C,
MomentaryUnwantedNoise = 0x0D,
SustainedNoise = 0x0E,
SustainedNoiseEnd = 0x0F,
IntroEnd = 0x10,
MainpartEnd = 0x11,
VerseEnd = 0x12,
RefrainEnd = 0x13,
ThemeEnd = 0x14,
Profanity = 0x15,
ProfanityEnd = 0x16,
NotPredefined = 0xFF,
}
impl From<u8> for EventType {
fn from(value: u8) -> Self {
match value {
0x01 => EventType::EndOfInitialSilence,
0x02 => EventType::IntroStart,
0x03 => EventType::MainpartStart,
0x04 => EventType::OutroStart,
0x05 => EventType::OutroEnd,
0x06 => EventType::VerseStart,
0x07 => EventType::RefrainStart,
0x08 => EventType::InterludeStart,
0x09 => EventType::ThemeStart,
0x0A => EventType::VariationStart,
0x0B => EventType::KeyChange,
0x0C => EventType::TimeChange,
0x0D => EventType::MomentaryUnwantedNoise,
0x0E => EventType::SustainedNoise,
0x0F => EventType::SustainedNoiseEnd,
0x10 => EventType::IntroEnd,
0x11 => EventType::MainpartEnd,
0x12 => EventType::VerseEnd,
0x13 => EventType::RefrainEnd,
0x14 => EventType::ThemeEnd,
0x15 => EventType::Profanity,
0x16 => EventType::ProfanityEnd,
0xFF => EventType::NotPredefined,
_ => EventType::Padding,
}
}
}
#[derive(Clone)]
pub struct APIC {
pub encoding: TextEncoding,
pub mime: String,
pub type_: PictureType,
pub desc: String,
pub data: Vec<u8>,
}
impl fmt::Debug for APIC {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
struct AllBytes<'a>(&'a [u8]);
impl<'a> fmt::Debug for AllBytes<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[")?;
for (i, byte) in self.0.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}", byte)?;
}
write!(f, "]")
}
}
f.debug_struct("APIC")
.field("encoding", &self.encoding)
.field("mime", &self.mime)
.field("type_", &self.type_)
.field("desc", &self.desc)
.field("data", &AllBytes(&self.data))
.finish()
}
}
impl APIC {
pub fn new(
encoding: TextEncoding,
mime: String,
type_: PictureType,
desc: String,
data: Vec<u8>,
) -> Self {
Self {
encoding,
mime,
type_,
desc,
data,
}
}
}
#[derive(Debug, Clone)]
pub struct POPM {
pub email: String,
pub rating: u8,
pub count: Option<u32>,
}
impl POPM {
pub fn new(email: String, rating: u8, count: Option<u32>) -> Self {
Self {
email,
rating,
count,
}
}
}
impl HasEncoding for APIC {
fn get_encoding(&self) -> TextEncoding {
self.encoding
}
fn set_encoding(&mut self, encoding: TextEncoding) {
self.encoding = encoding;
}
}
impl Frame for APIC {
fn frame_id(&self) -> &str {
"APIC"
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut data = vec![self.encoding.to_byte()];
data.extend(self.mime.as_bytes());
data.push(0);
data.push(self.type_ as u8);
data.extend(self.encoding.encode_text(&self.desc)?);
data.extend(self.encoding.null_terminator());
data.extend(&self.data);
Ok(data)
}
fn description(&self) -> String {
format!(
"APIC: {} ({}) - {} bytes",
self.desc,
self.type_,
self.data.len()
)
}
fn hash_key(&self) -> String {
format!("APIC:{}", self.desc)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn convert_encoding_for_version(&mut self, version: (u8, u8)) {
HasEncoding::convert_encoding_for_version(self, version);
}
}
impl Default for APIC {
fn default() -> Self {
Self {
encoding: TextEncoding::default(), mime: String::new(),
type_: PictureType::CoverFront, desc: String::new(),
data: Vec::new(),
}
}
}
impl Frame for POPM {
fn frame_id(&self) -> &str {
"POPM"
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.extend(self.email.as_bytes());
data.push(0);
data.push(self.rating);
if let Some(count) = self.count {
let count_bytes = count.to_be_bytes();
data.extend(&count_bytes);
}
Ok(data)
}
fn description(&self) -> String {
match self.count {
Some(count) => format!(
"POPM: {} - rating {}/255, count {}",
self.email, self.rating, count
),
None => format!("POPM: {} - rating {}/255", self.email, self.rating),
}
}
fn hash_key(&self) -> String {
format!("POPM:{}", self.email)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
#[derive(Debug, Clone)]
pub struct USLT {
pub encoding: TextEncoding,
pub language: [u8; 3],
pub description: String,
pub text: String,
}
impl USLT {
pub fn new(encoding: TextEncoding, lang: [u8; 3], desc: String, text: String) -> Self {
Self {
encoding,
language: lang,
description: desc,
text,
}
}
}
impl HasEncoding for USLT {
fn get_encoding(&self) -> TextEncoding {
self.encoding
}
fn set_encoding(&mut self, encoding: TextEncoding) {
self.encoding = encoding;
}
fn convert_encoding_for_version(&mut self, version: (u8, u8)) {
let current = self.get_encoding();
if !current.is_valid_for_version(version) {
self.set_encoding(TextEncoding::Utf16);
}
}
}
impl Frame for USLT {
fn frame_id(&self) -> &str {
"USLT"
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut data = vec![self.encoding.to_byte()];
data.extend_from_slice(&self.language);
data.extend(self.encoding.encode_text(&self.description)?);
data.extend(self.encoding.null_terminator());
data.extend(self.encoding.encode_text(&self.text)?);
data.extend(self.encoding.null_terminator());
Ok(data)
}
fn description(&self) -> String {
let lang_str = std::str::from_utf8(&self.language).unwrap_or("unknown");
format!("USLT: {} ({})", self.description, lang_str)
}
fn text_values(&self) -> Option<Vec<String>> {
Some(vec![self.text.clone()])
}
fn hash_key(&self) -> String {
let lang_str = std::str::from_utf8(&self.language).unwrap_or("unknown");
if self.description.is_empty() {
format!("USLT::{}", lang_str)
} else {
format!("USLT:{}:{}", self.description, lang_str)
}
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn convert_encoding_for_version(&mut self, version: (u8, u8)) {
HasEncoding::convert_encoding_for_version(self, version);
}
}
impl Default for USLT {
fn default() -> Self {
Self {
encoding: TextEncoding::default(), language: *b"XXX", description: String::new(),
text: String::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct GEOB {
pub encoding: TextEncoding,
pub mime_type: String,
pub filename: String,
pub description: String,
pub data: Vec<u8>,
}
impl GEOB {
pub fn new(mime_type: String, filename: String, description: String, data: Vec<u8>) -> Self {
Self {
encoding: TextEncoding::Utf8,
mime_type,
filename,
description,
data,
}
}
}
impl HasEncoding for GEOB {
fn get_encoding(&self) -> TextEncoding {
self.encoding
}
fn set_encoding(&mut self, encoding: TextEncoding) {
self.encoding = encoding;
}
}
impl Frame for GEOB {
fn frame_id(&self) -> &str {
"GEOB"
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut data = vec![self.encoding.to_byte()];
data.extend(self.mime_type.as_bytes());
data.push(0); data.extend(self.encoding.encode_text(&self.filename)?);
data.extend(self.encoding.null_terminator());
data.extend(self.encoding.encode_text(&self.description)?);
data.extend(self.encoding.null_terminator());
data.extend(&self.data);
Ok(data)
}
fn description(&self) -> String {
format!(
"GEOB: {} ({}) - {} bytes",
self.description,
self.mime_type,
self.data.len()
)
}
fn hash_key(&self) -> String {
format!("GEOB:{}", self.description)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn convert_encoding_for_version(&mut self, version: (u8, u8)) {
HasEncoding::convert_encoding_for_version(self, version);
}
}
#[derive(Debug, Clone)]
pub struct WXXX {
pub encoding: TextEncoding,
pub description: String,
pub url: String,
}
impl WXXX {
pub fn new(description: String, url: String) -> Self {
Self {
encoding: TextEncoding::Utf8,
description,
url,
}
}
}
impl HasEncoding for WXXX {
fn get_encoding(&self) -> TextEncoding {
self.encoding
}
fn set_encoding(&mut self, encoding: TextEncoding) {
self.encoding = encoding;
}
}
impl Frame for WXXX {
fn frame_id(&self) -> &str {
"WXXX"
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut data = vec![self.encoding.to_byte()];
data.extend(self.encoding.encode_text(&self.description)?);
data.extend(self.encoding.null_terminator());
data.extend(self.url.as_bytes());
Ok(data)
}
fn description(&self) -> String {
format!("WXXX: {} -> {}", self.description, self.url)
}
fn text_values(&self) -> Option<Vec<String>> {
Some(vec![self.url.clone()])
}
fn hash_key(&self) -> String {
format!("WXXX:{}", self.description)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn convert_encoding_for_version(&mut self, version: (u8, u8)) {
HasEncoding::convert_encoding_for_version(self, version);
}
}
#[derive(Debug)]
pub struct CHAP {
pub element_id: String,
pub start_time: u32,
pub end_time: u32,
pub start_offset: u32,
pub end_offset: u32,
pub sub_frames: std::collections::HashMap<String, Box<dyn Frame>>,
}
impl CHAP {
pub fn new(
element_id: String,
start_time: u32,
end_time: u32,
start_offset: u32,
end_offset: u32,
) -> Self {
Self {
element_id,
start_time,
end_time,
start_offset,
end_offset,
sub_frames: std::collections::HashMap::new(),
}
}
pub fn add_frame(&mut self, frame: Box<dyn Frame>) {
let hash_key = frame.hash_key();
self.sub_frames.insert(hash_key, frame);
}
pub fn get_frame(&self, hash_key: &str) -> Option<&dyn Frame> {
self.sub_frames.get(hash_key).map(|b| b.as_ref())
}
pub fn remove_frame(&mut self, hash_key: &str) -> Option<Box<dyn Frame>> {
self.sub_frames.remove(hash_key)
}
pub(crate) fn to_data_for_version(&self, version: (u8, u8)) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.extend(self.element_id.as_bytes());
data.push(0);
data.extend(&self.start_time.to_be_bytes());
data.extend(&self.end_time.to_be_bytes());
data.extend(&self.start_offset.to_be_bytes());
data.extend(&self.end_offset.to_be_bytes());
for frame in self.sub_frames.values() {
let frame_data = serialize_frame_for_version(frame.as_ref(), version)?;
append_embedded_frame(&mut data, frame.frame_id(), &frame_data, version)?;
}
Ok(data)
}
}
impl Frame for CHAP {
fn frame_id(&self) -> &str {
"CHAP"
}
fn to_data(&self) -> Result<Vec<u8>> {
self.to_data_for_version((2, 4))
}
fn description(&self) -> String {
format!(
"CHAP: {} ({}ms - {}ms)",
self.element_id, self.start_time, self.end_time
)
}
fn hash_key(&self) -> String {
format!("CHAP:{}", self.element_id)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl Default for CHAP {
fn default() -> Self {
Self {
element_id: String::new(),
start_time: 0,
end_time: 0,
start_offset: 0xFFFFFFFF,
end_offset: 0xFFFFFFFF,
sub_frames: std::collections::HashMap::new(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CTOCFlags(pub u8);
impl CTOCFlags {
pub const ORDERED: u8 = 0x01;
pub const TOP_LEVEL: u8 = 0x02;
pub fn new(value: u8) -> Self {
Self(value)
}
pub fn is_ordered(&self) -> bool {
(self.0 & Self::ORDERED) != 0
}
pub fn is_top_level(&self) -> bool {
(self.0 & Self::TOP_LEVEL) != 0
}
pub fn set_ordered(&mut self, ordered: bool) {
if ordered {
self.0 |= Self::ORDERED;
} else {
self.0 &= !Self::ORDERED;
}
}
pub fn set_top_level(&mut self, top_level: bool) {
if top_level {
self.0 |= Self::TOP_LEVEL;
} else {
self.0 &= !Self::TOP_LEVEL;
}
}
}
impl From<u8> for CTOCFlags {
fn from(value: u8) -> Self {
Self(value)
}
}
impl From<CTOCFlags> for u8 {
fn from(flags: CTOCFlags) -> u8 {
flags.0
}
}
#[derive(Debug)]
pub struct CTOC {
pub element_id: String,
pub flags: CTOCFlags,
pub child_element_ids: Vec<String>,
pub sub_frames: std::collections::HashMap<String, Box<dyn Frame>>,
}
impl CTOC {
pub fn new(element_id: String, flags: CTOCFlags, child_element_ids: Vec<String>) -> Self {
Self {
element_id,
flags,
child_element_ids,
sub_frames: std::collections::HashMap::new(),
}
}
pub fn add_frame(&mut self, frame: Box<dyn Frame>) {
let hash_key = frame.hash_key();
self.sub_frames.insert(hash_key, frame);
}
pub fn get_frame(&self, hash_key: &str) -> Option<&dyn Frame> {
self.sub_frames.get(hash_key).map(|b| b.as_ref())
}
pub fn remove_frame(&mut self, hash_key: &str) -> Option<Box<dyn Frame>> {
self.sub_frames.remove(hash_key)
}
pub fn add_child(&mut self, element_id: String) {
self.child_element_ids.push(element_id);
}
pub(crate) fn to_data_for_version(&self, version: (u8, u8)) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.extend(self.element_id.as_bytes());
data.push(0);
data.push(self.flags.0);
data.push(self.child_element_ids.len() as u8);
for child_id in &self.child_element_ids {
data.extend(child_id.as_bytes());
data.push(0);
}
for frame in self.sub_frames.values() {
let frame_data = serialize_frame_for_version(frame.as_ref(), version)?;
append_embedded_frame(&mut data, frame.frame_id(), &frame_data, version)?;
}
Ok(data)
}
}
impl Frame for CTOC {
fn frame_id(&self) -> &str {
"CTOC"
}
fn to_data(&self) -> Result<Vec<u8>> {
self.to_data_for_version((2, 4))
}
fn description(&self) -> String {
format!(
"CTOC: {} (flags={}, children={})",
self.element_id,
self.flags.0,
self.child_element_ids.join(",")
)
}
fn hash_key(&self) -> String {
format!("CTOC:{}", self.element_id)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl Default for CTOC {
fn default() -> Self {
Self {
element_id: String::new(),
flags: CTOCFlags::new(0),
child_element_ids: Vec::new(),
sub_frames: std::collections::HashMap::new(),
}
}
}
pub mod chapters {
use super::{CHAP, CTOC};
use std::collections::HashMap;
#[derive(Debug)]
pub struct ChapterNode<'a> {
pub element_id: String,
pub chapter: Option<&'a CHAP>,
pub toc: Option<&'a CTOC>,
pub children: Vec<ChapterNode<'a>>,
}
impl<'a> ChapterNode<'a> {
pub fn is_chapter(&self) -> bool {
self.chapter.is_some()
}
pub fn is_toc(&self) -> bool {
self.toc.is_some()
}
pub fn get_all_chapters(&self) -> Vec<&'a CHAP> {
let mut chapters = Vec::new();
if let Some(chap) = self.chapter {
chapters.push(chap);
}
for child in &self.children {
chapters.extend(child.get_all_chapters());
}
chapters
}
}
pub fn build_chapter_tree<'a>(
chapters: &'a HashMap<String, CHAP>,
tocs: &'a HashMap<String, CTOC>,
) -> Option<ChapterNode<'a>> {
let root_toc = tocs.values().find(|toc| toc.flags.is_top_level())?;
Some(build_node_recursive(&root_toc.element_id, chapters, tocs))
}
fn build_node_recursive<'a>(
element_id: &str,
chapters: &'a HashMap<String, CHAP>,
tocs: &'a HashMap<String, CTOC>,
) -> ChapterNode<'a> {
if let Some(chapter) = chapters.get(element_id) {
return ChapterNode {
element_id: element_id.to_string(),
chapter: Some(chapter),
toc: None,
children: Vec::new(),
};
}
if let Some(toc) = tocs.get(element_id) {
let children: Vec<ChapterNode<'a>> = toc
.child_element_ids
.iter()
.map(|child_id| build_node_recursive(child_id, chapters, tocs))
.collect();
return ChapterNode {
element_id: element_id.to_string(),
chapter: None,
toc: Some(toc),
children,
};
}
ChapterNode {
element_id: element_id.to_string(),
chapter: None,
toc: None,
children: Vec::new(),
}
}
pub fn find_chapter_at_time(
chapters: &HashMap<String, CHAP>,
timestamp_ms: u32,
) -> Option<&CHAP> {
chapters
.values()
.find(|chapter| timestamp_ms >= chapter.start_time && timestamp_ms < chapter.end_time)
}
pub fn validate_chapter_structure(
chapters: &HashMap<String, CHAP>,
tocs: &HashMap<String, CTOC>,
) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
for (id, chapter) in chapters {
if chapter.start_time >= chapter.end_time {
errors.push(format!(
"Chapter '{}' has invalid time range: {} >= {}",
id, chapter.start_time, chapter.end_time
));
}
}
let mut chapter_list: Vec<_> = chapters.values().collect();
chapter_list.sort_by_key(|c| c.start_time);
for i in 0..chapter_list.len().saturating_sub(1) {
let current = chapter_list[i];
let next = chapter_list[i + 1];
if current.end_time > next.start_time {
errors.push(format!(
"Chapters '{}' and '{}' overlap: {} > {}",
current.element_id, next.element_id, current.end_time, next.start_time
));
}
}
for (toc_id, toc) in tocs {
for child_id in &toc.child_element_ids {
if !chapters.contains_key(child_id) && !tocs.contains_key(child_id) {
errors.push(format!(
"TOC '{}' references missing element '{}'",
toc_id, child_id
));
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
pub fn get_total_duration(chapters: &HashMap<String, CHAP>) -> u32 {
chapters.values().map(|c| c.end_time).max().unwrap_or(0)
}
pub fn get_chapter_count(chapters: &HashMap<String, CHAP>) -> usize {
chapters.len()
}
}
#[derive(Debug, Clone)]
pub struct SYLT {
pub encoding: TextEncoding,
pub language: [u8; 3],
pub format: TimeStampFormat,
pub content_type: u8,
pub description: String,
pub lyrics: Vec<(String, u32)>,
}
impl SYLT {
pub fn new(
encoding: TextEncoding,
language: [u8; 3],
format: TimeStampFormat,
content_type: u8,
description: String,
lyrics: Vec<(String, u32)>,
) -> Self {
Self {
encoding,
language,
format,
content_type,
description,
lyrics,
}
}
pub fn add_lyric(&mut self, text: String, timestamp: u32) {
self.lyrics.push((text, timestamp));
}
pub fn sort_lyrics(&mut self) {
self.lyrics.sort_by_key(|(_, timestamp)| *timestamp);
}
pub fn get_lyric_at(&self, timestamp: u32) -> Option<&str> {
self.lyrics
.iter()
.rev()
.find(|(_, ts)| *ts <= timestamp)
.map(|(text, _)| text.as_str())
}
}
impl HasEncoding for SYLT {
fn get_encoding(&self) -> TextEncoding {
self.encoding
}
fn set_encoding(&mut self, encoding: TextEncoding) {
self.encoding = encoding;
}
}
impl Frame for SYLT {
fn frame_id(&self) -> &str {
"SYLT"
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.push(self.encoding.to_byte());
data.extend(&self.language);
data.push(self.format as u8);
data.push(self.content_type);
data.extend(self.encoding.encode_text(&self.description)?);
data.extend(self.encoding.null_terminator());
for (text, timestamp) in &self.lyrics {
data.extend(self.encoding.encode_text(text)?);
data.extend(self.encoding.null_terminator());
data.extend(×tamp.to_be_bytes());
}
Ok(data)
}
fn description(&self) -> String {
let unit = if self.format == TimeStampFormat::Mpeg {
"frames"
} else {
"ms"
};
format!(
"SYLT: {} ({} {} entries)",
self.description,
self.lyrics.len(),
unit
)
}
fn hash_key(&self) -> String {
format!(
"SYLT:{}:{}",
self.description,
String::from_utf8_lossy(&self.language)
)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn convert_encoding_for_version(&mut self, version: (u8, u8)) {
HasEncoding::convert_encoding_for_version(self, version);
}
}
impl Default for SYLT {
fn default() -> Self {
Self {
encoding: TextEncoding::default(),
language: *b"XXX",
format: TimeStampFormat::Milliseconds,
content_type: 1, description: String::new(),
lyrics: Vec::new(),
}
}
}
#[derive(Debug)]
pub struct ETCO {
pub format: TimeStampFormat,
pub events: Vec<(EventType, u32)>,
}
impl ETCO {
pub fn new(format: TimeStampFormat, events: Vec<(EventType, u32)>) -> Self {
Self { format, events }
}
pub fn add_event(&mut self, event_type: EventType, timestamp: u32) {
self.events.push((event_type, timestamp));
}
pub fn sort_events(&mut self) {
self.events.sort_by_key(|(_, timestamp)| *timestamp);
}
pub fn get_event_at(&self, timestamp: u32) -> Option<&EventType> {
self.events
.iter()
.rev()
.find(|(_, ts)| *ts <= timestamp)
.map(|(event_type, _)| event_type)
}
}
impl Frame for ETCO {
fn frame_id(&self) -> &str {
"ETCO"
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.push(self.format as u8);
for (event_type, timestamp) in &self.events {
data.push(*event_type as u8);
data.extend(×tamp.to_be_bytes());
}
Ok(data)
}
fn description(&self) -> String {
let unit = if self.format == TimeStampFormat::Mpeg {
"frames"
} else {
"ms"
};
format!("ETCO: {} events ({})", self.events.len(), unit)
}
fn hash_key(&self) -> String {
"ETCO".to_string()
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl Default for ETCO {
fn default() -> Self {
Self {
format: TimeStampFormat::Milliseconds,
events: Vec::new(),
}
}
}
#[derive(Debug)]
pub struct SYTC {
pub format: TimeStampFormat,
pub tempo_data: Vec<u8>,
}
impl SYTC {
pub fn new(format: TimeStampFormat, tempo_data: Vec<u8>) -> Self {
Self { format, tempo_data }
}
pub fn parse_tempo_changes(&self) -> Vec<(u8, u32)> {
let mut changes = Vec::new();
let mut pos = 0;
while pos + 5 <= self.tempo_data.len() {
let tempo = self.tempo_data[pos];
let timestamp = u32::from_be_bytes([
self.tempo_data[pos + 1],
self.tempo_data[pos + 2],
self.tempo_data[pos + 3],
self.tempo_data[pos + 4],
]);
changes.push((tempo, timestamp));
pos += 5;
}
changes
}
pub fn set_tempo_changes(&mut self, changes: &[(u8, u32)]) {
self.tempo_data.clear();
for (tempo, timestamp) in changes {
self.tempo_data.push(*tempo);
self.tempo_data.extend(×tamp.to_be_bytes());
}
}
pub fn get_tempo_at(&self, timestamp: u32) -> Option<u8> {
let changes = self.parse_tempo_changes();
changes
.iter()
.rev()
.find(|(_, ts)| *ts <= timestamp)
.map(|(tempo, _)| *tempo)
}
}
impl Frame for SYTC {
fn frame_id(&self) -> &str {
"SYTC"
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.push(self.format as u8);
data.extend(&self.tempo_data);
Ok(data)
}
fn description(&self) -> String {
let changes = self.parse_tempo_changes();
let unit = if self.format == TimeStampFormat::Mpeg {
"frames"
} else {
"ms"
};
format!("SYTC: {} tempo changes ({})", changes.len(), unit)
}
fn hash_key(&self) -> String {
"SYTC".to_string()
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl Default for SYTC {
fn default() -> Self {
Self {
format: TimeStampFormat::Milliseconds,
tempo_data: Vec::new(),
}
}
}
#[derive(Debug, Default)]
pub struct RVA2 {
pub identification: String,
pub channels: Vec<(ChannelType, f32, f32)>,
}
impl RVA2 {
pub fn new(identification: String, channels: Vec<(ChannelType, f32, f32)>) -> Self {
Self {
identification,
channels,
}
}
pub fn add_channel(
&mut self,
channel: ChannelType,
gain_db: f32,
peak: f32,
) -> crate::Result<()> {
if !gain_db.is_finite() {
return Err(crate::AudexError::InvalidData(format!(
"Gain value must be finite, got: {}",
gain_db
)));
}
if !peak.is_finite() {
return Err(crate::AudexError::InvalidData(format!(
"Peak value must be finite, got: {}",
peak
)));
}
self.channels.push((channel, gain_db, peak));
Ok(())
}
pub fn get_channel(&self, channel: ChannelType) -> Option<(f32, f32)> {
self.channels
.iter()
.find(|(ch, _, _)| *ch == channel)
.map(|(_, gain, peak)| (*gain, *peak))
}
pub fn get_master(&self) -> Option<(f32, f32)> {
self.get_channel(ChannelType::MasterVolume)
}
pub fn track_gain(gain_db: f32, peak: f32) -> Self {
Self {
identification: "track".to_string(),
channels: vec![(ChannelType::MasterVolume, gain_db, peak)],
}
}
pub fn album_gain(gain_db: f32, peak: f32) -> Self {
Self {
identification: "album".to_string(),
channels: vec![(ChannelType::MasterVolume, gain_db, peak)],
}
}
}
impl fmt::Display for RVA2 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let parts: Vec<String> = self
.channels
.iter()
.map(|(channel, gain_db, peak)| {
let sign = if *gain_db >= 0.0 { "+" } else { "" };
format!("{}: {}{:.4} dB/{:.4}", channel, sign, gain_db, peak)
})
.collect();
write!(f, "{}", parts.join(", "))
}
}
impl Frame for RVA2 {
fn frame_id(&self) -> &str {
"RVA2"
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.extend(TextEncoding::Latin1.encode_text(&self.identification)?);
data.push(0);
for (channel, gain_db, peak) in &self.channels {
data.push(*channel as u8);
let gain_value = (*gain_db * 512.0).round() as i16;
data.extend(&gain_value.to_be_bytes());
let peak_bits: u8 = 16;
data.push(peak_bits);
let max_value = (1u32 << (peak_bits - 1)) as f32;
let peak_value = (*peak * max_value).round() as u16;
data.extend(&peak_value.to_be_bytes());
}
Ok(data)
}
fn description(&self) -> String {
format!("{}", self)
}
fn hash_key(&self) -> String {
format!("RVA2:{}", self.identification)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
#[derive(Debug)]
pub struct RVAD {
pub flags: u8,
pub bits: u8,
pub adjustments: Vec<i32>,
}
impl RVAD {
pub fn new(flags: u8, bits: u8, adjustments: Vec<i32>) -> Self {
Self {
flags,
bits,
adjustments,
}
}
pub fn get_right(&self) -> Option<i32> {
self.adjustments.first().copied()
}
pub fn get_left(&self) -> Option<i32> {
self.adjustments.get(1).copied()
}
pub fn stereo(right: i32, left: i32) -> Self {
let mut flags = 0u8;
if right >= 0 {
flags |= 0x01;
}
if left >= 0 {
flags |= 0x02;
}
Self {
flags,
bits: 16, adjustments: vec![right.abs(), left.abs()],
}
}
}
impl Frame for RVAD {
fn frame_id(&self) -> &str {
"RVAD"
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.push(self.flags);
data.push(self.bits);
let bytes_per_value = self.bits.div_ceil(8) as usize;
for (i, value) in self.adjustments.iter().enumerate() {
let mut abs_value = value.unsigned_abs();
let mut value_bytes = vec![0u8; bytes_per_value];
for j in (0..bytes_per_value).rev() {
value_bytes[j] = (abs_value & 0xFF) as u8;
abs_value >>= 8;
}
data.extend(&value_bytes);
if i >= 11 {
break;
}
}
Ok(data)
}
fn description(&self) -> String {
format!(
"RVAD: {} adjustments ({} bits)",
self.adjustments.len(),
self.bits
)
}
fn hash_key(&self) -> String {
"RVAD".to_string()
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl Default for RVAD {
fn default() -> Self {
Self {
flags: 0,
bits: 16,
adjustments: Vec::new(),
}
}
}
#[derive(Debug)]
pub struct ASPI {
pub data_start: u32,
pub data_length: u32,
pub num_points: u16,
pub bits_per_point: u8,
pub index_points: Vec<u16>,
}
impl ASPI {
pub fn new(data_start: u32, data_length: u32, index_points: Vec<u16>) -> Self {
let num_points = index_points.len() as u16;
let bits_per_point = if index_points.iter().all(|&v| v <= 255) {
8
} else {
16
};
Self {
data_start,
data_length,
num_points,
bits_per_point,
index_points,
}
}
pub fn get_fraction(&self, index: usize) -> Option<u16> {
self.index_points.get(index).copied()
}
pub fn offset_at_index(&self, index: usize) -> Option<u64> {
if index >= self.index_points.len() {
return None;
}
let fraction = self.index_points[index] as f64;
let max_value = if self.bits_per_point == 8 {
255.0
} else {
65535.0
};
let ratio = fraction / max_value;
Some(self.data_start as u64 + (self.data_length as f64 * ratio) as u64)
}
}
impl Frame for ASPI {
fn frame_id(&self) -> &str {
"ASPI"
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.extend_from_slice(&self.data_start.to_be_bytes());
data.extend_from_slice(&self.data_length.to_be_bytes());
data.extend_from_slice(&self.num_points.to_be_bytes());
data.push(self.bits_per_point);
for &point in &self.index_points {
if self.bits_per_point == 8 {
data.push(point as u8);
} else {
data.extend_from_slice(&point.to_be_bytes());
}
}
Ok(data)
}
fn description(&self) -> String {
format!("Audio Seek Point Index ({} points)", self.num_points)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl Default for ASPI {
fn default() -> Self {
Self {
data_start: 0,
data_length: 0,
num_points: 0,
bits_per_point: 16,
index_points: Vec::new(),
}
}
}
#[derive(Debug, Default)]
pub struct MCDI {
pub data: Vec<u8>,
}
impl MCDI {
pub fn new(data: Vec<u8>) -> Self {
Self { data }
}
}
impl Frame for MCDI {
fn frame_id(&self) -> &str {
"MCDI"
}
fn to_data(&self) -> Result<Vec<u8>> {
Ok(self.data.clone())
}
fn description(&self) -> String {
format!("Music CD Identifier ({} bytes)", self.data.len())
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
#[derive(Debug, Default)]
pub struct PCNT {
pub count: u64,
}
impl PCNT {
pub fn new(count: u64) -> Self {
Self { count }
}
pub fn increment(&mut self) {
self.count = self.count.saturating_add(1);
}
pub fn increment_by(&mut self, amount: u64) {
self.count = self.count.saturating_add(amount);
}
}
impl Frame for PCNT {
fn frame_id(&self) -> &str {
"PCNT"
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut count = self.count;
let mut data = Vec::new();
if count == 0 {
data.push(0);
} else {
while count > 0 {
data.insert(0, (count & 0xFF) as u8);
count >>= 8;
}
}
Ok(data)
}
fn description(&self) -> String {
format!("Play Counter ({})", self.count)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
#[derive(Debug, Default)]
pub struct UFID {
pub owner: String,
pub data: Vec<u8>,
}
impl UFID {
pub fn new(owner: String, data: Vec<u8>) -> Self {
Self { owner, data }
}
pub fn musicbrainz_recording_id(mbid: &str) -> Self {
Self {
owner: "http://musicbrainz.org".to_string(),
data: mbid.as_bytes().to_vec(),
}
}
}
impl Frame for UFID {
fn frame_id(&self) -> &str {
"UFID"
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.extend_from_slice(self.owner.as_bytes());
data.push(0);
data.extend_from_slice(&self.data);
Ok(data)
}
fn description(&self) -> String {
format!("Unique File Identifier ({})", self.owner)
}
fn hash_key(&self) -> String {
format!("UFID:{}", self.owner)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
#[derive(Debug, Default)]
pub struct PRIV {
pub owner: String,
pub data: Vec<u8>,
}
impl PRIV {
pub fn new(owner: String, data: Vec<u8>) -> Self {
Self { owner, data }
}
}
impl Frame for PRIV {
fn frame_id(&self) -> &str {
"PRIV"
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.extend_from_slice(self.owner.as_bytes());
data.push(0);
data.extend_from_slice(&self.data);
Ok(data)
}
fn description(&self) -> String {
format!("Private Frame ({})", self.owner)
}
fn hash_key(&self) -> String {
let data_hash = if self.data.is_empty() {
String::new()
} else {
format!(
"{:02x}{:02x}",
self.data.first().unwrap_or(&0),
self.data.last().unwrap_or(&0)
)
};
format!("PRIV:{}:{}", self.owner, data_hash)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
#[derive(Debug, Default)]
pub struct LINK {
pub frameid: String,
pub url: String,
pub data: Vec<u8>,
}
impl LINK {
pub fn new(frameid: String, url: String, data: Vec<u8>) -> Self {
Self { frameid, url, data }
}
}
impl Frame for LINK {
fn frame_id(&self) -> &str {
"LINK"
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut data = Vec::new();
let mut frameid_bytes = self.frameid.as_bytes().to_vec();
frameid_bytes.resize(4, b' ');
data.extend_from_slice(&frameid_bytes[..4]);
data.extend_from_slice(self.url.as_bytes());
data.push(0);
data.extend_from_slice(&self.data);
Ok(data)
}
fn description(&self) -> String {
format!("Linked Information ({} -> {})", self.frameid, self.url)
}
fn hash_key(&self) -> String {
let data_hash = if self.data.is_empty() {
String::new()
} else {
format!(
"{:02x}{:02x}",
self.data.first().unwrap_or(&0),
self.data.last().unwrap_or(&0)
)
};
format!("LINK:{}:{}:{}", self.frameid, self.url, data_hash)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
#[derive(Debug, Default)]
pub struct SEEK {
pub offset: u32,
}
impl SEEK {
pub fn new(offset: u32) -> Self {
Self { offset }
}
}
impl Frame for SEEK {
fn frame_id(&self) -> &str {
"SEEK"
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut offset = self.offset;
let mut data = Vec::new();
if offset == 0 {
data.push(0);
} else {
while offset > 0 {
data.insert(0, (offset & 0xFF) as u8);
offset >>= 8;
}
}
Ok(data)
}
fn description(&self) -> String {
format!("Seek Frame (offset: {} bytes)", self.offset)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
#[derive(Debug)]
pub struct POSS {
pub format: u8,
pub position: u64,
}
impl POSS {
pub fn new(format: u8, position: u64) -> Self {
Self { format, position }
}
pub fn mpeg_frames(position: u64) -> Self {
Self {
format: 1,
position,
}
}
pub fn milliseconds(position: u64) -> Self {
Self {
format: 2,
position,
}
}
}
impl Frame for POSS {
fn frame_id(&self) -> &str {
"POSS"
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.push(self.format);
let mut pos = self.position;
if pos == 0 {
data.push(0);
} else {
while pos > 0 {
data.insert(1, (pos & 0xFF) as u8);
pos >>= 8;
}
}
Ok(data)
}
fn description(&self) -> String {
let format_str = match self.format {
1 => "MPEG frames",
2 => "milliseconds",
_ => "unknown",
};
format!(
"Position Synchronization ({} {})",
self.position, format_str
)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl Default for POSS {
fn default() -> Self {
Self {
format: 1,
position: 0,
}
}
}
#[derive(Debug, Clone)]
pub struct OWNE {
pub encoding: TextEncoding,
pub price: String,
pub date: String,
pub seller: String,
}
impl OWNE {
pub fn new(encoding: TextEncoding, price: String, date: String, seller: String) -> Self {
Self {
encoding,
price,
date,
seller,
}
}
}
impl HasEncoding for OWNE {
fn get_encoding(&self) -> TextEncoding {
self.encoding
}
fn set_encoding(&mut self, encoding: TextEncoding) {
self.encoding = encoding;
}
}
impl Frame for OWNE {
fn frame_id(&self) -> &str {
"OWNE"
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.push(self.encoding as u8);
data.extend_from_slice(self.price.as_bytes());
data.push(0);
let date_bytes = self.date.as_bytes();
if date_bytes.len() >= 8 {
data.extend_from_slice(&date_bytes[..8]);
} else {
data.extend_from_slice(date_bytes);
data.resize(data.len() + (8 - date_bytes.len()), b'0');
}
data.extend_from_slice(self.seller.as_bytes());
data.push(0);
Ok(data)
}
fn description(&self) -> String {
format!("Ownership ({}, {})", self.price, self.seller)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn convert_encoding_for_version(&mut self, version: (u8, u8)) {
HasEncoding::convert_encoding_for_version(self, version);
}
}
impl Default for OWNE {
fn default() -> Self {
Self {
encoding: TextEncoding::Utf8,
price: String::new(),
date: "19700101".to_string(),
seller: String::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct COMR {
pub encoding: TextEncoding,
pub price: String,
pub valid_until: String,
pub contact: String,
pub received_as: u8,
pub seller: String,
pub description: String,
pub picture_mime: Option<String>,
pub picture: Option<Vec<u8>>,
}
impl COMR {
pub fn new(
encoding: TextEncoding,
price: String,
valid_until: String,
contact: String,
received_as: u8,
seller: String,
description: String,
) -> Self {
Self {
encoding,
price,
valid_until,
contact,
received_as,
seller,
description,
picture_mime: None,
picture: None,
}
}
pub fn with_picture(mut self, mime: String, data: Vec<u8>) -> Self {
self.picture_mime = Some(mime);
self.picture = Some(data);
self
}
}
impl HasEncoding for COMR {
fn get_encoding(&self) -> TextEncoding {
self.encoding
}
fn set_encoding(&mut self, encoding: TextEncoding) {
self.encoding = encoding;
}
}
impl Frame for COMR {
fn frame_id(&self) -> &str {
"COMR"
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.push(self.encoding as u8);
data.extend_from_slice(self.price.as_bytes());
data.push(0);
let date_bytes = self.valid_until.as_bytes();
if date_bytes.len() >= 8 {
data.extend_from_slice(&date_bytes[..8]);
} else {
data.extend_from_slice(date_bytes);
data.resize(data.len() + (8 - date_bytes.len()), b'0');
}
data.extend_from_slice(self.contact.as_bytes());
data.push(0);
data.push(self.received_as);
data.extend_from_slice(self.seller.as_bytes());
data.push(0);
data.extend_from_slice(self.description.as_bytes());
data.push(0);
if let (Some(mime), Some(pic)) = (&self.picture_mime, &self.picture) {
data.extend_from_slice(mime.as_bytes());
data.push(0);
data.extend_from_slice(pic);
}
Ok(data)
}
fn description(&self) -> String {
format!("Commercial ({}, {})", self.price, self.seller)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn convert_encoding_for_version(&mut self, version: (u8, u8)) {
HasEncoding::convert_encoding_for_version(self, version);
}
}
impl Default for COMR {
fn default() -> Self {
Self {
encoding: TextEncoding::Utf8,
price: String::new(),
valid_until: "19700101".to_string(),
contact: String::new(),
received_as: 0,
seller: String::new(),
description: String::new(),
picture_mime: None,
picture: None,
}
}
}
#[derive(Debug)]
pub struct ENCR {
pub owner: String,
pub method_symbol: u8,
pub data: Vec<u8>,
}
impl ENCR {
pub fn new(owner: String, method_symbol: u8, data: Vec<u8>) -> Self {
Self {
owner,
method_symbol,
data,
}
}
}
impl Frame for ENCR {
fn frame_id(&self) -> &str {
"ENCR"
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.extend_from_slice(self.owner.as_bytes());
data.push(0);
data.push(self.method_symbol);
data.extend_from_slice(&self.data);
Ok(data)
}
fn description(&self) -> String {
format!("Encryption Method Registration ({})", self.owner)
}
fn hash_key(&self) -> String {
format!("ENCR:{}", self.owner)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl Default for ENCR {
fn default() -> Self {
Self {
owner: String::new(),
method_symbol: 0x80,
data: Vec::new(),
}
}
}
#[derive(Debug)]
pub struct GRID {
pub owner: String,
pub group_symbol: u8,
pub data: Vec<u8>,
}
impl GRID {
pub fn new(owner: String, group_symbol: u8, data: Vec<u8>) -> Self {
Self {
owner,
group_symbol,
data,
}
}
}
impl Frame for GRID {
fn frame_id(&self) -> &str {
"GRID"
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.extend_from_slice(self.owner.as_bytes());
data.push(0);
data.push(self.group_symbol);
data.extend_from_slice(&self.data);
Ok(data)
}
fn description(&self) -> String {
format!("Group Identification Registration ({})", self.owner)
}
fn hash_key(&self) -> String {
format!("GRID:{}", self.group_symbol)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl Default for GRID {
fn default() -> Self {
Self {
owner: String::new(),
group_symbol: 0x80,
data: Vec::new(),
}
}
}
#[derive(Debug, Default)]
pub struct AENC {
pub owner: String,
pub preview_start: u16,
pub preview_length: u16,
pub data: Vec<u8>,
}
impl AENC {
pub fn new(owner: String, preview_start: u16, preview_length: u16, data: Vec<u8>) -> Self {
Self {
owner,
preview_start,
preview_length,
data,
}
}
}
impl Frame for AENC {
fn frame_id(&self) -> &str {
"AENC"
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.extend_from_slice(self.owner.as_bytes());
data.push(0);
data.extend_from_slice(&self.preview_start.to_be_bytes());
data.extend_from_slice(&self.preview_length.to_be_bytes());
data.extend_from_slice(&self.data);
Ok(data)
}
fn description(&self) -> String {
format!("Audio Encryption ({})", self.owner)
}
fn hash_key(&self) -> String {
format!("AENC:{}", self.owner)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
#[derive(Debug, Default)]
pub struct RBUF {
pub size: u32,
pub embedded_info: Option<bool>,
pub offset: Option<u32>,
}
impl RBUF {
pub fn new(size: u32) -> Self {
Self {
size: size & 0x00FFFFFF, embedded_info: None,
offset: None,
}
}
pub fn with_info(size: u32, embedded_info: bool, offset: u32) -> Self {
Self {
size: size & 0x00FFFFFF,
embedded_info: Some(embedded_info),
offset: Some(offset),
}
}
}
impl Frame for RBUF {
fn frame_id(&self) -> &str {
"RBUF"
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut data = Vec::new();
let size_bytes = self.size.to_be_bytes();
data.extend_from_slice(&size_bytes[1..4]);
if let Some(info) = self.embedded_info {
data.push(if info { 1 } else { 0 });
if let Some(offset) = self.offset {
data.extend_from_slice(&offset.to_be_bytes());
}
}
Ok(data)
}
fn description(&self) -> String {
format!("Recommended Buffer Size ({} bytes)", self.size)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
#[derive(Debug)]
pub struct SIGN {
pub group_symbol: u8,
pub signature: Vec<u8>,
}
impl SIGN {
pub fn new(group_symbol: u8, signature: Vec<u8>) -> Self {
Self {
group_symbol,
signature,
}
}
}
impl Frame for SIGN {
fn frame_id(&self) -> &str {
"SIGN"
}
fn to_data(&self) -> Result<Vec<u8>> {
let mut data = Vec::new();
data.push(self.group_symbol);
data.extend_from_slice(&self.signature);
Ok(data)
}
fn description(&self) -> String {
format!("Signature Frame (group {})", self.group_symbol)
}
fn hash_key(&self) -> String {
let sig_hash = if self.signature.is_empty() {
String::new()
} else {
format!(
"{:02x}{:02x}",
self.signature.first().unwrap_or(&0),
self.signature.last().unwrap_or(&0)
)
};
format!("SIGN:{}:{}", self.group_symbol, sig_hash)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl Default for SIGN {
fn default() -> Self {
Self {
group_symbol: 0x80,
signature: Vec::new(),
}
}
}