Add support for AIFF chunks

Signed-off-by: Serial <69764315+Serial-ATA@users.noreply.github.com>
This commit is contained in:
Serial 2021-07-07 17:29:53 -04:00
parent ca52551807
commit e949d2bb85
14 changed files with 304 additions and 31 deletions

View file

@ -38,9 +38,10 @@ format-opus = ["ogg_pager"]
format-vorbis = ["ogg_pager"]
format-ape = ["ape"]
format-id3 = ["id3", "filepath"]
format-aiff = []
format-riff = []
format-ogg = ["format-flac", "format-opus", "format-vorbis"]
all_tags = ["format-ogg", "format-mp4", "format-id3", "format-riff", "format-ape"]
all_tags = ["format-ogg", "format-mp4", "format-id3", "format-riff", "format-ape", "format-aiff"]
[dev-dependencies]
criterion = { version = "0.3", features = ["html_reports"] }

View file

@ -0,0 +1,127 @@
use crate::{LoftyError, Result};
use byteorder::{BigEndian, LittleEndian, ReadBytesExt};
use std::cmp::{max, min};
use std::fs::File;
use std::io::{Read, Seek, SeekFrom, Write};
pub(crate) fn read_from<T>(data: &mut T) -> Result<(Option<String>, Option<String>)>
where
T: Read + Seek,
{
let mut name_id: Option<String> = None;
let mut author_id: Option<String> = None;
data.seek(SeekFrom::Start(12))?;
while let (Ok(fourcc), Ok(size)) = (
data.read_u32::<LittleEndian>(),
data.read_u32::<BigEndian>(),
) {
match &fourcc.to_le_bytes() {
f if f == b"NAME" && name_id.is_none() => {
let mut name = vec![0; size as usize];
data.read_exact(&mut name)?;
name_id = Some(String::from_utf8(name)?);
},
f if f == b"AUTH" && author_id.is_none() => {
let mut auth = vec![0; size as usize];
data.read_exact(&mut auth)?;
author_id = Some(String::from_utf8(auth)?);
},
_ => {
data.seek(SeekFrom::Current(i64::from(size)))?;
},
}
}
if (&None, &None) == (&name_id, &author_id) {
return Err(LoftyError::InvalidData("AIFF file contains no text chunks"));
}
Ok((name_id, author_id))
}
pub(crate) fn write_to(
data: &mut File,
metadata: (Option<&String>, Option<&String>),
) -> Result<()> {
let mut text_chunks = Vec::new();
if let Some(name_id) = metadata.0 {
let len = (name_id.len() as u32).to_be_bytes();
text_chunks.extend(b"NAME".iter());
text_chunks.extend(len.iter());
text_chunks.extend(name_id.as_bytes().iter());
}
if let Some(author_id) = metadata.1 {
let len = (author_id.len() as u32).to_be_bytes();
text_chunks.extend(b"AUTH".iter());
text_chunks.extend(len.iter());
text_chunks.extend(author_id.as_bytes().iter());
}
data.seek(SeekFrom::Start(12))?;
let mut name: Option<(usize, usize)> = None;
let mut auth: Option<(usize, usize)> = None;
while let (Ok(fourcc), Ok(size)) = (
data.read_u32::<LittleEndian>(),
data.read_u32::<BigEndian>(),
) {
let pos = (data.seek(SeekFrom::Current(0))? - 8) as usize;
match &fourcc.to_le_bytes() {
f if f == b"NAME" && name.is_none() => name = Some((pos, (pos + 8 + size as usize))),
f if f == b"AUTH" && auth.is_none() => auth = Some((pos, (pos + 8 + size as usize))),
_ => {
data.seek(SeekFrom::Current(i64::from(size)))?;
},
}
}
data.seek(SeekFrom::Start(0))?;
let mut file_bytes = Vec::new();
data.read_to_end(&mut file_bytes)?;
match (name, auth) {
(Some((n_pos, n_end)), Some((a_pos, a_end))) => {
let first_start = min(n_pos, a_pos);
let first_end = min(n_end, a_end);
let last_start = max(n_pos, a_pos);
let last_end = max(n_end, a_end);
file_bytes.drain(last_start..last_end);
file_bytes.splice(first_start..first_end, text_chunks);
},
(Some((start, end)), None) | (None, Some((start, end))) => {
file_bytes.splice(start..end, text_chunks);
},
(None, None) => {
data.seek(SeekFrom::Start(16))?;
let mut size = [0; 4];
data.read_exact(&mut size)?;
let comm_end = (20 + u32::from_le_bytes(size)) as usize;
file_bytes.splice(comm_end..comm_end, text_chunks);
},
}
let total_size = ((file_bytes.len() - 8) as u32).to_be_bytes();
file_bytes.splice(4..8, total_size.to_vec());
data.seek(SeekFrom::Start(0))?;
data.set_len(0)?;
data.write_all(&*file_bytes)?;
Ok(())
}

