mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-12-13 06:02:32 +00:00
Read AIFF COMT
chunks
This commit is contained in:
parent
d49a06888f
commit
76e788243f
6 changed files with 161 additions and 52 deletions
|
@ -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),
|
||||||
},
|
},
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")]
|
||||||
|
|
|
@ -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};
|
|
||||||
|
|
11
src/lib.rs
11
src/lib.rs
|
@ -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.
Loading…
Reference in a new issue