use crate::error::AnalysisError;
#[derive(Debug, Clone)]
pub struct SilenceDetector {
pub threshold_db: f32,
pub min_duration_ms: u32,
pub frame_size: usize,
}
impl Default for SilenceDetector {
fn default() -> Self {
Self {
threshold_db: -40.0,
min_duration_ms: 500,
frame_size: 2048,
}
}
}
#[derive(Debug, Clone)]
pub struct SilenceRegion {
pub start_sample: usize,
pub end_sample: usize,
pub duration_seconds: f32,
}
pub fn detect_and_trim(
samples: &[f32],
sample_rate: u32,
detector: SilenceDetector,
) -> Result<(Vec<f32>, Vec<(usize, usize)>), AnalysisError> {
if samples.is_empty() {
return Ok((Vec::new(), Vec::new()));
}
if sample_rate == 0 {
return Err(AnalysisError::InvalidInput("Sample rate must be > 0".to_string()));
}
if detector.frame_size == 0 {
return Err(AnalysisError::InvalidInput("Frame size must be > 0".to_string()));
}
if detector.frame_size > samples.len() {
log::warn!("Frame size ({}) larger than audio length ({}), treating as single frame",
detector.frame_size, samples.len());
}
log::debug!("Detecting silence: {} samples at {} Hz, threshold={:.1} dB, min_duration={} ms",
samples.len(), sample_rate, detector.threshold_db, detector.min_duration_ms);
let threshold_linear = 10.0_f32.powf(detector.threshold_db / 20.0);
let hop_size = detector.frame_size / 2; let num_frames = if samples.len() >= detector.frame_size {
(samples.len() - detector.frame_size) / hop_size + 1
} else {
1 };
let mut frame_rms = Vec::with_capacity(num_frames);
let mut frame_starts = Vec::with_capacity(num_frames);
for i in 0..num_frames {
let start = i * hop_size;
let end = (start + detector.frame_size).min(samples.len());
let sum_sq: f32 = samples[start..end]
.iter()
.map(|&x| x * x)
.sum();
let rms = if end > start {
(sum_sq / (end - start) as f32).sqrt()
} else {
0.0
};
frame_rms.push(rms);
frame_starts.push(start);
}
let mut frame_is_silent = Vec::with_capacity(num_frames);
for &rms in &frame_rms {
frame_is_silent.push(rms <= threshold_linear);
}
let min_duration_samples = (detector.min_duration_ms as f32 / 1000.0 * sample_rate as f32) as usize;
let min_duration_frames = (min_duration_samples + hop_size - 1) / hop_size;
let mut silence_regions: Vec<SilenceRegion> = Vec::new();
let mut in_silence = false;
let mut silence_start_frame = 0;
for (frame_idx, &is_silent) in frame_is_silent.iter().enumerate() {
if is_silent && !in_silence {
in_silence = true;
silence_start_frame = frame_idx;
} else if !is_silent && in_silence {
in_silence = false;
let silence_end_frame = frame_idx;
let silence_duration_frames = silence_end_frame - silence_start_frame;
if silence_duration_frames >= min_duration_frames ||
silence_start_frame == 0 ||
silence_end_frame == num_frames {
let start_sample = frame_starts[silence_start_frame];
let end_sample = if silence_end_frame < frame_starts.len() {
frame_starts[silence_end_frame]
} else {
samples.len()
};
silence_regions.push(SilenceRegion {
start_sample,
end_sample,
duration_seconds: (end_sample - start_sample) as f32 / sample_rate as f32,
});
}
}
}
if in_silence {
let silence_duration_frames = num_frames - silence_start_frame;
if silence_duration_frames >= min_duration_frames || silence_start_frame == 0 {
let start_sample = frame_starts[silence_start_frame];
silence_regions.push(SilenceRegion {
start_sample,
end_sample: samples.len(),
duration_seconds: (samples.len() - start_sample) as f32 / sample_rate as f32,
});
}
}
let trim_start = if let Some(first_region) = silence_regions.first() {
if first_region.start_sample == 0 {
first_region.end_sample
} else {
0
}
} else {
0
};
let trim_end = if let Some(last_region) = silence_regions.last() {
if last_region.end_sample == samples.len() {
last_region.start_sample
} else {
samples.len()
}
} else {
samples.len()
};
let trim_start = trim_start.min(trim_end);
let trim_end = trim_end.max(trim_start);
let trimmed = if trim_start < trim_end && trim_end <= samples.len() {
samples[trim_start..trim_end].to_vec()
} else {
Vec::new()
};
let silence_map: Vec<(usize, usize)> = silence_regions
.iter()
.map(|r| (r.start_sample, r.end_sample))
.collect();
log::debug!("Silence detection: trimmed from {} to {} samples, found {} silence regions",
samples.len(), trimmed.len(), silence_map.len());
Ok((trimmed, silence_map))
}
#[cfg(test)]
mod tests {
use super::*;
fn generate_test_audio_with_silence(
total_samples: usize,
audio_start: usize,
audio_end: usize,
amplitude: f32,
) -> Vec<f32> {
let mut samples = vec![0.0f32; total_samples];
for i in audio_start..audio_end.min(total_samples) {
samples[i] = amplitude * (i as f32 / 1000.0).sin(); }
samples
}
#[test]
fn test_detect_and_trim_leading_trailing() {
let total_samples = 44100 * 3; let audio_start = 44100; let audio_end = 44100 * 2; let samples = generate_test_audio_with_silence(total_samples, audio_start, audio_end, 0.5);
let detector = SilenceDetector::default();
let (trimmed, silence_map) = detect_and_trim(&samples, 44100, detector).unwrap();
assert!(trimmed.len() < samples.len(), "Should trim some silence");
assert!(!trimmed.is_empty(), "Should keep audio content");
assert!(!silence_map.is_empty(), "Should detect silence regions: {:?}", silence_map);
}
#[test]
fn test_detect_and_trim_all_silent() {
let samples = vec![0.0f32; 44100];
let detector = SilenceDetector::default();
let (trimmed, _silence_map) = detect_and_trim(&samples, 44100, detector).unwrap();
assert!(trimmed.is_empty() || trimmed.iter().all(|&x| x.abs() < 1e-6),
"All silent audio should be trimmed");
}
#[test]
fn test_detect_and_trim_no_silence() {
let mut samples = vec![0.0f32; 44100];
for i in 0..samples.len() {
samples[i] = 0.5 * (i as f32 / 1000.0).sin();
}
let detector = SilenceDetector {
threshold_db: -60.0, ..Default::default()
};
let (trimmed, _silence_map) = detect_and_trim(&samples, 44100, detector).unwrap();
assert!(trimmed.len() > samples.len() / 2,
"Should keep most audio when no silence detected");
}
#[test]
fn test_detect_and_trim_invalid_parameters() {
let samples = vec![0.5f32; 44100];
let detector = SilenceDetector::default();
let result = detect_and_trim(&samples, 0, detector.clone());
assert!(result.is_err());
let mut bad_detector = detector.clone();
bad_detector.frame_size = 0;
let result = detect_and_trim(&samples, 44100, bad_detector);
assert!(result.is_err());
}
#[test]
fn test_detect_and_trim_empty_samples() {
let samples = vec![];
let detector = SilenceDetector::default();
let (trimmed, silence_map) = detect_and_trim(&samples, 44100, detector).unwrap();
assert!(trimmed.is_empty());
assert!(silence_map.is_empty());
}
#[test]
fn test_detect_and_trim_threshold_sensitivity() {
let mut samples = vec![0.0f32; 44100 * 2];
for i in 0..22050 {
samples[i] = 0.01; }
for i in 22050..44100 {
samples[i] = 0.5; }
let detector_low = SilenceDetector {
threshold_db: -60.0,
..Default::default()
};
let (_, silence_map_low) = detect_and_trim(&samples, 44100, detector_low).unwrap();
let detector_high = SilenceDetector {
threshold_db: -20.0,
..Default::default()
};
let (_, silence_map_high) = detect_and_trim(&samples, 44100, detector_high).unwrap();
let total_silence_low: usize = silence_map_low.iter()
.map(|(start, end)| end - start)
.sum();
let total_silence_high: usize = silence_map_high.iter()
.map(|(start, end)| end - start)
.sum();
assert!(total_silence_high >= total_silence_low,
"Higher threshold should detect more silence");
}
#[test]
fn test_detect_and_trim_min_duration() {
let mut samples = vec![0.5f32; 44100 * 2];
for i in 10000..15000 {
samples[i] = 0.0;
}
let detector = SilenceDetector {
min_duration_ms: 500, ..Default::default()
};
let (_, _silence_map) = detect_and_trim(&samples, 44100, detector).unwrap();
}
}