1use super::{AudioSource, Backend, SAMPLE_RATE};
6use crate::settings::Settings;
7use std::collections::{HashMap, HashSet};
8use std::hash::{Hash, Hasher};
9use std::sync::{Arc, Mutex};
10
11pub const PEAKS_PER_SEC: u32 = 100;
12
13pub struct Peaks {
14 pub min: Vec<f32>,
16 pub max: Vec<f32>,
18}
19
20impl Peaks {
21 pub fn len(&self) -> usize {
22 self.min.len()
23 }
24 pub fn is_empty(&self) -> bool {
25 self.min.is_empty()
26 }
27 pub fn range(&self, a: f64, b: f64) -> (f32, f32) {
29 let i0 = ((a * PEAKS_PER_SEC as f64).floor().max(0.0)) as usize;
30 let i1 = ((b * PEAKS_PER_SEC as f64).ceil().max(0.0)) as usize;
31 let i1 = i1.min(self.len()).max(i0 + 1);
32 if i0 >= self.len() {
33 return (0.0, 0.0);
34 }
35 let (mut lo, mut hi) = (f32::MAX, f32::MIN);
36 for i in i0..i1 {
37 lo = lo.min(self.min[i]);
38 hi = hi.max(self.max[i]);
39 }
40 (lo, hi)
41 }
42}
43
44#[derive(Default)]
45struct State {
46 ready: HashMap<u64, Arc<Peaks>>,
48 pending: HashSet<u64>,
49}
50
51pub struct WaveformCache {
52 backend: Backend,
53 ctx: eframe::egui::Context,
54 state: Arc<Mutex<State>>,
55}
56
57fn mem_key(path: &str, stream: usize) -> u64 {
59 let mut h = std::collections::hash_map::DefaultHasher::new();
60 (path, stream).hash(&mut h);
61 h.finish()
62}
63
64impl WaveformCache {
65 pub fn new(ctx: eframe::egui::Context, backend: Backend) -> Self {
66 Self { backend, ctx, state: Arc::default() }
67 }
68 pub fn set_backend(&mut self, b: Backend) {
69 self.backend = b;
70 }
71 pub fn clear(&mut self) {
75 self.state = Arc::default();
76 }
77 pub fn get(&mut self, path: &str, stream: usize) -> Option<Arc<Peaks>> {
80 let key = mem_key(path, stream);
81 let mut st = self.state.lock().ok()?;
82 if let Some(p) = st.ready.get(&key) {
83 return Some(p.clone());
84 }
85 if st.pending.insert(key) {
86 let (state, ctx, backend, path) = (self.state.clone(), self.ctx.clone(), self.backend, path.to_string());
87 std::thread::spawn(move || {
88 let peaks =
91 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| load_or_compute(&path, stream, backend)))
92 .unwrap_or_else(|_| Peaks { min: Vec::new(), max: Vec::new() });
93 if let Ok(mut st) = state.lock() {
94 st.pending.remove(&key);
95 st.ready.insert(key, Arc::new(peaks));
96 }
97 ctx.request_repaint();
98 });
99 }
100 None
101 }
102}
103
104fn cache_file(path: &str, stream: usize) -> Option<std::path::PathBuf> {
105 let meta = std::fs::metadata(path).ok()?;
106 let mtime = meta.modified().ok()?.duration_since(std::time::UNIX_EPOCH).ok()?.as_secs();
107 let mut h = std::collections::hash_map::DefaultHasher::new();
108 (path, meta.len(), mtime, stream).hash(&mut h);
109 Some(Settings::cache_dir().join(format!("{:016x}.peaks", h.finish())))
110}
111
112fn load_or_compute(path: &str, stream: usize, backend: Backend) -> Peaks {
114 let file = cache_file(path, stream);
115 if let Some(bytes) = file.as_ref().and_then(|f| std::fs::read(f).ok()) {
116 if !bytes.is_empty() && bytes.len() % 8 == 0 {
117 let mut p = Peaks { min: Vec::with_capacity(bytes.len() / 8), max: Vec::with_capacity(bytes.len() / 8) };
118 for c in bytes.chunks_exact(8) {
119 p.min.push(f32::from_le_bytes([c[0], c[1], c[2], c[3]]));
120 p.max.push(f32::from_le_bytes([c[4], c[5], c[6], c[7]]));
121 }
122 return p;
123 }
124 }
125 let Ok(mut src) = super::open_audio(path, stream, backend) else {
126 return Peaks { min: Vec::new(), max: Vec::new() };
127 };
128 let duration = src.duration();
129 let peaks = compute_peaks(&mut *src, duration);
130 if let Some(f) = file {
131 if !peaks.is_empty() {
132 let mut bytes = Vec::with_capacity(peaks.len() * 8);
133 for (lo, hi) in peaks.min.iter().zip(&peaks.max) {
134 bytes.extend_from_slice(&lo.to_le_bytes());
135 bytes.extend_from_slice(&hi.to_le_bytes());
136 }
137 let tmp = f.with_extension("tmp");
138 let _ = std::fs::create_dir_all(Settings::cache_dir());
139 if std::fs::write(&tmp, bytes).is_ok() {
140 let _ = std::fs::rename(&tmp, &f);
141 }
142 }
143 }
144 peaks
145}
146
147pub(crate) fn compute_peaks(source: &mut dyn AudioSource, duration: f64) -> Peaks {
150 const BLOCK: usize = 4800;
151 const BUCKET: usize = (SAMPLE_RATE / PEAKS_PER_SEC) as usize; let mut p = Peaks { min: Vec::new(), max: Vec::new() };
153 let mut buf = vec![0f32; BLOCK * 2];
154 let mut frame: u64 = 0;
155 loop {
156 let t = frame as f64 / SAMPLE_RATE as f64;
157 if t > 24.0 * 3600.0 {
158 break; }
160 source.read_at(t, &mut buf);
161 if t >= duration && buf.iter().all(|&x| x == 0.0) {
162 break;
163 }
164 for b in buf.chunks_exact(BUCKET * 2) {
165 let (mut lo, mut hi) = (f32::MAX, f32::MIN);
166 for s in b.chunks_exact(2) {
167 let m = (s[0] + s[1]) * 0.5;
168 lo = lo.min(m);
169 hi = hi.max(m);
170 }
171 p.min.push(lo);
172 p.max.push(hi);
173 }
174 frame += BLOCK as u64;
175 }
176 p
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use crate::media::ffpipe;
183
184 #[test]
185 fn peaks_from_test_clip() {
186 let mut src = ffpipe::open_audio(&ffpipe::tests::test_mp4(), 0).unwrap();
187 let d = src.duration();
188 let p = compute_peaks(&mut *src, d);
189 let expect = (d * PEAKS_PER_SEC as f64) as usize;
190 assert!(p.len().abs_diff(expect) <= 10, "len {} vs {expect}", p.len());
191 let hi = p.max.iter().cloned().fold(f32::MIN, f32::max);
192 let lo = p.min.iter().cloned().fold(f32::MAX, f32::min);
193 assert!(hi > 0.1 && lo < -0.1, "{lo}..{hi}");
194 let (a, b) = p.range(0.5, 0.6);
195 assert!(a < -0.1 && b > 0.1);
196 }
197
198 #[test]
200 fn cache_get_async_and_disk() {
201 let path = ffpipe::tests::test_mp4();
202 let poll = |c: &mut WaveformCache, path: &str| {
203 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60);
204 assert!(c.get(path, 1).is_none(), "first get must not block");
205 loop {
206 if let Some(p) = c.get(path, 1) {
207 return p;
208 }
209 assert!(std::time::Instant::now() < deadline, "waveform never finished");
210 std::thread::sleep(std::time::Duration::from_millis(20));
211 }
212 };
213 let mut c = WaveformCache::new(eframe::egui::Context::default(), Backend::Ffmpeg);
214 let first = poll(&mut c, &path);
215 assert!(first.len() > 350 && first.max.iter().any(|&x| x > 0.1));
216 let mut c2 = WaveformCache::new(eframe::egui::Context::default(), Backend::Ffmpeg);
217 let second = poll(&mut c2, &path);
218 assert_eq!(first.len(), second.len());
219 assert_eq!(first.max, second.max);
220 let mut c3 = WaveformCache::new(eframe::egui::Context::default(), Backend::Ffmpeg);
222 assert!(poll(&mut c3, "Z:/definitely/missing.mp4").is_empty());
223 }
224
225 #[test]
227 fn clear_forgets_rewritten_file() {
228 let poll = |c: &mut WaveformCache, path: &str| {
229 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60);
230 loop {
231 if let Some(p) = c.get(path, 0) {
232 return p;
233 }
234 assert!(std::time::Instant::now() < deadline, "waveform never finished");
235 std::thread::sleep(std::time::Duration::from_millis(20));
236 }
237 };
238 let path = std::path::Path::new(&ffpipe::tests::test_mp4())
239 .with_file_name(format!("{}-rewrite.mp4", std::process::id()));
240 std::fs::copy(ffpipe::tests::test_mp4(), &path).unwrap();
241 let path = path.to_string_lossy().into_owned();
242 let mut c = WaveformCache::new(eframe::egui::Context::default(), Backend::Ffmpeg);
243 let old = poll(&mut c, &path);
244 assert!(old.len() > 350, "{}", old.len());
245 let st = std::process::Command::new("ffmpeg")
246 .args(["-y", "-loglevel", "error", "-f", "lavfi", "-i", "sine=frequency=440:duration=1", "-c:a", "aac"])
247 .arg(&path)
248 .status()
249 .expect("ffmpeg on PATH");
250 assert!(st.success());
251 assert_eq!(poll(&mut c, &path).len(), old.len(), "stale until cleared");
252 c.clear();
253 let new = poll(&mut c, &path);
254 assert!(new.len() < 150, "{} (old {})", new.len(), old.len());
255 }
256
257 #[test]
258 fn peaks_range_bounds() {
259 let p = Peaks { min: vec![-0.5, -0.2], max: vec![0.5, 0.2] };
260 assert_eq!(p.range(0.0, 0.01), (-0.5, 0.5));
261 assert_eq!(p.range(0.01, 0.02), (-0.2, 0.2));
262 assert_eq!(p.range(5.0, 6.0), (0.0, 0.0));
263 }
264
265 #[test]
266 fn mem_key_differs_per_stream() {
267 assert_ne!(mem_key("a.mp4", 0), mem_key("a.mp4", 1));
268 assert_eq!(mem_key("a.mp4", 0), mem_key("a.mp4", 0));
269 }
270}