Skip to main content

max / audiofiles

6.0 KB · 205 lines History Blame Raw
1 //! Waveform data generation: downsamples decoded audio to min/max peak pairs for display.
2
3 use crate::db::Database;
4 use crate::error::{unix_now, CoreError};
5 use tracing::instrument;
6
7 /// Pre-computed waveform display data: min/max peak pairs per bucket.
8 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
9 pub struct WaveformData {
10 /// Number of display buckets.
11 pub num_buckets: usize,
12 /// Interleaved min/max pairs: [min0, max0, min1, max1, ...]. Length = num_buckets * 2.
13 pub peaks: Vec<f32>,
14 /// Source sample rate.
15 pub sample_rate: u32,
16 /// Total duration in seconds.
17 pub duration: f64,
18 }
19
20 /// Generate waveform display data by downsampling mono samples into min/max peak pairs.
21 #[instrument(skip_all)]
22 pub fn generate_waveform(samples: &[f32], sample_rate: u32, num_buckets: usize) -> WaveformData {
23 if samples.is_empty() || num_buckets == 0 || sample_rate == 0 {
24 return WaveformData {
25 num_buckets: 0,
26 peaks: Vec::new(),
27 sample_rate,
28 duration: 0.0,
29 };
30 }
31
32 let duration = samples.len() as f64 / sample_rate as f64;
33
34 let bucket_size = samples.len() as f64 / num_buckets as f64;
35 let mut peaks = Vec::with_capacity(num_buckets * 2);
36
37 for i in 0..num_buckets {
38 let start = (i as f64 * bucket_size) as usize;
39 let end = ((i + 1) as f64 * bucket_size) as usize;
40 let end = end.min(samples.len());
41
42 if start >= end {
43 peaks.push(0.0);
44 peaks.push(0.0);
45 continue;
46 }
47
48 let mut min_val = f32::MAX;
49 let mut max_val = f32::MIN;
50 for &s in &samples[start..end] {
51 if s < min_val {
52 min_val = s;
53 }
54 if s > max_val {
55 max_val = s;
56 }
57 }
58
59 peaks.push(min_val);
60 peaks.push(max_val);
61 }
62
63 WaveformData {
64 num_buckets,
65 peaks,
66 sample_rate,
67 duration,
68 }
69 }
70
71 /// Save waveform data to the database, serializing peaks as a little-endian f32 blob.
72 #[instrument(skip_all)]
73 pub fn save_waveform(db: &Database, hash: &str, waveform: &WaveformData) -> Result<(), CoreError> {
74 let now = unix_now();
75
76 // Serialize peaks as little-endian f32 bytes
77 let mut blob = Vec::with_capacity(waveform.peaks.len() * 4);
78 for &val in &waveform.peaks {
79 blob.extend_from_slice(&val.to_le_bytes());
80 }
81
82 db.conn().execute(
83 "INSERT OR REPLACE INTO waveform_data (hash, num_buckets, peak_data, sample_rate, duration, generated_at)
84 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
85 rusqlite::params![
86 hash,
87 waveform.num_buckets as i64,
88 blob,
89 waveform.sample_rate,
90 waveform.duration,
91 now,
92 ],
93 )?;
94 Ok(())
95 }
96
97 /// Load waveform data from the database.
98 #[instrument(skip_all)]
99 pub fn load_waveform(db: &Database, hash: &str) -> Option<WaveformData> {
100 let mut stmt = db
101 .conn()
102 .prepare(
103 "SELECT num_buckets, peak_data, sample_rate, duration FROM waveform_data WHERE hash = ?1",
104 )
105 .ok()?;
106
107 stmt.query_row([hash], |row| {
108 let num_buckets: i64 = row.get(0)?;
109 let blob: Vec<u8> = row.get(1)?;
110 let sample_rate: u32 = row.get(2)?;
111 let duration: f64 = row.get(3)?;
112
113 // Deserialize little-endian f32 bytes
114 let peaks: Vec<f32> = blob
115 .chunks_exact(4)
116 .map(|chunk| f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
117 .collect();
118
119 Ok(WaveformData {
120 num_buckets: num_buckets as usize,
121 peaks,
122 sample_rate,
123 duration,
124 })
125 })
126 .ok()
127 }
128
129 #[cfg(test)]
130 mod tests {
131 use super::*;
132
133 #[test]
134 fn generate_sine_wave() {
135 // Generate a 1-second 440Hz sine wave at 44100Hz
136 let sample_rate = 44100u32;
137 let samples: Vec<f32> = (0..sample_rate as usize)
138 .map(|i| (2.0 * std::f32::consts::PI * 440.0 * i as f32 / sample_rate as f32).sin())
139 .collect();
140
141 let waveform = generate_waveform(&samples, sample_rate, 100);
142
143 assert_eq!(waveform.num_buckets, 100);
144 assert_eq!(waveform.peaks.len(), 200);
145 assert!((waveform.duration - 1.0).abs() < 0.001);
146
147 // Each bucket should have reasonable min/max values for a sine wave
148 for i in 0..100 {
149 let min_val = waveform.peaks[i * 2];
150 let max_val = waveform.peaks[i * 2 + 1];
151 assert!(min_val <= max_val);
152 assert!(min_val >= -1.0);
153 assert!(max_val <= 1.0);
154 }
155 }
156
157 #[test]
158 fn generate_empty_samples() {
159 let waveform = generate_waveform(&[], 44100, 50);
160 assert_eq!(waveform.num_buckets, 0);
161 assert!(waveform.peaks.is_empty());
162 }
163
164 #[test]
165 fn generate_zero_buckets() {
166 let samples = vec![0.5; 1000];
167 let waveform = generate_waveform(&samples, 44100, 0);
168 assert_eq!(waveform.num_buckets, 0);
169 assert!(waveform.peaks.is_empty());
170 }
171
172 #[test]
173 fn save_and_load_roundtrip() {
174 let db = Database::open_in_memory().unwrap();
175
176 // Need a sample row for the FK reference
177 crate::test_helpers::insert_fake_sample(&db, "test_hash");
178
179 let waveform = WaveformData {
180 num_buckets: 3,
181 peaks: vec![-0.5, 0.8, -0.3, 0.6, -0.1, 0.9],
182 sample_rate: 44100,
183 duration: 1.5,
184 };
185
186 save_waveform(&db, "test_hash", &waveform).unwrap();
187 let loaded = load_waveform(&db, "test_hash").unwrap();
188
189 assert_eq!(loaded.num_buckets, 3);
190 assert_eq!(loaded.peaks.len(), 6);
191 assert_eq!(loaded.sample_rate, 44100);
192 assert!((loaded.duration - 1.5).abs() < f64::EPSILON);
193
194 for (a, b) in waveform.peaks.iter().zip(loaded.peaks.iter()) {
195 assert!((a - b).abs() < f32::EPSILON);
196 }
197 }
198
199 #[test]
200 fn load_nonexistent_returns_none() {
201 let db = Database::open_in_memory().unwrap();
202 assert!(load_waveform(&db, "nonexistent").is_none());
203 }
204 }
205