Add Speex support

This commit is contained in:
Serial 2022-01-31 18:19:11 -05:00
parent 64f0eff19d
commit c1725de93d
No known key found for this signature in database
GPG key ID: DA95198DC17C4568
30 changed files with 548 additions and 185 deletions

View file

@ -5,71 +5,66 @@ use criterion::{criterion_group, criterion_main, Criterion};
use std::io::Cursor;
macro_rules! test_read_path {
($function:ident, $path:expr) => {
fn $function() {
Probe::open($path).unwrap().read(true).unwrap();
}
($c:ident, [$(($NAME:literal, $path:expr)),+]) => {
let mut g = $c.benchmark_group("File reading (Inferred from Path)");
$(
g.bench_function($NAME, |b| b.iter(|| Probe::open($path).unwrap().read(true).unwrap()));
)+
};
}
test_read_path!(read_aiff_path, "tests/files/assets/a.aiff");
test_read_path!(read_ape_path, "tests/files/assets/a.ape");
test_read_path!(read_flac_path, "tests/files/assets/a.flac");
test_read_path!(read_m4a_path, "tests/files/assets/m4a_codec_aac.m4a");
test_read_path!(read_mp3_path, "tests/files/assets/a.mp3");
test_read_path!(read_vorbis_path, "tests/files/assets/a.ogg");
test_read_path!(read_opus_path, "tests/files/assets/a.opus");
test_read_path!(read_riff_path, "tests/files/assets/a.wav");
fn path_infer_read(c: &mut Criterion) {
let mut g = c.benchmark_group("File reading (Inferred from Path)");
g.bench_function("AIFF", |b| b.iter(read_aiff_path));
g.bench_function("APE", |b| b.iter(read_ape_path));
g.bench_function("FLAC", |b| b.iter(read_flac_path));
g.bench_function("MP4", |b| b.iter(read_m4a_path));
g.bench_function("MP3", |b| b.iter(read_mp3_path));
g.bench_function("VORBIS", |b| b.iter(read_vorbis_path));
g.bench_function("OPUS", |b| b.iter(read_opus_path));
g.bench_function("RIFF", |b| b.iter(read_riff_path));
test_read_path!(
c,
[
("AIFF", "tests/files/assets/full_test.aiff"),
("APE", "tests/files/assets/full_test.ape"),
("FLAC", "tests/files/assets/full_test.flac"),
("MP4", "tests/files/assets/m4a_codec_aac.m4a"),
("MP3", "tests/files/assets/full_test.mp3"),
("VORBIS", "tests/files/assets/full_test.ogg"),
("OPUS", "tests/files/assets/full_test.opus"),
("RIFF", "tests/files/assets/wav_format_pcm.wav")
]
);
}
macro_rules! test_read_file {
($function:ident, $name:ident, $path:expr) => {
const $name: &[u8] = include_bytes!($path);
($c:ident, [$(($NAME:ident, $path:expr)),+]) => {
let mut g = $c.benchmark_group("File reading (Inferred from Content)");
fn $function() {
Probe::new(Cursor::new($name))
.guess_file_type()
.unwrap()
.read(true)
.unwrap();
}
};
$(
const $NAME: &[u8] = include_bytes!($path);
g.bench_function(
stringify!($NAME),
|b| b.iter(|| {
Probe::new(Cursor::new($NAME))
.guess_file_type()
.unwrap()
.read(true)
.unwrap()
})
);
)+
}
}
test_read_file!(read_aiff_file, AIFF, "../tests/files/assets/a.aiff");
test_read_file!(read_ape_file, APE, "../tests/files/assets/a.ape");
test_read_file!(read_flac_file, FLAC, "../tests/files/assets/a.flac");
test_read_file!(
read_m4a_file,
MP4,
"../tests/files/assets/m4a_codec_aac.m4a"
);
test_read_file!(read_mp3_file, MP3, "../tests/files/assets/a.mp3");
test_read_file!(read_vorbis_file, VORBIS, "../tests/files/assets/a.ogg");
test_read_file!(read_opus_file, OPUS, "../tests/files/assets/a.opus");
test_read_file!(read_riff_file, RIFF, "../tests/files/assets/a.wav");
fn content_infer_read(c: &mut Criterion) {
let mut g = c.benchmark_group("File reading (Inferred from Content)");
g.bench_function("AIFF", |b| b.iter(read_aiff_file));
g.bench_function("APE", |b| b.iter(read_ape_file));
g.bench_function("FLAC", |b| b.iter(read_flac_file));
g.bench_function("MP4", |b| b.iter(read_m4a_file));
g.bench_function("MP3", |b| b.iter(read_mp3_file));
g.bench_function("VORBIS", |b| b.iter(read_vorbis_file));
g.bench_function("OPUS", |b| b.iter(read_opus_file));
g.bench_function("RIFF", |b| b.iter(read_riff_file));
test_read_file!(
c,
[
(AIFF, "../tests/files/assets/full_test.aiff"),
(APE, "../tests/files/assets/full_test.ape"),
(FLAC, "../tests/files/assets/full_test.flac"),
(MP4, "../tests/files/assets/m4a_codec_aac.m4a"),
(MP3, "../tests/files/assets/full_test.mp3"),
(VORBIS, "../tests/files/assets/full_test.ogg"),
(OPUS, "../tests/files/assets/full_test.opus"),
(RIFF, "../tests/files/assets/wav_format_pcm.wav")
]
);
}
criterion_group!(benches, path_infer_read, content_infer_read);

View file

@ -100,7 +100,7 @@ mod tests {
file_name: Some(String::from("a.mp3")),
descriptor: Some(String::from("Test Asset")),
},
data: crate::tag_utils::test_utils::read_path("tests/files/assets/a.mp3"),
data: crate::tag_utils::test_utils::read_path("tests/files/assets/full_test.mp3"),
};
let cont = crate::tag_utils::test_utils::read_path("tests/tags/assets/id3v2/test.geob");
@ -119,7 +119,7 @@ mod tests {
file_name: Some(String::from("a.mp3")),
descriptor: Some(String::from("Test Asset")),
},
data: crate::tag_utils::test_utils::read_path("tests/files/assets/a.mp3"),
data: crate::tag_utils::test_utils::read_path("tests/files/assets/full_test.mp3"),
};
let encoded = to_encode.as_bytes();

View file

@ -34,11 +34,11 @@
//! // First, create a probe.
//! // This will guess the format from the extension
//! // ("mp3" in this case), but we can guess from the content if we want to.
//! let tagged_file = read_from_path("tests/files/assets/a.mp3", false)?;
//! let tagged_file = read_from_path("tests/files/assets/full_test.mp3", false)?;
//!
//! // Let's guess the format from the content just in case.
//! // This is not necessary in this case!
//! let tagged_file2 = Probe::open("tests/files/assets/a.mp3")?
//! let tagged_file2 = Probe::open("tests/files/assets/full_test.mp3")?
//! .guess_file_type()?
//! .read(false)?;
//! # Ok(())
@ -54,7 +54,7 @@
//! use std::fs::File;
//!
//! // Let's read from an open file
//! let mut file = File::open("tests/files/assets/a.mp3")?;
//! let mut file = File::open("tests/files/assets/full_test.mp3")?;
//!
//! // Here, we have to guess the file type prior to reading
//! let tagged_file = read_from(&mut file, false)?;
@ -69,7 +69,7 @@
//! # fn main() -> Result<(), LoftyError> {
//! use lofty::read_from_path;
//!
//! let tagged_file = read_from_path("tests/files/assets/a.mp3", false)?;
//! let tagged_file = read_from_path("tests/files/assets/full_test.mp3", false)?;
//!
//! // Get the primary tag (ID3v2 in this case)
//! let id3v2 = tagged_file.primary_tag().unwrap();
@ -90,7 +90,7 @@
//! use lofty::{AudioFile, TagType};
//! use std::fs::File;
//!
//! let mut file_content = File::open("tests/files/assets/a.mp3")?;
//! let mut file_content = File::open("tests/files/assets/full_test.mp3")?;
//!
//! // We are expecting an MP3 file
//! let mpeg_file = Mp3File::read_from(&mut file_content, true)?;

View file

@ -5,3 +5,5 @@ pub const VORBIS_SETUP_HEAD: &[u8] = &[5, 118, 111, 114, 98, 105, 115];
pub const OPUSTAGS: &[u8] = &[79, 112, 117, 115, 84, 97, 103, 115];
pub const OPUSHEAD: &[u8] = &[79, 112, 117, 115, 72, 101, 97, 100];
pub const SPEEXHEADER: &[u8] = &[83, 112, 101, 101, 120, 32, 32, 32];

View file

@ -7,6 +7,7 @@ pub(crate) mod constants;
pub(crate) mod flac;
pub(crate) mod opus;
pub(crate) mod read;
pub(crate) mod speex;
#[cfg(feature = "vorbis_comments")]
pub(crate) mod tag;
pub(crate) mod vorbis;
@ -14,14 +15,20 @@ pub(crate) mod vorbis;
pub(crate) mod write;
use crate::error::{FileDecodingError, Result};
use crate::types::file::FileType;
// Exports
#[cfg(feature = "vorbis_comments")]
pub use crate::ogg::tag::VorbisComments;
pub use crate::ogg::flac::FlacFile;
pub use crate::ogg::opus::properties::OpusProperties;
pub use crate::ogg::opus::OpusFile;
#[cfg(feature = "vorbis_comments")]
pub use crate::ogg::tag::VorbisComments;
pub use crate::ogg::speex::properties::SpeexProperties;
pub use crate::ogg::speex::SpeexFile;
pub use crate::ogg::vorbis::properties::VorbisProperties;
pub use crate::ogg::vorbis::VorbisFile;
use crate::types::file::FileType;
use std::io::{Read, Seek};

View file

@ -1,6 +1,5 @@
use super::find_last_page;
use crate::error::{FileDecodingError, Result};
use crate::types::file::FileType;
use crate::error::Result;
use crate::types::properties::FileProperties;
use std::io::{Read, Seek, SeekFrom};
@ -96,15 +95,19 @@ where
(end - first_page.start, end)
};
let mut properties = OpusProperties::default();
let first_page_abgp = first_page.abgp;
// Skip identification header
let first_page_content = &mut &first_page.content()[8..];
let version = first_page_content.read_u8()?;
let channels = first_page_content.read_u8()?;
properties.version = first_page_content.read_u8()?;
properties.channels = first_page_content.read_u8()?;
let pre_skip = first_page_content.read_u16::<LittleEndian>()?;
let input_sample_rate = first_page_content.read_u32::<LittleEndian>()?;
properties.input_sample_rate = first_page_content.read_u32::<LittleEndian>()?;
// Subtract the identification and metadata packet length from the total
let audio_size = stream_len - data.seek(SeekFrom::Current(0))?;
@ -112,30 +115,13 @@ where
let last_page = find_last_page(data)?;
let last_page_abgp = last_page.abgp;
last_page_abgp
.checked_sub(first_page_abgp + u64::from(pre_skip))
.map_or_else(
|| {
Err(
FileDecodingError::new(FileType::Opus, "File contains incorrect PCM values")
.into(),
)
},
|frame_count| {
let length = frame_count * 1000 / 48000;
let duration = Duration::from_millis(length as u64);
if let Some(frame_count) = last_page_abgp.checked_sub(first_page_abgp + u64::from(pre_skip)) {
let length = frame_count * 1000 / 48000;
properties.duration = Duration::from_millis(length as u64);
let overall_bitrate = ((file_length * 8) / length) as u32;
let audio_bitrate = (audio_size * 8 / length) as u32;
properties.overall_bitrate = ((file_length * 8) / length) as u32;
properties.audio_bitrate = (audio_size * 8 / length) as u32;
}
Ok(OpusProperties {
duration,
overall_bitrate,
audio_bitrate,
channels,
version,
input_sample_rate,
})
},
)
Ok(properties)
}

