fix: timeline waveforms from the previous project after File > Open

Waveform peaks were cached by item id and never cleared, and item ids repeat across
projects: after opening a project the timeline drew the previous project's waveforms
(a 7-minute mission-audio clip showed as silent). The cache is now reset with the media
pool, and an item whose source is not available is no longer left pending forever.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-01 10:56:50 -04:00
co-authored by Claude Opus 5.5
parent 54e2ca0994
commit c14fc8e1b7
3 changed files with 40 additions and 1 deletions
+1
View File
@@ -250,6 +250,7 @@ impl FilmcraftApp {
FrameServer::default_workers(),
));
self.textures.clear();
self.tl.reset_media_caches();
}
}
+12 -1
View File
@@ -42,6 +42,16 @@ pub struct TlState {
zoom_anchor: Option<(f64, f32)>,
}
impl TlState {
/// Forget cached waveform peaks: they are keyed by item id, and ids repeat across projects
/// (opening another project would otherwise show the old project's waveforms). Peak jobs
/// still running finish into the old maps and are dropped.
pub fn reset_media_caches(&mut self) {
self.peaks = Default::default();
self.peaks_pending = Default::default();
}
}
#[derive(Clone, Debug)]
pub enum Drag {
Scrub,
@@ -544,6 +554,8 @@ fn request_peaks(app: &mut FilmcraftApp, item: ItemId) -> Option<Arc<Vec<(f32, f
if let Some(p) = app.tl.peaks.lock().unwrap_or_else(|e| e.into_inner()).get(&item) {
return Some(p.clone());
}
// No source: try again on a later frame instead of leaving the item pending forever.
let src = app.session.source(item)?;
{
let mut pend = app.tl.peaks_pending.lock().unwrap_or_else(|e| e.into_inner());
if pend.contains(&item) {
@@ -551,7 +563,6 @@ fn request_peaks(app: &mut FilmcraftApp, item: ItemId) -> Option<Arc<Vec<(f32, f
}
pend.push(item);
}
let src = app.session.source(item)?;
let peaks = app.tl.peaks.clone();
let pending = app.tl.peaks_pending.clone();
let dur = src.info().duration;
+27
View File
@@ -442,3 +442,30 @@ fn keyboard_shortcuts_dialog_assigns_live_and_cancel_restores() {
assert!(d.sequence()["playhead"].as_i64().unwrap() < ph, "Timeline ▸ Left steps back");
assert_eq!(track_clips(&d.sequence(), 0).len(), n0 + 1);
}
/// Waveform peaks are cached per item id; ids repeat across projects, so opening another project
/// must not reuse the previous project's peaks (it showed a long mission-audio clip as silent).
#[test]
fn waveform_peaks_do_not_survive_opening_another_project() {
let mut d = Driver::demo();
let mut first = None;
for _ in 0..2000 {
d.frames(1);
let peaks = d.harness.state().tl.peaks.lock().unwrap().iter().map(|(k, v)| (*k, v.clone())).next();
if peaks.is_some() {
first = peaks;
break;
}
std::thread::sleep(std::time::Duration::from_millis(5));
}
let (item, old) = first.expect("the demo timeline computes waveform peaks");
// Reopen the same project from disk: same item ids, new media pool.
let path = std::env::temp_dir().join(format!("fc-peaks-{}.fcproj", std::process::id()));
let path = path.to_string_lossy().to_string();
d.exec("file.saveAs", json!({"path": path}));
d.exec("file.open", json!({"path": path}));
d.frames(2);
let _ = std::fs::remove_file(&path);
let now = d.harness.state().tl.peaks.lock().unwrap().get(&item).cloned();
assert!(now.is_none_or(|p| !std::sync::Arc::ptr_eq(&p, &old)), "peaks from the previous project were reused");
}