craft-aaf: use referenced slot rates for source offsets

* craft-aaf: use referenced slot rate for source offsets
This commit is contained in:
Dylan M. Taylor authored and GitHub committed 2026-11-12 07:04:54 -05:00
1 parent c95885f6d9
commit 3728fd1ffb
4 files changed
+60 -3

No files matched your search

+14 -2
View File
@@ -386,7 +386,6 @@ impl R<'_> {
fn clip(&mut self, c: &Obj, rate: (i64, i64), kind: CKind, master_key: Option<String>) -> CItem {
let len = units_to_ticks(len_of(c), rate.0, rate.1);
let start = units_to_ticks(c.i64(pid::START_TIME).unwrap_or(0), rate.0, rate.1);
let name = tagged_values(c, pid::COMPONENT_USER_COMMENTS).into_iter().find(|(k, _)| k == "Clip Name").map(|(_, v)| v).unwrap_or_default();
let (Some(id), Some(slot)) = (c.mob_id(pid::SOURCE_ID), c.u32(pid::SOURCE_MOB_SLOT_ID)) else { return CItem::Filler(len) };
if id == [0; 32] {
@@ -395,6 +394,13 @@ impl R<'_> {
if let Some(mob) = self.mobs.get(&id).copied().filter(|m| m.class == cls::COMPOSITION_MOB) {
return self.nest_clip(c, NestClip { mob, mob_id: id, slot_id: slot, rate, kind, name });
}
let source_rate = self
.mobs
.get(&id)
.and_then(|mob| mob.objs(pid::SLOTS).iter().find(|s| s.u32(pid::SLOT_ID) == Some(slot)))
.and_then(|s| rate_of(s, pid::EDIT_RATE))
.unwrap_or(rate);
let start = units_to_ticks(c.i64(pid::START_TIME).unwrap_or(0), source_rate.0, source_rate.1);
match self.resolve(id, slot, start, kind, master_key, 0) {
Some((source, t)) => CItem::Clip(CClip { len, source, start: t, gain: None, name }),
None => CItem::Filler(len),
@@ -491,9 +497,15 @@ impl R<'_> {
if mob.class == cls::MASTER_MOB {
let key = master_key.unwrap_or_else(|| hex(&id));
let clip = first_clip(seg)?;
let inner = units_to_ticks(clip.i64(pid::START_TIME).unwrap_or(0), rate.0, rate.1);
let next = clip.mob_id(pid::SOURCE_ID)?;
let next_slot = clip.u32(pid::SOURCE_MOB_SLOT_ID)?;
let source_rate = self
.mobs
.get(&next)
.and_then(|m| m.objs(pid::SLOTS).iter().find(|s| s.u32(pid::SLOT_ID) == Some(next_slot)))
.and_then(|s| rate_of(s, pid::EDIT_RATE))
.unwrap_or(rate);
let inner = units_to_ticks(clip.i64(pid::START_TIME).unwrap_or(0), source_rate.0, source_rate.1);
if next == [0; 32] {
return None;
}
+21
View File
@@ -473,6 +473,27 @@ fn audio_tracks_keep_their_names() {
}
}
#[test]
fn sound_source_offsets_use_audio_sample_rate_on_disk() {
let doc = crate::testdoc::sound_only(None);
let bytes = written(&doc, false);
let root = store::read(&bytes).unwrap();
let mut all = Vec::new();
walk(&root, &mut all);
let starts: Vec<i64> = all.iter().filter(|o| o.class == cls::SOURCE_CLIP).filter_map(|o| o.i64(pid::START_TIME)).collect();
assert!(starts.contains(&19_200), "expected 0.4 seconds at 48 kHz: {starts:?}");
let back = read_clean(&bytes);
let clip = back.compositions[0].tracks[0]
.items
.iter()
.find_map(|i| match i {
CItem::Clip(c) => Some(c),
_ => None,
})
.unwrap();
assert_eq!(clip.start, craft_timeline::time::FrameRate::FPS_25.tick_of(10));
}
#[test]
fn flattened_selectors_and_nested_scopes_are_reported() {
for (class, property, message) in
+1 -1
View File
@@ -360,7 +360,7 @@ pub(crate) fn write(doc: &Document, version: craft_cfb::Version, identity: &Iden
// ---- nested compositions: a clip refers to one as it does to a master mob, through the
// composition's first track of the clip's kind (a broken-out channel: that channel's track)
let mut refs = Refs { mobs: master_ref, rates: HashMap::new() };
let mut refs = Refs { mobs: master_ref, rates: doc.sources.iter().enumerate().map(|(i, s)| (i, source_rate(s))).collect() };
let nested_ids: Vec<[u8; 32]> = doc.nested.iter().map(|_| ids.next()).collect();
for (si, s) in doc.sources.iter().enumerate() {
let Some((comp, id)) = s.nested.and_then(|n| Some((doc.nested.get(n)?, nested_ids.get(n)?))) else { continue };
+24
View File
@@ -217,3 +217,27 @@ fn a_composition_nested_in_itself_is_a_gap() {
assert_eq!(back.nested.len(), 1);
assert!(matches!(back.nested[0].tracks[0].items[..], [CItem::Filler(_)]), "{:?}", back.nested[0].tracks[0].items);
}
#[test]
fn aaf_source_start_uses_referenced_slot_rate() {
let doc = sound_only(None);
let bytes = aaf_bytes(&doc, false);
let back = read_clean(&bytes, true);
let original = doc.compositions[0].tracks[0]
.items
.iter()
.find_map(|item| match item {
CItem::Clip(c) => Some(c.start),
_ => None,
})
.unwrap();
let actual = back.compositions[0].tracks[0]
.items
.iter()
.find_map(|item| match item {
CItem::Clip(c) => Some(c.start),
_ => None,
})
.unwrap();
assert_eq!(actual, original);
}