1use std::path::Path;
7
8use image::{Rgba, RgbaImage};
9
10pub const WIDTH: u32 = 280;
12pub const HEIGHT: u32 = 140;
13pub const COLOR: [u8; 3] = [0x00, 0x72, 0xEF];
15
16const BLOCK: usize = 256;
19
20pub fn peaks(path: &Path, ext: &str, buckets: usize) -> Option<Vec<(f32, f32)>> {
23 let mut source = crate::audio::open(path, ext).ok()?;
24 let mut blocks: Vec<(f32, f32)> = Vec::new();
25 let (mut lo, mut hi, mut n) = (f32::MAX, f32::MIN, 0usize);
26 while let Some(block) = source.next_block() {
27 for frame in block.frames() {
28 let s = frame.iter().sum::<f32>() / frame.len() as f32;
29 lo = lo.min(s);
30 hi = hi.max(s);
31 n += 1;
32 if n == BLOCK {
33 blocks.push((lo, hi));
34 (lo, hi, n) = (f32::MAX, f32::MIN, 0);
35 }
36 }
37 }
38 if n > 0 {
39 blocks.push((lo, hi));
40 }
41 (!blocks.is_empty()).then(|| rebucket(&blocks, buckets))
42}
43
44pub fn rebucket(blocks: &[(f32, f32)], buckets: usize) -> Vec<(f32, f32)> {
46 let buckets = buckets.max(1);
47 (0..buckets)
48 .map(|b| {
49 let start = b * blocks.len() / buckets;
50 let end = ((b + 1) * blocks.len() / buckets).max(start + 1).min(blocks.len());
51 blocks.get(start..end).unwrap_or(&[]).iter().fold((0.0f32, 0.0f32), |(l, h), &(a, z)| (l.min(a), h.max(z)))
52 })
53 .collect()
54}
55
56pub fn render(peaks: &[(f32, f32)], height: u32, color: [u8; 3]) -> RgbaImage {
60 let width = peaks.len().max(1) as u32;
61 let mut img = RgbaImage::new(width, height);
62 let loudest = peaks.iter().map(|&(l, h)| l.abs().max(h.abs())).fold(0.0f32, f32::max);
63 let scale = if loudest > 0.0 { 1.0 / loudest } else { 0.0 };
64 let mid = height as f32 / 2.0;
65 for (x, &(l, h)) in peaks.iter().enumerate() {
66 let top = (mid - h * scale * mid).floor().clamp(0.0, height as f32 - 1.0) as u32;
67 let bottom = (mid - l * scale * mid).ceil().clamp(top as f32 + 1.0, height as f32) as u32;
68 for y in top..bottom {
69 img.put_pixel(x as u32, y, Rgba([color[0], color[1], color[2], 255]));
70 }
71 }
72 img
73}
74
75pub fn thumbnail(path: &Path, ext: &str) -> Option<RgbaImage> {
77 let p = peaks(path, ext, WIDTH as usize)?;
78 Some(render(&p, HEIGHT, COLOR))
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use crate::media::tests::wav;
85
86 #[test]
87 fn peaks_follow_the_envelope() {
88 let dir = tempfile::tempdir().unwrap();
89 let p = dir.path().join("ramp.wav");
90 std::fs::write(&p, wav(8000, 2.0, |t| (t / 2.0) * (t * 220.0 * std::f64::consts::TAU).sin())).unwrap();
92 let peaks = peaks(&p, "wav", 400).unwrap();
93 assert_eq!(peaks.len(), 400);
94 let amp = |i: usize| peaks[i].1 - peaks[i].0;
95 assert!(amp(10) < amp(200) && amp(200) < amp(390), "{} {} {}", amp(10), amp(200), amp(390));
96 assert!(peaks[399].1 > 0.9 && peaks[399].0 < -0.9);
97 }
98
99 #[test]
100 fn renders_a_waveform() {
101 let dir = tempfile::tempdir().unwrap();
102 let p = dir.path().join("tone.wav");
103 std::fs::write(&p, wav(8000, 1.0, |t| 0.5 * (t * 440.0 * std::f64::consts::TAU).sin())).unwrap();
104 let img = thumbnail(&p, "wav").unwrap();
105 assert_eq!(img.dimensions(), (WIDTH, HEIGHT));
106 assert_eq!(img.get_pixel(140, 1).0, [0x00, 0x72, 0xEF, 255]);
108 assert_eq!(img.get_pixel(140, 70).0[3], 255);
109 let silent = render(&[(0.0, 0.0); 4], 10, COLOR);
110 assert!(silent.pixels().filter(|p| p.0[3] > 0).count() <= 4, "a silent file is a thin line");
111 }
112
113 #[test]
114 fn rebuckets() {
115 let blocks = [(-0.1, 0.1), (-0.5, 0.2), (-0.2, 0.9), (0.0, 0.0)];
116 assert_eq!(rebucket(&blocks, 2), [(-0.5, 0.2), (-0.2, 0.9)]);
117 assert_eq!(rebucket(&blocks, 8).len(), 8);
118 }
119}