Skip to content

Commit

Permalink
meta: Add support for id3v2 CHAP and CTOC frames
Browse files Browse the repository at this point in the history
These were standardised in https://id3.org/id3v2-chapters-1.0.

Something I initially considered (and did) was to use `Cue`s and
`CuePoint`s instead of creating new types, but decided against it
because:

- `Cue` is tied to the format, whereas these are tied to the metadata.
- According to the spec, chapters can overlap, and so must have a
  duration.
- It is unclear to me what the index should be.
- `CuePoint`s cannot have another `CuePoint` within them, which may be
  an issue for multi-level tables of content.
- Chapters and tables of contents would end up mixed as `Cue`s despite
  being different types.

Resolves: #16
  • Loading branch information
Tommoa committed Oct 8, 2022
1 parent 17ffc93 commit 6db6e6f
Show file tree
Hide file tree
Showing 4 changed files with 306 additions and 11 deletions.
3 changes: 2 additions & 1 deletion CONTRIBUTORS
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ FelixMcFelix [https://github.com/FelixMcFelix]
Herohtar [https://github.com/herohtar]
nicholaswyoung [https://github.com/nicholaswyoung]
richardmitic [https://github.com/richardmitic]
terrorfisch [https://github.com/terrorfisch]
terrorfisch [https://github.com/terrorfisch]
Tommoa [https://github.com/Tommoa]
74 changes: 72 additions & 2 deletions symphonia-core/src/meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ pub enum StandardTagKey {
Arranger,
Artist,
Bpm,
Chapter,
Comment,
Compilation,
Composer,
Expand Down Expand Up @@ -186,6 +187,7 @@ pub enum StandardTagKey {
SortArtist,
SortComposer,
SortTrackTitle,
TableOfContents,
TaggingDate,
TrackNumber,
TrackSubtitle,
Expand Down Expand Up @@ -331,6 +333,48 @@ impl fmt::Display for Tag {
}
}

/// A description of a single chapter within an audio file.
#[derive(Clone, Debug, Default)]
pub struct Chapter {
/// The millisecond offset from the beginning of the file to the start of the chapter.
pub start_ms: u32,
/// The millisecond offset from the beginning of the file to the end of the chapter.
pub end_ms: u32,
/// The zero-based count of bytes from the beginning of the file to the first byte of the first
/// audio frame in the chapter. If these bytes are all set to 0xFF then the value should be
/// ignored and the start time value should be utilized.
pub start_byte: u32,
/// The zero-based count of bytes from the beginning of the file to the first byte of the audio
/// frame following the end of the chapter. If these bytes are all set to 0xFF then the value
/// should be ignored and the end time value should be utilized.
pub end_byte: u32,
/// The tags associated with the chapter.
pub tags: Vec<Tag>,
}

/// An item that belongs to a `TableOfContents`.
#[derive(Clone, Debug)]
pub enum TableOfContentsItem {
/// A `Chapter`.
Chapter(Chapter),
/// A sub-`TableOfContents`.
TableOfContents(TableOfContents),
}

/// `TableOfContents` describes different sections and chapters of an audio stream.
#[derive(Clone, Debug, Default)]
pub struct TableOfContents {
/// `ordered` should be set to `true` if the entries in the `items` list are ordered or set to
/// `false` if they not are ordered. This provides a hint as to whether the entries should be
/// played as a continuous ordered sequence or played individually.
pub ordered: bool,
/// The items that belong to this `TableOfContents`. They may be either a `Chapter` or a
/// sub-`TableOfContents`.
pub items: Vec<TableOfContentsItem>,
/// The tags associated with this `TableOfContents`.
pub tags: Vec<Tag>,
}

/// A 2 dimensional (width and height) size type.
#[derive(Copy, Clone, Debug, Default)]
pub struct Size {
Expand Down Expand Up @@ -392,6 +436,8 @@ pub struct VendorData {
#[derive(Clone, Debug, Default)]
pub struct MetadataRevision {
tags: Vec<Tag>,
table_of_contents: Option<TableOfContents>,
extra_chapters: Vec<Chapter>,
visuals: Vec<Visual>,
vendor_data: Vec<VendorData>,
}
Expand All @@ -402,6 +448,19 @@ impl MetadataRevision {
&self.tags
}

/// Gets an immutable reference to the base `TableOfContents` in this revision.
/// If there is more than one base `TableOfContents`, this returns the one that was most
/// recently parsed.
pub fn table_of_contents(&self) -> Option<&TableOfContents> {
self.table_of_contents.as_ref()
}

/// Gets an immutable slice to the `Chapters`s that are not related to any `TableOfContents` in
/// this revision.
pub fn extra_chapters(&self) -> &[Chapter] {
&self.extra_chapters
}

/// Gets an immutable slice to the `Visual`s in this revision.
pub fn visuals(&self) -> &[Visual] {
&self.visuals
Expand Down Expand Up @@ -431,6 +490,18 @@ impl MetadataBuilder {
self
}

/// Add a `Chapter` to the metadata.
pub fn add_chapter(&mut self, chapter: Chapter) -> &mut Self {
self.metadata.extra_chapters.push(chapter);
self
}

/// Add a `TableOfContents` to the metadata.
pub fn add_table_of_contents(&mut self, toc: TableOfContents) -> &mut Self {
self.metadata.table_of_contents = Some(toc);
self
}

/// Add a `Visual` to the metadata.
pub fn add_visual(&mut self, visual: Visual) -> &mut Self {
self.metadata.visuals.push(visual);
Expand Down Expand Up @@ -483,8 +554,7 @@ impl<'a> Metadata<'a> {
pub fn pop(&mut self) -> Option<MetadataRevision> {
if self.revisions.len() > 1 {
self.revisions.pop_front()
}
else {
} else {
None
}
}
Expand Down
55 changes: 49 additions & 6 deletions symphonia-metadata/src/id3v2/frames.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ use super::util;
// CRM Encrypted meta frame
// x PIC APIC Attached picture
// ASPI Audio seek point index
// x CHAP Chapter Chapter frame
// x COM COMM Comment Comments
// COMR Commercial frame
// x CTOC TableOfContents Chapter table-of-contents frame
// ENCR Encryption method registration
// EQU EQUA Equalisation
// EQU2 Equalisation (2)
Expand Down Expand Up @@ -255,8 +257,10 @@ lazy_static! {
// m.insert(b"AENC", read_null_frame);
m.insert(b"APIC", (read_apic_frame as FrameParser, None));
// m.insert(b"ASPI", read_null_frame);
m.insert(b"CHAP", (read_chap_frame, Some(StandardTagKey::Chapter)));
m.insert(b"COMM", (read_comm_uslt_frame, Some(StandardTagKey::Comment)));
// m.insert(b"COMR", read_null_frame);
m.insert(b"CTOC", (read_ctoc_frame, Some(StandardTagKey::TableOfContents)));
// m.insert(b"ENCR", read_null_frame);
// m.insert(b"EQU2", read_null_frame);
// m.insert(b"EQUA", read_null_frame);
Expand Down Expand Up @@ -657,8 +661,7 @@ fn read_text_frame(
let text = scan_text(reader, encoding, len)?;

tags.push(Tag::new(std_key, id, Value::from(text)));
}
else {
} else {
break;
}
}
Expand Down Expand Up @@ -699,8 +702,7 @@ fn read_txxx_frame(
if len > 0 {
let text = scan_text(reader, encoding, len)?;
tags.push(Tag::new(std_key, &key, Value::from(text)));
}
else {
} else {
break;
}
}
Expand Down Expand Up @@ -765,6 +767,48 @@ fn read_priv_frame(
Ok(FrameResult::Tag(tag))
}

/// Reads a `CHAP` (chapter)
fn read_chap_frame(
reader: &mut BufReader<'_>,
std_key: Option<StandardTagKey>,
_id: &str,
) -> Result<FrameResult> {
// Scan for the element_id
let element_id = format!(
"CHAP:{}",
scan_text(reader, Encoding::Iso8859_1, reader.bytes_available() as usize)?
);

// There is other useful chapter data, but we parse it elsewhere
let data_buf = reader.read_buf_bytes_ref(reader.bytes_available() as usize)?;

// Create a Tag.
let tag = Tag::new(std_key, &element_id, Value::from(data_buf));

Ok(FrameResult::Tag(tag))
}

/// Reads a `CTOC` (table of contents)
fn read_ctoc_frame(
reader: &mut BufReader<'_>,
std_key: Option<StandardTagKey>,
_id: &str,
) -> Result<FrameResult> {
// Scan for the element_id
let element_id = format!(
"CTOC:{}",
scan_text(reader, Encoding::Iso8859_1, reader.bytes_available() as usize)?
);

// There is other useful table of contents data, but we parse it elsewhere
let data_buf = reader.read_buf_bytes_ref(reader.bytes_available() as usize)?;

// Create a Tag.
let tag = Tag::new(std_key, &element_id, Value::from(data_buf));

Ok(FrameResult::Tag(tag))
}

/// Reads a `COMM` (comment) or `USLT` (unsynchronized comment) frame.
fn read_comm_uslt_frame(
reader: &mut BufReader<'_>,
Expand All @@ -785,8 +829,7 @@ fn read_comm_uslt_frame(
// an error would break far too many files to be worth it.
let key = if validate_lang_code(lang) {
format!("{}!{}", id, as_ascii_str(&lang))
}
else {
} else {
id.to_string()
};

Expand Down

0 comments on commit 6db6e6f

Please sign in to comment.