Skip to content

Commit

Permalink
core (formats,meta): Replace and rewrite cues with chapters.
Browse files Browse the repository at this point in the history
What most other media players and libraries call "chapters", Symphonia
called "cues". Since cues were based off of FLAC's embedded cuesheets,
cues were not able to fully represent the chapter structure of other
media formats. Therefore, replace cues with chapters.

Chapters in Symphonia are a tree-like data structure: `ChapterGroup`s
(nodes) may contain one or more `Chapter`s (leaves), or other
`ChapterGroup`s. Both may contain optional metadata.

Unlike previously, `Chapters` are now defined by a start time, but also an
optional end time. `Chapters` may also overlap.

Last, like metadata, chapters may be pre-populated and passed to the format
reader. This allows chapters stored in ID3v2 tags and other non-native
metadata formats to be passed to the format reader.

Fixes #16.
  • Loading branch information
pdeljanov committed Oct 22, 2024
1 parent 4b5cb75 commit dc287cc
Show file tree
Hide file tree
Showing 20 changed files with 1,034 additions and 402 deletions.
1 change: 1 addition & 0 deletions CONTRIBUTORS
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ Herohtar [https://github.com/herohtar]
nicholaswyoung [https://github.com/nicholaswyoung]
richardmitic [https://github.com/richardmitic]
terrorfisch [https://github.com/terrorfisch]
Tommoa [https://github.com/Tommoa]
123 changes: 66 additions & 57 deletions symphonia-bundle-flac/src/demuxer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pub struct FlacReader<'s> {
reader: MediaSourceStream<'s>,
metadata: MetadataLog,
tracks: Vec<Track>,
cues: Vec<Cue>,
chapters: Option<ChapterGroup>,
index: Option<SeekIndex>,
first_frame_offset: u64,
parser: PacketParser,
Expand Down Expand Up @@ -76,8 +76,8 @@ impl<'s> FlacReader<'s> {
let mut metadata_builder = MetadataBuilder::new();

let mut reader = mss;
let mut tracks = Vec::new();
let mut cues = Vec::new();
let mut track = None;
let mut chapters = None;
let mut index = None;
let mut parser = Default::default();

Expand All @@ -89,15 +89,23 @@ impl<'s> FlacReader<'s> {
let mut block_stream = ScopedStream::new(&mut reader, u64::from(header.block_len));

match header.block_type {
// The StreamInfo block is parsed into a track.
MetadataBlockType::StreamInfo => {
// Only a single stream information block is allowed.
if track.is_none() {
track = Some(read_stream_info_block(&mut block_stream, &mut parser)?);
}
else {
return decode_error("flac: found more than one stream info block");
}
}
MetadataBlockType::Application => {
// TODO: Store vendor data.
read_application_block(&mut block_stream, header.block_len)?;
}
// SeekTable blocks are parsed into a SeekIndex.
MetadataBlockType::SeekTable => {
// Check if a SeekTable has already be parsed. If one has, then the file is
// invalid, atleast for seeking. Either way, it's a violation of the
// specification.
// Only a single seek table block is allowed.
if index.is_none() {
let mut new_index = SeekIndex::new();
read_seek_table_block(&mut block_stream, header.block_len, &mut new_index)?;
Expand All @@ -113,16 +121,20 @@ impl<'s> FlacReader<'s> {
}
// Cuesheet blocks are parsed into Cues.
MetadataBlockType::Cuesheet => {
read_cuesheet_block(&mut block_stream, &mut cues)?;
// A cuesheet block must appear before the stream information block so that the
// timebase is known to calculate the cue times. This should always be the case
// since the stream information block must always be the first metadata block.
if let Some(tb) = track.as_ref().and_then(|track| track.time_base) {
chapters = Some(read_cuesheet_block(&mut block_stream, tb)?);
}
else {
return decode_error("flac: cuesheet block before stream info");
}
}
// Picture blocks are read as Visuals.
MetadataBlockType::Picture => {
read_picture_block(&mut block_stream, &mut metadata_builder)?;
}
// StreamInfo blocks are parsed into Streams.
MetadataBlockType::StreamInfo => {
read_stream_info_block(&mut block_stream, &mut tracks, &mut parser)?;
}
// Padding blocks are skipped.
MetadataBlockType::Padding => {
block_stream.ignore_bytes(u64::from(header.block_len))?;
Expand Down Expand Up @@ -150,8 +162,15 @@ impl<'s> FlacReader<'s> {
}
}

// A single stream information block is mandatory. So it is an error for track to be `None`
// after iterating over all metadata blocks.
let tracks = match track {
Some(track) => vec![track],
_ => return decode_error("flac: missing stream info block"),
};

// Commit any read metadata to the metadata log.
let mut metadata = opts.metadata.unwrap_or_default();
let mut metadata = opts.external_data.metadata.unwrap_or_default();
metadata.push(metadata_builder.metadata());

// Synchronize the packet parser to the first audio frame.
Expand All @@ -161,7 +180,7 @@ impl<'s> FlacReader<'s> {
// metadata blocks have been read.
let first_frame_offset = reader.pos();

Ok(FlacReader { reader, metadata, tracks, cues, index, first_frame_offset, parser })
Ok(FlacReader { reader, metadata, tracks, chapters, index, first_frame_offset, parser })
}
}

Expand Down Expand Up @@ -197,8 +216,8 @@ impl FormatReader for FlacReader<'_> {
self.metadata.metadata()
}

fn cues(&self) -> &[Cue] {
&self.cues
fn chapters(&self) -> Option<&ChapterGroup> {
self.chapters.as_ref()
}

fn tracks(&self) -> &[Track] {
Expand Down Expand Up @@ -360,57 +379,47 @@ impl FormatReader for FlacReader<'_> {
/// Reads a StreamInfo block and populates the reader with stream information.
fn read_stream_info_block<B: ReadBytes + FiniteStream>(
reader: &mut B,
tracks: &mut Vec<Track>,
parser: &mut PacketParser,
) -> Result<()> {
// Only one StreamInfo block, and therefore only one Track, is allowed per media source stream.
if tracks.is_empty() {
// Ensure the block length is correct for a stream information block before allocating a
// buffer for it.
if !StreamInfo::is_valid_size(reader.byte_len()) {
return decode_error("flac: invalid stream info block size");
}

// Read the stream information block as a boxed slice so that it may be attached as extra
// data on the codec parameters.
let extra_data = reader.read_boxed_slice_exact(reader.byte_len() as usize)?;

// Parse the stream info block.
let info = StreamInfo::read(&mut BufReader::new(&extra_data))?;
) -> Result<Track> {
// Ensure the block length is correct for a stream information block before allocating a
// buffer for it.
if !StreamInfo::is_valid_size(reader.byte_len()) {
return decode_error("flac: invalid stream info block size");
}

// Populate the codec parameters with the basic audio parameters of the track.
let mut codec_params = AudioCodecParameters::new();
// Read the stream information block as a boxed slice so that it may be attached as extra
// data on the codec parameters.
let extra_data = reader.read_boxed_slice_exact(reader.byte_len() as usize)?;

codec_params
.for_codec(CODEC_ID_FLAC)
.with_extra_data(extra_data)
.with_sample_rate(info.sample_rate)
.with_bits_per_sample(info.bits_per_sample)
.with_channels(info.channels.clone());
// Parse the stream info block.
let info = StreamInfo::read(&mut BufReader::new(&extra_data))?;

if let Some(md5) = info.md5 {
codec_params.with_verification_code(VerificationCheck::Md5(md5));
}
// Populate the codec parameters with the basic audio parameters of the track.
let mut codec_params = AudioCodecParameters::new();

// Populate the track.
let mut track = Track::new(0);
codec_params
.for_codec(CODEC_ID_FLAC)
.with_extra_data(extra_data)
.with_sample_rate(info.sample_rate)
.with_bits_per_sample(info.bits_per_sample)
.with_channels(info.channels.clone());

track.with_codec_params(CodecParameters::Audio(codec_params));
if let Some(md5) = info.md5 {
codec_params.with_verification_code(VerificationCheck::Md5(md5));
}

// Total samples per channel (also the total number of frames) is optional.
if let Some(num_frames) = info.n_samples {
track.with_num_frames(num_frames);
}
// Populate the track.
let mut track = Track::new(0);

// Reset the packet parser.
parser.reset(info);
track.with_codec_params(CodecParameters::Audio(codec_params));

// Add the track.
tracks.push(track);
}
else {
return decode_error("flac: found more than one stream info block");
// Total samples per channel (also the total number of frames) is optional.
if let Some(num_frames) = info.n_samples {
track.with_num_frames(num_frames);
}

Ok(())
// Reset the packet parser.
parser.reset(info);

Ok(track)
}
10 changes: 5 additions & 5 deletions symphonia-bundle-mp3/src/demuxer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const MP3_FORMAT_INFO: FormatInfo =
pub struct MpaReader<'s> {
reader: MediaSourceStream<'s>,
tracks: Vec<Track>,
cues: Vec<Cue>,
chapters: Option<ChapterGroup>,
metadata: MetadataLog,
enable_gapless: bool,
first_packet_pos: u64,
Expand Down Expand Up @@ -201,8 +201,8 @@ impl FormatReader for MpaReader<'_> {
self.metadata.metadata()
}

fn cues(&self) -> &[Cue] {
&self.cues
fn chapters(&self) -> Option<&ChapterGroup> {
self.chapters.as_ref()
}

fn tracks(&self) -> &[Track] {
Expand Down Expand Up @@ -467,8 +467,8 @@ impl<'s> MpaReader<'s> {
Ok(MpaReader {
reader: mss,
tracks: vec![track],
cues: Vec::new(),
metadata: opts.metadata.unwrap_or_default(),
chapters: opts.external_data.chapters,
metadata: opts.external_data.metadata.unwrap_or_default(),
enable_gapless: opts.enable_gapless,
first_packet_pos,
next_packet_ts: 0,
Expand Down
10 changes: 5 additions & 5 deletions symphonia-codec-aac/src/adts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const ADTS_FORMAT_INFO: FormatInfo = FormatInfo {
pub struct AdtsReader<'s> {
reader: MediaSourceStream<'s>,
tracks: Vec<Track>,
cues: Vec<Cue>,
chapters: Option<ChapterGroup>,
metadata: MetadataLog,
first_frame_pos: u64,
next_packet_ts: u64,
Expand Down Expand Up @@ -75,8 +75,8 @@ impl<'s> AdtsReader<'s> {
Ok(AdtsReader {
reader: mss,
tracks: vec![track],
cues: Vec::new(),
metadata: opts.metadata.unwrap_or_default(),
chapters: opts.external_data.chapters,
metadata: opts.external_data.metadata.unwrap_or_default(),
first_frame_pos,
next_packet_ts: 0,
})
Expand Down Expand Up @@ -296,8 +296,8 @@ impl FormatReader for AdtsReader<'_> {
self.metadata.metadata()
}

fn cues(&self) -> &[Cue] {
&self.cues
fn chapters(&self) -> Option<&ChapterGroup> {
self.chapters.as_ref()
}

fn tracks(&self) -> &[Track] {
Expand Down
Loading

0 comments on commit dc287cc

Please sign in to comment.