use crate::EvaluationError;
use scirs2_core::ndarray::Array1;
use voirs_sdk::AudioBuffer;
#[derive(Debug, Clone)]
pub struct P56LoudnessResult {
pub active_speech_level_dbov: f32,
pub activity_factor: f32,
pub active_rms_level_dbov: f32,
pub total_duration_sec: f32,
pub active_speech_duration_sec: f32,
pub peak_level_dbov: f32,
pub num_active_segments: usize,
pub avg_segment_duration_sec: f32,
pub clipping_detected: bool,
}
#[derive(Debug, Clone)]
pub struct P56Config {
pub vad_threshold_db: f32,
pub hangover_time_sec: f32,
pub min_segment_duration_sec: f32,
pub window_size_samples: usize,
pub hop_size_samples: usize,
pub clipping_threshold: f32,
}
impl Default for P56Config {
fn default() -> Self {
Self {
vad_threshold_db: 15.9, hangover_time_sec: 0.2, min_segment_duration_sec: 0.1, window_size_samples: 256, hop_size_samples: 128, clipping_threshold: 0.99, }
}
}
pub struct P56LoudnessMeter {
sample_rate: u32,
config: P56Config,
hangover_samples: usize,
min_segment_samples: usize,
}
impl P56LoudnessMeter {
pub fn new(sample_rate: u32) -> Result<Self, EvaluationError> {
Self::new_with_config(sample_rate, P56Config::default())
}
pub fn new_with_config(sample_rate: u32, config: P56Config) -> Result<Self, EvaluationError> {
if sample_rate == 0 {
return Err(EvaluationError::InvalidInput {
message: "Sample rate must be greater than 0".to_string(),
});
}
let hangover_samples = (config.hangover_time_sec * sample_rate as f32) as usize;
let min_segment_samples = (config.min_segment_duration_sec * sample_rate as f32) as usize;
Ok(Self {
sample_rate,
config,
hangover_samples,
min_segment_samples,
})
}
pub fn measure_loudness(
&self,
audio: &AudioBuffer,
) -> Result<P56LoudnessResult, EvaluationError> {
if audio.sample_rate() != self.sample_rate {
return Err(EvaluationError::InvalidInput {
message: format!(
"Audio sample rate {} does not match meter sample rate {}",
audio.sample_rate(),
self.sample_rate
),
});
}
let samples = if audio.channels() == 1 {
audio.samples().to_vec()
} else {
self.convert_to_mono(audio.samples(), audio.channels() as usize)
};
if samples.is_empty() {
return Err(EvaluationError::InvalidInput {
message: "Audio buffer is empty".to_string(),
});
}
let peak_value = samples.iter().map(|&x| x.abs()).fold(0.0_f32, f32::max);
let clipping_detected = peak_value >= self.config.clipping_threshold;
let frame_rms = self.calculate_frame_rms(&samples);
let active_frames = self.detect_active_speech(&frame_rms);
let (active_rms, activity_factor, num_segments, avg_segment_duration) =
self.calculate_active_speech_metrics(&frame_rms, &active_frames);
let active_speech_level_dbov = Self::linear_to_dbov(active_rms);
let active_rms_level_dbov = active_speech_level_dbov;
let peak_level_dbov = Self::linear_to_dbov(peak_value);
let total_duration_sec = samples.len() as f32 / self.sample_rate as f32;
let active_frames_count = active_frames.iter().filter(|&&x| x).count();
let active_speech_duration_sec =
(active_frames_count * self.config.hop_size_samples) as f32 / self.sample_rate as f32;
Ok(P56LoudnessResult {
active_speech_level_dbov,
activity_factor,
active_rms_level_dbov,
total_duration_sec,
active_speech_duration_sec,
peak_level_dbov,
num_active_segments: num_segments,
avg_segment_duration_sec: avg_segment_duration,
clipping_detected,
})
}
fn convert_to_mono(&self, samples: &[f32], channels: usize) -> Vec<f32> {
let frames = samples.len() / channels;
let mut mono = Vec::with_capacity(frames);
for frame_idx in 0..frames {
let mut sum = 0.0;
for ch in 0..channels {
sum += samples[frame_idx * channels + ch];
}
mono.push(sum / channels as f32);
}
mono
}
fn calculate_frame_rms(&self, samples: &[f32]) -> Vec<f32> {
let num_frames =
(samples.len() - self.config.window_size_samples) / self.config.hop_size_samples + 1;
let mut rms_levels = Vec::with_capacity(num_frames);
for frame_idx in 0..num_frames {
let start = frame_idx * self.config.hop_size_samples;
let end = (start + self.config.window_size_samples).min(samples.len());
let window = &samples[start..end];
let sum_squares: f32 = window.iter().map(|&x| x * x).sum();
let rms = (sum_squares / window.len() as f32).sqrt();
rms_levels.push(rms);
}
rms_levels
}
fn detect_active_speech(&self, frame_rms: &[f32]) -> Vec<bool> {
if frame_rms.is_empty() {
return Vec::new();
}
let max_rms = frame_rms.iter().copied().fold(0.0_f32, f32::max);
if max_rms == 0.0 {
return vec![false; frame_rms.len()];
}
let threshold_linear = max_rms * 10.0_f32.powf(-self.config.vad_threshold_db / 20.0);
let mut active: Vec<bool> = frame_rms
.iter()
.map(|&rms| rms >= threshold_linear)
.collect();
let hangover_frames = self.hangover_samples / self.config.hop_size_samples;
for i in 0..active.len() {
if active[i] {
for j in 1..=hangover_frames {
if i + j < active.len() {
active[i + j] = true;
}
}
}
}
let min_frames = self.min_segment_samples / self.config.hop_size_samples;
self.filter_short_segments(&active, min_frames)
}
fn filter_short_segments(&self, active: &[bool], min_frames: usize) -> Vec<bool> {
let mut filtered = vec![false; active.len()];
let mut segment_start: Option<usize> = None;
for (i, &is_active) in active.iter().enumerate() {
if is_active && segment_start.is_none() {
segment_start = Some(i);
} else if !is_active && segment_start.is_some() {
let start = segment_start.expect("value should be present");
let segment_length = i - start;
if segment_length >= min_frames {
for j in start..i {
filtered[j] = true;
}
}
segment_start = None;
}
}
if let Some(start) = segment_start {
let segment_length = active.len() - start;
if segment_length >= min_frames {
for j in start..active.len() {
filtered[j] = true;
}
}
}
filtered
}
fn calculate_active_speech_metrics(
&self,
frame_rms: &[f32],
active_frames: &[bool],
) -> (f32, f32, usize, f32) {
let active_count = active_frames.iter().filter(|&&x| x).count();
if active_count == 0 {
return (0.0, 0.0, 0, 0.0);
}
let sum_squares: f32 = frame_rms
.iter()
.zip(active_frames.iter())
.filter(|(_, &active)| active)
.map(|(&rms, _)| rms * rms)
.sum();
let active_rms = (sum_squares / active_count as f32).sqrt();
let activity_factor = active_count as f32 / frame_rms.len() as f32;
let mut num_segments = 0;
let mut in_segment = false;
let mut total_segment_duration = 0.0;
for &is_active in active_frames {
if is_active && !in_segment {
num_segments += 1;
in_segment = true;
} else if !is_active && in_segment {
in_segment = false;
}
if in_segment {
total_segment_duration +=
self.config.hop_size_samples as f32 / self.sample_rate as f32;
}
}
let avg_segment_duration = if num_segments > 0 {
total_segment_duration / num_segments as f32
} else {
0.0
};
(
active_rms,
activity_factor,
num_segments,
avg_segment_duration,
)
}
fn linear_to_dbov(level: f32) -> f32 {
if level > 0.0 {
20.0 * level.log10()
} else {
-100.0 }
}
#[must_use]
pub fn dbov_to_linear(dbov: f32) -> f32 {
10.0_f32.powf(dbov / 20.0)
}
pub fn normalize_to_target_level(
&self,
audio: &AudioBuffer,
target_level_dbov: f32,
) -> Result<AudioBuffer, EvaluationError> {
let result = self.measure_loudness(audio)?;
if result.activity_factor < 0.01 {
return Err(EvaluationError::InvalidInput {
message: "Insufficient active speech for normalization".to_string(),
});
}
let current_level = result.active_speech_level_dbov;
let gain_db = target_level_dbov - current_level;
let gain_linear = Self::dbov_to_linear(gain_db);
let normalized_samples: Vec<f32> =
audio.samples().iter().map(|&x| x * gain_linear).collect();
Ok(AudioBuffer::new(
normalized_samples,
audio.sample_rate(),
audio.channels(),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_p56_meter_creation() {
let meter = P56LoudnessMeter::new(16000);
assert!(meter.is_ok());
}
#[test]
fn test_dbov_conversion() {
assert!((P56LoudnessMeter::linear_to_dbov(1.0) - 0.0).abs() < 0.01);
assert!((P56LoudnessMeter::linear_to_dbov(0.5) - (-6.02)).abs() < 0.01);
assert!((P56LoudnessMeter::linear_to_dbov(0.1) - (-20.0)).abs() < 0.01);
let original = 0.3;
let dbov = P56LoudnessMeter::linear_to_dbov(original);
let recovered = P56LoudnessMeter::dbov_to_linear(dbov);
assert!((original - recovered).abs() < 0.001);
}
#[test]
fn test_silence_measurement() {
let meter = P56LoudnessMeter::new(16000).unwrap();
let silence = AudioBuffer::new(vec![0.0; 16000], 16000, 1);
let result = meter.measure_loudness(&silence).unwrap();
assert_eq!(result.activity_factor, 0.0);
assert_eq!(result.num_active_segments, 0);
assert!(result.active_speech_level_dbov < -90.0); }
#[test]
fn test_constant_signal_measurement() {
let meter = P56LoudnessMeter::new(16000).unwrap();
let constant = AudioBuffer::new(vec![0.5; 16000], 16000, 1);
let result = meter.measure_loudness(&constant).unwrap();
assert!(result.activity_factor > 0.9); assert!(result.active_speech_level_dbov > -10.0); assert!(result.active_speech_level_dbov < 0.0); }
#[test]
fn test_clipping_detection() {
let meter = P56LoudnessMeter::new(16000).unwrap();
let mut samples = vec![0.5_f32; 16000];
samples[1000] = 1.0; let clipped = AudioBuffer::new(samples, 16000, 1);
let result = meter.measure_loudness(&clipped).unwrap();
assert!(result.clipping_detected);
let normal = AudioBuffer::new(vec![0.5; 16000], 16000, 1);
let result_normal = meter.measure_loudness(&normal).unwrap();
assert!(!result_normal.clipping_detected);
}
#[test]
fn test_speech_like_signal() {
let meter = P56LoudnessMeter::new(16000).unwrap();
let mut samples = Vec::with_capacity(19200);
samples.extend(vec![0.3; 8000]);
samples.extend(vec![0.0; 3200]);
samples.extend(vec![0.3; 8000]);
let audio = AudioBuffer::new(samples, 16000, 1);
let result = meter.measure_loudness(&audio).unwrap();
assert!(result.num_active_segments >= 1);
assert!(result.activity_factor > 0.6 && result.activity_factor <= 1.0);
assert!(result.active_speech_duration_sec > 0.7);
assert!(result.active_speech_duration_sec < 1.3);
}
#[test]
fn test_normalization() {
let meter = P56LoudnessMeter::new(16000).unwrap();
let audio = AudioBuffer::new(vec![0.1; 16000], 16000, 1);
let target_level = -20.0; let normalized = meter
.normalize_to_target_level(&audio, target_level)
.unwrap();
let result = meter.measure_loudness(&normalized).unwrap();
assert!((result.active_speech_level_dbov - target_level).abs() < 1.0);
}
#[test]
fn test_sample_rate_mismatch() {
let meter = P56LoudnessMeter::new(16000).unwrap();
let audio = AudioBuffer::new(vec![0.1; 22050], 22050, 1);
let result = meter.measure_loudness(&audio);
assert!(result.is_err());
}
#[test]
fn test_multi_channel_conversion() {
let meter = P56LoudnessMeter::new(16000).unwrap();
let mut stereo_samples = Vec::new();
for _ in 0..8000 {
stereo_samples.push(0.3_f32);
stereo_samples.push(0.5_f32);
}
let stereo = AudioBuffer::new(stereo_samples, 16000, 2);
let result = meter.measure_loudness(&stereo).unwrap();
assert!(result.activity_factor > 0.0);
assert!(result.active_speech_level_dbov < 0.0);
}
#[test]
fn test_custom_config() {
let config = P56Config {
vad_threshold_db: 20.0, ..Default::default()
};
let meter = P56LoudnessMeter::new_with_config(16000, config).unwrap();
let audio = AudioBuffer::new(vec![0.1; 16000], 16000, 1);
let result = meter.measure_loudness(&audio).unwrap();
assert!(result.active_speech_level_dbov <= 0.0);
assert!(result.total_duration_sec > 0.0);
}
}