Files
Psychotoxical-psysonic/src-tauri/patches/symphonia-format-isomp4/src/stream.rs
T
Psychotoxical a4c400cc69 fix(audio): correctly parse iTunSMPB gapless info from older iTunes M4A files
M4A files store the iTunSMPB value behind a 16-byte binary 'data' atom
header. The previous parser took bytes directly after the ASCII key,
landing in the binary header and producing a corrupted parts array
(total_valid=348 instead of ~13M samples), which caused take_duration
to exhaust the source in ~8ms — silent no-playback.

Fix: search for the " 00000000 " sentinel within 128 bytes of the key
to locate the actual value string, falling back to the old offset for
ID3v2/MP3 files where no binary header exists.

Also patches symphonia-format-isomp4 (local crate under patches/) to:
- Tolerate SL descriptor predefined=0x01 (older iTunes M4A)
- Convert descriptor.unwrap() panic to a proper decode error
- Gracefully skip malformed trak atoms (e.g. MJPEG cover-art streams)
  instead of failing the entire probe

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 18:54:10 +02:00

445 lines
16 KiB
Rust

// Symphonia
// Copyright (c) 2019-2022 The Project Symphonia Developers.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
use symphonia_core::errors::{decode_error, Error, Result};
use crate::atoms::{stsz::SampleSize, Co64Atom, MoofAtom, MoovAtom, MvexAtom, StcoAtom, TrafAtom};
use std::ops::Range;
use std::sync::Arc;
/// Sample data information.
pub struct SampleDataDesc {
/// The starting byte position within the media data of the group of samples that contains the
/// sample described.
pub base_pos: u64,
/// The offset relative to the base position of the sample described.
pub offset: Option<u64>,
/// The size of the sample.
pub size: u32,
}
/// Timing information for one sample.
pub struct SampleTiming {
/// The timestamp of the sample.
pub ts: u64,
/// The duration of the sample.
pub dur: u32,
}
pub trait StreamSegment: Send + Sync {
/// Gets the sequence number of this segment.
fn sequence_num(&self) -> u32;
/// Gets the first and last sample numbers for the track `track_num`.
fn track_sample_range(&self, track_num: usize) -> Range<u32>;
/// Gets the first and last sample timestamps for the track `track_num`.
fn track_ts_range(&self, track_num: usize) -> Range<u64>;
/// Get the timestamp and duration for the sample indicated by `sample_num` for the track
/// `track_num`.
fn sample_timing(&self, track_num: usize, sample_num: u32) -> Result<Option<SampleTiming>>;
/// Get the sample number of the sample containing the timestamp indicated by `ts` for track
// `track_num`.
fn ts_sample(&self, track_num: usize, ts: u64) -> Result<Option<u32>>;
/// Get the byte position of the group of samples containing the sample indicated by
/// `sample_num` for track `track_num`, and it's size.
///
/// Optionally, the offset of the sample relative to the aforementioned byte position can be
/// returned.
fn sample_data(
&self,
track_num: usize,
sample_num: u32,
get_offset: bool,
) -> Result<SampleDataDesc>;
}
/// Track-to-stream sequencing information.
#[derive(Copy, Clone, Debug, Default)]
struct SequenceInfo {
/// The sample number of the first sample of a track in a fragment.
first_sample: u32,
/// The timestamp of the first sample of a track in a fragment.
first_ts: u64,
/// The total duration of all samples of a track in a fragment.
total_sample_duration: u64,
/// The total sample count of a track in a fragment.
total_sample_count: u32,
/// If present in the moof segment, this is the index of the track fragment atom for the track
/// this sequence information is associated with.
traf_idx: Option<usize>,
}
pub struct MoofSegment {
moof: MoofAtom,
mvex: Arc<MvexAtom>,
seq: Vec<SequenceInfo>,
}
impl MoofSegment {
/// Instantiate a new segment from a `MoofAtom`.
pub fn new(moof: MoofAtom, mvex: Arc<MvexAtom>, prev: &dyn StreamSegment) -> MoofSegment {
let mut seq = Vec::with_capacity(mvex.trexs.len());
// Calculate the sequence information for each track, even if not present in the fragment.
for (track_num, trex) in mvex.trexs.iter().enumerate() {
let mut info = SequenceInfo {
first_sample: prev.track_sample_range(track_num).end,
first_ts: prev.track_ts_range(track_num).end,
..Default::default()
};
// Find the track fragment for the track.
for (traf_idx, traf) in moof.trafs.iter().enumerate() {
if trex.track_id != traf.tfhd.track_id {
continue;
}
// Calculate the total duration of all runs in the fragment for the track.
let default_dur =
traf.tfhd.default_sample_duration.unwrap_or(trex.default_sample_duration);
for trun in traf.truns.iter() {
info.total_sample_duration += trun.total_duration(default_dur);
}
info.total_sample_count = traf.total_sample_count;
info.traf_idx = Some(traf_idx);
}
seq.push(info);
}
MoofSegment { moof, mvex, seq }
}
/// Try to get the Track Fragment atom associated with the track identified by `track_num`.
fn try_get_traf(&self, track_num: usize) -> Option<&TrafAtom> {
debug_assert!(track_num < self.seq.len());
self.seq[track_num].traf_idx.map(|idx| &self.moof.trafs[idx])
}
}
impl StreamSegment for MoofSegment {
fn sequence_num(&self) -> u32 {
self.moof.mfhd.sequence_number
}
fn sample_timing(&self, track_num: usize, sample_num: u32) -> Result<Option<SampleTiming>> {
// Get the track fragment associated with track_num.
let traf = match self.try_get_traf(track_num) {
Some(traf) => traf,
None => return Ok(None),
};
let mut sample_num_rel = sample_num - self.seq[track_num].first_sample;
let mut trun_ts_offset = self.seq[track_num].first_ts;
let default_dur = traf
.tfhd
.default_sample_duration
.unwrap_or(self.mvex.trexs[track_num].default_sample_duration);
for trun in traf.truns.iter() {
// If the sample is contained within the this track run, get the timing of of the
// sample.
if sample_num_rel < trun.sample_count {
let (ts, dur) = trun.sample_timing(sample_num_rel, default_dur);
return Ok(Some(SampleTiming { ts: trun_ts_offset + ts, dur }));
}
let trun_dur = trun.total_duration(default_dur);
sample_num_rel -= trun.sample_count;
trun_ts_offset += trun_dur;
}
Ok(None)
}
fn ts_sample(&self, track_num: usize, ts: u64) -> Result<Option<u32>> {
// Get the track fragment associated with track_num.
let traf = match self.try_get_traf(track_num) {
Some(traf) => traf,
None => return Ok(None),
};
let mut sample_num = self.seq[track_num].first_sample;
let mut ts_accum = self.seq[track_num].first_ts;
let default_dur = traf
.tfhd
.default_sample_duration
.unwrap_or(self.mvex.trexs[track_num].default_sample_duration);
for trun in traf.truns.iter() {
// Get the total duration of this track run.
let trun_dur = trun.total_duration(default_dur);
// If the timestamp after the track run is greater than the desired timestamp, then the
// desired sample must be in this run of samples.
if ts_accum + trun_dur > ts {
sample_num += trun.ts_sample(ts - ts_accum, default_dur);
return Ok(Some(sample_num));
}
sample_num += trun.sample_count;
ts_accum += trun_dur;
}
Ok(None)
}
fn sample_data(
&self,
track_num: usize,
sample_num: u32,
get_offset: bool,
) -> Result<SampleDataDesc> {
// Get the track fragment associated with track_num.
let traf = self.try_get_traf(track_num).unwrap();
// If an explicit anchor-point is set, then use that for the position, otherwise use the
// first-byte of the enclosing moof atom.
let traf_base_pos = match traf.tfhd.base_data_offset {
Some(pos) => pos,
_ => self.moof.moof_base_pos,
};
let mut sample_num_rel = sample_num - self.seq[track_num].first_sample;
let mut trun_offset = traf_base_pos;
let default_size =
traf.tfhd.default_sample_size.unwrap_or(self.mvex.trexs[track_num].default_sample_size);
for trun in traf.truns.iter() {
// If a data offset is present for this track fragment run, then calculate the new base
// position for the run. When a data offset is not present, do nothing because this run
// follows the previous run.
if let Some(offset) = trun.data_offset {
// The offset for the run is relative to the anchor-point defined in the track
// fragment header.
trun_offset = if offset.is_negative() {
traf_base_pos - u64::from(offset.wrapping_abs() as u32)
}
else {
traf_base_pos + offset as u64
};
}
if sample_num_rel < trun.sample_count {
let (offset, size) = if get_offset {
// Get the size and offset of the sample.
let (offset, size) = trun.sample_offset(sample_num_rel, default_size);
(Some(offset), size)
}
else {
// Just get the size of the sample.
let size = trun.sample_size(sample_num_rel, default_size);
(None, size)
};
return Ok(SampleDataDesc { base_pos: trun_offset, size, offset });
}
// Get the total size of the track fragment run.
let trun_size = trun.total_size(default_size);
sample_num_rel -= trun.sample_count;
trun_offset += trun_size;
}
decode_error("isomp4: invalid sample index")
}
fn track_sample_range(&self, track_num: usize) -> Range<u32> {
debug_assert!(track_num < self.seq.len());
let track = &self.seq[track_num];
track.first_sample..track.first_sample + track.total_sample_count
}
fn track_ts_range(&self, track_num: usize) -> Range<u64> {
debug_assert!(track_num < self.seq.len());
let track = &self.seq[track_num];
track.first_ts..track.first_ts + track.total_sample_duration
}
}
fn get_chunk_offset(
stco: &Option<StcoAtom>,
co64: &Option<Co64Atom>,
chunk: usize,
) -> Result<Option<u64>> {
// Get the offset from either the stco or co64 atoms.
if let Some(stco) = stco.as_ref() {
// 32-bit offset
if let Some(offset) = stco.chunk_offsets.get(chunk) {
Ok(Some(u64::from(*offset)))
}
else {
decode_error("isomp4: missing stco entry")
}
}
else if let Some(co64) = co64.as_ref() {
// 64-bit offset
if let Some(offset) = co64.chunk_offsets.get(chunk) {
Ok(Some(*offset))
}
else {
decode_error("isomp4: missing co64 entry")
}
}
else {
// This should never happen because it is mandatory to have either a stco or co64 atom.
decode_error("isomp4: missing stco or co64 atom")
}
}
pub struct MoovSegment {
moov: MoovAtom,
}
impl MoovSegment {
/// Instantiate a segment from the provide moov atom.
pub fn new(moov: MoovAtom) -> MoovSegment {
MoovSegment { moov }
}
}
impl StreamSegment for MoovSegment {
fn sequence_num(&self) -> u32 {
// The segment defined by the moov atom is always 0.
0
}
fn sample_timing(&self, track_num: usize, sample_num: u32) -> Result<Option<SampleTiming>> {
// Get the trak atom associated with track_num.
debug_assert!(track_num < self.moov.traks.len());
let trak = &self.moov.traks[track_num];
// Find the sample timing. Note, complexity of O(N).
let timing = trak.mdia.minf.stbl.stts.find_timing_for_sample(sample_num);
if let Some((ts, dur)) = timing {
Ok(Some(SampleTiming { ts, dur }))
}
else {
Ok(None)
}
}
fn ts_sample(&self, track_num: usize, ts: u64) -> Result<Option<u32>> {
// Get the trak atom associated with track_num.
debug_assert!(track_num < self.moov.traks.len());
let trak = &self.moov.traks[track_num];
// Find the sample timestamp. Note, complexity of O(N).
Ok(trak.mdia.minf.stbl.stts.find_sample_for_timestamp(ts))
}
fn sample_data(
&self,
track_num: usize,
sample_num: u32,
get_offset: bool,
) -> Result<SampleDataDesc> {
// Get the trak atom associated with track_num.
debug_assert!(track_num < self.moov.traks.len());
let trak = &self.moov.traks[track_num];
// Get the constituent tables.
let stsz = &trak.mdia.minf.stbl.stsz;
let stsc = &trak.mdia.minf.stbl.stsc;
let stco = &trak.mdia.minf.stbl.stco;
let co64 = &trak.mdia.minf.stbl.co64;
// Find the sample-to-chunk mapping. Note, complexity of O(log N).
let group = stsc
.find_entry_for_sample(sample_num)
.ok_or(Error::DecodeError("invalid sample index"))?;
// Index of the sample relative to the chunk group.
let sample_in_group = sample_num - group.first_sample;
// Index of the chunk containing the sample relative to the chunk group.
let chunk_in_group = sample_in_group / group.samples_per_chunk;
// Index of the chunk containing the sample relative to the entire stream.
let chunk_in_stream = group.first_chunk + chunk_in_group;
// Get the byte position of the first sample of the chunk containing the sample.
let base_pos = get_chunk_offset(stco, co64, chunk_in_stream as usize)?.unwrap();
// Determine the absolute sample byte position if requested by calculating the offset of
// the sample from the base position of the chunk.
let offset = if get_offset {
// Index of the sample relative to the chunk containing the sample.
let sample_in_chunk = sample_in_group - (chunk_in_group * group.samples_per_chunk);
// Calculat the byte offset of the sample relative to the chunk containing it.
let offset = match stsz.sample_sizes {
SampleSize::Constant(size) => {
// Constant size samples can be calculated directly.
u64::from(sample_in_chunk) * u64::from(size)
}
SampleSize::Variable(ref entries) => {
// For variable size samples, sum the sizes of all the samples preceeding the
// desired sample in the chunk.
let chunk_first_sample = (sample_num - sample_in_chunk) as usize;
if let Some(samples) = entries.get(chunk_first_sample..sample_num as usize) {
samples.iter().map(|&size| u64::from(size)).sum()
}
else {
return decode_error("isomp4: missing one or more stsz entries");
}
}
};
Some(offset)
}
else {
None
};
// Get the size in bytes of the sample.
let size = match stsz.sample_sizes {
SampleSize::Constant(size) => size,
SampleSize::Variable(ref entries) => {
if let Some(size) = entries.get(sample_num as usize) {
*size
}
else {
return decode_error("isomp4: missing stsz entry");
}
}
};
Ok(SampleDataDesc { base_pos, size, offset })
}
fn track_sample_range(&self, track_num: usize) -> Range<u32> {
debug_assert!(track_num < self.moov.traks.len());
0..self.moov.traks[track_num].mdia.minf.stbl.stsz.sample_count
}
fn track_ts_range(&self, track_num: usize) -> Range<u64> {
debug_assert!(track_num < self.moov.traks.len());
0..self.moov.traks[track_num].mdia.minf.stbl.stts.total_duration
}
}