View file

@ -6,3 +6,6 @@ pub(crate) mod riff;
#[cfg(any(feature = "format-opus", feature = "format-vorbis"))]
pub(crate) mod ogg;
#[cfg(feature = "format-aiff")]
pub(crate) mod aiff;

View file

@ -86,7 +86,6 @@ where
Ok(())
}
#[cfg(feature = "format-riff")]
pub(crate) fn write_to(data: &mut File, metadata: HashMap<String, String>) -> Result<()> {
let mut packet = Vec::new();

View file

@ -0,0 +1,68 @@
use crate::components::logic::aiff;
use crate::{
Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite, Result, TagType, ToAny, ToAnyTag,
};
use std::fs::File;
use std::io::{Read, Seek};
use lofty_attr::impl_tag;
#[derive(Default)]
struct AiffInnerTag {
name_id: Option<String>,
author_id: Option<String>,
}
#[impl_tag(AiffInnerTag, TagType::AiffText)]
pub struct AiffTag;
impl AiffTag {
#[allow(missing_docs)]
#[allow(clippy::missing_errors_doc)]
pub fn read_from<R>(reader: &mut R) -> Result<Self>
where
R: Read + Seek,
{
let (name_id, author_id) = aiff::read_from(reader)?;
Ok(Self {
inner: AiffInnerTag { name_id, author_id },
})
}
}
impl AudioTagEdit for AiffTag {
fn title(&self) -> Option<&str> {
self.inner.name_id.as_deref()
}
fn set_title(&mut self, title: &str) {
self.inner.name_id = Some(title.to_string())
}
fn remove_title(&mut self) {
self.inner.name_id = None
}
fn artist_str(&self) -> Option<&str> {
self.inner.author_id.as_deref()
}
fn set_artist(&mut self, artist: &str) {
self.inner.author_id = Some(artist.to_string())
}
fn remove_artist(&mut self) {
self.inner.author_id = None
}
}
impl AudioTagWrite for AiffTag {
fn write_to(&self, file: &mut File) -> Result<()> {
aiff::write_to(
file,
(self.inner.name_id.as_ref(), self.inner.author_id.as_ref()),
)
}
}

View file

@ -123,8 +123,8 @@ impl AudioTagEdit for ApeTag {
fn album_title(&self) -> Option<&str> {
self.get_value("Album")
}
fn set_album_title(&mut self, v: &str) {
self.set_value("Album", v)
fn set_album_title(&mut self, title: &str) {
self.set_value("Album", title)
}
fn remove_album_title(&mut self) {
self.remove_key("Album")

View file

@ -143,8 +143,8 @@ impl AudioTagEdit for Id3v2Tag {
fn album_title(&self) -> Option<&str> {
self.inner.album()
}
fn set_album_title(&mut self, v: &str) {
self.inner.set_album(v)
fn set_album_title(&mut self, title: &str) {
self.inner.set_album(title)
}
fn remove_album_title(&mut self) {
self.inner.remove_album();

View file

@ -24,6 +24,12 @@ pub use riff_tag::RiffTag;
feature = "format-flac"
))]
pub(crate) mod ogg_tag;
#[cfg(feature = "format-aiff")]
pub(crate) mod aiff_tag;
#[cfg(feature = "format-aiff")]
pub use aiff_tag::AiffTag;
#[cfg(any(
feature = "format-vorbis",
feature = "format-opus",

View file

@ -90,8 +90,8 @@ impl AudioTagEdit for Mp4Tag {
self.inner.album()
}
fn set_album_title(&mut self, v: &str) {
self.inner.set_album(v)
fn set_album_title(&mut self, title: &str) {
self.inner.set_album(title)
}
fn remove_album_title(&mut self) {
self.inner.remove_album();

View file

@ -177,7 +177,9 @@ impl TryFrom<metaflac::Tag> for OggTag {
return Ok(tag);
}
Err(LoftyError::InvalidData("Flac file contains no vorbis comment blocks"))
Err(LoftyError::InvalidData(
"Flac file contains no vorbis comment blocks",
))
}
}

View file

@ -1,9 +1,8 @@
use crate::components::logic::riff;
use crate::{
Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite, Picture, Result, TagType, ToAny, ToAnyTag,
Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite, Result, TagType, ToAny, ToAnyTag,
};
use std::borrow::Cow;
use std::collections::HashMap;
use std::fs::File;
use std::io::{Read, Seek};
@ -86,8 +85,8 @@ impl AudioTagEdit for RiffTag {
self.get_value("IPRD").or_else(|| self.get_value("ALBU"))
}
fn set_album_title(&mut self, v: &str) {
self.set_value("IPRD", v)
fn set_album_title(&mut self, title: &str) {
self.set_value("IPRD", title)
}
fn remove_album_title(&mut self) {

View file

@ -131,6 +131,8 @@ impl Tag {
feature = "format-opus"
))]
TagType::Ogg(format) => Ok(Box::new(OggTag::read_from(reader, &format)?)),
#[cfg(feature = "format-aiff")]
TagType::AiffText => Ok(Box::new(AiffTag::read_from(reader)?)),
}
}
}
@ -158,6 +160,10 @@ pub enum TagType {
/// Metadata stored in a RIFF INFO chunk
/// Common file extensions: `.wav, .wave, .riff`
RiffInfo,
#[cfg(feature = "format-aiff")]
/// Metadata stored in AIFF text chunks
/// Common file extensions: `.aiff, .aif`
AiffText,
}
#[derive(Clone, Debug, PartialEq)]
@ -238,17 +244,21 @@ impl TagType {
70 if sig.starts_with(&FORM) => {
use byteorder::{BigEndian, LittleEndian, ReadBytesExt};
data.seek(SeekFrom::Start(8))?;
let mut id = [0; 4];
data.read_exact(&mut id)?;
if &id != b"AIFF" && &id != b"AIFC" {
return Err(LoftyError::UnknownFormat);
}
let mut found_id3 = false;
while 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;
@ -265,8 +275,7 @@ impl TagType {
return Ok(Self::Id3v2(Id3Format::Form));
}
// TODO: support AIFF chunks?
Err(LoftyError::UnknownFormat)
Ok(Self::AiffText)
},
#[cfg(feature = "format-flac")]
102 if sig.starts_with(&FLAC) => Ok(Self::Ogg(OggFormat::Flac)),

View file

@ -47,7 +47,9 @@ pub trait AudioTagEdit {
}
/// Returns the track year
fn year(&self) -> Option<i32> { None }
fn year(&self) -> Option<i32> {
None
}
/// Sets the track year
fn set_year(&mut self, _year: i32) {}
/// Removes the track year
@ -63,14 +65,18 @@ pub trait AudioTagEdit {
}
/// Returns the album title
fn album_title(&self) -> Option<&str> { None }
fn album_title(&self) -> Option<&str> {
None
}
/// Sets the album title
fn set_album_title(&mut self, v: &str) {}
fn set_album_title(&mut self, _title: &str) {}
/// Removes the album title
fn remove_album_title(&mut self) {}
/// Returns the album artist string
fn album_artist_str(&self) -> Option<&str> { None }
fn album_artist_str(&self) -> Option<&str> {
None
}
/// Splits the artist string into a `Vec`
fn album_artists(&self, delimiter: &str) -> Option<Vec<&str>> {
self.album_artist_str()
@ -92,21 +98,27 @@ pub trait AudioTagEdit {
}
/// Returns the front cover
fn front_cover(&self) -> Option<Picture> { None }
fn front_cover(&self) -> Option<Picture> {
None
}
/// Sets the front cover
fn set_front_cover(&mut self, _cover: Picture) {}
/// Removes the front cover
fn remove_front_cover(&mut self) {}
/// Returns the front cover
fn back_cover(&self) -> Option<Picture> { None }
fn back_cover(&self) -> Option<Picture> {
None
}
/// Sets the front cover
fn set_back_cover(&mut self, _cover: Picture) {}
/// Removes the front cover
fn remove_back_cover(&mut self) {}
/// Returns an `Iterator` over all pictures stored in the track
fn pictures(&self) -> Option<Cow<'static, [Picture]>> { None }
fn pictures(&self) -> Option<Cow<'static, [Picture]>> {
None
}
/// Returns the track number and total tracks
fn track(&self) -> (Option<u32>, Option<u32>) {
@ -124,14 +136,18 @@ pub trait AudioTagEdit {
}
/// Returns the track number
fn track_number(&self) -> Option<u32> { None }
fn track_number(&self) -> Option<u32> {
None
}
/// Sets the track number
fn set_track_number(&mut self, _track_number: u32) {}
/// Removes the track number
fn remove_track_number(&mut self) {}
/// Returns the total tracks
fn total_tracks(&self) -> Option<u32> { None }
fn total_tracks(&self) -> Option<u32> {
None
}
/// Sets the total tracks
fn set_total_tracks(&mut self, _total_track: u32) {}
/// Removes the total tracks
@ -148,14 +164,18 @@ pub trait AudioTagEdit {
}
/// Returns the disc number
fn disc_number(&self) -> Option<u32> { None }
fn disc_number(&self) -> Option<u32> {
None
}
/// Sets the disc number
fn set_disc_number(&mut self, _disc_number: u32) {}
/// Removes the disc number
fn remove_disc_number(&mut self) {}
/// Returns the total discs
fn total_discs(&self) -> Option<u32> { None }
fn total_discs(&self) -> Option<u32> {
None
}
/// Sets the total discs
fn set_total_discs(&mut self, _total_discs: u32) {}
/// Removes the total discs
@ -206,6 +226,8 @@ pub trait ToAnyTag: ToAny {
TagType::Ogg(_) => Box::new(OggTag::from(self.to_anytag())),
#[cfg(feature = "format-riff")]
TagType::RiffInfo => Box::new(RiffTag::from(self.to_anytag())),
#[cfg(feature = "format-aiff")]
TagType::AiffText => Box::new(AiffTag::from(self.to_anytag())),
}
}
}

View file

@ -210,3 +210,40 @@ full_test!(test_flac, "tests/assets/a.flac");
full_test!(test_m4a, "tests/assets/a.m4a");
full_test!(test_ogg, "tests/assets/a.ogg");
full_test!(test_opus, "tests/assets/a.opus");
// AIFF text chunks only provide 2 values
#[test]
fn test_aiff_text() {
let file = "tests/assets/a_text.aiff";
println!("-- Adding tags --");
println!("Reading file");
let mut tag = Tag::default().read_from_path_signature(file).unwrap();
println!("Setting title");
tag.set_title("foo title");
println!("Setting artist");
tag.set_artist("foo artist");
println!("Writing");
tag.write_to_path(file).unwrap();
println!("-- Verifying tags --");
println!("Reading file");
let mut tag = Tag::default().read_from_path_signature(file).unwrap();
println!("Verifying title");
assert_eq!(tag.title(), Some("foo title"));
println!("Verifying artist");
assert_eq!(tag.artist_str(), Some("foo artist"));
println!("-- Removing tags --");
println!("Removing title");
tag.remove_title();
println!("Removing artist");
tag.remove_artist();
println!("Writing");
tag.write_to_path(file).unwrap()
}