Properties for Opus and Vorbis

This commit is contained in:
Serial 2021-07-21 21:56:04 -04:00
parent c0457f1ce7
commit f95e7cfdff
6 changed files with 321 additions and 297 deletions

View file

@ -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<Vec<Page>> {
let mut pages: Vec<Page> = 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<Vec<Page>> {
}
}
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<Vec<Page>> {
Ok(pages)
}
pub(self) fn reach_metadata<T>(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<R>(data: &mut R) -> Result<Page>
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<Page> = 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(())
}

View file

@ -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<R>(
@ -14,62 +16,80 @@ pub(in crate::components) fn read_properties<R>(
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::<LittleEndian>()?;
let sample_rate = cursor.read_u32::<LittleEndian>()?;
let channels = first_page_content.read_u8()?;
let pre_skip = first_page_content.read_u16::<LittleEndian>()?;
let sample_rate = first_page_content.read_u32::<LittleEndian>()?;
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<Page> = 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<u8>, 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(())
}

View file

@ -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<Picture>,
HashMap<UniCase<String>, String>,
FileProperties,
OggFormat,
);
fn read_properties<R>(data: &mut R, header_sig: &[u8]) -> Result<FileProperties>
fn read_properties<R>(data: &mut R, header_sig: &[u8], first_page: Page) -> Result<FileProperties>
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<T>(
mut data: T,
data: &mut T,
header_sig: &[u8],
comment_sig: &[u8],
format: OggFormat,
@ -49,16 +48,17 @@ pub(crate) fn read_from<T>(
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<u8> = 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))
}

View file

@ -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<R>(
data: &mut R,
@ -13,5 +17,174 @@ pub(in crate::components) fn read_properties<R>(
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::<LittleEndian>()?;
// 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<u8>,
first_md_content: Vec<u8>,
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::<LittleEndian>()?;
c.seek(SeekFrom::Current(i64::from(vendor_len)))?;
let total_comments = c.read_u32::<LittleEndian>()?;
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::<LittleEndian>()?;
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(())
}

View file

@ -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<u8>,
first_md_content: Vec<u8>,
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::<LittleEndian>()?;
c.seek(SeekFrom::Current(i64::from(vendor_len)))?;
let total_comments = c.read_u32::<LittleEndian>()?;
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::<LittleEndian>()?;
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(())

View file

@ -39,42 +39,6 @@ impl Default for OggInnerTag {
}
}
impl OggInnerTag {
fn read_from<R>(reader: &mut R, format: &OggFormat) -> Result<Self>
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<OGGTags> 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> {