diff --git a/Cargo.toml b/Cargo.toml index 0a935ae8..e2a414fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ lofty_attr = { path = "lofty_attr" } # Debug logging log = "0.4.17" # OGG Vorbis/Opus -ogg_pager = "0.3.2" +ogg_pager = { path = "ogg_pager" } # Key maps once_cell = "1.13.0" paste = "1.0.7" diff --git a/ogg_pager/src/error.rs b/ogg_pager/src/error.rs index a3d13e56..a119a716 100644 --- a/ogg_pager/src/error.rs +++ b/ogg_pager/src/error.rs @@ -15,6 +15,8 @@ pub enum PageError { MissingMagic, /// The reader contains too much data for a single page TooMuchData, + /// The reader contains too little data to extract the expected information + NotEnoughData, /// Any std::io::Error Io(std::io::Error), } @@ -28,6 +30,9 @@ impl fmt::Display for PageError { PageError::BadSegmentCount => write!(f, "Page has a segment count < 1"), PageError::MissingMagic => write!(f, "Page is missing a magic signature"), PageError::TooMuchData => write!(f, "Too much data was provided"), + PageError::NotEnoughData => { + write!(f, "Too little data is available for the expected read") + }, PageError::Io(err) => write!(f, "{}", err), } } diff --git a/ogg_pager/src/header.rs b/ogg_pager/src/header.rs new file mode 100644 index 00000000..c9ad91c6 --- /dev/null +++ b/ogg_pager/src/header.rs @@ -0,0 +1,94 @@ +use crate::{PageError, Result}; + +use std::io::{Read, Seek}; + +use byteorder::{LittleEndian, ReadBytesExt}; + +// An OGG page header +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub struct PageHeader { + /// The position in the stream the page started at + pub start: u64, + pub(crate) header_type_flag: u8, + /// The page's absolute granule position + pub abgp: u64, + /// The page's stream serial number + pub stream_serial: u32, + /// The page's sequence number + pub sequence_number: u32, + pub(crate) checksum: u32, +} + +impl PageHeader { + pub fn new(header_type_flag: u8, abgp: u64, stream_serial: u32, sequence_number: u32) -> Self { + Self { + start: 0, + header_type_flag, + abgp, + stream_serial, + sequence_number, + checksum: 0, + } + } + + pub fn read(data: &mut R) -> Result<(Self, Vec)> + where + R: Read + Seek, + { + let start = data.stream_position()?; + + let mut sig = [0; 4]; + data.read_exact(&mut sig)?; + + if &sig != b"OggS" { + return Err(PageError::MissingMagic); + } + + // Version, always 0 + let version = data.read_u8()?; + + if version != 0 { + return Err(PageError::InvalidVersion); + } + + let header_type_flag = data.read_u8()?; + + let abgp = data.read_u64::()?; + let stream_serial = data.read_u32::()?; + let sequence_number = data.read_u32::()?; + let checksum = data.read_u32::()?; + + let segments = data.read_u8()?; + + if segments < 1 { + return Err(PageError::BadSegmentCount); + } + + let mut segment_table = vec![0; segments as usize]; + data.read_exact(&mut segment_table)?; + + let header = Self { + start, + header_type_flag, + abgp, + stream_serial, + sequence_number, + checksum, + }; + + Ok((header, segment_table)) + } + + /// Returns the page's header type flag + pub fn header_type_flag(&self) -> u8 { + self.header_type_flag + } + + /// Returns the page's checksum + /// + /// NOTE: This will not generate a new CRC. It will return + /// the CRC as-is. Use [`Page::gen_crc`] to generate a new one. + pub fn checksum(&self) -> u32 { + self.checksum + } +} diff --git a/ogg_pager/src/lib.rs b/ogg_pager/src/lib.rs index 3d4ca48d..0f89729f 100644 --- a/ogg_pager/src/lib.rs +++ b/ogg_pager/src/lib.rs @@ -2,13 +2,13 @@ mod crc; mod error; +mod header; use std::io::{Read, Seek, SeekFrom}; -use byteorder::{LittleEndian, ReadBytesExt}; - pub use crc::crc32; pub use error::{PageError, Result}; +pub use header::PageHeader; const CONTINUED_PACKET: u8 = 0x01; @@ -23,16 +23,7 @@ pub const CONTAINS_LAST_PAGE_OF_BITSTREAM: u8 = 0x04; #[derive(Clone, PartialEq, Eq, Debug)] pub struct Page { content: Vec, - header_type: u8, - /// The page's absolute granule position - pub abgp: u64, - /// The page's stream serial number - pub serial: u32, - /// The page's sequence number - pub seq_num: u32, - checksum: u32, - /// The position in the stream the page started at - pub start: u64, + header: PageHeader, /// The position in the stream the page ended pub end: u64, } @@ -66,13 +57,7 @@ impl Page { /// ident_header_packet, /// ); /// ``` - pub fn new( - header_type_flag: u8, - abgp: u64, - stream_serial: u32, - sequence_number: u32, - content: Vec, - ) -> Result { + pub fn new(header: PageHeader, content: Vec) -> Result { let content_len = content.len(); if content_len > MAX_CONTENT_SIZE { @@ -81,16 +66,19 @@ impl Page { Ok(Self { content, - header_type: header_type_flag, - abgp, - serial: stream_serial, - seq_num: sequence_number, - checksum: 0, - start: 0, + header, end: content_len as u64, }) } + pub fn header(&self) -> &PageHeader { + &self.header + } + + pub fn header_mut(&mut self) -> &mut PageHeader { + &mut self.header + } + /// Convert the Page to Vec for writing /// /// NOTE: This will write the checksum as is. It is likely [`Page::gen_crc`] will have @@ -105,11 +93,11 @@ impl Page { bytes.extend(b"OggS"); bytes.push(0); // Version - bytes.push(self.header_type); - bytes.extend(self.abgp.to_le_bytes()); - bytes.extend(self.serial.to_le_bytes()); - bytes.extend(self.seq_num.to_le_bytes()); - bytes.extend(self.checksum.to_le_bytes()); + bytes.push(self.header.header_type_flag); + bytes.extend(self.header.abgp.to_le_bytes()); + bytes.extend(self.header.stream_serial.to_le_bytes()); + bytes.extend(self.header.sequence_number.to_le_bytes()); + bytes.extend(self.header.checksum.to_le_bytes()); bytes.push(segment_table.len() as u8); bytes.append(&mut segment_table); bytes.extend(self.content.iter()); @@ -129,37 +117,7 @@ impl Page { where V: Read + Seek, { - let start = data.stream_position()?; - - let mut sig = [0; 4]; - data.read_exact(&mut sig)?; - - if &sig != b"OggS" { - return Err(PageError::MissingMagic); - } - - // Version, always 0 - let version = data.read_u8()?; - - if version != 0 { - return Err(PageError::InvalidVersion); - } - - let header_type = data.read_u8()?; - - let abgp = data.read_u64::()?; - let serial = data.read_u32::()?; - let seq_num = data.read_u32::()?; - let checksum = data.read_u32::()?; - - let segments = data.read_u8()?; - - if segments < 1 { - return Err(PageError::BadSegmentCount); - } - - let mut segment_table = vec![0; segments as usize]; - data.read_exact(&mut segment_table)?; + let (header, segment_table) = PageHeader::read(data)?; let mut content: Vec = Vec::new(); let content_len: u16 = segment_table.iter().map(|&b| u16::from(b)).sum(); @@ -175,12 +133,7 @@ impl Page { Ok(Page { content, - header_type, - abgp, - serial, - seq_num, - checksum, - start, + header, end, }) } @@ -191,7 +144,7 @@ impl Page { /// /// See [`Page::as_bytes`] pub fn gen_crc(&mut self) -> Result<()> { - self.checksum = crc::crc32(&self.as_bytes()?); + self.header.checksum = crc::crc32(&self.as_bytes()?); Ok(()) } @@ -220,19 +173,21 @@ impl Page { let remaining = 65025 - self_len; self.content.extend(content[0..remaining].iter()); - self.header_type = 0; - self.abgp = 1_u64.wrapping_neg(); // -1 in two's complement indicates that no packets finish on this page + self.header.header_type_flag = 0; + self.header.abgp = 1_u64.wrapping_neg(); // -1 in two's complement indicates that no packets finish on this page self.end += remaining as u64; let mut p = Page { content: content[remaining..].to_vec(), - header_type: 1, - abgp: 0, - serial: self.serial, - seq_num: self.seq_num + 1, - checksum: 0, - start: self.end, - end: self.start + content.len() as u64, + header: PageHeader { + start: self.end, + header_type_flag: 1, + abgp: 0, + stream_serial: self.header.stream_serial, + sequence_number: self.header.sequence_number + 1, + checksum: 0, + }, + end: self.header().start + content.len() as u64, }; p.gen_crc()?; @@ -253,19 +208,6 @@ impl Page { self.content } - /// Returns the page's header type flag - pub fn header_type(&self) -> u8 { - self.header_type - } - - /// Returns the page's checksum - /// - /// NOTE: This will not generate a new CRC. It will return - /// the CRC as-is. Use [`Page::gen_crc`] to generate a new one. - pub fn checksum(&self) -> u32 { - self.checksum - } - /// Returns the page's segment table /// /// # Errors @@ -299,22 +241,24 @@ pub fn paginate(packet: &[u8], stream_serial: u32, abgp: u64, flags: u8) -> Vec< for (idx, page) in packet.chunks(MAX_CONTENT_SIZE).enumerate() { let p = Page { content: page.to_vec(), - header_type: { - if first_page { - if flags & CONTAINS_FIRST_PAGE_OF_BITSTREAM == 0x02 { - CONTAINS_LAST_PAGE_OF_BITSTREAM + header: PageHeader { + start: pos, + header_type_flag: { + if first_page { + if flags & CONTAINS_FIRST_PAGE_OF_BITSTREAM == 0x02 { + CONTAINS_LAST_PAGE_OF_BITSTREAM + } else { + 0 + } } else { - 0 + CONTINUED_PACKET } - } else { - CONTINUED_PACKET - } + }, + abgp, + stream_serial, + sequence_number: (idx + 1) as u32, + checksum: 0, }, - abgp, - serial: stream_serial, - seq_num: (idx + 1) as u32, - checksum: 0, - start: pos, end: { pos += page.len() as u64; pos @@ -327,7 +271,7 @@ pub fn paginate(packet: &[u8], stream_serial: u32, abgp: u64, flags: u8) -> Vec< if flags & CONTAINS_LAST_PAGE_OF_BITSTREAM == 0x04 { if let Some(last) = pages.last_mut() { - last.header_type |= CONTAINS_LAST_PAGE_OF_BITSTREAM; + last.header.header_type_flag |= CONTAINS_LAST_PAGE_OF_BITSTREAM; } } @@ -339,7 +283,7 @@ pub fn paginate(packet: &[u8], stream_serial: u32, abgp: u64, flags: u8) -> Vec< break; } - p.abgp = 1_u64.wrapping_neg(); + p.header.abgp = 1_u64.wrapping_neg(); } } @@ -379,9 +323,135 @@ pub fn segment_table(length: usize) -> Result> { Ok(segments) } +pub struct Packets { + content: Vec, + packet_sizes: Vec, +} + +impl Packets { + pub fn read(data: &mut R) -> Result + where + R: Read + Seek, + { + Self::read_count(data, -1) + } + + pub fn read_count(data: &mut R, count: isize) -> Result + where + R: Read + Seek, + { + let mut content = Vec::new(); + let mut packet_sizes = Vec::new(); + + if count == 0 || count < -1 { + return Ok(Self { + content, + packet_sizes, + }); + } + + let mut read = 0; + 'outer: loop { + if let Ok(page) = Page::read(data, false) { + let segment_table = page.segment_table()?; + + let mut packet_size = 0_u64; + for i in segment_table { + packet_size += i as u64; + + if i < 255 { + if count != -1 { + read += 1; + } + + content.extend_from_slice(&page.content[0..packet_size as usize]); + packet_sizes.push(packet_size); + packet_size = 0; + + if read == count { + break 'outer; + } + } + } + + continue; + } + + break; + } + + if count != -1 && packet_sizes.len() != read as usize { + return Err(PageError::NotEnoughData); + } + + Ok(Self { + content, + packet_sizes, + }) + } + + pub fn get(&self, idx: usize) -> Option<&[u8]> { + if idx >= self.content.len() { + return None; + } + + let start_pos = match idx { + // Packet 0 starts at pos 0 + 0 => 0, + // Anything else we have to get the size of the previous packet + other => self.packet_sizes[other - 1] as usize, + }; + + if let Some(packet_size) = self.packet_sizes.get(idx) { + return Some(&self.content[start_pos..start_pos + *packet_size as usize]); + } + + None + } +} + +pub struct PacketsIter<'a> { + content: &'a [u8], + packet_sizes: &'a [u64], + cap: usize, +} + +impl<'a> Iterator for PacketsIter<'a> { + type Item = &'a [u8]; + + fn next(&mut self) -> Option { + if self.cap == 0 { + return None; + } + + let packet_size = self.packet_sizes[0]; + + self.cap -= 1; + self.packet_sizes = &self.packet_sizes[1..]; + + let (ret, remaining) = self.content.split_at(packet_size as usize); + self.content = remaining; + + Some(ret) + } +} + +impl<'a> IntoIterator for &'a Packets { + type Item = &'a [u8]; + type IntoIter = PacketsIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + PacketsIter { + content: &self.content, + packet_sizes: &self.packet_sizes, + cap: self.packet_sizes.len(), + } + } +} + #[cfg(test)] mod tests { - use crate::{paginate, segment_table, Page}; + use crate::{paginate, segment_table, Page, PageHeader}; use std::io::Cursor; #[test] @@ -391,12 +461,14 @@ mod tests { 0x4F, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64, 0x01, 0x02, 0x38, 0x01, 0x80, 0xBB, 0, 0, 0, 0, 0, ], - header_type: 2, - abgp: 0, - serial: 1759377061, - seq_num: 0, - checksum: 3579522525, - start: 0, + header: PageHeader { + start: 0, + header_type_flag: 2, + abgp: 0, + stream_serial: 1759377061, + sequence_number: 0, + checksum: 3579522525, + }, end: 47, }; @@ -427,21 +499,23 @@ mod tests { ); for (i, page) in pages.into_iter().enumerate() { - assert_eq!(page.serial, 1234); + let header = page.header; + + assert_eq!(header.stream_serial, 1234); if i + 1 == len { - assert_eq!(page.abgp, 0); + assert_eq!(header.abgp, 0); } else { // -1 - assert_eq!(page.abgp, u64::MAX); + assert_eq!(header.abgp, u64::MAX); } - assert_eq!(page.seq_num, (i + 1) as u32); + assert_eq!(header.sequence_number, (i + 1) as u32); if i == 0 { - assert_eq!(page.header_type, 0); + assert_eq!(header.header_type_flag, 0); } else { - assert_eq!(page.header_type, 1); + assert_eq!(header.header_type_flag, 1); } } } diff --git a/src/ogg/mod.rs b/src/ogg/mod.rs index a130dacf..c68c6f04 100644 --- a/src/ogg/mod.rs +++ b/src/ogg/mod.rs @@ -34,10 +34,10 @@ pub use speex::SpeexFile; pub use vorbis::properties::VorbisProperties; pub use vorbis::VorbisFile; -pub(self) fn verify_signature(page: &Page, sig: &[u8]) -> Result<()> { +pub(self) fn verify_signature(content: &[u8], sig: &[u8]) -> Result<()> { let sig_len = sig.len(); - if page.content().len() < sig_len || &page.content()[..sig_len] != sig { + if content.len() < sig_len || &content[..sig_len] != sig { decode_err!(@BAIL Vorbis, "File missing magic signature"); } diff --git a/src/ogg/opus/mod.rs b/src/ogg/opus/mod.rs index d590b573..cc9a3a3d 100644 --- a/src/ogg/opus/mod.rs +++ b/src/ogg/opus/mod.rs @@ -38,7 +38,7 @@ impl AudioFile for OpusFile { let file_information = super::read::read_from(reader, OPUSHEAD, OPUSTAGS)?; Ok(Self { - properties: if parse_options.read_properties { properties::read_properties(reader, &file_information.1)? } else { OpusProperties::default() }, + properties: if parse_options.read_properties { properties::read_properties(reader, file_information.1, &file_information.2)? } else { OpusProperties::default() }, #[cfg(feature = "vorbis_comments")] // Safe to unwrap, a metadata packet is mandatory in Opus vorbis_comments_tag: file_information.0.unwrap(), diff --git a/src/ogg/opus/properties.rs b/src/ogg/opus/properties.rs index af6eb164..9e08ecce 100644 --- a/src/ogg/opus/properties.rs +++ b/src/ogg/opus/properties.rs @@ -7,7 +7,7 @@ use std::io::{Read, Seek, SeekFrom}; use std::time::Duration; use byteorder::{LittleEndian, ReadBytesExt}; -use ogg_pager::Page; +use ogg_pager::{Packets, PageHeader}; /// An Opus file's audio properties #[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] @@ -66,35 +66,35 @@ impl OpusProperties { } } -pub(in crate::ogg) fn read_properties(data: &mut R, first_page: &Page) -> Result +pub(in crate::ogg) fn read_properties( + data: &mut R, + first_page_header: PageHeader, + packets: &Packets, +) -> Result where R: Read + Seek, { - let (stream_len, file_length) = { - let current = data.stream_position()?; - let end = data.seek(SeekFrom::End(0))?; - data.seek(SeekFrom::Start(current))?; - - (end - first_page.start, end) - }; - let mut properties = OpusProperties::default(); - let first_page_abgp = first_page.abgp; + let first_page_abgp = first_page_header.abgp; + + // Safe to unwrap, it is impossible to get this far without + // an identification packet. + let identification_packet = packets.get(0).unwrap(); // Skip identification header - let first_page_content = &mut &first_page.content()[8..]; + let identification_packet_reader = &mut &identification_packet[8..]; - properties.version = first_page_content.read_u8()?; - properties.channels = first_page_content.read_u8()?; + properties.version = identification_packet_reader.read_u8()?; + properties.channels = identification_packet_reader.read_u8()?; - let pre_skip = first_page_content.read_u16::()?; + let pre_skip = identification_packet_reader.read_u16::()?; - properties.input_sample_rate = first_page_content.read_u32::()?; + properties.input_sample_rate = identification_packet_reader.read_u32::()?; - let _output_gain = first_page_content.read_u16::()?; + let _output_gain = identification_packet_reader.read_u16::()?; - let channel_mapping_family = first_page_content.read_u8()?; + let channel_mapping_family = identification_packet_reader.read_u8()?; // https://datatracker.ietf.org/doc/html/rfc7845.html#section-5.1.1 if (channel_mapping_family == 0 && properties.channels > 2) @@ -103,18 +103,27 @@ where decode_err!(@BAIL Opus, "Invalid channel count for mapping family"); } - // Subtract the identification and metadata packet length from the total - let audio_size = stream_len - data.stream_position()?; + // Get the last pages' absolute granule position let last_page = find_last_page(data)?; - let last_page_abgp = last_page.abgp; + let last_page_abgp = last_page.header().abgp; + + // Get the stream length + + // Also safe to unwrap, metadata is checked prior + let metadata_packet = packets.get(1).unwrap(); + + let header_size = identification_packet.len() + metadata_packet.len(); + + let file_length = data.seek(SeekFrom::End(0))?; + let stream_len = file_length - header_size as u64; if let Some(frame_count) = last_page_abgp.checked_sub(first_page_abgp + u64::from(pre_skip)) { let length = (frame_count as f64) * 1000.0 / 48000.0_f64 + 0.5; properties.duration = Duration::from_millis(length as u64); properties.overall_bitrate = ((file_length as f64) * 8.0 / length) as u32; - properties.audio_bitrate = ((audio_size as f64) * 8.0 / length) as u32; + properties.audio_bitrate = ((stream_len as f64) * 8.0 / length) as u32; } Ok(properties) diff --git a/src/ogg/read.rs b/src/ogg/read.rs index d8364f62..1566e30d 100644 --- a/src/ogg/read.rs +++ b/src/ogg/read.rs @@ -10,13 +10,13 @@ use std::io::{Read, Seek, SeekFrom}; #[cfg(feature = "vorbis_comments")] use byteorder::{LittleEndian, ReadBytesExt}; -use ogg_pager::Page; +use ogg_pager::{Packets, PageHeader}; #[cfg(feature = "vorbis_comments")] -pub type OGGTags = (Option, Page); +pub type OGGTags = (Option, PageHeader, Packets); #[cfg(not(feature = "vorbis_comments"))] -pub type OGGTags = (Option<()>, Page); +pub type OGGTags = (Option<()>, PageHeader, Packets); #[cfg(feature = "vorbis_comments")] pub(crate) fn read_comments(data: &mut R, mut len: u64, tag: &mut VorbisComments) -> Result<()> @@ -99,39 +99,38 @@ pub(crate) fn read_from(data: &mut T, header_sig: &[u8], comment_sig: &[u8]) where T: Read + Seek, { - let first_page = Page::read(data, false)?; - verify_signature(&first_page, header_sig)?; + // TODO: Would be nice if we didn't have to read just to seek and reread immediately + let start = data.stream_position()?; + let (first_page_header, _) = PageHeader::read(data)?; - let md_page = Page::read(data, false)?; - verify_signature(&md_page, comment_sig)?; + data.seek(SeekFrom::Start(start))?; - let mut md_pages: Vec = Vec::new(); + // Read the first 3 packets, which are all headers + let packets = Packets::read_count(data, 3)?; - md_pages.extend_from_slice(&md_page.content()[comment_sig.len()..]); + let identification_packet = packets + .get(0) + .ok_or_else(|| decode_err!("OGG: Expected identification packet"))?; + verify_signature(identification_packet, header_sig)?; - while let Ok(page) = Page::read(data, false) { - if md_pages.len() > 125_829_120 { - err!(TooMuchData); - } + let mut metadata_packet = packets + .get(1) + .ok_or_else(|| decode_err!("OGG: Expected comment packet"))?; + verify_signature(metadata_packet, comment_sig)?; - if page.header_type() & 0x01 == 1 { - md_pages.extend_from_slice(page.content()); - } else { - data.seek(SeekFrom::Start(page.start))?; - break; - } - } + // Remove the signature from the packet + metadata_packet = &metadata_packet[comment_sig.len()..]; #[cfg(feature = "vorbis_comments")] { let mut tag = VorbisComments::default(); - let reader = &mut &md_pages[..]; + let reader = &mut metadata_packet; read_comments(reader, reader.len() as u64, &mut tag)?; - Ok((Some(tag), first_page)) + Ok((Some(tag), first_page_header, packets)) } #[cfg(not(feature = "vorbis_comments"))] - Ok((None, first_page)) + Ok((None, first_page_header, packets)) } diff --git a/src/ogg/speex/mod.rs b/src/ogg/speex/mod.rs index 1cffede4..a6be94f2 100644 --- a/src/ogg/speex/mod.rs +++ b/src/ogg/speex/mod.rs @@ -37,7 +37,7 @@ impl AudioFile for SpeexFile { let file_information = super::read::read_from(reader, SPEEXHEADER, &[])?; Ok(Self { - properties: if parse_options.read_properties { properties::read_properties(reader, &file_information.1)? } else { SpeexProperties::default() }, + properties: if parse_options.read_properties { properties::read_properties(reader, file_information.1, &file_information.2)? } else { SpeexProperties::default() }, #[cfg(feature = "vorbis_comments")] // Safe to unwrap, a metadata packet is mandatory in Speex vorbis_comments_tag: file_information.0.unwrap(), diff --git a/src/ogg/speex/properties.rs b/src/ogg/speex/properties.rs index 9d05eb48..64bb937d 100644 --- a/src/ogg/speex/properties.rs +++ b/src/ogg/speex/properties.rs @@ -7,7 +7,7 @@ use std::io::{Read, Seek, SeekFrom}; use std::time::Duration; use byteorder::{LittleEndian, ReadBytesExt}; -use ogg_pager::Page; +use ogg_pager::{Packets, PageHeader}; /// A Speex file's audio properties #[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] @@ -84,13 +84,19 @@ impl SpeexProperties { } } -pub(in crate::ogg) fn read_properties(data: &mut R, first_page: &Page) -> Result +pub(in crate::ogg) fn read_properties( + data: &mut R, + first_page_header: PageHeader, + packets: &Packets, +) -> Result where R: Read + Seek, { - let first_page_abgp = first_page.abgp; + // Safe to unwrap, it is impossible to get to this point without an + // identification header. + let identification_packet = packets.get(0).unwrap(); - if first_page.content().len() < 80 { + if identification_packet.len() < 80 { decode_err!(@BAIL Speex, "Header packet too small"); } @@ -101,49 +107,64 @@ where // Skipping: // Speex string ("Speex ", 8) // Speex version (20) - let first_page_content = &mut &first_page.content()[28..]; + let identification_packet_reader = &mut &identification_packet[28..]; - properties.version = first_page_content.read_u32::()?; + properties.version = identification_packet_reader.read_u32::()?; // Total size of the speex header - let _header_size = first_page_content.read_u32::()?; + let _header_size = identification_packet_reader.read_u32::()?; - properties.sample_rate = first_page_content.read_u32::()?; - properties.mode = first_page_content.read_u32::()?; + properties.sample_rate = identification_packet_reader.read_u32::()?; + properties.mode = identification_packet_reader.read_u32::()?; // Version ID of the bitstream - let _mode_bitstream_version = first_page_content.read_u32::()?; + let _mode_bitstream_version = identification_packet_reader.read_u32::()?; - let channels = first_page_content.read_u32::()?; + let channels = identification_packet_reader.read_u32::()?; if channels != 1 && channels != 2 { decode_err!(@BAIL Speex, "Found invalid channel count, must be mono or stereo"); } properties.channels = channels as u8; - properties.nominal_bitrate = first_page_content.read_i32::()?; + properties.nominal_bitrate = identification_packet_reader.read_i32::()?; // The size of the frames in samples - let _frame_size = first_page_content.read_u32::()?; + let _frame_size = identification_packet_reader.read_u32::()?; - properties.vbr = first_page_content.read_u32::()? == 1; - - let last_page = find_last_page(data)?; - let last_page_abgp = last_page.abgp; + properties.vbr = identification_packet_reader.read_u32::()? == 1; + let last_page = find_last_page(data); let file_length = data.seek(SeekFrom::End(0))?; - if let Some(frame_count) = last_page_abgp.checked_sub(first_page_abgp) { + // This is used for bitrate calculation, it should be the length in + // milliseconds, but if we can't determine it then we'll just use 1000. + let mut length = 1000; + if let Ok(last_page) = last_page { + let first_page_abgp = first_page_header.abgp; + let last_page_abgp = last_page.header().abgp; + if properties.sample_rate > 0 { - let length = ((frame_count as f64) * 1000.0) / f64::from(properties.sample_rate) + 0.5; - properties.duration = Duration::from_millis(length as u64); + let total_samples = last_page_abgp.saturating_sub(first_page_abgp); - properties.overall_bitrate = ((file_length as f64) * 8.0 / length) as u32; - - // TODO: Calculate with the stream length, and make this the fallback - properties.audio_bitrate = (properties.nominal_bitrate as u64 / 1000) as u32; + // Best case scenario + if total_samples > 0 { + length = total_samples * 1000 / u64::from(properties.sample_rate); + properties.duration = Duration::from_millis(length); + } else { + log::debug!( + "Speex: The file contains invalid PCM values, unable to calculate length" + ); + } + } else { + log::debug!("Speex: Sample rate = 0, unable to calculate length"); } } + if properties.nominal_bitrate > 0 { + properties.overall_bitrate = (file_length.saturating_mul(8) / length) as u32; + properties.audio_bitrate = (properties.nominal_bitrate as u64 / 1000) as u32; + } + Ok(properties) } diff --git a/src/ogg/vorbis/mod.rs b/src/ogg/vorbis/mod.rs index d3dc3a43..d0e76ab8 100644 --- a/src/ogg/vorbis/mod.rs +++ b/src/ogg/vorbis/mod.rs @@ -41,7 +41,7 @@ impl AudioFile for VorbisFile { super::read::read_from(reader, VORBIS_IDENT_HEAD, VORBIS_COMMENT_HEAD)?; Ok(Self { - properties: if parse_options.read_properties { properties::read_properties(reader, &file_information.1)? } else { VorbisProperties::default() }, + properties: if parse_options.read_properties { properties::read_properties(reader, file_information.1, &file_information.2)? } else { VorbisProperties::default() }, #[cfg(feature = "vorbis_comments")] // Safe to unwrap, a metadata packet is mandatory in OGG Vorbis vorbis_comments_tag: file_information.0.unwrap(), diff --git a/src/ogg/vorbis/properties.rs b/src/ogg/vorbis/properties.rs index 07a31f33..6df5c5bf 100644 --- a/src/ogg/vorbis/properties.rs +++ b/src/ogg/vorbis/properties.rs @@ -6,7 +6,7 @@ use std::io::{Read, Seek, SeekFrom}; use std::time::Duration; use byteorder::{LittleEndian, ReadBytesExt}; -use ogg_pager::Page; +use ogg_pager::{Packets, PageHeader}; /// An OGG Vorbis file's audio properties #[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] @@ -85,17 +85,19 @@ impl VorbisProperties { pub(in crate::ogg) fn read_properties( data: &mut R, - first_page: &Page, + first_page_header: PageHeader, + packets: &Packets, ) -> Result where R: Read + Seek, { - let first_page_abgp = first_page.abgp; - let mut properties = VorbisProperties::default(); + // It's impossible to get this far without the identification packet, safe to unwrap + let first_packet = packets.get(0).unwrap(); + // Skip identification header - let first_page_content = &mut &first_page.content()[7..]; + let first_page_content = &mut &first_packet[7..]; properties.version = first_page_content.read_u32::()?; @@ -106,22 +108,37 @@ where properties.bitrate_nominal = first_page_content.read_i32::()?; properties.bitrate_minimum = first_page_content.read_i32::()?; - let last_page = find_last_page(data)?; - let last_page_abgp = last_page.abgp; - + let last_page = find_last_page(data); let file_length = data.seek(SeekFrom::End(0))?; - if let Some(frame_count) = last_page_abgp.checked_sub(first_page_abgp) { + // This is used for bitrate calculation, it should be the length in + // milliseconds, but if we can't determine it then we'll just use 1000. + let mut length = 1000; + if let Ok(last_page) = last_page { + let first_page_abgp = first_page_header.abgp; + let last_page_abgp = last_page.header().abgp; + if properties.sample_rate > 0 { - let length = frame_count * 1000 / u64::from(properties.sample_rate); - properties.duration = Duration::from_millis(length); + let total_samples = last_page_abgp.saturating_sub(first_page_abgp); - properties.overall_bitrate = ((file_length * 8) / length) as u32; - - // TODO: Calculate with the stream length, and make this the fallback - properties.audio_bitrate = (properties.bitrate_nominal as u64 / 1000) as u32; + // Best case scenario + if total_samples > 0 { + length = total_samples * 1000 / u64::from(properties.sample_rate); + properties.duration = Duration::from_millis(length); + } else { + log::debug!( + "Vorbis: The file contains invalid PCM values, unable to calculate length" + ); + } + } else { + log::debug!("Vorbis: Sample rate = 0, unable to calculate length"); } } + if properties.bitrate_nominal > 0 { + properties.overall_bitrate = (file_length.saturating_mul(8) / length) as u32; + properties.audio_bitrate = (properties.bitrate_nominal as u64 / 1000) as u32; + } + Ok(properties) } diff --git a/src/ogg/vorbis/write.rs b/src/ogg/vorbis/write.rs index 2cddc14d..0ee62c78 100644 --- a/src/ogg/vorbis/write.rs +++ b/src/ogg/vorbis/write.rs @@ -36,8 +36,8 @@ pub(crate) fn write_to( loop { let p = Page::read(data, false)?; - if p.header_type() & 0x01 != 1 { - data.seek(SeekFrom::Start(p.start))?; + if p.header().header_type_flag() & 0x01 != 1 { + data.seek(SeekFrom::Start(p.header().start))?; data.read_to_end(&mut remaining)?; reached_md_end = true; diff --git a/src/ogg/write.rs b/src/ogg/write.rs index 87958fea..47855cbe 100644 --- a/src/ogg/write.rs +++ b/src/ogg/write.rs @@ -146,7 +146,7 @@ where { let first_page = Page::read(data, false)?; - let ser = first_page.serial; + let ser = first_page.header().stream_serial; let mut writer = Vec::new(); writer.write_all(&first_page.as_bytes()?)?; @@ -155,7 +155,7 @@ where let comment_signature = format.comment_signature(); if let Some(comment_signature) = comment_signature { - verify_signature(&first_md_page, comment_signature)?; + verify_signature(first_md_page.content(), comment_signature)?; } let comment_signature = comment_signature.unwrap_or_default(); @@ -211,8 +211,8 @@ fn replace_packet( loop { let p = Page::read(data, true)?; - if p.header_type() & 0x01 != 0x01 { - data.seek(SeekFrom::Start(p.start))?; + if p.header().header_type_flag() & 0x01 != 0x01 { + data.seek(SeekFrom::Start(p.header().start))?; reached_md_end = true; break; }