app: extract audio waveforms off the UI thread

rebuild_timeline synchronously extracted every audio clip's waveform,
and the extractor decodes the clip's ENTIRE audio inline — with two
40-minute files on the timeline, opening the project froze the UI for
seconds ("the project takes a while to open"). Extraction now runs on
a background thread; the cache bumps a version counter on insert and
the engine tick repaints the timeline when it changes.
This commit is contained in:
2026-08-26 04:23:11 +08:00
parent 09c7e7de32
commit 96df9293e5
2 changed files with 29 additions and 1 deletions
+17 -1
View File
@@ -1038,6 +1038,9 @@ pub struct RealEngine {
/// The sending half of `thumb_rx` (cloned into every job).
thumb_tx: Mutex<mpsc::Sender<ThumbEvent>>,
proxy_runs: Vec<ProxyRun>,
/// Last waveform-cache version seen by the tick (background waveform
/// extractions bump it on insert → repaint the timeline).
waveform_version_seen: u64,
/// The multicam angle-frame cache (rendered grid cells keyed by
/// (multicam node, source), LRU-capped). An `Arc` so the background
/// angle workers' completions can reach it; the mutex keeps the engine
@@ -1179,6 +1182,7 @@ impl RealEngine {
thumb_rx: Mutex::new(thumb_rx),
thumb_tx: Mutex::new(thumb_tx),
proxy_runs: Vec::new(),
waveform_version_seen: 0,
multicam_frames: Arc::new(Mutex::new(MulticamFrameCache::default())),
multicam_rx: Mutex::new(multicam_rx),
multicam_tx: Mutex::new(multicam_tx),
@@ -2906,7 +2910,11 @@ impl RealEngine {
return;
};
let duration_frames = (clip.range.end.0 - clip.range.start.0).max(1);
cache.refresh(clip.id.0, &filename, duration_frames);
// 后台提取:全音频解码按素材时长走(40 分钟 HEVC 要秒级),
// 在 UI 线程上同步跑会把打开工程/重建时间轴卡死;落盘后由
// 缓存 version 触发重绘(见 drain_proxy_runs 处的检查)。
let clip_id = clip.id.0;
std::thread::spawn(move || cache.refresh(clip_id, &filename, duration_frames));
}
/// Looks up the snapshot clip's block node by `ClipId` (the id IS the
@@ -3187,6 +3195,14 @@ impl EngineGateway for RealEngine {
// the next fills for the resting playheads (the schedule skips
// playing monitors, so playback keeps the proxy path).
self.drain_full_res();
// 后台波形提取落盘(version 递增)→ 重绘时间轴显示波形。
if let Some(cache) = self.waveform_cache() {
let v = cache.version();
if v != self.waveform_version_seen {
self.waveform_version_seen = v;
cx.notify();
}
}
self.drain_thumbnails();
self.drain_proxy_runs(cx);
self.drain_multicam_frames(cx);
+12
View File
@@ -66,6 +66,11 @@ pub struct WaveformCache {
map: Mutex<HashMap<u64, Arc<ClipWaveform>>>,
/// Timeline frame rate (frames per second) for frame→time mapping.
fps: f32,
/// Bumped on every insert so the UI can repaint when a background
/// extraction lands (the extraction itself runs off the UI thread —
/// a 40-minute file's full audio decode blocks project open for
/// seconds when run inline).
version: std::sync::atomic::AtomicU64,
}
impl WaveformCache {
@@ -74,9 +79,15 @@ impl WaveformCache {
Arc::new(WaveformCache {
map: Mutex::new(HashMap::new()),
fps: if fps > 0.0 { fps } else { 25.0 },
version: std::sync::atomic::AtomicU64::new(0),
})
}
/// Monotonic insert counter (repaint trigger for async refresh).
pub fn version(&self) -> u64 {
self.version.load(std::sync::atomic::Ordering::Relaxed)
}
/// The cached waveform of `clip`, if extracted.
pub fn get(&self, clip: u64) -> Option<Arc<ClipWaveform>> {
self.map.lock().unwrap_or_else(|e| e.into_inner()).get(&clip).cloned()
@@ -96,6 +107,7 @@ impl WaveformCache {
let mut map = self.map.lock().unwrap_or_else(|e| e.into_inner());
if !map.contains_key(&clip) {
map.insert(clip, Arc::new(waveform));
self.version.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
}
}