#![allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EnergyMode {
Rms,
Peak,
}
#[derive(Debug, Clone)]
pub struct EnergyContourConfig {
pub sample_rate: f32,
pub window_size: usize,
pub hop_size: usize,
pub mode: EnergyMode,
}
impl Default for EnergyContourConfig {
fn default() -> Self {
Self {
sample_rate: 44100.0,
window_size: 1024,
hop_size: 512,
mode: EnergyMode::Rms,
}
}
}
#[derive(Debug, Clone)]
pub struct EnergyContour {
pub values: Vec<f32>,
pub hop_size: usize,
pub sample_rate: f32,
}
impl EnergyContour {
#[must_use]
pub fn len(&self) -> usize {
self.values.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
#[must_use]
pub fn max_energy(&self) -> f32 {
self.values
.iter()
.copied()
.fold(f32::NEG_INFINITY, f32::max)
}
#[must_use]
pub fn min_energy(&self) -> f32 {
self.values.iter().copied().fold(f32::INFINITY, f32::min)
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn mean_energy(&self) -> f32 {
if self.values.is_empty() {
return 0.0;
}
self.values.iter().sum::<f32>() / self.values.len() as f32
}
#[must_use]
pub fn normalize(&self) -> Self {
let peak = self.max_energy();
if peak <= 0.0 {
return self.clone();
}
Self {
values: self.values.iter().map(|&v| v / peak).collect(),
hop_size: self.hop_size,
sample_rate: self.sample_rate,
}
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn smooth(&self, radius: usize) -> Self {
if radius == 0 || self.values.is_empty() {
return self.clone();
}
let n = self.values.len();
let mut smoothed = Vec::with_capacity(n);
for i in 0..n {
let lo = i.saturating_sub(radius);
let hi = (i + radius + 1).min(n);
let sum: f32 = self.values[lo..hi].iter().sum();
smoothed.push(sum / (hi - lo) as f32);
}
Self {
values: smoothed,
hop_size: self.hop_size,
sample_rate: self.sample_rate,
}
}
#[must_use]
pub fn to_db(&self) -> Vec<f32> {
self.values
.iter()
.map(|&v| 20.0 * (v.max(1e-10)).log10())
.collect()
}
#[must_use]
pub fn activity_frames(&self, threshold: f32) -> Vec<usize> {
self.values
.iter()
.enumerate()
.filter(|(_, &v)| v >= threshold)
.map(|(i, _)| i)
.collect()
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn frame_to_time(&self, frame: usize) -> f32 {
frame as f32 * self.hop_size as f32 / self.sample_rate
}
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn extract_energy(samples: &[f32], config: &EnergyContourConfig) -> EnergyContour {
if samples.is_empty() || config.hop_size == 0 || config.window_size == 0 {
return EnergyContour {
values: Vec::new(),
hop_size: config.hop_size,
sample_rate: config.sample_rate,
};
}
let n_frames = samples.len().saturating_sub(config.window_size) / config.hop_size + 1;
let mut values = Vec::with_capacity(n_frames);
for i in 0..n_frames {
let start = i * config.hop_size;
let end = (start + config.window_size).min(samples.len());
let frame = &samples[start..end];
let energy = match config.mode {
EnergyMode::Rms => {
let sum_sq: f32 = frame.iter().map(|&s| s * s).sum();
(sum_sq / frame.len() as f32).sqrt()
}
EnergyMode::Peak => frame.iter().map(|s| s.abs()).fold(0.0f32, f32::max),
};
values.push(energy);
}
EnergyContour {
values,
hop_size: config.hop_size,
sample_rate: config.sample_rate,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn default_cfg() -> EnergyContourConfig {
EnergyContourConfig::default()
}
#[test]
fn test_extract_empty() {
let c = extract_energy(&[], &default_cfg());
assert!(c.is_empty());
}
#[test]
fn test_extract_silence_rms() {
let silence = vec![0.0f32; 4096];
let c = extract_energy(&silence, &default_cfg());
assert!(!c.is_empty());
for &v in &c.values {
assert!(v < 1e-6);
}
}
#[test]
fn test_extract_peak_mode() {
let mut sig = vec![0.0f32; 2048];
sig[100] = 0.75;
let cfg = EnergyContourConfig {
mode: EnergyMode::Peak,
..default_cfg()
};
let c = extract_energy(&sig, &cfg);
assert!(c.max_energy() >= 0.74);
}
#[test]
fn test_max_min_energy() {
let sig = vec![0.5f32; 4096];
let c = extract_energy(&sig, &default_cfg());
assert!(c.max_energy() > 0.0);
assert!(c.min_energy() >= 0.0);
}
#[test]
fn test_mean_energy() {
let sig = vec![1.0f32; 4096];
let c = extract_energy(&sig, &default_cfg());
assert!((c.mean_energy() - 1.0).abs() < 0.01);
}
#[test]
fn test_normalize() {
let sig = vec![0.5f32; 4096];
let c = extract_energy(&sig, &default_cfg());
let n = c.normalize();
assert!((n.max_energy() - 1.0).abs() < 1e-5);
}
#[test]
fn test_normalize_zeros() {
let c = EnergyContour {
values: vec![0.0; 3],
hop_size: 512,
sample_rate: 44100.0,
};
let n = c.normalize();
assert_eq!(n.values, vec![0.0; 3]);
}
#[test]
fn test_smooth() {
let c = EnergyContour {
values: vec![0.0, 0.0, 1.0, 0.0, 0.0],
hop_size: 512,
sample_rate: 44100.0,
};
let s = c.smooth(1);
assert!((s.values[2] - 1.0 / 3.0).abs() < 0.01);
}
#[test]
fn test_smooth_zero_radius() {
let c = EnergyContour {
values: vec![1.0, 2.0],
hop_size: 512,
sample_rate: 44100.0,
};
let s = c.smooth(0);
assert_eq!(s.values, c.values);
}
#[test]
fn test_to_db() {
let c = EnergyContour {
values: vec![1.0, 0.1],
hop_size: 512,
sample_rate: 44100.0,
};
let db = c.to_db();
assert!((db[0] - 0.0).abs() < 0.01); assert!((db[1] - (-20.0)).abs() < 0.01); }
#[test]
fn test_activity_frames() {
let c = EnergyContour {
values: vec![0.1, 0.5, 0.8, 0.2, 0.9],
hop_size: 512,
sample_rate: 44100.0,
};
let active = c.activity_frames(0.5);
assert_eq!(active, vec![1, 2, 4]);
}
#[test]
fn test_frame_to_time() {
let c = EnergyContour {
values: vec![0.0; 10],
hop_size: 512,
sample_rate: 44100.0,
};
let t = c.frame_to_time(1);
assert!((t - 512.0 / 44100.0).abs() < 1e-5);
}
#[test]
fn test_len_matches_frames() {
let sig = vec![0.5f32; 8192];
let c = extract_energy(&sig, &default_cfg());
assert_eq!(c.len(), c.values.len());
}
#[test]
fn test_contour_len_empty() {
let c = EnergyContour {
values: Vec::new(),
hop_size: 512,
sample_rate: 44100.0,
};
assert_eq!(c.len(), 0);
assert!(c.is_empty());
}
}