1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
//! `duration-sanity` — degenerate clip durations, misses against a
//! declared duration pin, and channels within one clip that end at
//! different times (an engine clamp-holds the shorter channels, which
//! usually means a partial export).
use crate::check::{Check, CheckCtx};
use crate::evaluation::CheckOutput;
use crate::finding::{Finding, Severity};
/// Channel end-time spread beyond this is flagged (half a frame at
/// 30 fps).
pub const END_SPREAD_TOLERANCE_S: f32 = 0.017;
pub struct DurationSanity;
impl Check for DurationSanity {
fn id(&self) -> &'static str {
"duration-sanity"
}
fn evaluate(&self, ctx: &CheckCtx) -> CheckOutput {
let mut findings = Vec::new();
let doc = ctx.doc;
for (index, clip) in doc.clips.iter().enumerate() {
let duration_pin = ctx.expectations(index).duration_s;
let valid_duration_pin = duration_pin.filter(|pin| {
pin.value.is_finite()
&& pin.value > 0.0
&& pin.tolerance.is_finite()
&& pin.tolerance >= 0.0
});
if let Some(pin) = duration_pin
&& valid_duration_pin.is_none()
{
findings.push(
Finding::new(
self.id(),
Severity::Error,
format!(
"invalid declared duration pin (value {}s, tolerance {}s) — \
value must be finite and positive, and tolerance must be \
finite and non-negative",
pin.value, pin.tolerance
),
)
.clip(&clip.name)
.measured(clip.duration_s),
);
}
if clip.tracks.is_empty() {
findings.push(
Finding::new(self.id(), Severity::Warning, "clip has no tracks")
.clip(&clip.name),
);
if valid_duration_pin.is_none() {
continue;
}
}
if clip.duration_s <= 0.0 || !clip.duration_s.is_finite() {
let mut finding = Finding::new(
self.id(),
Severity::Error,
format!("degenerate clip duration ({}s)", clip.duration_s),
)
.clip(&clip.name)
.measured(clip.duration_s);
if let Some(pin) = valid_duration_pin {
finding = finding.expected(pin.value);
}
findings.push(finding);
continue;
}
if let Some(pin) = valid_duration_pin
&& (clip.duration_s - pin.value).abs() > pin.tolerance
{
findings.push(
Finding::new(
self.id(),
Severity::Error,
format!(
"measured duration {:.4}s disagrees with the declared \
{:.4} ± {:.4}s — the clip may have been sliced or \
exported with a dropped frame",
clip.duration_s, pin.value, pin.tolerance
),
)
.clip(&clip.name)
.measured(clip.duration_s)
.expected(pin.value),
);
}
// Single-key tracks are pinned values, not truncated
// channels — a common bake idiom — so they don't count
// toward the end spread.
let ends: Vec<f32> = clip
.tracks
.iter()
.filter(|t| t.key_count() >= 2)
.map(|t| t.end_time())
.filter(|end| end.is_finite())
.collect();
if ends.is_empty() {
continue;
}
let max = ends.iter().copied().fold(f32::MIN, f32::max);
let min = ends.iter().copied().fold(f32::MAX, f32::min);
if max - min > END_SPREAD_TOLERANCE_S {
findings.push(
Finding::new(
self.id(),
Severity::Warning,
format!(
"channels end at different times ({min:.3}s..{max:.3}s) — \
shorter channels will be clamp-held"
),
)
.clip(&clip.name)
.measured((max - min) as f64)
.expected(0.0f64),
);
}
}
CheckOutput::from_coverage(findings, Vec::new(), Vec::new())
}
}