Support AIFF

This commit is contained in:
Serial 2021-04-22 12:57:20 -04:00
parent fbfe0e916b
commit dc23ec1ffd
12 changed files with 103 additions and 48 deletions

View file

@ -17,6 +17,7 @@ in order to parse metadata in different file formats.
| File Format | Extensions | Read | Write | Metadata Format(s) |
|-------------|-------------------------------------------|------|-------|----------------------|
| Ape | `ape` |**X** |**X** | `APEv2` |
| AIFF | `aiff` |**X** |**X** | `ID3v2` |
| FLAC | `flac` |**X** |**X** | `Vorbis Comments` |
| MP3 | `mp3` |**X** |**X** | `ID3v2` |
| MP4 | `mp4`, `m4a`, `m4b`, `m4p`, `m4v`, `isom` |**X** |**X** | `Vorbis Comments` |

View file

@ -1,5 +1,5 @@
use criterion::{criterion_group, criterion_main, Criterion};
use lofty::{DetermineFrom, Tag};
use lofty::{DetermineFrom, Tag, TagType};
macro_rules! test_read {
($function:ident, $path:expr) => {

View file

@ -1,6 +1,6 @@
#![cfg(feature = "id3")]
use crate::tag::RiffFormat;
use crate::tag::ID3Underlying;
use crate::{
impl_tag, Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite, Error, MimeType, Picture,
Result, TagType, ToAny, ToAnyTag,
@ -16,25 +16,30 @@ use std::path::Path;
#[cfg(feature = "duration")]
use std::time::Duration;
impl_tag!(Id3v2Tag, Id3v2InnerTag, TagType::Id3v2);
impl_tag!(Id3v2Tag, Id3v2InnerTag, TagType::Id3v2(ID3Underlying::Default));
impl Id3v2Tag {
#[allow(clippy::missing_errors_doc)]
pub fn read_from_path<P>(path: P, format: TagType) -> Result<Self>
pub fn read_from_path<P>(path: P, format: ID3Underlying) -> Result<Self>
where
P: AsRef<Path>,
{
return match format {
TagType::Id3v2 => Ok(Self {
ID3Underlying::Default => Ok(Self {
inner: Id3v2InnerTag::read_from_path(&path)?,
#[cfg(feature = "duration")]
duration: Some(mp3_duration::from_path(&path)?),
}),
TagType::Riff(RiffFormat::ID3) => Ok(Self {
ID3Underlying::RIFF => Ok(Self {
inner: Id3v2InnerTag::read_from_wav(&path)?,
#[cfg(feature = "duration")]
duration: None, // TODO
}),
ID3Underlying::Form => Ok(Self {
inner: Id3v2InnerTag::read_from_aiff(&path)?,
#[cfg(feature = "duration")]
duration: None, // TODO
}),
_ => unreachable!(),
};
}
@ -191,11 +196,14 @@ impl AudioTagWrite for Id3v2Tag {
file.read(&mut id)?;
file.seek(SeekFrom::Start(0))?;
if &id == b"RIFF" {
self.inner
.write_to_wav(file.path()?, id3::Version::Id3v24)?;
} else {
self.inner.write_to(file, id3::Version::Id3v24)?;
match &id {
b"RIFF" => self
.inner
.write_to_wav(file.path()?, id3::Version::Id3v24)?,
b"FORM" => self
.inner
.write_to_aiff(file.path()?, id3::Version::Id3v24)?,
_ => self.inner.write_to(file, id3::Version::Id3v24)?,
}
Ok(())
@ -203,10 +211,14 @@ impl AudioTagWrite for Id3v2Tag {
fn write_to_path(&self, path: &str) -> Result<()> {
let id = &std::fs::read(&path)?[0..4];
if &id == b"RIFF" {
self.inner.write_to_wav(path, id3::Version::Id3v24)?;
} else {
self.inner.write_to_path(path, id3::Version::Id3v24)?;
match id {
b"RIFF" => self
.inner
.write_to_wav(path, id3::Version::Id3v24)?,
b"FORM" => self
.inner
.write_to_aiff(path, id3::Version::Id3v24)?,
_ => self.inner.write_to_path(path, id3::Version::Id3v24)?,
}
Ok(())

View file

@ -1,7 +1,6 @@
#![cfg(feature = "riff")]
use crate::components::logic;
use crate::tag::RiffFormat;
use crate::{
impl_tag, Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite, Picture, Result, TagType,
ToAny, ToAnyTag,
@ -43,7 +42,7 @@ impl RiffTag {
}
}
impl_tag!(RiffTag, RiffInnerTag, TagType::Riff(RiffFormat::Info));
impl_tag!(RiffTag, RiffInnerTag, TagType::RiffInfo);
impl RiffTag {
fn get_value(&self, key: &str) -> Option<&str> {

View file

@ -16,6 +16,7 @@
//! | File Format | Extensions | Read | Write | Metadata Format(s) |
//! |-------------|-------------------------------------------|------|-------|----------------------|
//! | Ape | `ape` |**X** |**X** | `APEv2` |
//! | AIFF | `aiff` |**X** |**X** | `ID3v2` |
//! | FLAC | `flac` |**X** |**X** | `Vorbis Comments` |
//! | MP3 | `mp3` |**X** |**X** | `ID3v2` |
//! | MP4 | `mp4`, `m4a`, `m4b`, `m4p`, `m4v`, `isom` |**X** |**X** | `Vorbis Comments` |
@ -88,7 +89,7 @@ pub use crate::types::{
};
mod tag;
pub use crate::tag::{DetermineFrom, RiffFormat, Tag, TagType, VorbisFormat};
pub use crate::tag::{DetermineFrom, ID3Underlying, Tag, TagType, VorbisFormat};
mod error;
pub use crate::error::{Error, Result};

View file

@ -1,12 +1,15 @@
#[allow(clippy::wildcard_imports)]
use crate::components::tags::*;
use crate::{AudioTag, Error, Result};
use std::io::Seek;
use std::path::Path;
#[cfg(feature = "ape")]
const MAC: [u8; 3] = [77, 65, 67];
#[cfg(feature = "id3")]
const ID3: [u8; 3] = [73, 68, 51];
#[cfg(feature = "id3")]
const FORM: [u8; 4] = [70, 79, 82, 77];
#[cfg(feature = "mp4")]
const FTYP: [u8; 4] = [102, 116, 121, 112];
#[cfg(feature = "opus")]
@ -78,13 +81,11 @@ impl Tag {
#[cfg(feature = "ape")]
TagType::Ape => Ok(Box::new(ApeTag::read_from_path(path)?)),
#[cfg(feature = "id3")]
TagType::Id3v2 | TagType::Riff(RiffFormat::ID3) => {
Ok(Box::new(Id3v2Tag::read_from_path(path, tag_type)?))
},
TagType::Id3v2(underlying) => Ok(Box::new(Id3v2Tag::read_from_path(path, underlying)?)),
#[cfg(feature = "mp4")]
TagType::Mp4 => Ok(Box::new(Mp4Tag::read_from_path(path)?)),
#[cfg(feature = "riff")]
TagType::Riff(RiffFormat::Info) => Ok(Box::new(RiffTag::read_from_path(path)?)),
TagType::RiffInfo => Ok(Box::new(RiffTag::read_from_path(path)?)),
#[cfg(any(feature = "vorbis", feature = "flac", feature = "opus"))]
TagType::Vorbis(format) => Ok(Box::new(VorbisTag::read_from_path(path, format.clone())?)),
}
@ -98,8 +99,8 @@ pub enum TagType {
/// Common file extensions: `.ape`
Ape,
#[cfg(feature = "id3")]
/// Common file extensions: `.mp3`
Id3v2,
/// Represents multiple formats, see [`ID3Format`] for extensions.
Id3v2(ID3Underlying),
#[cfg(feature = "mp4")]
/// Common file extensions: `.mp4, .m4a, .m4p, .m4b, .m4r, .m4v`
Mp4,
@ -107,8 +108,9 @@ pub enum TagType {
/// Represents multiple formats, see [`VorbisFormat`] for extensions.
Vorbis(VorbisFormat),
#[cfg(feature = "riff")]
/// Represents multiple formats, see [`RiffFormat`] for extensions.
Riff(RiffFormat),
/// Metadata stored in a RIFF INFO chunk
/// Common file extensions: `.wav, .wave, .riff`
RiffInfo,
}
#[derive(Clone, Debug, PartialEq)]
@ -127,14 +129,15 @@ pub enum VorbisFormat {
}
#[derive(Clone, Debug, PartialEq)]
#[cfg(feature = "riff")]
/// Metadata format in the RIFF chunk
pub enum RiffFormat {
/// Metadata is stored in a RIFF INFO list
Info,
#[cfg(feature = "id3")]
/// Metadata is stored in an ID3 tag
ID3,
#[cfg(feature = "id3")]
/// ID3 tag's underlying format
pub enum ID3Underlying {
/// MP3
Default,
/// AIFF
Form,
/// WAV/WAVE
RIFF,
}
impl TagType {
@ -143,7 +146,11 @@ impl TagType {
#[cfg(feature = "ape")]
"ape" => Ok(Self::Ape),
#[cfg(feature = "id3")]
"mp3" => Ok(Self::Id3v2),
"aiff" => Ok(Self::Id3v2(ID3Underlying::Form)),
#[cfg(feature = "id3")]
"mp3" => Ok(Self::Id3v2(ID3Underlying::Default)),
#[cfg(all(feature = "riff", feature = "id3"))]
"wav" | "wave" | "riff" => Ok(Self::Id3v2(ID3Underlying::RIFF)),
#[cfg(feature = "opus")]
"opus" => Ok(Self::Vorbis(VorbisFormat::Opus)),
#[cfg(feature = "flac")]
@ -152,8 +159,6 @@ impl TagType {
"ogg" | "oga" => Ok(Self::Vorbis(VorbisFormat::Ogg)),
#[cfg(feature = "mp4")]
"m4a" | "m4b" | "m4p" | "m4v" | "isom" | "mp4" => Ok(Self::Mp4),
#[cfg(all(feature = "riff", feature = "id3"))]
"wav" | "wave" => Ok(Self::Riff(RiffFormat::ID3)),
_ => Err(Error::UnsupportedFormat(ext.to_owned())),
}
}
@ -166,7 +171,45 @@ impl TagType {
#[cfg(feature = "ape")]
77 if data.starts_with(&MAC) => Ok(Self::Ape),
#[cfg(feature = "id3")]
73 if data.starts_with(&ID3) => Ok(Self::Id3v2),
73 if data.starts_with(&ID3) => Ok(Self::Id3v2(ID3Underlying::Default)),
#[cfg(feature = "id3")]
70 if data.starts_with(&FORM) => {
use byteorder::{LittleEndian, BigEndian, ReadBytesExt};
use std::io::{Cursor, SeekFrom};
let mut data = Cursor::new(data);
let mut found_id3 = false;
loop {
if let (Ok(fourcc), Ok(size)) = (
data.read_u32::<LittleEndian>(),
data.read_u32::<BigEndian>(),
) {
if fourcc.to_le_bytes() == FORM {
data.seek(SeekFrom::Current(4))?;
continue;
}
if fourcc.to_le_bytes()[..3] == ID3 {
found_id3 = true;
break;
}
data.seek(SeekFrom::Current(
u32::from_be_bytes(size.to_be_bytes()) as i64
))?;
} else {
break;
}
}
if found_id3 {
return Ok(Self::Id3v2(ID3Underlying::Form));
}
// TODO: support AIFF chunks?
Err(Error::UnknownFormat)
},
#[cfg(feature = "flac")]
102 if data.starts_with(&FLAC) => Ok(Self::Vorbis(VorbisFormat::Flac)),
#[cfg(any(feature = "vorbis", feature = "opus"))]
@ -190,10 +233,9 @@ impl TagType {
let mut data = Cursor::new(&data[12..]);
let mut reading = true;
let mut found_id3 = false;
while reading {
loop {
if let (Ok(fourcc), Ok(size)) = (
data.read_u32::<LittleEndian>(),
data.read_u32::<LittleEndian>(),
@ -205,16 +247,16 @@ impl TagType {
data.set_position(data.position() + size as u64)
} else {
reading = false
break;
}
}
if found_id3 {
return Ok(Self::Riff(RiffFormat::ID3));
return Ok(Self::Id3v2(ID3Underlying::RIFF));
}
}
Ok(Self::Riff(RiffFormat::Info))
Ok(Self::RiffInfo)
},
#[cfg(feature = "mp4")]
_ if data[4..8] == FTYP => Ok(Self::Mp4),

View file

@ -1,6 +1,5 @@
#[allow(clippy::wildcard_imports)]
use crate::components::tags::*;
use crate::tag::RiffFormat;
use crate::{Album, AnyTag, Picture, Result, TagType};
use std::fs::File;
@ -115,13 +114,13 @@ pub trait ToAnyTag: ToAny {
#[cfg(feature = "ape")]
TagType::Ape => Box::new(ApeTag::from(self.to_anytag())),
#[cfg(feature = "id3")]
TagType::Id3v2 | TagType::Riff(RiffFormat::ID3) => Box::new(Id3v2Tag::from(self.to_anytag())),
TagType::Id3v2(_) => Box::new(Id3v2Tag::from(self.to_anytag())),
#[cfg(feature = "mp4")]
TagType::Mp4 => Box::new(Mp4Tag::from(self.to_anytag())),
#[cfg(any(feature = "vorbis", feature = "flac", feature = "opus"))]
TagType::Vorbis(_) => Box::new(VorbisTag::from(self.to_anytag())),
#[cfg(feature = "riff")]
TagType::Riff(RiffFormat::Info) => Box::new(RiffTag::from(self.to_anytag())),
TagType::RiffInfo => Box::new(RiffTag::from(self.to_anytag())),
}
}
}

Binary file not shown.

BIN
tests/assets/a.aiff Normal file

Binary file not shown.

Binary file not shown.

View file

@ -1,4 +1,4 @@
use lofty::{DetermineFrom, Tag, TagType, ToAnyTag, VorbisTag};
use lofty::{DetermineFrom, Tag, TagType, ToAnyTag, VorbisTag, ID3Underlying};
#[test]
#[cfg(all(feature = "id3", feature = "flac"))]
@ -15,7 +15,7 @@ fn test_inner() {
let tag: VorbisTag = innertag.into();
// Turn the VorbisTag into a Box<dyn AudioTag>
let id3tag = tag.to_dyn_tag(TagType::Id3v2);
let id3tag = tag.to_dyn_tag(TagType::Id3v2(ID3Underlying::Default));
// Write Box<dyn AudioTag> to `a.mp3`
id3tag

View file

@ -104,6 +104,7 @@ full_test!(test_ape, "tests/assets/a.ape");
// ID3v2
full_test!(test_mp3, "tests/assets/a.mp3");
full_test!(test_aiff, "tests/assets/a.aiff");
full_test!(test_wav_id3, "tests/assets/a-id3.wav");
// RIFF INFO