82
src/ogg/speex/mod.rs Normal file
View file

@ -0,0 +1,82 @@
pub(super) mod properties;
#[cfg(feature = "vorbis_comments")]
pub(in crate::ogg) mod write;
#[cfg(feature = "vorbis_comments")]
use super::tag::VorbisComments;
use crate::error::Result;
use crate::ogg::constants::SPEEXHEADER;
use crate::types::file::{AudioFile, FileType, TaggedFile};
use crate::types::properties::FileProperties;
use crate::types::tag::TagType;
use properties::SpeexProperties;
use std::io::{Read, Seek};
/// An OGG Speex file
pub struct SpeexFile {
#[cfg(feature = "vorbis_comments")]
/// The vorbis comments contained in the file
///
/// NOTE: While a metadata packet is required, it isn't required to actually have any data.
pub(crate) vorbis_comments: VorbisComments,
/// The file's audio properties
pub(crate) properties: SpeexProperties,
}
impl From<SpeexFile> for TaggedFile {
fn from(input: SpeexFile) -> Self {
Self {
ty: FileType::Speex,
properties: FileProperties::from(input.properties),
#[cfg(feature = "vorbis_comments")]
tags: vec![input.vorbis_comments.into()],
#[cfg(not(feature = "vorbis_comments"))]
tags: Vec::new(),
}
}
}
impl AudioFile for SpeexFile {
type Properties = SpeexProperties;
fn read_from<R>(reader: &mut R, read_properties: bool) -> Result<Self>
where
R: Read + Seek,
{
let file_information = super::read::read_from(reader, SPEEXHEADER, &[])?;
Ok(Self {
properties: if read_properties { properties::read_properties(reader, &file_information.1)? } else { SpeexProperties::default() },
#[cfg(feature = "vorbis_comments")]
// Safe to unwrap, a metadata packet is mandatory in Speex
vorbis_comments: file_information.0.unwrap(),
})
}
fn properties(&self) -> &Self::Properties {
&self.properties
}
fn contains_tag(&self) -> bool {
true
}
fn contains_tag_type(&self, tag_type: &TagType) -> bool {
tag_type == &TagType::VorbisComments
}
}
impl SpeexFile {
#[cfg(feature = "vorbis_comments")]
/// Returns a reference to the Vorbis comments tag
pub fn vorbis_comments(&self) -> &VorbisComments {
&self.vorbis_comments
}
#[cfg(feature = "vorbis_comments")]
/// Returns a mutable reference to the Vorbis comments tag
pub fn vorbis_comments_mut(&mut self) -> &mut VorbisComments {
&mut self.vorbis_comments
}
}

175
src/ogg/speex/properties.rs Normal file
View file

@ -0,0 +1,175 @@
use crate::error::{FileDecodingError, Result};
use crate::ogg::find_last_page;
use crate::types::file::FileType;
use crate::types::properties::FileProperties;
use std::io::{Read, Seek, SeekFrom};
use std::time::Duration;
use byteorder::{LittleEndian, ReadBytesExt};
use ogg_pager::Page;
#[derive(Debug, Copy, Clone, PartialEq, Default)]
/// A Speex file's audio properties
pub struct SpeexProperties {
duration: Duration,
version: u32,
sample_rate: u32,
mode: u32,
channels: u8,
vbr: bool,
overall_bitrate: u32,
audio_bitrate: u32,
nominal_bitrate: u32,
}
impl From<SpeexProperties> for FileProperties {
fn from(input: SpeexProperties) -> Self {
Self {
duration: input.duration,
overall_bitrate: Some(input.overall_bitrate),
audio_bitrate: Some(input.audio_bitrate),
sample_rate: Some(input.sample_rate),
bit_depth: None,
channels: Some(input.channels),
}
}
}
impl SpeexProperties {
/// Create a new [`SpeexProperties`]
pub const fn new(
duration: Duration,
version: u32,
sample_rate: u32,
mode: u32,
channels: u8,
vbr: bool,
overall_bitrate: u32,
audio_bitrate: u32,
nominal_bitrate: u32,
) -> Self {
Self {
duration,
version,
sample_rate,
mode,
channels,
vbr,
overall_bitrate,
audio_bitrate,
nominal_bitrate,
}
}
/// Duration
pub fn duration(&self) -> Duration {
self.duration
}
/// Speex version
pub fn version(&self) -> u32 {
self.version
}
/// Sample rate
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
/// Speex encoding mode
pub fn mode(&self) -> u32 {
self.mode
}
/// Channel count
pub fn channels(&self) -> u8 {
self.channels
}
/// Whether the file makes use of variable bitrate
pub fn vbr(&self) -> bool {
self.vbr
}
/// Overall bitrate (kbps)
pub fn overall_bitrate(&self) -> u32 {
self.overall_bitrate
}
/// Audio bitrate (kbps)
pub fn audio_bitrate(&self) -> u32 {
self.audio_bitrate
}
/// Audio bitrate (kbps)
pub fn nominal_bitrate(&self) -> u32 {
self.nominal_bitrate
}
}
pub(in crate::ogg) fn read_properties<R>(data: &mut R, first_page: &Page) -> Result<SpeexProperties>
where
R: Read + Seek,
{
let first_page_abgp = first_page.abgp;
if first_page.content().len() < 80 {
return Err(FileDecodingError::new(FileType::Speex, "Header packet too small").into());
}
let mut properties = SpeexProperties::default();
// The content we need comes 28 bytes into the packet
//
// Skipping:
// Speex string ("Speex ", 8)
// Speex version (20)
let first_page_content = &mut &first_page.content()[28..];
properties.version = first_page_content.read_u32::<LittleEndian>()?;
// Total size of the speex header
let _header_size = first_page_content.read_u32::<LittleEndian>()?;
properties.sample_rate = first_page_content.read_u32::<LittleEndian>()?;
properties.mode = first_page_content.read_u32::<LittleEndian>()?;
// Version ID of the bitstream
let _mode_bitstream_version = first_page_content.read_u32::<LittleEndian>()?;
let channels = first_page_content.read_u32::<LittleEndian>()?;
if channels != 1 && channels != 2 {
return Err(FileDecodingError::new(
FileType::Speex,
"Found invalid channel count, must be mono or stereo",
)
.into());
}
properties.channels = channels as u8;
properties.nominal_bitrate = first_page_content.read_u32::<LittleEndian>()?;
// The size of the frames in samples
let _frame_size = first_page_content.read_u32::<LittleEndian>()?;
properties.vbr = first_page_content.read_u32::<LittleEndian>()? == 1;
let last_page = find_last_page(data)?;
let last_page_abgp = last_page.abgp;
let file_length = data.seek(SeekFrom::End(0))?;
if let Some(frame_count) = last_page_abgp.checked_sub(first_page_abgp) {
if properties.sample_rate > 0 {
let length = frame_count * 1000 / u64::from(properties.sample_rate);
properties.duration = Duration::from_millis(length as u64);
properties.overall_bitrate = ((file_length * 8) / length) as u32;
properties.audio_bitrate = properties.nominal_bitrate / 1000;
}
}
Ok(properties)
}

46
src/ogg/speex/write.rs Normal file
View file

