mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-11-10 06:34:18 +00:00
Support AIFF
This commit is contained in:
parent
fbfe0e916b
commit
dc23ec1ffd
12 changed files with 103 additions and 48 deletions
|
@ -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` |
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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};
|
||||
|
|
92
src/tag.rs
92
src/tag.rs
|
@ -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),
|
||||
|
|
|
@ -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
BIN
tests/assets/a.aiff
Normal file
Binary file not shown.
Binary file not shown.
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue