From dd0eb415493bc2e3dae2be6034693108dbd241af Mon Sep 17 00:00:00 2001 From: Serial <69764315+Serial-ATA@users.noreply.github.com> Date: Sat, 24 Jul 2021 11:12:19 -0400 Subject: [PATCH] Stop using metaflac for reading, add AudioTagEdit::properties --- src/components/logic/ogg/flac.rs | 133 +++++++++++++++++++++++++++++ src/components/logic/ogg/mod.rs | 2 + src/components/logic/ogg/read.rs | 79 ++++++++++------- src/components/logic/ogg/vorbis.rs | 31 ++++--- src/components/tags/aiff_tag.rs | 4 + src/components/tags/ape_tag.rs | 4 + src/components/tags/id3_tag.rs | 4 + src/components/tags/mp4_tag.rs | 4 + src/components/tags/ogg_tag.rs | 8 +- src/components/tags/riff_tag.rs | 4 + src/traits.rs | 5 +- 11 files changed, 228 insertions(+), 50 deletions(-) create mode 100644 src/components/logic/ogg/flac.rs diff --git a/src/components/logic/ogg/flac.rs b/src/components/logic/ogg/flac.rs new file mode 100644 index 00000000..6b7d65ba --- /dev/null +++ b/src/components/logic/ogg/flac.rs @@ -0,0 +1,133 @@ +use super::read::{read_comments, OGGTags}; + +use crate::{FileProperties, LoftyError, OggFormat, Picture, Result}; + +use std::collections::HashMap; +use std::io::{Read, Seek, SeekFrom}; +use std::time::Duration; + +use byteorder::{BigEndian, ReadBytesExt}; +use unicase::UniCase; + +fn read_properties(stream_info: &mut R, stream_length: u64) -> Result +where + R: Read, +{ + // Skip 4 bytes + // Minimum block size (2) + // Maximum block size (2) + stream_info.read_u16::()?; + + // Skip 6 bytes + // Minimum frame size (3) + // Maximum frame size (3) + stream_info.read_uint::(6)?; + + // Read 24 bits + // Sample rate (20) + // Number of channels (3) + // First bit of bits per sample (1) + let info = stream_info.read_uint::(3)?; + + let sample_rate = info >> 4; + let channels = (info & 0x15) + 1; + + // There are still 4 bits remaining of the bits per sample + // This number isn't used, so just discard it + let total_samples_first = stream_info.read_u8()? << 4; + + // Read the remaining 32 bits of the total samples + let total_samples = stream_info.read_u32::()? | u32::from(total_samples_first); + + let (duration, bitrate) = if sample_rate > 0 && total_samples > 0 { + let length = (u64::from(total_samples) * 1000) / sample_rate; + + ( + Duration::from_millis(length), + ((stream_length * 8) / length) as u32, + ) + } else { + (Duration::ZERO, 0) + }; + + Ok(FileProperties { + duration, + bitrate: Some(bitrate), + sample_rate: Some(sample_rate as u32), + channels: Some(channels as u8), + }) +} + +pub(crate) fn read_from(data: &mut R) -> Result +where + R: Read + Seek, +{ + let mut marker = [0; 4]; + data.read_exact(&mut marker)?; + + if &marker != b"fLaC" { + return Err(LoftyError::InvalidData( + "FLAC file missing \"fLaC\" stream marker", + )); + } + + let mut byte = data.read_u8()?; + + if (byte & 0x7f) != 0 { + return Err(LoftyError::InvalidData( + "FLAC file missing mandatory STREAMINFO block", + )); + } + + let mut last_block = (byte & 0x80) != 0; + + let stream_info_len = data.read_uint::(3)? as u32; + + if stream_info_len < 18 { + return Err(LoftyError::InvalidData("FLAC file has an invalid STREAMINFO block size (< 18)")) + } + + let mut stream_info_data = vec![0; stream_info_len as usize]; + data.read_exact(&mut stream_info_data)?; + + let mut vendor = String::new(); + let mut comments = HashMap::, String>::new(); + let mut pictures = Vec::::new(); + + while !last_block { + byte = data.read_u8()?; + last_block = (byte & 0x80) != 0; + let block_type = byte & 0x7f; + + let block_len = data.read_uint::(3)? as u32; + + match block_type { + 4 => { + let mut comment_data = vec![0; block_len as usize]; + data.read_exact(&mut comment_data)?; + + vendor = read_comments(&mut &*comment_data, &mut comments, &mut pictures)? + }, + 6 => { + let mut picture_data = vec![0; block_len as usize]; + data.read_exact(&mut picture_data)?; + + pictures.push(Picture::from_apic_bytes(&*picture_data)?) + }, + _ => { + data.seek(SeekFrom::Current(i64::from(block_len)))?; + continue; + }, + } + } + + let stream_length = { + let current = data.seek(SeekFrom::Current(0))?; + let end = data.seek(SeekFrom::End(0))?; + end - current + }; + + let properties = read_properties(&mut &*stream_info_data, stream_length)?; + + Ok((vendor, pictures, comments, properties, OggFormat::Flac)) +} diff --git a/src/components/logic/ogg/mod.rs b/src/components/logic/ogg/mod.rs index b505fae6..b526358f 100644 --- a/src/components/logic/ogg/mod.rs +++ b/src/components/logic/ogg/mod.rs @@ -8,6 +8,8 @@ pub(crate) mod constants; pub(crate) mod read; pub(crate) mod write; +#[cfg(feature = "format-flac")] +pub(crate) mod flac; #[cfg(feature = "format-opus")] mod opus; #[cfg(feature = "format-vorbis")] diff --git a/src/components/logic/ogg/read.rs b/src/components/logic/ogg/read.rs index 314c77e6..53f08488 100644 --- a/src/components/logic/ogg/read.rs +++ b/src/components/logic/ogg/read.rs @@ -39,6 +39,50 @@ where Ok(properties) } +pub(crate) fn read_comments( + data: &mut R, + storage: &mut HashMap, String>, + pictures: &mut Vec, +) -> Result +where + R: Read, +{ + let vendor_len = data.read_u32::()?; + + let mut vendor = vec![0; vendor_len as usize]; + data.read_exact(&mut vendor)?; + + let vendor = match String::from_utf8(vendor) { + Ok(v) => v, + Err(_) => { + return Err(LoftyError::InvalidData( + "OGG file has an invalid vendor string", + )) + }, + }; + + let comments_total_len = data.read_u32::()?; + + for _ in 0..comments_total_len { + let comment_len = data.read_u32::()?; + + let mut comment_bytes = vec![0; comment_len as usize]; + data.read_exact(&mut comment_bytes)?; + + let comment = String::from_utf8(comment_bytes)?; + + let split: Vec<&str> = comment.splitn(2, '=').collect(); + + if split[0] == "METADATA_BLOCK_PICTURE" { + pictures.push(Picture::from_apic_bytes(split[1].as_bytes())?) + } else { + storage.insert(UniCase::from(split[0].to_string()), split[1].to_string()); + } + } + + Ok(vendor) +} + pub(crate) fn read_from( data: &mut T, header_sig: &[u8], @@ -75,40 +119,9 @@ where let mut pictures = Vec::new(); let reader = &mut &md_pages[..]; - - let vendor_len = reader.read_u32::()?; - let mut vendor = vec![0; vendor_len as usize]; - reader.read_exact(&mut vendor)?; - - let vendor_str = match String::from_utf8(vendor) { - Ok(v) => v, - Err(_) => { - return Err(LoftyError::InvalidData( - "OGG file has an invalid vendor string", - )) - }, - }; - - let comments_total_len = reader.read_u32::()?; - - for _ in 0..comments_total_len { - let comment_len = reader.read_u32::()?; - - let mut comment_bytes = vec![0; comment_len as usize]; - reader.read_exact(&mut comment_bytes)?; - - let comment = String::from_utf8(comment_bytes)?; - - let split: Vec<&str> = comment.splitn(2, '=').collect(); - - if split[0] == "METADATA_BLOCK_PICTURE" { - pictures.push(Picture::from_apic_bytes(split[1].as_bytes())?) - } else { - md.insert(UniCase::from(split[0].to_string()), split[1].to_string()); - } - } + let vendor = read_comments(reader, &mut md, &mut pictures)?; let properties = read_properties(data, header_sig, &first_page)?; - Ok((vendor_str, pictures, md, properties, format)) + Ok((vendor, pictures, md, properties, format)) } diff --git a/src/components/logic/ogg/vorbis.rs b/src/components/logic/ogg/vorbis.rs index e13323bb..bac82c2a 100644 --- a/src/components/logic/ogg/vorbis.rs +++ b/src/components/logic/ogg/vorbis.rs @@ -46,20 +46,25 @@ where let last_page = find_last_page(data)?; let last_page_abgp = last_page.abgp; - last_page_abgp.checked_sub(first_page_abgp).map_or_else(|| Err(LoftyError::InvalidData( - "OGG file contains incorrect PCM values", - )), |frame_count| { - let length = frame_count * 1000 / u64::from(sample_rate); - let duration = Duration::from_millis(length as u64); - let bitrate = ((audio_size * 8) / length) as u32; + last_page_abgp.checked_sub(first_page_abgp).map_or_else( + || { + Err(LoftyError::InvalidData( + "OGG file contains incorrect PCM values", + )) + }, + |frame_count| { + let length = frame_count * 1000 / u64::from(sample_rate); + let duration = Duration::from_millis(length as u64); + let bitrate = ((audio_size * 8) / length) as u32; - Ok(FileProperties { - duration, - bitrate: Some(bitrate), - sample_rate: Some(sample_rate), - channels: Some(channels), - }) - }) + Ok(FileProperties { + duration, + bitrate: Some(bitrate), + sample_rate: Some(sample_rate), + channels: Some(channels), + }) + }, + ) } pub fn write_to( diff --git a/src/components/tags/aiff_tag.rs b/src/components/tags/aiff_tag.rs index 53adf87b..4ed130d0 100644 --- a/src/components/tags/aiff_tag.rs +++ b/src/components/tags/aiff_tag.rs @@ -80,6 +80,10 @@ impl AudioTagEdit for AiffTag { fn tag_type(&self) -> TagType { TagType::AiffText } + + fn properties(&self) -> &FileProperties { + &self.properties + } } impl AudioTagWrite for AiffTag { diff --git a/src/components/tags/ape_tag.rs b/src/components/tags/ape_tag.rs index d236a28f..dd548eb7 100644 --- a/src/components/tags/ape_tag.rs +++ b/src/components/tags/ape_tag.rs @@ -294,6 +294,10 @@ impl AudioTagEdit for ApeTag { fn remove_key(&mut self, key: &str) { self.remove_key(key) } + + fn properties(&self) -> &FileProperties { + &self.properties + } } impl AudioTagWrite for ApeTag { diff --git a/src/components/tags/id3_tag.rs b/src/components/tags/id3_tag.rs index bc4f76d9..15446236 100644 --- a/src/components/tags/id3_tag.rs +++ b/src/components/tags/id3_tag.rs @@ -389,6 +389,10 @@ impl AudioTagEdit for Id3v2Tag { fn remove_key(&mut self, key: &str) { self.inner.remove(key) } + + fn properties(&self) -> &FileProperties { + &self.properties + } } impl AudioTagWrite for Id3v2Tag { diff --git a/src/components/tags/mp4_tag.rs b/src/components/tags/mp4_tag.rs index 3b5b5250..ee78a5b5 100644 --- a/src/components/tags/mp4_tag.rs +++ b/src/components/tags/mp4_tag.rs @@ -314,6 +314,10 @@ impl AudioTagEdit for Mp4Tag { fn tag_type(&self) -> TagType { TagType::Mp4 } + + fn properties(&self) -> &FileProperties { + &self.properties + } } impl AudioTagWrite for Mp4Tag { diff --git a/src/components/tags/ogg_tag.rs b/src/components/tags/ogg_tag.rs index 4ff28a94..bfae9d9a 100644 --- a/src/components/tags/ogg_tag.rs +++ b/src/components/tags/ogg_tag.rs @@ -10,8 +10,6 @@ use crate::{ Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite, FileProperties, LoftyError, OggFormat, Picture, PictureType, Result, TagType, ToAny, ToAnyTag, }; - -#[cfg(any(feature = "format-opus", feature = "format-vorbis"))] use crate::components::logic::ogg::read::OGGTags; use std::borrow::Cow; @@ -161,7 +159,7 @@ impl OggTag { }, #[cfg(feature = "format-flac")] OggFormat::Flac => { - let tag = metaflac::Tag::read_from(reader)?; + let tag = ogg::flac::read_from(reader)?; tag.try_into()? }, @@ -380,6 +378,10 @@ impl AudioTagEdit for OggTag { fn remove_key(&mut self, key: &str) { self.remove_key(key) } + + fn properties(&self) -> &FileProperties { + &self.properties + } } impl AudioTagWrite for OggTag { diff --git a/src/components/tags/riff_tag.rs b/src/components/tags/riff_tag.rs index bbafcacb..da13e8e1 100644 --- a/src/components/tags/riff_tag.rs +++ b/src/components/tags/riff_tag.rs @@ -150,6 +150,10 @@ impl AudioTagEdit for RiffTag { fn remove_key(&mut self, key: &str) { self.remove_key(key) } + + fn properties(&self) -> &FileProperties { + &self.properties + } } impl AudioTagWrite for RiffTag { diff --git a/src/traits.rs b/src/traits.rs index 8b0a2951..50dc73b9 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,6 +1,6 @@ #[allow(clippy::wildcard_imports)] use crate::components::tags::*; -use crate::{Album, AnyTag, Picture, Result, TagType}; +use crate::{Album, AnyTag, Picture, Result, TagType, FileProperties}; use std::borrow::Cow; use std::fs::{File, OpenOptions}; @@ -128,6 +128,9 @@ pub trait AudioTagEdit { /// /// See [`get_key`][AudioTagEdit::get_key]'s note fn remove_key(&mut self, _key: &str) {} + + /// Returns the [`FileProperties`][crate::FileProperties] + fn properties(&self) -> &FileProperties; } /// Functions for writing to a file