@ -0,0 +1,46 @@
use crate::error::{FileEncodingError, Result};
use crate::types::file::FileType;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom, Write};
use ogg_pager::Page;
pub(crate) fn write_to(
data: &mut File,
writer: &mut Vec<u8>,
ser: u32,
pages: &mut [Page],
) -> Result<()> {
let reached_md_end: bool;
loop {
let p = Page::read(data, true)?;
if p.header_type() & 0x01 != 0x01 {
data.seek(SeekFrom::Start(p.start as u64))?;
reached_md_end = true;
break;
}
}
if !reached_md_end {
return Err(
FileEncodingError::new(FileType::Speex, "File ends with comment header").into(),
);
}
let mut remaining = Vec::new();
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,5 +1,5 @@
use crate::error::{ErrorKind, LoftyError, Result};
use crate::ogg::constants::{OPUSHEAD, VORBIS_IDENT_HEAD};
use crate::ogg::write::OGGFormat;
use crate::probe::Probe;
use crate::types::file::FileType;
use crate::types::item::{ItemKey, ItemValue, TagItem};
@ -172,11 +172,6 @@ impl From<VorbisComments> for Tag {
fn from(input: VorbisComments) -> Self {
let mut tag = Tag::new(TagType::VorbisComments);
tag.items.push(TagItem::new(
ItemKey::EncoderSoftware,
ItemValue::Text(input.vendor),
));
for (k, v) in input.items {
tag.items.push(TagItem::new(
ItemKey::from_key(TagType::VorbisComments, &k),
@ -184,6 +179,18 @@ impl From<VorbisComments> for Tag {
));
}
// We need to preserve the vendor string
if !tag
.items
.iter()
.any(|i| i.key() == &ItemKey::EncoderSoftware)
{
tag.items.push(TagItem::new(
ItemKey::EncoderSoftware,
ItemValue::Text(input.vendor),
));
}
for (pic, _info) in input.pictures {
tag.push_picture(pic)
}
@ -247,8 +254,9 @@ impl<'a> VorbisCommentsRef<'a> {
match f_ty {
Some(FileType::FLAC) => super::flac::write::write_to(file, self),
Some(FileType::Opus) => super::write::write(file, self, OPUSHEAD),
Some(FileType::Vorbis) => super::write::write(file, self, VORBIS_IDENT_HEAD),
Some(FileType::Opus) => super::write::write(file, self, OGGFormat::Opus),
Some(FileType::Vorbis) => super::write::write(file, self, OGGFormat::Vorbis),
Some(FileType::Speex) => super::write::write(file, self, OGGFormat::Speex),
_ => Err(LoftyError::new(ErrorKind::UnsupportedTag)),
}
}

View file

@ -1,6 +1,5 @@
use super::find_last_page;
use crate::error::{FileDecodingError, Result};
use crate::types::file::FileType;
use crate::error::Result;
use crate::types::properties::FileProperties;
use std::io::{Read, Seek, SeekFrom};
@ -117,48 +116,34 @@ where
{
let first_page_abgp = first_page.abgp;
let mut properties = VorbisProperties::default();
// Skip identification header
let first_page_content = &mut &first_page.content()[7..];
let version = first_page_content.read_u32::<LittleEndian>()?;
properties.version = first_page_content.read_u32::<LittleEndian>()?;
let channels = first_page_content.read_u8()?;
let sample_rate = first_page_content.read_u32::<LittleEndian>()?;
properties.channels = first_page_content.read_u8()?;
properties.sample_rate = first_page_content.read_u32::<LittleEndian>()?;
let bitrate_maximum = first_page_content.read_i32::<LittleEndian>()?;
let bitrate_nominal = first_page_content.read_i32::<LittleEndian>()?;
let bitrate_minimum = first_page_content.read_i32::<LittleEndian>()?;
properties.bitrate_maximum = first_page_content.read_i32::<LittleEndian>()?;
properties.bitrate_nominal = first_page_content.read_i32::<LittleEndian>()?;
properties.bitrate_minimum = first_page_content.read_i32::<LittleEndian>()?;
let last_page = find_last_page(data)?;
let last_page_abgp = last_page.abgp;
let file_length = data.seek(SeekFrom::End(0))?;
last_page_abgp.checked_sub(first_page_abgp).map_or_else(
|| {
Err(
FileDecodingError::new(FileType::Vorbis, "File contains incorrect PCM values")
.into(),
)
},
|frame_count| {
let length = frame_count * 1000 / u64::from(sample_rate);
let duration = Duration::from_millis(length as u64);
if let Some(frame_count) = last_page_abgp.checked_sub(first_page_abgp) {
if properties.sample_rate > 0 {
let length = frame_count * 1000 / u64::from(properties.sample_rate);
properties.duration = Duration::from_millis(length as u64);
let overall_bitrate = ((file_length * 8) / length) as u32;
let audio_bitrate = bitrate_nominal as u64 / 1000;
properties.overall_bitrate = ((file_length * 8) / length) as u32;
properties.audio_bitrate = (properties.bitrate_nominal as u64 / 1000) as u32;
}
}
Ok(VorbisProperties {
duration,
overall_bitrate,
audio_bitrate: audio_bitrate as u32,
sample_rate,
channels,
version,
bitrate_maximum,
bitrate_nominal,
bitrate_minimum,
})
},
)
Ok(properties)
}

View file

@ -12,10 +12,28 @@ use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use ogg_pager::Page;
pub(in crate) fn write_to(data: &mut File, tag: &Tag, sig: &[u8]) -> Result<()> {
#[derive(PartialEq, Copy, Clone)]
pub(crate) enum OGGFormat {
Opus,
Vorbis,
Speex,
}
impl OGGFormat {
#[allow(clippy::trivially_copy_pass_by_ref)]
pub(crate) fn comment_signature(&self) -> Option<&[u8]> {
match self {
OGGFormat::Opus => Some(OPUSTAGS),
OGGFormat::Vorbis => Some(VORBIS_COMMENT_HEAD),
OGGFormat::Speex => None,
}
}
}
pub(in crate) fn write_to(data: &mut File, tag: &Tag, format: OGGFormat) -> Result<()> {
match tag.tag_type() {
#[cfg(feature = "vorbis_comments")]
TagType::VorbisComments => write(data, &mut Into::<VorbisCommentsRef>::into(tag), sig),
TagType::VorbisComments => write(data, &mut Into::<VorbisCommentsRef>::into(tag), format),
_ => Err(LoftyError::new(ErrorKind::UnsupportedTag)),
}
}
@ -85,7 +103,7 @@ pub(super) fn create_pages(
}
#[cfg(feature = "vorbis_comments")]
pub(super) fn write(data: &mut File, tag: &mut VorbisCommentsRef, sig: &[u8]) -> Result<()> {
pub(super) fn write(data: &mut File, tag: &mut VorbisCommentsRef, format: OGGFormat) -> Result<()> {
let first_page = Page::read(data, false)?;
let ser = first_page.serial;
@ -94,10 +112,18 @@ pub(super) fn write(data: &mut File, tag: &mut VorbisCommentsRef, sig: &[u8]) ->
writer.write_all(&*first_page.as_bytes()?)?;
let first_md_page = Page::read(data, false)?;
verify_signature(&first_md_page, sig)?;
let comment_signature = format.comment_signature();
let verify_sig = comment_signature.is_some();
let comment_signature = format.comment_signature().unwrap_or(&[]);
if verify_sig {
verify_signature(&first_md_page, comment_signature)?;
}
// Retain the file's vendor string
let md_reader = &mut &first_md_page.content()[sig.len()..];
let md_reader = &mut &first_md_page.content()[comment_signature.len()..];
let vendor_len = md_reader.read_u32::<LittleEndian>()?;
let mut vendor = vec![0; vendor_len as usize];
@ -105,14 +131,14 @@ pub(super) fn write(data: &mut File, tag: &mut VorbisCommentsRef, sig: &[u8]) ->
let mut packet = Cursor::new(Vec::new());
packet.write_all(sig)?;
packet.write_all(comment_signature)?;
packet.write_u32::<LittleEndian>(vendor_len)?;
packet.write_all(&vendor)?;
let mut pages = create_pages(tag, &mut packet)?;
match sig {
VORBIS_COMMENT_HEAD => {
match format {
OGGFormat::Vorbis => {
super::vorbis::write::write_to(
data,
&mut writer,
@ -121,10 +147,12 @@ pub(super) fn write(data: &mut File, tag: &mut VorbisCommentsRef, sig: &[u8]) ->
&mut pages,
)?;
},
OPUSTAGS => {
OGGFormat::Opus => {
super::opus::write::write_to(data, &mut writer, ser, &mut pages)?;
},
_ => unreachable!(),
OGGFormat::Speex => {
super::speex::write::write_to(data, &mut writer, ser, &mut pages)?;
},
}
data.seek(SeekFrom::Start(0))?;

View file

@ -7,6 +7,7 @@ use crate::mp3::Mp3File;
use crate::mp4::Mp4File;
use crate::ogg::flac::FlacFile;
use crate::ogg::opus::OpusFile;
use crate::ogg::speex::SpeexFile;
use crate::ogg::vorbis::VorbisFile;
use crate::types::file::{AudioFile, FileType, TaggedFile};
@ -29,7 +30,7 @@ use std::path::Path;
/// # fn main() -> Result<(), LoftyError> {
/// use lofty::FileType;
///
/// let probe = Probe::open("tests/files/assets/a.mp3")?;
/// let probe = Probe::open("tests/files/assets/full_test.mp3")?;
///
/// // Inferred from the `mp3` extension
/// assert_eq!(probe.file_type(), Some(FileType::MP3));
@ -45,7 +46,7 @@ use std::path::Path;
/// use lofty::FileType;
///
/// // Our same path probe with a guessed file type
/// let probe = Probe::open("tests/files/assets/a.mp3")?.guess_file_type()?;
/// let probe = Probe::open("tests/files/assets/full_test.mp3")?.guess_file_type()?;
///
/// // Inferred from the file's content
/// assert_eq!(probe.file_type(), Some(FileType::MP3));
@ -225,6 +226,7 @@ impl<R: Read + Seek> Probe<R> {
FileType::Vorbis => VorbisFile::read_from(reader, read_properties)?.into(),
FileType::WAV => WavFile::read_from(reader, read_properties)?.into(),
FileType::MP4 => Mp4File::read_from(reader, read_properties)?.into(),
FileType::Speex => SpeexFile::read_from(reader, read_properties)?.into(),
}),
None => Err(LoftyError::new(ErrorKind::UnknownFormat)),
}

View file

@ -37,9 +37,11 @@ pub(crate) fn write_tag(tag: &Tag, file: &mut File, file_type: FileType) -> Resu
#[cfg(feature = "mp4_ilst")]
FileType::MP4 => mp4::ilst::write::write_to(file, &mut Into::<IlstRef>::into(tag)),
#[cfg(feature = "vorbis_comments")]
FileType::Opus => ogg::write::write_to(file, tag, ogg::constants::OPUSTAGS),
FileType::Opus => ogg::write::write_to(file, tag, ogg::write::OGGFormat::Opus),
#[cfg(feature = "vorbis_comments")]
FileType::Vorbis => ogg::write::write_to(file, tag, ogg::constants::VORBIS_COMMENT_HEAD),
FileType::Speex => ogg::write::write_to(file, tag, ogg::write::OGGFormat::Speex),
#[cfg(feature = "vorbis_comments")]
FileType::Vorbis => ogg::write::write_to(file, tag, ogg::write::OGGFormat::Vorbis),
FileType::WAV => iff::wav::write::write_to(file, tag),
_ => Err(LoftyError::new(ErrorKind::UnsupportedTag)),
}

View file

@ -190,6 +190,7 @@ pub enum FileType {
MP4,
Opus,
Vorbis,
Speex,
WAV,
}
@ -217,7 +218,9 @@ impl FileType {
#[cfg(all(not(feature = "ape"), feature = "id3v1"))]
FileType::MP3 => TagType::Id3v1,
FileType::APE => TagType::Ape,
FileType::FLAC | FileType::Opus | FileType::Vorbis => TagType::VorbisComments,
FileType::FLAC | FileType::Opus | FileType::Vorbis | FileType::Speex => {
TagType::VorbisComments
},
FileType::MP4 => TagType::Mp4Ilst,
}
}
@ -238,7 +241,9 @@ impl FileType {
#[cfg(feature = "ape")]
FileType::APE | FileType::MP3 if tag_type == &TagType::Ape => true,
#[cfg(feature = "vorbis_comments")]
FileType::Opus | FileType::FLAC | FileType::Vorbis => tag_type == &TagType::VorbisComments,
FileType::Opus | FileType::FLAC | FileType::Vorbis | FileType::Speex => {
tag_type == &TagType::VorbisComments
},
#[cfg(feature = "mp4_ilst")]
FileType::MP4 => tag_type == &TagType::Mp4Ilst,
#[cfg(feature = "riff_info_list")]
@ -263,6 +268,7 @@ impl FileType {
"flac" => Some(Self::FLAC),
"ogg" => Some(Self::Vorbis),
"mp4" | "m4a" | "m4b" | "m4p" | "m4r" | "m4v" | "3gp" => Some(Self::MP4),
"spx" => Some(Self::Speex),
_ => None,
}
}
@ -363,6 +369,8 @@ impl FileType {
return Some(Self::Vorbis);
} else if &buf[28..36] == b"OpusHead" {
return Some(Self::Opus);
} else if &buf[28..] == b"Speex " {
return Some(Self::Speex);
}
None

View file

@ -5,7 +5,7 @@ use std::io::{Seek, SeekFrom, Write};
#[test]
fn read() {
// Here we have an AIFF file with both an ID3v2 chunk and text chunks
let file = lofty::read_from_path("tests/files/assets/a.aiff", false).unwrap();
let file = lofty::read_from_path("tests/files/assets/full_test.aiff", false).unwrap();
assert_eq!(file.file_type(), &FileType::AIFF);
@ -18,7 +18,7 @@ fn read() {
#[test]
fn write() {
let mut file = temp_file!("tests/files/assets/a.aiff");
let mut file = temp_file!("tests/files/assets/full_test.aiff");
let mut tagged_file = lofty::read_from(&mut file, false).unwrap();
@ -41,10 +41,10 @@ fn write() {
#[test]
fn remove_text_chunks() {
crate::remove_tag!("tests/files/assets/a.aiff", TagType::AiffText);
crate::remove_tag!("tests/files/assets/full_test.aiff", TagType::AiffText);
}
#[test]
fn remove_id3v2() {
crate::remove_tag!("tests/files/assets/a.aiff", TagType::Id3v2);
crate::remove_tag!("tests/files/assets/full_test.aiff", TagType::Id3v2);
}

View file

@ -5,7 +5,7 @@ use std::io::{Seek, SeekFrom, Write};
#[test]
fn read() {
// Here we have an APE file with an ID3v2, ID3v1, and an APEv2 tag
let file = lofty::read_from_path("tests/files/assets/a.ape", false).unwrap();
let file = lofty::read_from_path("tests/files/assets/full_test.ape", false).unwrap();
assert_eq!(file.file_type(), &FileType::APE);
@ -22,7 +22,7 @@ fn read() {
#[test]
fn write() {
// We don't write an ID3v2 tag here since it's against the spec
let mut file = temp_file!("tests/files/assets/a.ape");
let mut file = temp_file!("tests/files/assets/full_test.ape");
let mut tagged_file = lofty::read_from(&mut file, false).unwrap();
@ -45,10 +45,10 @@ fn write() {
#[test]
fn remove_ape() {
crate::remove_tag!("tests/files/assets/a.ape", TagType::Ape);
crate::remove_tag!("tests/files/assets/full_test.ape", TagType::Ape);
}
#[test]
fn remove_id3v1() {
crate::remove_tag!("tests/files/assets/a.ape", TagType::Id3v1);
crate::remove_tag!("tests/files/assets/full_test.ape", TagType::Id3v1);
}

Binary file not shown.

View file

@ -5,7 +5,7 @@ use std::io::{Seek, SeekFrom, Write};
#[test]
fn read() {
// Here we have an MP3 file with an ID3v2, ID3v1, and an APEv2 tag
let file = lofty::read_from_path("tests/files/assets/a.mp3", false).unwrap();
let file = lofty::read_from_path("tests/files/assets/full_test.mp3", false).unwrap();
assert_eq!(file.file_type(), &FileType::MP3);
@ -45,7 +45,7 @@ fn read_with_junk_bytes_between_frames() {
#[test]
fn write() {
let mut file = temp_file!("tests/files/assets/a.mp3");
let mut file = temp_file!("tests/files/assets/full_test.mp3");
let mut tagged_file = lofty::read_from(&mut file, false).unwrap();
@ -73,15 +73,15 @@ fn write() {
#[test]
fn remove_id3v2() {
crate::remove_tag!("tests/files/assets/a.mp3", TagType::Id3v2);
crate::remove_tag!("tests/files/assets/full_test.mp3", TagType::Id3v2);
}
#[test]
fn remove_id3v1() {
crate::remove_tag!("tests/files/assets/a.mp3", TagType::Id3v1);
crate::remove_tag!("tests/files/assets/full_test.mp3", TagType::Id3v1);
}
#[test]
fn remove_ape() {
crate::remove_tag!("tests/files/assets/a.mp3", TagType::Ape);
crate::remove_tag!("tests/files/assets/full_test.mp3", TagType::Ape);
}

View file

@ -7,48 +7,63 @@ use std::io::{Seek, SeekFrom, Write};
#[test]
fn opus_read() {
read("tests/files/assets/a.opus", &FileType::Opus)
read("tests/files/assets/full_test.opus", &FileType::Opus)
}
#[test]
fn opus_write() {
write("tests/files/assets/a.opus", &FileType::Opus)
write("tests/files/assets/full_test.opus", &FileType::Opus)
}
#[test]
fn opus_remove() {
remove("tests/files/assets/a.opus", TagType::VorbisComments)
remove("tests/files/assets/full_test.opus", TagType::VorbisComments)
}
#[test]
fn flac_read() {
// FLAC does **not** require a Vorbis comment block be present, this file has one
read("tests/files/assets/a.flac", &FileType::FLAC)
read("tests/files/assets/full_test.flac", &FileType::FLAC)
}
#[test]
fn flac_write() {
write("tests/files/assets/a.flac", &FileType::FLAC)
write("tests/files/assets/full_test.flac", &FileType::FLAC)
}
#[test]
fn flac_remove() {
crate::remove_tag!("tests/files/assets/a.flac", TagType::VorbisComments);
crate::remove_tag!("tests/files/assets/full_test.flac", TagType::VorbisComments);
}
#[test]
fn vorbis_read() {
read("tests/files/assets/a.ogg", &FileType::Vorbis)
read("tests/files/assets/full_test.ogg", &FileType::Vorbis)
}
#[test]
fn vorbis_write() {
write("tests/files/assets/a.ogg", &FileType::Vorbis)
write("tests/files/assets/full_test.ogg", &FileType::Vorbis)
}
#[test]
fn vorbis_remove() {
remove("tests/files/assets/a.ogg", TagType::VorbisComments)
remove("tests/files/assets/full_test.ogg", TagType::VorbisComments)
}
#[test]
fn speex_read() {
read("tests/files/assets/full_test.spx", &FileType::Speex)
}
#[test]
fn speex_write() {
write("tests/files/assets/full_test.spx", &FileType::Speex)
}
#[test]
fn speex_remove() {
remove("tests/files/assets/full_test.spx", TagType::VorbisComments)
}
fn read(path: &str, file_type: &FileType) {

View file

@ -69,7 +69,7 @@ macro_rules! set_artist {
$file_write.seek(std::io::SeekFrom::Start(0)).unwrap();
assert!($tag.save_to(&mut $file_write).is_ok());
$tag.save_to(&mut $file_write).unwrap();
};
}

View file

@ -5,7 +5,7 @@ use std::io::{Seek, SeekFrom, Write};
#[test]
fn read() {
// Here we have a WAV file with both an ID3v2 chunk and a RIFF INFO chunk
let file = lofty::read_from_path("tests/files/assets/a.wav", false).unwrap();
let file = lofty::read_from_path("tests/files/assets/wav_format_pcm.wav", false).unwrap();
assert_eq!(file.file_type(), &FileType::WAV);
@ -18,7 +18,7 @@ fn read() {
#[test]
fn write() {
let mut file = temp_file!("tests/files/assets/a.wav");
let mut file = temp_file!("tests/files/assets/wav_format_pcm.wav");
let mut tagged_file = lofty::read_from(&mut file, false).unwrap();
@ -41,10 +41,10 @@ fn write() {
#[test]
fn remove_id3v2() {
crate::remove_tag!("tests/files/assets/a.wav", TagType::Id3v2);
crate::remove_tag!("tests/files/assets/wav_format_pcm.wav", TagType::Id3v2);
}
#[test]
fn remove_riff_info() {
crate::remove_tag!("tests/files/assets/a.wav", TagType::RiffInfo);
crate::remove_tag!("tests/files/assets/wav_format_pcm.wav", TagType::RiffInfo);
}

View file

@ -2,7 +2,9 @@ use lofty::ape::{ApeFile, ApeProperties};
use lofty::iff::{AiffFile, WavFile, WavFormat, WavProperties};
use lofty::mp3::{ChannelMode, Layer, Mp3File, Mp3Properties, MpegVersion};
use lofty::mp4::{Mp4Codec, Mp4File, Mp4Properties};
use lofty::ogg::{FlacFile, OpusFile, OpusProperties, VorbisFile, VorbisProperties};
use lofty::ogg::{
FlacFile, OpusFile, OpusProperties, SpeexFile, SpeexProperties, VorbisFile, VorbisProperties,
};
use lofty::{AudioFile, FileProperties};
use std::fs::File;
@ -63,6 +65,18 @@ const MP4_ALAC_PROPERTIES: Mp4Properties = Mp4Properties::new(
const OPUS_PROPERTIES: OpusProperties =
OpusProperties::new(Duration::from_millis(1428), 120, 120, 2, 1, 48000);
const SPEEX_PROPERTIES: SpeexProperties = SpeexProperties::new(
Duration::from_millis(1469),
1,
32000,
2,
2,
false,
32,
29,
29600,
);
const VORBIS_PROPERTIES: VorbisProperties = VorbisProperties::new(
Duration::from_millis(1450),
96,
@ -100,7 +114,7 @@ where
#[test]
fn aiff_properties() {
assert_eq!(
get_properties::<AiffFile>("tests/files/assets/a.aiff"),
get_properties::<AiffFile>("tests/files/assets/full_test.aiff"),
AIFF_PROPERTIES
);
}
@ -108,7 +122,7 @@ fn aiff_properties() {
#[test]
fn ape_properties() {
assert_eq!(
get_properties::<ApeFile>("tests/files/assets/a.ape"),
get_properties::<ApeFile>("tests/files/assets/full_test.ape"),
APE_PROPERTIES
);
}
@ -116,7 +130,7 @@ fn ape_properties() {
#[test]
fn flac_properties() {
assert_eq!(
get_properties::<FlacFile>("tests/files/assets/a.flac"),
get_properties::<FlacFile>("tests/files/assets/full_test.flac"),
FLAC_PROPERTIES
)
}
@ -124,7 +138,7 @@ fn flac_properties() {
#[test]
fn mp3_properties() {
assert_eq!(
get_properties::<Mp3File>("tests/files/assets/a.mp3"),
get_properties::<Mp3File>("tests/files/assets/full_test.mp3"),
MP3_PROPERTIES
)
}
@ -148,15 +162,23 @@ fn mp4_alac_properties() {
#[test]
fn opus_properties() {
assert_eq!(
get_properties::<OpusFile>("tests/files/assets/a.opus"),
get_properties::<OpusFile>("tests/files/assets/full_test.opus"),
OPUS_PROPERTIES
)
}
#[test]
fn speex_properties() {
assert_eq!(
get_properties::<SpeexFile>("tests/files/assets/full_test.spx"),
SPEEX_PROPERTIES
)
}
#[test]
fn vorbis_properties() {
assert_eq!(
get_properties::<VorbisFile>("tests/files/assets/a.ogg"),
get_properties::<VorbisFile>("tests/files/assets/full_test.ogg"),
VORBIS_PROPERTIES
)
}
@ -164,7 +186,7 @@ fn vorbis_properties() {
#[test]
fn wav_properties() {
assert_eq!(
get_properties::<WavFile>("tests/files/assets/a.wav"),
get_properties::<WavFile>("tests/files/assets/wav_format_pcm.wav"),
WAV_PROPERTIES
)
}