From f95e7cfdffb77bb1501a474689245360c8721ad0 Mon Sep 17 00:00:00 2001 From: Serial <69764315+Serial-ATA@users.noreply.github.com> Date: Wed, 21 Jul 2021 21:56:04 -0400 Subject: [PATCH] Properties for Opus and Vorbis --- src/components/logic/ogg/mod.rs | 57 +++++---- src/components/logic/ogg/opus.rs | 114 ++++++++++-------- src/components/logic/ogg/read.rs | 27 +++-- src/components/logic/ogg/vorbis.rs | 179 ++++++++++++++++++++++++++++- src/components/logic/ogg/write.rs | 169 ++------------------------- src/components/tags/ogg_tag.rs | 72 +++++------- 6 files changed, 321 insertions(+), 297 deletions(-) diff --git a/src/components/logic/ogg/mod.rs b/src/components/logic/ogg/mod.rs index bf0a0b16..b505fae6 100644 --- a/src/components/logic/ogg/mod.rs +++ b/src/components/logic/ogg/mod.rs @@ -5,23 +5,26 @@ use ogg_pager::Page; use crate::{LoftyError, Result}; pub(crate) mod constants; -mod opus; pub(crate) mod read; -mod vorbis; pub(crate) mod write; +#[cfg(feature = "format-opus")] +mod opus; +#[cfg(feature = "format-vorbis")] +mod vorbis; + pub fn page_from_packet(packet: &mut [u8]) -> Result> { let mut pages: Vec = Vec::new(); let reader = &mut &packet[..]; - let mut start = 0_usize; + let mut start = 0_u64; let mut i = 0; while !reader.is_empty() { let header_type = if i == 0 { 0 } else { 1_u8 }; - let size = std::cmp::min(65025, reader.len()); + let size = std::cmp::min(65025_u64, reader.len() as u64); if i != 0 { if let Some(s) = start.checked_add(size) { @@ -31,7 +34,7 @@ pub fn page_from_packet(packet: &mut [u8]) -> Result> { } } - let mut content = vec![0; size]; + let mut content = vec![0; size as usize]; reader.read_exact(&mut content)?; let end = start + size; @@ -53,39 +56,31 @@ pub fn page_from_packet(packet: &mut [u8]) -> Result> { Ok(pages) } -pub(self) fn reach_metadata(mut data: T, sig: &[u8]) -> Result<()> -where - T: Read + Seek, -{ - let first_page = Page::read(&mut data, false)?; +pub(self) fn verify_signature(page: &Page, sig: &[u8]) -> Result<()> { + let sig_len = sig.len(); - let head = first_page.content; - let (ident, head) = head.split_at(sig.len()); - - if ident != sig { + if page.content.len() < sig_len || &page.content[..sig_len] != sig { return Err(LoftyError::InvalidData("OGG file missing magic signature")); } - if head[10] != 0 { - let mut channel_mapping_info = [0; 1]; - data.read_exact(&mut channel_mapping_info)?; - - let mut channel_mapping = vec![0; channel_mapping_info[0] as usize]; - data.read_exact(&mut channel_mapping)?; - } - Ok(()) } -// Verify the 2nd page contains the comment header -pub(self) fn is_metadata(page: &Page, sig: &[u8]) -> Result<()> { - let sig_len = sig.len(); +pub(self) fn find_last_page(data: &mut R) -> Result +where + R: Read + Seek, +{ + let next_page = Page::read(data, true)?; - if page.content.len() < sig_len || &page.content[0..sig_len] != sig { - return Err(LoftyError::InvalidData( - "OGG file missing the mandatory comment header", - )); + // Find the last page + let mut pages: Vec = vec![next_page]; + + loop { + if let Ok(current) = Page::read(data, true) { + pages.push(current) + } else { + // Safe to unwrap since the Vec starts off with a Page + break Ok(pages.pop().unwrap()); + } } - - Ok(()) } diff --git a/src/components/logic/ogg/opus.rs b/src/components/logic/ogg/opus.rs index 878cfee4..23f0beb3 100644 --- a/src/components/logic/ogg/opus.rs +++ b/src/components/logic/ogg/opus.rs @@ -1,9 +1,11 @@ -use crate::{FileProperties, Result, LoftyError}; +use super::find_last_page; +use crate::{FileProperties, LoftyError, Result}; -use std::io::{Cursor, Read, Seek, SeekFrom}; +use std::io::{Read, Seek, SeekFrom, Write}; use byteorder::{LittleEndian, ReadBytesExt}; use ogg_pager::Page; +use std::fs::File; use std::time::Duration; pub(in crate::components) fn read_properties( @@ -14,62 +16,80 @@ pub(in crate::components) fn read_properties( where R: Read + Seek, { - let first_page_abgp = first_page.abgp as i64; - - let mut cursor = Cursor::new(&*first_page.content); + let first_page_abgp = first_page.abgp; // Skip identification header and version - cursor.seek(SeekFrom::Start(11))?; + let first_page_content = &mut &first_page.content[11..]; - let channels = cursor.read_u8()?; - let pre_skip = cursor.read_u16::()?; - let sample_rate = cursor.read_u32::()?; + let channels = first_page_content.read_u8()?; + let pre_skip = first_page_content.read_u16::()?; + let sample_rate = first_page_content.read_u32::()?; let _first_comment_page = Page::read(data, true)?; - // Skip over the metadata packet - loop { - let page = Page::read(data, true)?; + // Skip over the metadata packet + loop { + let page = Page::read(data, true)?; - if page.header_type != 1 { - data.seek(SeekFrom::Start(page.start as u64))?; - break - } - } + if page.header_type != 1 { + data.seek(SeekFrom::Start(page.start as u64))?; + break; + } + } // Subtract the identification and metadata packet length from the total let audio_size = stream_len - data.seek(SeekFrom::Current(0))?; - let next_page = Page::read(data, true)?; + let last_page = find_last_page(data)?; + let last_page_abgp = last_page.abgp; - // Find the last page - let mut pages: Vec = vec![next_page]; + return if let Some(frame_count) = last_page_abgp.checked_sub(first_page_abgp + pre_skip as u64) + { + let length = frame_count * 1000 / 48000; + let duration = Duration::from_millis(length as u64); + let bitrate = ((audio_size * 8) / length) as u32; - let last_page = loop { - if let Ok(current) = Page::read(data, true) { - pages.push(current) - } else { - // Safe to unwrap since the Vec starts off with a Page - break pages.pop().unwrap() - } - }; - - let last_page_abgp = last_page.abgp as i64; - - let frame_count = last_page_abgp - first_page_abgp - pre_skip as i64; - - if frame_count < 0 { - return Err(LoftyError::InvalidData("OGG file contains incorrect PCM values")) - } - - let length = frame_count * 1000 / 48000; - 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), + }) + } else { + Err(LoftyError::InvalidData( + "OGG file contains incorrect PCM values", + )) + }; +} + +pub fn write_to(data: &mut File, writer: &mut Vec, ser: u32, pages: &mut [Page]) -> Result<()> { + let reached_md_end: bool; + let mut remaining = Vec::new(); + + loop { + let p = Page::read(data, true)?; + + if p.header_type != 1 { + data.seek(SeekFrom::Start(p.start as u64))?; + reached_md_end = true; + break; + } + } + + if !reached_md_end { + return Err(LoftyError::InvalidData("OGG file ends with comment header")); + } + + data.read_to_end(&mut remaining)?; + + for mut p in pages.iter_mut() { + p.serial = ser; + p.gen_crc(); + + writer.write_all(&*p.as_bytes())?; + } + + writer.write_all(&*remaining)?; + + Ok(()) } diff --git a/src/components/logic/ogg/read.rs b/src/components/logic/ogg/read.rs index e3c1859d..7d73a56a 100644 --- a/src/components/logic/ogg/read.rs +++ b/src/components/logic/ogg/read.rs @@ -1,11 +1,11 @@ -use super::{is_metadata, reach_metadata}; +use super::verify_signature; use crate::components::logic::ogg::constants::OPUSHEAD; +use crate::components::logic::ogg::{opus, vorbis}; use crate::{FileProperties, LoftyError, OggFormat, Picture, Result}; use std::collections::HashMap; use std::io::{Read, Seek, SeekFrom}; -use crate::components::logic::ogg::{opus, vorbis}; use byteorder::{LittleEndian, ReadBytesExt}; use ogg_pager::Page; use unicase::UniCase; @@ -14,10 +14,11 @@ pub type OGGTags = ( String, Vec, HashMap, String>, + FileProperties, OggFormat, ); -fn read_properties(data: &mut R, header_sig: &[u8]) -> Result +fn read_properties(data: &mut R, header_sig: &[u8], first_page: Page) -> Result where R: Read + Seek, { @@ -26,11 +27,9 @@ where let end = data.seek(SeekFrom::End(0))?; data.seek(SeekFrom::Start(current))?; - end - current + end - first_page.start }; - let first_page = Page::read(data, false)?; - let properties = if header_sig == OPUSHEAD { opus::read_properties(data, first_page, stream_len)? } else { @@ -41,7 +40,7 @@ where } pub(crate) fn read_from( - mut data: T, + data: &mut T, header_sig: &[u8], comment_sig: &[u8], format: OggFormat, @@ -49,16 +48,17 @@ pub(crate) fn read_from( where T: Read + Seek, { - reach_metadata(&mut data, header_sig)?; + let first_page = Page::read(data, false)?; + verify_signature(&first_page, header_sig)?; - let md_page = Page::read(&mut data, false)?; - is_metadata(&md_page, comment_sig)?; + let md_page = Page::read(data, false)?; + verify_signature(&md_page, comment_sig)?; let mut md_pages: Vec = Vec::new(); md_pages.extend(md_page.content[comment_sig.len()..].iter()); - while let Ok(page) = Page::read(&mut data, false) { + while let Ok(page) = Page::read(data, false) { if md_pages.len() > 125_829_120 { return Err(LoftyError::TooMuchData); } @@ -66,6 +66,7 @@ where if page.header_type == 1 { md_pages.extend(page.content.iter()); } else { + data.seek(SeekFrom::Start(page.start))?; break; } } @@ -107,5 +108,7 @@ where } } - Ok((vendor_str, pictures, md, format)) + let properties = read_properties(data, header_sig, first_page)?; + + Ok((vendor_str, pictures, md, properties, format)) } diff --git a/src/components/logic/ogg/vorbis.rs b/src/components/logic/ogg/vorbis.rs index da6cbdfd..e6043fbb 100644 --- a/src/components/logic/ogg/vorbis.rs +++ b/src/components/logic/ogg/vorbis.rs @@ -1,9 +1,13 @@ -use crate::{FileProperties, Result}; +use super::find_last_page; +use crate::components::logic::ogg::constants::VORBIS_SETUP_HEAD; +use crate::{FileProperties, LoftyError, Result}; -use std::io::{Cursor, Read, Seek, SeekFrom}; +use std::io::{Cursor, Read, Seek, SeekFrom, Write}; use byteorder::{LittleEndian, ReadBytesExt}; use ogg_pager::Page; +use std::fs::File; +use std::time::Duration; pub(in crate::components) fn read_properties( data: &mut R, @@ -13,5 +17,174 @@ pub(in crate::components) fn read_properties( where R: Read + Seek, { - Ok(FileProperties::default()) + let first_page_abgp = first_page.abgp; + + // Skip identification header and version + let first_page_content = &mut &first_page.content[11..]; + + let channels = first_page_content.read_u8()?; + let sample_rate = first_page_content.read_u32::()?; + + // Identification and metadata packets have already been skipped + // Have to find the end of the setup packet now + let header_end = loop { + let page = Page::read(data, false)?; + let segments = page.segments(); + + if let Some(seg_end) = segments.iter().position(|s| s != &255_u8) { + let packet_size: usize = segments[..seg_end].iter().map(|s| *s as usize).sum(); + + // Position in stream + page header (26 bytes long) + segment table + however long the packet is + let header_end = page.start + 26 + segments.len() as u64 + packet_size as u64; + + break header_end; + } + }; + + let audio_size = stream_len - header_end; + + let last_page = find_last_page(data)?; + let last_page_abgp = last_page.abgp; + + return if let Some(frame_count) = last_page_abgp.checked_sub(first_page_abgp) { + let length = frame_count * 1000 / sample_rate as u64; + 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), + }) + } else { + Err(LoftyError::InvalidData( + "OGG file contains incorrect PCM values", + )) + }; +} + +pub fn write_to( + data: &mut File, + writer: &mut Vec, + first_md_content: Vec, + ser: u32, + pages: &mut [Page], +) -> Result<()> { + let mut remaining = Vec::new(); + + let reached_md_end: bool; + + // Find the total comment count in the first page's content + let mut c = Cursor::new(first_md_content); + + // Skip the header + c.seek(SeekFrom::Start(7))?; + + // Skip the vendor + let vendor_len = c.read_u32::()?; + c.seek(SeekFrom::Current(i64::from(vendor_len)))?; + + let total_comments = c.read_u32::()?; + let comments_pos = c.seek(SeekFrom::Current(0))?; + + c.seek(SeekFrom::End(0))?; + + loop { + let p = Page::read(data, false)?; + + if p.header_type != 1 { + data.seek(SeekFrom::Start(p.start as u64))?; + data.read_to_end(&mut remaining)?; + + reached_md_end = true; + break; + } + + c.write_all(&p.content)?; + } + + if !reached_md_end { + return Err(LoftyError::InvalidData("OGG file ends with comment header")); + } + + c.seek(SeekFrom::Start(comments_pos))?; + + for _ in 0..total_comments { + let len = c.read_u32::()?; + c.seek(SeekFrom::Current(i64::from(len)))?; + } + + if c.read_u8()? != 1 { + return Err(LoftyError::InvalidData( + "OGG Vorbis file is missing a framing bit", + )); + } + + // Comments should be followed by the setup header + let mut header_ident = [0; 7]; + c.read_exact(&mut header_ident)?; + + if header_ident != VORBIS_SETUP_HEAD { + return Err(LoftyError::InvalidData( + "OGG Vorbis file is missing setup header", + )); + } + + c.seek(SeekFrom::Current(-7))?; + + let mut setup = Vec::new(); + c.read_to_end(&mut setup)?; + + let pages_len = pages.len() - 1; + + for (i, mut p) in pages.iter_mut().enumerate() { + p.serial = ser; + + if i == pages_len { + // Add back the framing bit + p.content.push(1); + + // The segment tables of current page and the setup header have to be combined + let mut seg_table = Vec::new(); + seg_table.extend(p.segments().iter()); + seg_table.extend(ogg_pager::segments(&*setup)); + + let mut seg_table_len = seg_table.len(); + + if seg_table_len > 255 { + seg_table = seg_table.split_at(255).0.to_vec(); + seg_table_len = 255; + } + + seg_table.insert(0, seg_table_len as u8); + + let page = p.extend(&*setup); + + let mut p_bytes = p.as_bytes(); + let seg_count = p_bytes[26] as usize; + + // Replace segment table and checksum + p_bytes.splice(26..27 + seg_count, seg_table); + p_bytes.splice(22..26, ogg_pager::crc32(&*p_bytes).to_le_bytes().to_vec()); + + writer.write_all(&*p_bytes)?; + + if let Some(mut page) = page { + page.serial = ser; + page.gen_crc(); + + writer.write_all(&*page.as_bytes())?; + } + + break; + } + + p.gen_crc(); + writer.write_all(&*p.as_bytes())?; + } + + writer.write_all(&*remaining)?; + + Ok(()) } diff --git a/src/components/logic/ogg/write.rs b/src/components/logic/ogg/write.rs index b8bb217c..0b2ddc77 100644 --- a/src/components/logic/ogg/write.rs +++ b/src/components/logic/ogg/write.rs @@ -1,17 +1,16 @@ -use super::{is_metadata, page_from_packet}; -use crate::{LoftyError, Picture, Result}; +use super::{opus, page_from_packet, verify_signature, vorbis}; +use crate::{Picture, Result}; #[cfg(feature = "format-opus")] use crate::components::logic::ogg::constants::OPUSTAGS; #[cfg(feature = "format-vorbis")] -use crate::components::logic::ogg::constants::{VORBIS_COMMENT_HEAD, VORBIS_SETUP_HEAD}; +use crate::components::logic::ogg::constants::VORBIS_COMMENT_HEAD; use std::borrow::Cow; use std::collections::HashMap; use std::fs::File; -use std::io::{Cursor, Read, Seek, SeekFrom, Write}; +use std::io::{Seek, SeekFrom, Write}; -use byteorder::{LittleEndian, ReadBytesExt}; use ogg_pager::Page; use unicase::UniCase; @@ -60,132 +59,6 @@ pub(crate) fn create_pages( Ok(()) } -#[cfg(feature = "format-vorbis")] -fn vorbis_write( - mut data: &mut File, - writer: &mut Vec, - first_md_content: Vec, - ser: u32, - pages: &mut [Page], -) -> Result<()> { - let mut remaining = Vec::new(); - - let reached_md_end: bool; - - // Find the total comment count in the first page's content - let mut c = Cursor::new(first_md_content); - - // Skip the header - c.seek(SeekFrom::Start(7))?; - - // Skip the vendor - let vendor_len = c.read_u32::()?; - c.seek(SeekFrom::Current(i64::from(vendor_len)))?; - - let total_comments = c.read_u32::()?; - let comments_pos = c.seek(SeekFrom::Current(0))?; - - c.seek(SeekFrom::End(0))?; - - loop { - let p = Page::read(&mut data, false)?; - - if p.header_type != 1 { - data.seek(SeekFrom::Start(p.start as u64))?; - data.read_to_end(&mut remaining)?; - - reached_md_end = true; - break; - } - - c.write_all(&p.content)?; - } - - if !reached_md_end { - return Err(LoftyError::InvalidData("OGG file ends with comment header")); - } - - c.seek(SeekFrom::Start(comments_pos))?; - - for _ in 0..total_comments { - let len = c.read_u32::()?; - c.seek(SeekFrom::Current(i64::from(len)))?; - } - - if c.read_u8()? != 1 { - return Err(LoftyError::InvalidData( - "OGG Vorbis file is missing a framing bit", - )); - } - - // Comments should be followed by the setup header - let mut header_ident = [0; 7]; - c.read_exact(&mut header_ident)?; - - if header_ident != VORBIS_SETUP_HEAD { - return Err(LoftyError::InvalidData( - "OGG Vorbis file is missing setup header", - )); - } - - c.seek(SeekFrom::Current(-7))?; - - let mut setup = Vec::new(); - c.read_to_end(&mut setup)?; - - let pages_len = pages.len() - 1; - - for (i, mut p) in pages.iter_mut().enumerate() { - p.serial = ser; - - if i == pages_len { - // Add back the framing bit - p.content.push(1); - - // The segment tables of current page and the setup header have to be combined - let mut seg_table = Vec::new(); - seg_table.extend(p.segments().iter()); - seg_table.extend(ogg_pager::segments(&*setup)); - - let mut seg_table_len = seg_table.len(); - - if seg_table_len > 255 { - seg_table = seg_table.split_at(255).0.to_vec(); - seg_table_len = 255; - } - - seg_table.insert(0, seg_table_len as u8); - - let page = p.extend(&*setup); - - let mut p_bytes = p.as_bytes(); - let seg_count = p_bytes[26] as usize; - - // Replace segment table and checksum - p_bytes.splice(26..27 + seg_count, seg_table); - p_bytes.splice(22..26, ogg_pager::crc32(&*p_bytes).to_le_bytes().to_vec()); - - writer.write_all(&*p_bytes)?; - - if let Some(mut page) = page { - page.serial = ser; - page.gen_crc(); - - writer.write_all(&*page.as_bytes())?; - } - - break; - } - - p.gen_crc(); - writer.write_all(&*p.as_bytes())?; - } - - writer.write_all(&*remaining)?; - - Ok(()) -} - fn write_to(mut data: &mut File, pages: &mut [Page], sig: &[u8]) -> Result<()> { let first_page = Page::read(&mut data, false)?; @@ -195,46 +68,20 @@ fn write_to(mut data: &mut File, pages: &mut [Page], sig: &[u8]) -> Result<()> { writer.write_all(&*first_page.as_bytes())?; let first_md_page = Page::read(&mut data, false)?; - is_metadata(&first_md_page, sig)?; + verify_signature(&first_md_page, sig)?; #[cfg(feature = "format-vorbis")] if sig == VORBIS_COMMENT_HEAD { - vorbis_write(data, &mut writer, first_md_page.content, ser, pages)?; + vorbis::write_to(data, &mut writer, first_md_page.content, ser, pages)?; } #[cfg(feature = "format-opus")] if sig == OPUSTAGS { - let reached_md_end: bool; - let mut remaining = Vec::new(); - - loop { - let p = Page::read(&mut data, false)?; - - if p.header_type != 1 { - data.seek(SeekFrom::Start(p.start as u64))?; - reached_md_end = true; - break; - } - } - - if !reached_md_end { - return Err(LoftyError::InvalidData("OGG file ends with comment header")); - } - - data.read_to_end(&mut remaining)?; - - for mut p in pages.iter_mut() { - p.serial = ser; - p.gen_crc(); - - writer.write_all(&*p.as_bytes())?; - } - - writer.write_all(&*remaining)?; + opus::write_to(data, &mut writer, ser, pages)?; } data.seek(SeekFrom::Start(0))?; - data.set_len(0)?; + data.set_len(first_page.end as u64)?; data.write_all(&*writer)?; Ok(()) diff --git a/src/components/tags/ogg_tag.rs b/src/components/tags/ogg_tag.rs index bf0a8711..523a4e26 100644 --- a/src/components/tags/ogg_tag.rs +++ b/src/components/tags/ogg_tag.rs @@ -39,42 +39,6 @@ impl Default for OggInnerTag { } } -impl OggInnerTag { - fn read_from(reader: &mut R, format: &OggFormat) -> Result - where - R: Read + Seek, - { - match format { - #[cfg(feature = "format-vorbis")] - OggFormat::Vorbis => { - let tag = ogg::read::read_from( - reader, - &VORBIS_IDENT_HEAD, - &VORBIS_COMMENT_HEAD, - OggFormat::Vorbis, - )?; - let vorbis_tag: OggTag = tag.try_into()?; - - Ok(vorbis_tag.inner) - }, - #[cfg(feature = "format-opus")] - OggFormat::Opus => { - let tag = ogg::read::read_from(reader, &OPUSHEAD, &OPUSTAGS, OggFormat::Opus)?; - let vorbis_tag: OggTag = tag.try_into()?; - - Ok(vorbis_tag.inner) - }, - #[cfg(feature = "format-flac")] - OggFormat::Flac => { - let tag = metaflac::Tag::read_from(reader)?; - let vorbis_tag: OggTag = tag.try_into()?; - - Ok(vorbis_tag.inner) - }, - } - } -} - cfg_if::cfg_if! { if #[cfg(feature = "format-opus")] { #[derive(LoftyTag)] @@ -121,8 +85,8 @@ impl TryFrom for OggTag { comments, pictures: (!pictures.is_empty()).then(|| Cow::from(pictures)), }, - properties: FileProperties::default(), // TODO - _format: TagType::Ogg(inp.3), + properties: inp.3, + _format: TagType::Ogg(inp.4), }) } } @@ -177,11 +141,33 @@ impl OggTag { where R: Read + Seek, { - Ok(Self { - inner: OggInnerTag::read_from(reader, &format)?, - properties: FileProperties::default(), // TODO - _format: TagType::Ogg(format), - }) + let tag: Self = match format { + #[cfg(feature = "format-vorbis")] + OggFormat::Vorbis => { + let tag = ogg::read::read_from( + reader, + &VORBIS_IDENT_HEAD, + &VORBIS_COMMENT_HEAD, + OggFormat::Vorbis, + )?; + + tag.try_into()? + }, + #[cfg(feature = "format-opus")] + OggFormat::Opus => { + let tag = ogg::read::read_from(reader, &OPUSHEAD, &OPUSTAGS, OggFormat::Opus)?; + + tag.try_into()? + }, + #[cfg(feature = "format-flac")] + OggFormat::Flac => { + let tag = metaflac::Tag::read_from(reader)?; + + tag.try_into()? + }, + }; + + Ok(tag) } fn get_value(&self, key: &str) -> Option<&str> {