mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2025-03-04 14:57:17 +00:00
Add Speex
support
This commit is contained in:
parent
64f0eff19d
commit
c1725de93d
30 changed files with 548 additions and 185 deletions
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
10
src/lib.rs
10
src/lib.rs
|
@ -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)?;
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
|
|
@ -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
82
src/ogg/speex/mod.rs
Normal 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
175
src/ogg/speex/properties.rs
Normal 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
46
src/ogg/speex/write.rs
Normal 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(())
|
||||
}
|
|
@ -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)),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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))?;
|
||||
|
|
|
@ -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)),
|
||||
}
|
||||
|
|
|
@ -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)),
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
BIN
tests/files/assets/full_test.spx
Normal file
BIN
tests/files/assets/full_test.spx
Normal file
Binary file not shown.
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue