Read AIFF COMT chunks

This commit is contained in:
Serial 2022-01-04 15:03:24 -05:00
parent d49a06888f
commit 76e788243f
6 changed files with 161 additions and 52 deletions

View file

@ -1,5 +1,5 @@
#[cfg(feature = "aiff_text_chunks")] #[cfg(feature = "aiff_text_chunks")]
use super::tag::AiffTextChunks; use super::tag::{AiffTextChunks, Comment};
use super::AiffFile; use super::AiffFile;
use crate::error::{LoftyError, Result}; use crate::error::{LoftyError, Result};
#[cfg(feature = "id3v2")] #[cfg(feature = "id3v2")]
@ -9,7 +9,7 @@ use crate::types::properties::FileProperties;
use std::io::{Read, Seek, SeekFrom}; use std::io::{Read, Seek, SeekFrom};
use byteorder::BigEndian; use byteorder::{BigEndian, ReadBytesExt};
pub(in crate::iff) fn verify_aiff<R>(data: &mut R) -> Result<()> pub(in crate::iff) fn verify_aiff<R>(data: &mut R) -> Result<()>
where where
@ -38,6 +38,9 @@ where
let mut text_chunks = AiffTextChunks::default(); let mut text_chunks = AiffTextChunks::default();
#[cfg(feature = "aiff_text_chunks")] #[cfg(feature = "aiff_text_chunks")]
let mut annotations = Vec::new(); let mut annotations = Vec::new();
#[cfg(feature = "aiff_text_chunks")]
let mut comments = Vec::new();
#[cfg(feature = "id3v2")] #[cfg(feature = "id3v2")]
let mut id3v2_tag: Option<Id3v2Tag> = None; let mut id3v2_tag: Option<Id3v2Tag> = None;
@ -47,8 +50,7 @@ where
match &chunks.fourcc { match &chunks.fourcc {
#[cfg(feature = "id3v2")] #[cfg(feature = "id3v2")]
b"ID3 " | b"id3 " => id3v2_tag = Some(chunks.id3_chunk(data)?), b"ID3 " | b"id3 " => id3v2_tag = Some(chunks.id3_chunk(data)?),
b"COMM" if read_properties => { b"COMM" if read_properties && comm.is_none() => {
if comm.is_none() {
if chunks.size < 18 { if chunks.size < 18 {
return Err(LoftyError::Aiff( return Err(LoftyError::Aiff(
"File has an invalid \"COMM\" chunk size (< 18)", "File has an invalid \"COMM\" chunk size (< 18)",
@ -56,7 +58,6 @@ where
} }
comm = Some(chunks.content(data)?); comm = Some(chunks.content(data)?);
}
}, },
b"SSND" if read_properties => { b"SSND" if read_properties => {
stream_len = chunks.size; stream_len = chunks.size;
@ -67,9 +68,28 @@ where
let value = String::from_utf8(chunks.content(data)?)?; let value = String::from_utf8(chunks.content(data)?)?;
annotations.push(value); annotations.push(value);
}, },
// These three chunks are expected to appear at most once per file, // These four chunks are expected to appear at most once per file,
// so there's no need to replace anything we already read // so there's no need to replace anything we already read
#[cfg(feature = "aiff_text_chunks")] #[cfg(feature = "aiff_text_chunks")]
b"COMT" if comments.is_empty() => {
let num_comments = data.read_u16::<BigEndian>()?;
for _ in 0..num_comments {
let timestamp = data.read_u32::<BigEndian>()?;
let marker_id = data.read_u16::<BigEndian>()?;
let size = data.read_u16::<BigEndian>()?;
let mut text = vec![0; size as usize];
data.read_exact(&mut text)?;
comments.push(Comment {
timestamp,
marker_id,
text: String::from_utf8(text)?,
})
}
},
#[cfg(feature = "aiff_text_chunks")]
b"NAME" if text_chunks.name.is_none() => { b"NAME" if text_chunks.name.is_none() => {
let value = String::from_utf8(chunks.content(data)?)?; let value = String::from_utf8(chunks.content(data)?)?;
text_chunks.name = Some(value); text_chunks.name = Some(value);
@ -93,10 +113,16 @@ where
} }
#[cfg(feature = "aiff_text_chunks")] #[cfg(feature = "aiff_text_chunks")]
{
if !annotations.is_empty() { if !annotations.is_empty() {
text_chunks.annotations = Some(annotations); text_chunks.annotations = Some(annotations);
} }
if !comments.is_empty() {
text_chunks.comments = Some(comments);
}
}
let properties = if read_properties { let properties = if read_properties {
if comm.is_none() { if comm.is_none() {
return Err(LoftyError::Aiff("File does not contain a \"COMM\" chunk")); return Err(LoftyError::Aiff("File does not contain a \"COMM\" chunk"));
@ -124,6 +150,7 @@ where
author: None, author: None,
copyright: None, copyright: None,
annotations: None, annotations: None,
comments: None,
} => None, } => None,
_ => Some(text_chunks), _ => Some(text_chunks),
}, },

View file

@ -1,4 +1,4 @@
use crate::error::Result; use crate::error::{LoftyError, Result};
use crate::iff::chunk::Chunks; use crate::iff::chunk::Chunks;
use crate::types::item::{ItemKey, ItemValue, TagItem}; use crate::types::item::{ItemKey, ItemValue, TagItem};
use crate::types::tag::{Accessor, Tag, TagType}; use crate::types::tag::{Accessor, Tag, TagType};
@ -10,8 +10,27 @@ use std::path::Path;
use byteorder::BigEndian; use byteorder::BigEndian;
#[cfg(feature = "aiff_text_chunks")] /// Represents an AIFF `COMT` chunk
///
/// This is preferred over the `ANNO` chunk, for its additional information.
#[derive(Default, Clone, Debug, PartialEq)] #[derive(Default, Clone, Debug, PartialEq)]
pub struct Comment {
/// The creation time of the comment
///
/// The unit is the number of seconds since January 1, 1904.
pub timestamp: u32,
/// An optional linking to a marker
///
/// This is for storing descriptions of markers as a comment.
/// An id of 0 means the comment is not linked to a marker,
/// otherwise it should be the ID of a marker.
pub marker_id: u16,
/// The comment itself
///
/// The size of the comment is restricted to [`u16::MAX`].
pub text: String,
}
/// `AIFF` text chunks /// `AIFF` text chunks
/// ///
/// ## Supported file types /// ## Supported file types
@ -33,6 +52,9 @@ use byteorder::BigEndian;
/// * [ItemKey::TrackArtist](crate::ItemKey::TrackArtist) /// * [ItemKey::TrackArtist](crate::ItemKey::TrackArtist)
/// * [ItemKey::CopyrightMessage](crate::ItemKey::CopyrightMessage) /// * [ItemKey::CopyrightMessage](crate::ItemKey::CopyrightMessage)
/// * [ItemKey::Comment](crate::ItemKey::Comment) /// * [ItemKey::Comment](crate::ItemKey::Comment)
///
/// When converting [Comment]s, only the `text` field will be preserved.
#[derive(Default, Clone, Debug, PartialEq)]
pub struct AiffTextChunks { pub struct AiffTextChunks {
/// The name of the piece /// The name of the piece
pub name: Option<String>, pub name: Option<String>,
@ -46,8 +68,10 @@ pub struct AiffTextChunks {
/// The use of these chunks is discouraged by spec, as the `comments` /// The use of these chunks is discouraged by spec, as the `comments`
/// field is more powerful. /// field is more powerful.
pub annotations: Option<Vec<String>>, pub annotations: Option<Vec<String>>,
// TODO: COMT chunk /// A more feature-rich comment
// pub comments: Option<Vec<Comment>> ///
/// These are preferred over `annotations`. See [`Comment`]
pub comments: Option<Vec<Comment>>,
} }
impl Accessor for AiffTextChunks { impl Accessor for AiffTextChunks {
@ -109,6 +133,7 @@ impl AiffTextChunks {
self.author.as_deref(), self.author.as_deref(),
self.copyright.as_deref(), self.copyright.as_deref(),
self.annotations.as_deref(), self.annotations.as_deref(),
self.comments.as_deref(),
) )
.write_to(file) .write_to(file)
} }
@ -136,6 +161,13 @@ impl From<AiffTextChunks> for Tag {
} }
} }
if let Some(comments) = input.comments {
for comt in comments {
tag.items
.push(TagItem::new(ItemKey::Comment, ItemValue::Text(comt.text)));
}
}
tag tag
} }
} }
@ -163,45 +195,54 @@ impl From<Tag> for AiffTextChunks {
Some(anno) Some(anno)
} }
}, },
comments: None,
} }
} }
} }
pub(crate) struct AiffTextChunksRef<'a, T: AsRef<str>, I: IntoIterator<Item = T>> { pub(crate) struct AiffTextChunksRef<'a, T, AI>
where
AI: IntoIterator<Item = T>,
{
pub name: Option<&'a str>, pub name: Option<&'a str>,
pub author: Option<&'a str>, pub author: Option<&'a str>,
pub copyright: Option<&'a str>, pub copyright: Option<&'a str>,
pub annotations: Option<I>, pub annotations: Option<AI>,
pub comments: Option<&'a [Comment]>,
} }
impl<'a, T: AsRef<str>, I: IntoIterator<Item = T>> AiffTextChunksRef<'a, T, I> { impl<'a, T, AI> AiffTextChunksRef<'a, T, AI>
where
T: AsRef<str>,
AI: IntoIterator<Item = T>,
{
pub(super) fn new( pub(super) fn new(
name: Option<&'a str>, name: Option<&'a str>,
author: Option<&'a str>, author: Option<&'a str>,
copyright: Option<&'a str>, copyright: Option<&'a str>,
annotations: Option<I>, annotations: Option<AI>,
) -> AiffTextChunksRef<'a, T, I> { comments: Option<&'a [Comment]>,
) -> AiffTextChunksRef<'a, T, AI> {
AiffTextChunksRef { AiffTextChunksRef {
name, name,
author, author,
copyright, copyright,
annotations, annotations,
comments,
} }
} }
}
impl<'a, T: AsRef<str>, I: IntoIterator<Item = T>> AiffTextChunksRef<'a, T, I> {
pub(crate) fn write_to(self, file: &mut File) -> Result<()> { pub(crate) fn write_to(self, file: &mut File) -> Result<()> {
AiffTextChunksRef::write_to_inner(file, self) AiffTextChunksRef::write_to_inner(file, self)
} }
fn write_to_inner(data: &mut File, mut tag: AiffTextChunksRef<T, I>) -> Result<()> { fn write_to_inner(data: &mut File, mut tag: AiffTextChunksRef<T, AI>) -> Result<()> {
fn write_chunk(writer: &mut Vec<u8>, key: &str, value: Option<&str>) { fn write_chunk(writer: &mut Vec<u8>, key: &str, value: Option<&str>) {
if let Some(val) = value { if let Some(val) = value {
if let Ok(len) = u32::try_from(val.len()) { if let Ok(len) = u32::try_from(val.len()) {
writer.extend(key.as_bytes().iter()); writer.extend(key.as_bytes());
writer.extend(len.to_be_bytes().iter()); writer.extend(len.to_be_bytes());
writer.extend(val.as_bytes().iter()); writer.extend(val.as_bytes());
} }
} }
} }
@ -220,6 +261,42 @@ impl<'a, T: AsRef<str>, I: IntoIterator<Item = T>> AiffTextChunksRef<'a, T, I> {
} }
} }
if let Some(comments) = tag.comments.take() {
let original_len = comments.len();
if let Ok(len) = u16::try_from(original_len) {
text_chunks.extend(b"COMT");
// Start with zeroed size
text_chunks.extend([0, 0, 0, 0]);
text_chunks.extend((len as u16).to_be_bytes());
for comt in comments {
text_chunks.extend(comt.timestamp.to_be_bytes());
text_chunks.extend(comt.marker_id.to_be_bytes());
if comt.text.len() > u16::MAX as usize {
return Err(LoftyError::TooMuchData);
}
text_chunks.extend((comt.text.len() as u16).to_be_bytes());
text_chunks.extend(comt.text.as_bytes());
}
// Get the size of the COMT chunk
let comt_len = text_chunks.len() - (original_len + 4);
if let Ok(chunk_len) = u32::try_from(comt_len) {
let len_bytes = chunk_len.to_be_bytes();
let size_start_idx = original_len + 3;
text_chunks[size_start_idx..(4 + size_start_idx)]
.clone_from_slice(&len_bytes[..4]);
} else {
return Err(LoftyError::TooMuchData);
}
}
}
let mut chunks_remove = Vec::new(); let mut chunks_remove = Vec::new();
let mut chunks = Chunks::<BigEndian>::new(); let mut chunks = Chunks::<BigEndian>::new();
@ -277,7 +354,7 @@ impl<'a, T: AsRef<str>, I: IntoIterator<Item = T>> AiffTextChunksRef<'a, T, I> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::iff::AiffTextChunks; use crate::iff::{AiffTextChunks, Comment};
use crate::{ItemKey, ItemValue, Tag, TagItem, TagType}; use crate::{ItemKey, ItemValue, Tag, TagItem, TagType};
use std::io::{Cursor, Read}; use std::io::{Cursor, Read};
@ -292,6 +369,18 @@ mod tests {
String::from("Qux annotation"), String::from("Qux annotation"),
String::from("Quux annotation"), String::from("Quux annotation"),
]), ]),
comments: Some(vec![
Comment {
timestamp: 1024,
marker_id: 0,
text: String::from("Quuz comment"),
},
Comment {
timestamp: 2048,
marker_id: 40,
text: String::from("Corge comment"),
},
]),
}; };
let mut tag = Vec::new(); let mut tag = Vec::new();
@ -330,15 +419,12 @@ mod tests {
Some("Baz copyright") Some("Baz copyright")
); );
let mut comments = tag.get_items(&ItemKey::Comment); let mut comments = tag.get_texts(&ItemKey::Comment);
assert_eq!( assert_eq!(comments.next(), Some("Qux annotation"));
comments.next().map(TagItem::value), assert_eq!(comments.next(), Some("Quux annotation"));
Some(&ItemValue::Text(String::from("Qux annotation"))) assert_eq!(comments.next(), Some("Quuz comment"));
); assert_eq!(comments.next(), Some("Corge comment"));
assert_eq!( assert!(comments.next().is_none());
comments.next().map(TagItem::value),
Some(&ItemValue::Text(String::from("Quux annotation")))
);
} }
#[test] #[test]
@ -374,5 +460,6 @@ mod tests {
String::from("Quux annotation") String::from("Quux annotation")
]) ])
); );
assert!(aiff_text.comments.is_none());
} }
} }

View file

@ -18,6 +18,7 @@ pub(crate) fn write_to(data: &mut File, tag: &Tag) -> Result<()> {
tag.get_string(&ItemKey::TrackArtist), tag.get_string(&ItemKey::TrackArtist),
tag.get_string(&ItemKey::CopyrightMessage), tag.get_string(&ItemKey::CopyrightMessage),
Some(tag.get_texts(&ItemKey::Comment)), Some(tag.get_texts(&ItemKey::Comment)),
None,
) )
.write_to(data), .write_to(data),
#[cfg(feature = "id3v2")] #[cfg(feature = "id3v2")]

View file

@ -3,11 +3,10 @@ pub(crate) mod aiff;
pub(crate) mod chunk; pub(crate) mod chunk;
pub(crate) mod wav; pub(crate) mod wav;
pub use crate::iff::aiff::AiffFile; pub use aiff::AiffFile;
pub use crate::iff::wav::WavFile; pub use wav::{WavFile, WavFormat, WavProperties};
#[cfg(feature = "aiff_text_chunks")] #[cfg(feature = "aiff_text_chunks")]
pub use crate::iff::aiff::tag::AiffTextChunks; pub use aiff::tag::{AiffTextChunks, Comment};
#[cfg(feature = "riff_info_list")] #[cfg(feature = "riff_info_list")]
pub use crate::iff::wav::tag::RiffInfoList; pub use wav::tag::RiffInfoList;
pub use wav::{WavFormat, WavProperties};

View file

@ -164,20 +164,15 @@ mod types;
pub use crate::error::{LoftyError, Result}; pub use crate::error::{LoftyError, Result};
pub use crate::probe::Probe; pub use crate::probe::{read_from, read_from_path, Probe};
pub use crate::types::{ pub use crate::types::{
file::{FileType, TaggedFile}, file::{AudioFile, FileType, TaggedFile},
item::{ItemKey, ItemValue, TagItem}, item::{ItemKey, ItemValue, TagItem},
picture::{MimeType, Picture, PictureType},
properties::FileProperties, properties::FileProperties,
tag::{Accessor, Tag, TagType}, tag::{Accessor, Tag, TagType},
}; };
pub use crate::types::file::AudioFile;
pub use crate::types::picture::{MimeType, Picture, PictureType};
#[cfg(feature = "vorbis_comments")] #[cfg(feature = "vorbis_comments")]
pub use crate::types::picture::PictureInformation; pub use crate::types::picture::PictureInformation;
pub use probe::{read_from, read_from_path};

Binary file not shown.