mirror of
https://github.com/storytold/filmcraft.git
synced 2026-10-30 04:57:19 -06:00
Merge pull request #1386 from marcoarnulfo/fix/nested-audio-preview-hash
fix(previews): a nest's audio preview follows the sound of its sequence (#1370)
This commit is contained in:
@@ -39,7 +39,7 @@
|
||||
//! size and rate match the sequence. Green (a valid preview exists) is decided by the preview
|
||||
//! store, which knows which hashes have files.
|
||||
|
||||
use filmcraft_project::{ClipId, ItemId, ItemKind, Project, Sequence, TrackItem, Transition};
|
||||
use filmcraft_project::{ClipId, ItemId, ItemKind, Project, Sequence, TrackItem, TrackKind, Transition};
|
||||
use filmcraft_time::{FrameRate, Tick};
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -166,7 +166,7 @@ pub fn video_segments(project: &Project, seq_id: ItemId) -> Vec<Segment> {
|
||||
match l {
|
||||
Layer::Item(ti, it) => {
|
||||
h.json(&("item", ti));
|
||||
hash_item(&mut h, project, it, t, 0);
|
||||
hash_item(&mut h, project, it, t, TrackKind::Video, 0);
|
||||
absolute |= prints_time(it);
|
||||
clips.push(it.id);
|
||||
}
|
||||
@@ -180,7 +180,7 @@ pub fn video_segments(project: &Project, seq_id: ItemId) -> Vec<Segment> {
|
||||
for side in [a, b] {
|
||||
match side {
|
||||
Some(it) => {
|
||||
hash_item(&mut h, project, it, t, 0);
|
||||
hash_item(&mut h, project, it, t, TrackKind::Video, 0);
|
||||
absolute |= prints_time(it);
|
||||
clips.push(it.id);
|
||||
}
|
||||
@@ -277,7 +277,7 @@ pub fn audio_segments(project: &Project, seq_id: ItemId) -> Vec<AudioSegment> {
|
||||
for side in [x.from, x.to] {
|
||||
match side.and_then(|id| tr.item(id)).filter(|i| i.enabled) {
|
||||
Some(it) => {
|
||||
hash_item(&mut h, project, it, t, 0);
|
||||
hash_item(&mut h, project, it, t, TrackKind::Audio, 0);
|
||||
heard = true;
|
||||
}
|
||||
None => h.json(&"none"),
|
||||
@@ -285,7 +285,7 @@ pub fn audio_segments(project: &Project, seq_id: ItemId) -> Vec<AudioSegment> {
|
||||
}
|
||||
} else if let Some(it) = tr.item_at(t).filter(|i| i.enabled) {
|
||||
h.json(&("item", ti));
|
||||
hash_item(&mut h, project, it, t, 0);
|
||||
hash_item(&mut h, project, it, t, TrackKind::Audio, 0);
|
||||
heard = true;
|
||||
}
|
||||
}
|
||||
@@ -300,7 +300,9 @@ fn prints_time(it: &TrackItem) -> bool {
|
||||
it.effects.iter().any(|e| e.enabled && e.effect == "timecode")
|
||||
}
|
||||
|
||||
fn hash_item(h: &mut Fnv128, project: &Project, it: &TrackItem, origin: Tick, depth: u32) {
|
||||
/// Hash a clip and its source as heard (`kind` Audio) or seen (`kind` Video): a nested sequence
|
||||
/// contributes only the tracks of that kind, so editing its picture keeps its sound's previews.
|
||||
fn hash_item(h: &mut Fnv128, project: &Project, it: &TrackItem, origin: Tick, kind: TrackKind, depth: u32) {
|
||||
let mut c = it.clone();
|
||||
c.id = Default::default();
|
||||
c.start -= origin;
|
||||
@@ -312,10 +314,10 @@ fn hash_item(h: &mut Fnv128, project: &Project, it: &TrackItem, origin: Tick, de
|
||||
c.name.clear();
|
||||
}
|
||||
h.json(&c);
|
||||
hash_source(h, project, it.item, depth);
|
||||
hash_source(h, project, it.item, kind, depth);
|
||||
}
|
||||
|
||||
fn hash_source(h: &mut Fnv128, project: &Project, id: ItemId, depth: u32) {
|
||||
fn hash_source(h: &mut Fnv128, project: &Project, id: ItemId, kind: TrackKind, depth: u32) {
|
||||
let Some(pi) = project.item(id) else {
|
||||
h.json(&"missing");
|
||||
return;
|
||||
@@ -338,18 +340,34 @@ fn hash_source(h: &mut Fnv128, project: &Project, id: ItemId, depth: u32) {
|
||||
}
|
||||
ItemKind::Subclip { parent, range, .. } => {
|
||||
h.json(&("subclip", range));
|
||||
hash_source(h, project, *parent, depth + 1);
|
||||
hash_source(h, project, *parent, kind, depth + 1);
|
||||
}
|
||||
ItemKind::AdjustmentLayer { .. } => h.json(&("adjustment", &pi.kind)),
|
||||
ItemKind::Graphic { .. } => h.json(&("graphic", &pi.kind)),
|
||||
ItemKind::Sequence(s) => {
|
||||
h.json(&("sequence", &s.settings));
|
||||
if kind == TrackKind::Audio {
|
||||
// the nest's sound is the nested sequence's whole mix (see `audio_segments`)
|
||||
h.json(&("mix", s.master_volume_db, &s.master_effects, &s.master_mixer, &s.submix_tracks, &s.multicam));
|
||||
for (ti, tr) in s.audio_tracks.iter().enumerate() {
|
||||
h.json(&("track", ti, tr.muted, tr.solo, tr.volume_db, tr.pan, tr.channels, &tr.effects, &tr.mixer, tr.items.len()));
|
||||
for it in &tr.items {
|
||||
hash_item(h, project, it, Tick::ZERO, kind, depth + 1);
|
||||
}
|
||||
for x in &tr.transitions {
|
||||
let mut xc = x.clone();
|
||||
xc.id = Default::default();
|
||||
h.json(&xc);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
// a nested sequence's captions are drawn into its picture
|
||||
h.json(&("captions", &s.caption_tracks));
|
||||
for (ti, tr) in s.video_tracks.iter().enumerate() {
|
||||
h.json(&("track", ti, tr.enabled, tr.items.len()));
|
||||
for it in &tr.items {
|
||||
hash_item(h, project, it, Tick::ZERO, depth + 1);
|
||||
hash_item(h, project, it, Tick::ZERO, kind, depth + 1);
|
||||
}
|
||||
for x in &tr.transitions {
|
||||
let mut xc = x.clone();
|
||||
|
||||
@@ -5,7 +5,7 @@ use filmcraft_media::{DemoScene, Generator, MediaSource};
|
||||
use filmcraft_project::{ClipId, ItemId, ItemKind, Label, MediaClip, MediaRef, ParamValue, Project, SequenceSettings, TrackKind, find_effect};
|
||||
use filmcraft_time::{FrameRate, TICKS_PER_SECOND, Tick, TimeRange};
|
||||
|
||||
use crate::preview::{Need, Segment, effect_cost_ms, segment_at, video_segments};
|
||||
use crate::preview::{Need, Segment, audio_segments, effect_cost_ms, segment_at, video_segments};
|
||||
|
||||
fn add_gen(p: &mut Project, g: GeneratorSource) -> ItemId {
|
||||
let info = g.info().clone();
|
||||
@@ -248,3 +248,54 @@ fn effect_costs() {
|
||||
println!("{ms:8.2} ms {id:24} tier {:.0}", effect_cost_ms(id));
|
||||
}
|
||||
}
|
||||
|
||||
/// Put `item` on track 0 of `kind` in `seq` from frame `start_f` for `dur_f` frames.
|
||||
fn place_kind(p: &mut Project, seq: ItemId, kind: TrackKind, item: ItemId, start_f: i64, dur_f: i64) -> ClipId {
|
||||
let r = FrameRate::FPS_24;
|
||||
let ti = p.make_track_item(item, kind, r.tick_of(start_f), TimeRange::new(Tick::ZERO, r.tick_of(dur_f)), r).unwrap();
|
||||
let id = ti.id;
|
||||
let t = &mut p.sequence_mut(seq).unwrap().tracks_mut(kind)[0];
|
||||
t.items.push(ti);
|
||||
t.sort();
|
||||
id
|
||||
}
|
||||
|
||||
fn audio_hashes(p: &Project, seq: ItemId) -> Vec<String> {
|
||||
audio_segments(p, seq).into_iter().map(|g| g.hash).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_nests_audio_previews_follow_the_sound_of_its_sequence() {
|
||||
// Inner has a picture clip and a sound clip; Outer nests Inner (picture and sound), and Top
|
||||
// nests Outer's sound
|
||||
let (mut p, matte, ocean, inner) = setup();
|
||||
let pic = place_kind(&mut p, inner, TrackKind::Video, matte, 0, 48);
|
||||
let snd = place_kind(&mut p, inner, TrackKind::Audio, ocean, 0, 48);
|
||||
let settings = p.sequence(inner).unwrap().settings.clone();
|
||||
let outer = p.new_sequence("outer", settings.clone(), 1, 1, None);
|
||||
place_kind(&mut p, outer, TrackKind::Video, inner, 24, 48);
|
||||
place_kind(&mut p, outer, TrackKind::Audio, inner, 24, 48);
|
||||
let top = p.new_sequence("top", settings, 1, 1, None);
|
||||
place_kind(&mut p, top, TrackKind::Audio, outer, 12, 72);
|
||||
let (audio, picture, deep) = (audio_hashes(&p, outer), video_segments(&p, outer), audio_hashes(&p, top));
|
||||
assert_eq!(audio.len(), 1);
|
||||
|
||||
// the sound inside the nest changes: the nest's audio preview is stale, its picture is not
|
||||
p.sequence_mut(inner).unwrap().find_item_mut(snd).unwrap().1.gain_db = -12.0;
|
||||
assert_ne!(audio_hashes(&p, outer), audio, "a clip's gain inside the nest");
|
||||
assert_ne!(audio_hashes(&p, top), deep, "a clip's gain two nests down");
|
||||
assert_eq!(bounds(&video_segments(&p, outer)), bounds(&picture));
|
||||
assert_eq!(by_frame(&video_segments(&p, outer), 30), by_frame(&picture, 30), "the picture did not change");
|
||||
let (audio, deep) = (audio_hashes(&p, outer), audio_hashes(&p, top));
|
||||
// the nested sequence's track mix counts too
|
||||
p.sequence_mut(inner).unwrap().audio_tracks[0].muted = true;
|
||||
assert_ne!(audio_hashes(&p, outer), audio, "a muted track inside the nest");
|
||||
p.sequence_mut(inner).unwrap().audio_tracks[0].muted = false;
|
||||
assert_eq!(audio_hashes(&p, outer), audio);
|
||||
|
||||
// the picture inside the nest changes: its audio preview stays valid
|
||||
add_fx(&mut p, inner, pic, "tint");
|
||||
assert_ne!(by_frame(&video_segments(&p, outer), 30), by_frame(&picture, 30));
|
||||
assert_eq!(audio_hashes(&p, outer), audio, "a video effect inside the nest");
|
||||
assert_eq!(audio_hashes(&p, top), deep);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user