//! Waveform data generation: downsamples decoded audio to min/max peak pairs for display. use crate::db::Database; use crate::error::{unix_now, CoreError}; use tracing::instrument; /// Pre-computed waveform display data: min/max peak pairs per bucket. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct WaveformData { /// Number of display buckets. pub num_buckets: usize, /// Interleaved min/max pairs: [min0, max0, min1, max1, ...]. Length = num_buckets * 2. pub peaks: Vec, /// Source sample rate. pub sample_rate: u32, /// Total duration in seconds. pub duration: f64, } /// Generate waveform display data by downsampling mono samples into min/max peak pairs. #[instrument(skip_all)] pub fn generate_waveform(samples: &[f32], sample_rate: u32, num_buckets: usize) -> WaveformData { if samples.is_empty() || num_buckets == 0 || sample_rate == 0 { return WaveformData { num_buckets: 0, peaks: Vec::new(), sample_rate, duration: 0.0, }; } let duration = samples.len() as f64 / sample_rate as f64; let bucket_size = samples.len() as f64 / num_buckets as f64; let mut peaks = Vec::with_capacity(num_buckets * 2); for i in 0..num_buckets { let start = (i as f64 * bucket_size) as usize; let end = ((i + 1) as f64 * bucket_size) as usize; let end = end.min(samples.len()); if start >= end { peaks.push(0.0); peaks.push(0.0); continue; } let mut min_val = f32::MAX; let mut max_val = f32::MIN; for &s in &samples[start..end] { if s < min_val { min_val = s; } if s > max_val { max_val = s; } } peaks.push(min_val); peaks.push(max_val); } WaveformData { num_buckets, peaks, sample_rate, duration, } } /// Save waveform data to the database, serializing peaks as a little-endian f32 blob. #[instrument(skip_all)] pub fn save_waveform(db: &Database, hash: &str, waveform: &WaveformData) -> Result<(), CoreError> { let now = unix_now(); // Serialize peaks as little-endian f32 bytes let mut blob = Vec::with_capacity(waveform.peaks.len() * 4); for &val in &waveform.peaks { blob.extend_from_slice(&val.to_le_bytes()); } db.conn().execute( "INSERT OR REPLACE INTO waveform_data (hash, num_buckets, peak_data, sample_rate, duration, generated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", rusqlite::params![ hash, waveform.num_buckets as i64, blob, waveform.sample_rate, waveform.duration, now, ], )?; Ok(()) } /// Load waveform data from the database. #[instrument(skip_all)] pub fn load_waveform(db: &Database, hash: &str) -> Option { let mut stmt = db .conn() .prepare( "SELECT num_buckets, peak_data, sample_rate, duration FROM waveform_data WHERE hash = ?1", ) .ok()?; stmt.query_row([hash], |row| { let num_buckets: i64 = row.get(0)?; let blob: Vec = row.get(1)?; let sample_rate: u32 = row.get(2)?; let duration: f64 = row.get(3)?; // Deserialize little-endian f32 bytes let peaks: Vec = blob .chunks_exact(4) .map(|chunk| f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) .collect(); Ok(WaveformData { num_buckets: num_buckets as usize, peaks, sample_rate, duration, }) }) .ok() } #[cfg(test)] mod tests { use super::*; #[test] fn generate_sine_wave() { // Generate a 1-second 440Hz sine wave at 44100Hz let sample_rate = 44100u32; let samples: Vec = (0..sample_rate as usize) .map(|i| (2.0 * std::f32::consts::PI * 440.0 * i as f32 / sample_rate as f32).sin()) .collect(); let waveform = generate_waveform(&samples, sample_rate, 100); assert_eq!(waveform.num_buckets, 100); assert_eq!(waveform.peaks.len(), 200); assert!((waveform.duration - 1.0).abs() < 0.001); // Each bucket should have reasonable min/max values for a sine wave for i in 0..100 { let min_val = waveform.peaks[i * 2]; let max_val = waveform.peaks[i * 2 + 1]; assert!(min_val <= max_val); assert!(min_val >= -1.0); assert!(max_val <= 1.0); } } #[test] fn generate_empty_samples() { let waveform = generate_waveform(&[], 44100, 50); assert_eq!(waveform.num_buckets, 0); assert!(waveform.peaks.is_empty()); } #[test] fn generate_zero_buckets() { let samples = vec![0.5; 1000]; let waveform = generate_waveform(&samples, 44100, 0); assert_eq!(waveform.num_buckets, 0); assert!(waveform.peaks.is_empty()); } #[test] fn save_and_load_roundtrip() { let db = Database::open_in_memory().unwrap(); // Need a sample row for the FK reference crate::test_helpers::insert_fake_sample(&db, "test_hash"); let waveform = WaveformData { num_buckets: 3, peaks: vec![-0.5, 0.8, -0.3, 0.6, -0.1, 0.9], sample_rate: 44100, duration: 1.5, }; save_waveform(&db, "test_hash", &waveform).unwrap(); let loaded = load_waveform(&db, "test_hash").unwrap(); assert_eq!(loaded.num_buckets, 3); assert_eq!(loaded.peaks.len(), 6); assert_eq!(loaded.sample_rate, 44100); assert!((loaded.duration - 1.5).abs() < f64::EPSILON); for (a, b) in waveform.peaks.iter().zip(loaded.peaks.iter()) { assert!((a - b).abs() < f32::EPSILON); } } #[test] fn load_nonexistent_returns_none() { let db = Database::open_in_memory().unwrap(); assert!(load_waveform(&db, "nonexistent").is_none()); } }