use hound::WavReader;
use std::error::Error;
use std::path::Path;
use std::sync::mpsc::Sender;
#[derive(Debug, Clone)]
pub struct WaveformProgress {
pub percentage: f32,
}
#[derive(Debug, Clone)]
pub struct TimelineWaveform {
peaks: Vec<(f32, f32)>,
}
impl TimelineWaveform {
pub fn from_wav_file_with_progress(
path: &Path,
target_peaks: usize,
progress_tx: Option<Sender<WaveformProgress>>,
) -> Result<Self, Box<dyn Error>> {
let mut reader = WavReader::open(path)?;
let spec = reader.spec();
let channels = spec.channels as usize;
let samples: Vec<f32> = match spec.sample_format {
hound::SampleFormat::Int => {
let bits = spec.bits_per_sample;
if !(1..=31).contains(&bits) {
return Err(format!("Unsupported bit depth: {bits} bits").into());
}
let max_value = (1i32 << (bits - 1)) as f32;
reader
.samples::<i32>()
.filter_map(|s| s.ok())
.map(|s| s as f32 / max_value)
.collect()
}
hound::SampleFormat::Float => reader.samples::<f32>().filter_map(|s| s.ok()).collect(),
};
let mono_samples: Vec<f32> = if channels == 2 {
samples
.chunks_exact(2)
.map(|chunk| (chunk[0] + chunk[1]) / 2.0)
.collect()
} else {
samples
};
let total_samples = mono_samples.len();
let samples_per_peak = total_samples.div_ceil(target_peaks);
let total_chunks = mono_samples.len().div_ceil(samples_per_peak);
let peaks: Vec<(f32, f32)> = mono_samples
.chunks(samples_per_peak)
.enumerate()
.map(|(idx, chunk)| {
if let Some(ref tx) = progress_tx
&& idx % 100 == 0
{
let _ = tx.send(WaveformProgress {
percentage: (idx as f32 / total_chunks as f32) * 100.0,
});
}
if chunk.is_empty() {
(0.0, 0.0)
} else {
let min = chunk.iter().copied().fold(f32::INFINITY, f32::min);
let max = chunk.iter().copied().fold(f32::NEG_INFINITY, f32::max);
(min, max)
}
})
.collect();
if let Some(ref tx) = progress_tx {
let _ = tx.send(WaveformProgress { percentage: 100.0 });
}
Ok(Self { peaks })
}
pub fn get_display_peaks(&self, display_width: usize) -> Vec<(f32, f32)> {
if display_width >= self.peaks.len() {
return self.peaks.clone();
}
let peaks_per_pixel = self.peaks.len() as f32 / display_width as f32;
(0..display_width)
.map(|i| {
let start_idx = (i as f32 * peaks_per_pixel) as usize;
let end_idx = ((i + 1) as f32 * peaks_per_pixel) as usize;
let end_idx = end_idx.min(self.peaks.len());
if start_idx >= self.peaks.len() {
return (0.0, 0.0);
}
let mut overall_min = f32::INFINITY;
let mut overall_max = f32::NEG_INFINITY;
for idx in start_idx..end_idx {
if let Some(&(min, max)) = self.peaks.get(idx) {
overall_min = overall_min.min(min);
overall_max = overall_max.max(max);
}
}
if overall_min.is_infinite() {
(0.0, 0.0)
} else {
(overall_min, overall_max)
}
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_display_peaks_same_size() {
let peaks = vec![(0.0, 1.0), (0.1, 0.9), (0.2, 0.8)];
let waveform = TimelineWaveform {
peaks: peaks.clone(),
};
let display_peaks = waveform.get_display_peaks(3);
assert_eq!(display_peaks.len(), 3);
assert_eq!(display_peaks, peaks);
}
#[test]
fn test_get_display_peaks_downsampled() {
let peaks = vec![(-1.0, 1.0), (-0.8, 0.8), (-0.6, 0.6), (-0.4, 0.4)];
let waveform = TimelineWaveform { peaks };
let display_peaks = waveform.get_display_peaks(2);
assert_eq!(display_peaks.len(), 2);
assert_eq!(display_peaks[0], (-1.0, 1.0));
assert_eq!(display_peaks[1], (-0.6, 0.6));
}
}