use crate::EvaluationError;
use scirs2_core::ndarray::{Array1, Array2};
use scirs2_core::random::Rng;
use scirs2_core::Complex32;
use scirs2_fft::{RealFftPlanner, RealToComplex};
use std::f32::consts::PI;
use std::sync::Mutex;
use voirs_sdk::{AudioBuffer, LanguageCode};
#[derive(Debug, Clone)]
pub struct LanguageSpecificParams {
pub language: LanguageCode,
pub frequency_weights: Vec<f32>,
pub temporal_weights: Vec<f32>,
pub intelligibility_threshold: f32,
}
#[derive(Debug, Clone)]
pub struct STOIConfidenceInterval {
pub score: f32,
pub lower_bound: f32,
pub upper_bound: f32,
pub confidence_level: f32,
pub standard_error: f32,
}
pub struct STOIEvaluator {
sample_rate: u32,
frame_len: usize,
overlap: usize,
octave_bands: Vec<(f32, f32)>,
fft_planner: Mutex<RealFftPlanner<f32>>,
language_params: Option<LanguageSpecificParams>,
}
impl STOIEvaluator {
pub fn new(sample_rate: u32) -> Result<Self, EvaluationError> {
let frame_len = ((sample_rate as f32 * 0.0256).round() as usize).max(256);
let overlap = frame_len / 2;
let octave_bands = Self::create_octave_bands(sample_rate);
let fft_planner = Mutex::new(RealFftPlanner::<f32>::new());
Ok(Self {
sample_rate,
frame_len,
overlap,
octave_bands,
fft_planner,
language_params: None,
})
}
pub async fn calculate_stoi(
&self,
clean: &AudioBuffer,
processed: &AudioBuffer,
) -> Result<f32, EvaluationError> {
self.validate_inputs(clean, processed)?;
let min_len = clean.samples().len().min(processed.samples().len());
let clean_samples = &clean.samples()[..min_len];
let processed_samples = &processed.samples()[..min_len];
let clean_filtered = self.apply_preemphasis(clean_samples)?;
let processed_filtered = self.apply_preemphasis(processed_samples)?;
let clean_stft = self.compute_stft(&clean_filtered)?;
let processed_stft = self.compute_stft(&processed_filtered)?;
let clean_bands = self.group_into_octave_bands(&clean_stft)?;
let processed_bands = self.group_into_octave_bands(&processed_stft)?;
let intermediate_intel =
self.compute_intermediate_intelligibility(&clean_bands, &processed_bands)?;
let stoi_score = self.compute_final_stoi(&intermediate_intel)?;
Ok(stoi_score)
}
pub async fn calculate_estoi(
&self,
clean: &AudioBuffer,
processed: &AudioBuffer,
) -> Result<f32, EvaluationError> {
let stoi_score = self.calculate_stoi(clean, processed).await?;
let enhanced_score = self
.apply_estoi_enhancement(clean, processed, stoi_score)
.await?;
Ok(enhanced_score)
}
fn validate_inputs(
&self,
clean: &AudioBuffer,
processed: &AudioBuffer,
) -> Result<(), EvaluationError> {
if clean.sample_rate() != self.sample_rate {
return Err(EvaluationError::InvalidInput {
message: format!(
"Clean signal sample rate {} doesn't match STOI evaluator rate {}",
clean.sample_rate(),
self.sample_rate
),
});
}
if processed.sample_rate() != self.sample_rate {
return Err(EvaluationError::InvalidInput {
message: format!(
"Processed signal sample rate {} doesn't match STOI evaluator rate {}",
processed.sample_rate(),
self.sample_rate
),
});
}
if clean.channels() != 1 || processed.channels() != 1 {
return Err(EvaluationError::InvalidInput {
message: "STOI requires mono audio".to_string(),
});
}
let min_samples = 3 * self.sample_rate as usize;
if clean.samples().len() < min_samples || processed.samples().len() < min_samples {
return Err(EvaluationError::InvalidInput {
message: "STOI requires at least 3 seconds of audio".to_string(),
});
}
Ok(())
}
fn apply_preemphasis(&self, signal: &[f32]) -> Result<Array1<f32>, EvaluationError> {
if signal.is_empty() {
return Ok(Array1::zeros(0));
}
let mut filtered = Array1::zeros(signal.len());
filtered[0] = signal[0];
for i in 1..signal.len() {
filtered[i] = signal[i] - 0.97 * signal[i - 1];
}
Ok(filtered)
}
fn compute_stft(&self, signal: &Array1<f32>) -> Result<Array2<Complex32>, EvaluationError> {
let hop_size = self.frame_len - self.overlap;
let num_frames = (signal.len() - self.overlap) / hop_size;
let num_freqs = self.frame_len / 2 + 1;
let mut stft = Array2::zeros((num_frames, num_freqs));
for (frame_idx, frame_start) in (0..signal.len() - self.frame_len + 1)
.step_by(hop_size)
.enumerate()
{
if frame_idx >= num_frames {
break;
}
let mut frame = Array1::zeros(self.frame_len);
for i in 0..self.frame_len {
if frame_start + i < signal.len() {
let window =
0.5 * (1.0 - (2.0 * PI * i as f32 / (self.frame_len - 1) as f32).cos());
frame[i] = signal[frame_start + i] * window;
}
}
let frame_slice =
frame
.as_slice()
.ok_or_else(|| EvaluationError::AudioProcessingError {
message: "Failed to get frame slice".to_string(),
source: None,
})?;
let spectrum = scirs2_fft::rfft(frame_slice, None)?;
for (freq_idx, &complex_val) in spectrum.iter().enumerate() {
if freq_idx < num_freqs {
stft[[frame_idx, freq_idx]] =
Complex32::new(complex_val.re as f32, complex_val.im as f32);
}
}
}
Ok(stft)
}
fn group_into_octave_bands(
&self,
stft: &Array2<Complex32>,
) -> Result<Array2<f32>, EvaluationError> {
let (num_frames, _) = stft.dim();
let num_bands = self.octave_bands.len();
let mut band_energies = Array2::zeros((num_frames, num_bands));
let freq_res = self.sample_rate as f32 / self.frame_len as f32;
for frame_idx in 0..num_frames {
for (band_idx, &(f_low, f_high)) in self.octave_bands.iter().enumerate() {
let bin_low = (f_low / freq_res).round() as usize;
let bin_high = (f_high / freq_res).round() as usize;
let mut band_energy = 0.0;
let mut bin_count = 0;
for bin_idx in bin_low..=bin_high.min(stft.ncols() - 1) {
band_energy += stft[[frame_idx, bin_idx]].norm_sqr();
bin_count += 1;
}
if bin_count > 0 {
let avg_energy = band_energy / bin_count as f32;
band_energies[[frame_idx, band_idx]] = avg_energy.max(0.0).sqrt();
}
}
}
Ok(band_energies)
}
fn compute_intermediate_intelligibility(
&self,
clean_bands: &Array2<f32>,
processed_bands: &Array2<f32>,
) -> Result<Array2<f32>, EvaluationError> {
let (num_frames, num_bands) = clean_bands.dim();
let mut intermediate = Array2::zeros((num_frames, num_bands));
let seg_len = 30.min(num_frames);
for band_idx in 0..num_bands {
for frame_start in 0..num_frames {
let frame_end = (frame_start + seg_len).min(num_frames);
let seg_len_actual = frame_end - frame_start;
if seg_len_actual < 10 {
continue;
}
let clean_seg =
clean_bands.slice(scirs2_core::ndarray::s![frame_start..frame_end, band_idx]);
let processed_seg = processed_bands
.slice(scirs2_core::ndarray::s![frame_start..frame_end, band_idx]);
let correlation = self.compute_correlation_coefficient(&clean_seg, &processed_seg);
let clipped_correlation = correlation.max(0.0);
for i in frame_start..frame_end {
intermediate[[i, band_idx]] = clipped_correlation;
}
}
}
Ok(intermediate)
}
fn compute_correlation_coefficient(
&self,
x: &scirs2_core::ndarray::ArrayView1<f32>,
y: &scirs2_core::ndarray::ArrayView1<f32>,
) -> f32 {
if x.len() != y.len() || x.is_empty() {
return 0.0;
}
let n = x.len() as f32;
let mean_x = x.mean().unwrap_or(0.0);
let mean_y = y.mean().unwrap_or(0.0);
let mut numerator = 0.0;
let mut sum_sq_x = 0.0;
let mut sum_sq_y = 0.0;
for (&xi, &yi) in x.iter().zip(y.iter()) {
let diff_x = xi - mean_x;
let diff_y = yi - mean_y;
numerator += diff_x * diff_y;
sum_sq_x += diff_x * diff_x;
sum_sq_y += diff_y * diff_y;
}
let denominator = (sum_sq_x * sum_sq_y).sqrt();
if denominator > 1e-12f32 {
(numerator / denominator).clamp(-1.0, 1.0) } else {
0.0
}
}
fn compute_final_stoi(&self, intermediate: &Array2<f32>) -> Result<f32, EvaluationError> {
if intermediate.is_empty() {
return Ok(0.0);
}
let (num_frames, num_bands) = intermediate.dim();
let frequency_weights = self.create_intelligibility_weights(num_bands);
let mut time_averaged_scores = Vec::with_capacity(num_frames);
for frame_idx in 0..num_frames {
let mut weighted_sum = 0.0;
let mut weight_sum = 0.0;
for band_idx in 0..num_bands {
let value = intermediate[[frame_idx, band_idx]];
let weight = frequency_weights[band_idx];
weighted_sum += value * weight;
weight_sum += weight;
}
if weight_sum > 0.0 {
time_averaged_scores.push(weighted_sum / weight_sum);
} else {
time_averaged_scores.push(0.0);
}
}
let reliability_weighted_score =
self.apply_temporal_reliability_weighting(&time_averaged_scores);
let enhanced_score = self.apply_intelligibility_mapping(reliability_weighted_score);
Ok(enhanced_score.max(0.0).min(1.0))
}
fn create_intelligibility_weights(&self, num_bands: usize) -> Vec<f32> {
let mut weights = Vec::with_capacity(num_bands);
for band_idx in 0..num_bands {
if band_idx < self.octave_bands.len() {
let (f_low, f_high) = self.octave_bands[band_idx];
let center_freq = (f_low + f_high) / 2.0;
let weight = if center_freq < 500.0 {
0.8 } else if center_freq < 2000.0 {
1.5 } else if center_freq < 4000.0 {
1.2 } else if center_freq < 6000.0 {
0.9 } else {
0.6 };
weights.push(weight);
} else {
weights.push(1.0);
}
}
weights
}
fn apply_temporal_reliability_weighting(&self, time_scores: &[f32]) -> f32 {
if time_scores.is_empty() {
return 0.0;
}
let window_size = 10.min(time_scores.len());
let mut reliable_scores = Vec::new();
let mut weights = Vec::new();
for i in 0..time_scores.len() {
let start = if i >= window_size / 2 {
i - window_size / 2
} else {
0
};
let end = (i + window_size / 2 + 1).min(time_scores.len());
let window = &time_scores[start..end];
let local_mean = window.iter().sum::<f32>() / window.len() as f32;
let local_var = window
.iter()
.map(|&x| (x - local_mean).powi(2))
.sum::<f32>()
/ window.len() as f32;
let reliability_weight = 1.0 / (1.0 + 10.0 * local_var);
reliable_scores.push(time_scores[i]);
weights.push(reliability_weight);
}
let weighted_sum: f32 = reliable_scores
.iter()
.zip(weights.iter())
.map(|(score, weight)| score * weight)
.sum();
let weight_sum: f32 = weights.iter().sum();
if weight_sum > 0.0 {
weighted_sum / weight_sum
} else {
time_scores.iter().sum::<f32>() / time_scores.len() as f32
}
}
fn apply_intelligibility_mapping(&self, raw_score: f32) -> f32 {
let x = raw_score.max(0.0).min(1.0);
if x < 0.3 {
x * x * 1.67 } else if x < 0.7 {
0.15 + (x - 0.3) * 1.75
} else {
0.85 + (x - 0.7) * 0.5
}
}
async fn apply_estoi_enhancement(
&self,
clean: &AudioBuffer,
processed: &AudioBuffer,
base_stoi: f32,
) -> Result<f32, EvaluationError> {
let spectral_enhancement = self.compute_spectral_enhancement(clean, processed).await?;
let enhanced_score = base_stoi * 0.8 + spectral_enhancement * 0.2;
Ok(enhanced_score.max(0.0).min(1.0))
}
pub fn set_language_params(&mut self, language: LanguageCode) {
self.language_params = Some(Self::create_language_params(language));
}
pub async fn calculate_language_adapted_stoi(
&self,
clean: &AudioBuffer,
processed: &AudioBuffer,
language: Option<LanguageCode>,
) -> Result<f32, EvaluationError> {
let language_params = if let Some(lang) = language {
Self::create_language_params(lang)
} else {
self.language_params
.clone()
.unwrap_or_else(|| Self::create_language_params(LanguageCode::EnUs))
};
self.validate_inputs(clean, processed)?;
let min_len = clean.samples().len().min(processed.samples().len());
let clean_samples = &clean.samples()[..min_len];
let processed_samples = &processed.samples()[..min_len];
let clean_filtered = self.apply_preemphasis(clean_samples)?;
let processed_filtered = self.apply_preemphasis(processed_samples)?;
let clean_stft = self.compute_stft(&clean_filtered)?;
let processed_stft = self.compute_stft(&processed_filtered)?;
let clean_bands = self.group_into_octave_bands(&clean_stft)?;
let processed_bands = self.group_into_octave_bands(&processed_stft)?;
let weighted_clean =
self.apply_language_frequency_weighting(&clean_bands, &language_params)?;
let weighted_processed =
self.apply_language_frequency_weighting(&processed_bands, &language_params)?;
let intermediate_intel =
self.compute_intermediate_intelligibility(&weighted_clean, &weighted_processed)?;
let temporal_weighted =
self.apply_language_temporal_weighting(&intermediate_intel, &language_params)?;
let base_score = self.compute_final_stoi(&temporal_weighted)?;
let calibrated_score = self.apply_language_calibration(base_score, &language_params);
Ok(calibrated_score)
}
pub async fn calculate_stoi_with_confidence(
&self,
clean: &AudioBuffer,
processed: &AudioBuffer,
confidence_level: f32,
num_bootstrap_samples: usize,
) -> Result<STOIConfidenceInterval, EvaluationError> {
if confidence_level <= 0.0 || confidence_level >= 1.0 {
return Err(EvaluationError::InvalidInput {
message: "Confidence level must be between 0 and 1".to_string(),
});
}
if num_bootstrap_samples < 10 {
return Err(EvaluationError::InvalidInput {
message: "Number of bootstrap samples must be at least 10".to_string(),
});
}
let base_score = self.calculate_stoi(clean, processed).await?;
let bootstrap_scores = self
.generate_bootstrap_samples(clean, processed, num_bootstrap_samples)
.await?;
let mean_score = bootstrap_scores.iter().sum::<f32>() / bootstrap_scores.len() as f32;
let variance = bootstrap_scores
.iter()
.map(|&score| (score - mean_score).powi(2))
.sum::<f32>()
/ (bootstrap_scores.len() - 1) as f32;
let standard_error = variance.max(0.0).sqrt();
let alpha = 1.0 - confidence_level;
let t_value = self.calculate_t_value(alpha / 2.0, bootstrap_scores.len() - 1);
let margin_of_error = t_value * standard_error;
let lower_bound = (base_score - margin_of_error).max(0.0);
let upper_bound = (base_score + margin_of_error).min(1.0);
Ok(STOIConfidenceInterval {
score: base_score,
lower_bound,
upper_bound,
confidence_level,
standard_error,
})
}
pub async fn calculate_language_adapted_stoi_with_confidence(
&self,
clean: &AudioBuffer,
processed: &AudioBuffer,
language: Option<LanguageCode>,
confidence_level: f32,
num_bootstrap_samples: usize,
) -> Result<STOIConfidenceInterval, EvaluationError> {
if confidence_level <= 0.0 || confidence_level >= 1.0 {
return Err(EvaluationError::InvalidInput {
message: "Confidence level must be between 0 and 1".to_string(),
});
}
let base_score = self
.calculate_language_adapted_stoi(clean, processed, language)
.await?;
let bootstrap_scores = self
.generate_language_adapted_bootstrap_samples(
clean,
processed,
language,
num_bootstrap_samples,
)
.await?;
let mean_score = bootstrap_scores.iter().sum::<f32>() / bootstrap_scores.len() as f32;
let variance = bootstrap_scores
.iter()
.map(|&score| (score - mean_score).powi(2))
.sum::<f32>()
/ (bootstrap_scores.len() - 1) as f32;
let standard_error = variance.max(0.0).sqrt();
let alpha = 1.0 - confidence_level;
let t_value = self.calculate_t_value(alpha / 2.0, bootstrap_scores.len() - 1);
let margin_of_error = t_value * standard_error;
let lower_bound = (base_score - margin_of_error).max(0.0);
let upper_bound = (base_score + margin_of_error).min(1.0);
Ok(STOIConfidenceInterval {
score: base_score,
lower_bound,
upper_bound,
confidence_level,
standard_error,
})
}
async fn compute_spectral_enhancement(
&self,
clean: &AudioBuffer,
processed: &AudioBuffer,
) -> Result<f32, EvaluationError> {
let clean_spectrum = self.compute_average_spectrum(clean.samples())?;
let processed_spectrum = self.compute_average_spectrum(processed.samples())?;
let mut total_distortion = 0.0;
let mut valid_bins = 0;
let min_len = clean_spectrum.len().min(processed_spectrum.len());
for i in 0..min_len {
if clean_spectrum[i] > 1e-12f32 && processed_spectrum[i] > 1e-12f32 {
let ratio = processed_spectrum[i] / clean_spectrum[i];
let safe_ratio = ratio.max(1e-12f32);
let log_ratio = safe_ratio.ln();
total_distortion += log_ratio * log_ratio;
valid_bins += 1;
}
}
if valid_bins > 0 {
let mean_distortion = total_distortion / valid_bins as f32;
let enhancement = (-mean_distortion).exp().min(1.0).max(0.0);
Ok(enhancement)
} else {
Ok(0.5)
}
}
fn compute_average_spectrum(&self, signal: &[f32]) -> Result<Vec<f32>, EvaluationError> {
let hop_size = self.frame_len / 2;
let mut avg_spectrum = vec![0.0f32; self.frame_len / 2 + 1];
let mut frame_count = 0;
for frame_start in (0..signal.len() - self.frame_len + 1).step_by(hop_size) {
let mut frame = vec![0.0; self.frame_len];
for i in 0..self.frame_len {
if frame_start + i < signal.len() {
frame[i] = signal[frame_start + i];
}
}
let spectrum = scirs2_fft::rfft(&frame, None)?;
for (i, &complex_val) in spectrum.iter().enumerate() {
if i < avg_spectrum.len() {
avg_spectrum[i] += complex_val.norm_sqr() as f32;
}
}
frame_count += 1;
}
if frame_count > 0 {
for value in &mut avg_spectrum {
*value /= frame_count as f32;
}
}
Ok(avg_spectrum)
}
fn create_octave_bands(sample_rate: u32) -> Vec<(f32, f32)> {
let mut bands = Vec::new();
let nyquist = sample_rate as f32 / 2.0;
let mut center_freq = 150.0;
while center_freq < nyquist {
let factor = 2_f32.powf(1.0 / 6.0); let f_low = center_freq / factor;
let f_high = center_freq * factor;
if f_high <= nyquist {
bands.push((f_low, f_high));
}
center_freq *= 2_f32.powf(1.0 / 3.0); }
bands
}
fn create_language_params(language: LanguageCode) -> LanguageSpecificParams {
match language {
LanguageCode::EnUs | LanguageCode::EnGb => LanguageSpecificParams {
language,
frequency_weights: vec![
1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5,
2.4, 2.3, 2.2, 2.1, 2.0, 1.9, 1.8, 1.7, 1.6, 1.5,
],
temporal_weights: vec![1.0; 30],
intelligibility_threshold: 0.5,
},
LanguageCode::JaJp => LanguageSpecificParams {
language,
frequency_weights: vec![
0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0, 2.1, 2.2, 2.3, 2.4,
2.5, 2.4, 2.3, 2.2, 2.1, 2.0, 1.9, 1.8, 1.7, 1.6,
],
temporal_weights: vec![1.0; 30],
intelligibility_threshold: 0.45,
},
LanguageCode::ZhCn => LanguageSpecificParams {
language,
frequency_weights: vec![
0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0, 2.1, 2.2, 2.3,
2.4, 2.5, 2.4, 2.3, 2.2, 2.1, 2.0, 1.9, 1.8, 1.7,
],
temporal_weights: vec![1.0; 30],
intelligibility_threshold: 0.4,
},
LanguageCode::EsEs | LanguageCode::EsMx => LanguageSpecificParams {
language,
frequency_weights: vec![
1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5,
2.4, 2.3, 2.2, 2.1, 2.0, 1.9, 1.8, 1.7, 1.6, 1.5,
],
temporal_weights: vec![1.0; 30],
intelligibility_threshold: 0.48,
},
LanguageCode::FrFr => LanguageSpecificParams {
language,
frequency_weights: vec![
1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5,
2.4, 2.3, 2.2, 2.1, 2.0, 1.9, 1.8, 1.7, 1.6, 1.5,
],
temporal_weights: vec![1.0; 30],
intelligibility_threshold: 0.52,
},
LanguageCode::DeDe => LanguageSpecificParams {
language,
frequency_weights: vec![
1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5,
2.4, 2.3, 2.2, 2.1, 2.0, 1.9, 1.8, 1.7, 1.6, 1.5,
],
temporal_weights: vec![1.0; 30],
intelligibility_threshold: 0.51,
},
_ => LanguageSpecificParams {
language,
frequency_weights: vec![1.0; 26],
temporal_weights: vec![1.0; 30],
intelligibility_threshold: 0.5,
},
}
}
fn apply_language_frequency_weighting(
&self,
bands: &Array2<f32>,
params: &LanguageSpecificParams,
) -> Result<Array2<f32>, EvaluationError> {
let (num_frames, num_bands) = bands.dim();
let mut weighted_bands = bands.clone();
let weights_len = params.frequency_weights.len();
for band_idx in 0..num_bands {
let weight = if band_idx < weights_len {
params.frequency_weights[band_idx]
} else {
1.0
};
for frame_idx in 0..num_frames {
weighted_bands[[frame_idx, band_idx]] *= weight;
}
}
Ok(weighted_bands)
}
fn apply_language_temporal_weighting(
&self,
intermediate: &Array2<f32>,
params: &LanguageSpecificParams,
) -> Result<Array2<f32>, EvaluationError> {
let (num_frames, num_bands) = intermediate.dim();
let mut weighted_intermediate = intermediate.clone();
let weights_len = params.temporal_weights.len();
for frame_idx in 0..num_frames {
let weight = if frame_idx < weights_len {
params.temporal_weights[frame_idx]
} else {
1.0
};
for band_idx in 0..num_bands {
weighted_intermediate[[frame_idx, band_idx]] *= weight;
}
}
Ok(weighted_intermediate)
}
fn apply_language_calibration(&self, base_score: f32, params: &LanguageSpecificParams) -> f32 {
let language_factor = match params.language {
LanguageCode::EnUs | LanguageCode::EnGb => 1.0,
LanguageCode::JaJp => 0.95, LanguageCode::ZhCn => 0.92, LanguageCode::EsEs | LanguageCode::EsMx => 0.98,
LanguageCode::FrFr => 0.97,
LanguageCode::DeDe => 0.96,
_ => 0.95,
};
let threshold_factor = if base_score < params.intelligibility_threshold {
0.85
} else {
1.05
};
let calibrated = base_score * language_factor * threshold_factor;
calibrated.max(0.0).min(1.0)
}
async fn generate_bootstrap_samples(
&self,
clean: &AudioBuffer,
processed: &AudioBuffer,
num_samples: usize,
) -> Result<Vec<f32>, EvaluationError> {
let mut bootstrap_scores = Vec::with_capacity(num_samples);
let signal_len = clean.samples().len().min(processed.samples().len());
for _ in 0..num_samples {
let bootstrap_len = (signal_len as f32 * 0.8) as usize;
let mut bootstrap_clean = Vec::with_capacity(bootstrap_len);
let mut bootstrap_processed = Vec::with_capacity(bootstrap_len);
for _ in 0..bootstrap_len {
let idx = scirs2_core::random::thread_rng().random_range(0..signal_len);
bootstrap_clean.push(clean.samples()[idx]);
bootstrap_processed.push(processed.samples()[idx]);
}
let bootstrap_clean_buffer = AudioBuffer::new(bootstrap_clean, clean.sample_rate(), 1);
let bootstrap_processed_buffer =
AudioBuffer::new(bootstrap_processed, processed.sample_rate(), 1);
let score = self
.calculate_stoi(&bootstrap_clean_buffer, &bootstrap_processed_buffer)
.await?;
bootstrap_scores.push(score);
}
Ok(bootstrap_scores)
}
async fn generate_language_adapted_bootstrap_samples(
&self,
clean: &AudioBuffer,
processed: &AudioBuffer,
language: Option<LanguageCode>,
num_samples: usize,
) -> Result<Vec<f32>, EvaluationError> {
let mut bootstrap_scores = Vec::with_capacity(num_samples);
let signal_len = clean.samples().len().min(processed.samples().len());
for _ in 0..num_samples {
let bootstrap_len = (signal_len as f32 * 0.8) as usize;
let mut bootstrap_clean = Vec::with_capacity(bootstrap_len);
let mut bootstrap_processed = Vec::with_capacity(bootstrap_len);
for _ in 0..bootstrap_len {
let idx = scirs2_core::random::thread_rng().random_range(0..signal_len);
bootstrap_clean.push(clean.samples()[idx]);
bootstrap_processed.push(processed.samples()[idx]);
}
let bootstrap_clean_buffer = AudioBuffer::new(bootstrap_clean, clean.sample_rate(), 1);
let bootstrap_processed_buffer =
AudioBuffer::new(bootstrap_processed, processed.sample_rate(), 1);
let score = self
.calculate_language_adapted_stoi(
&bootstrap_clean_buffer,
&bootstrap_processed_buffer,
language,
)
.await?;
bootstrap_scores.push(score);
}
Ok(bootstrap_scores)
}
fn calculate_t_value(&self, alpha: f32, degrees_of_freedom: usize) -> f32 {
match degrees_of_freedom {
df if df >= 30 => {
match alpha {
a if a <= 0.005 => 2.807,
a if a <= 0.01 => 2.576,
a if a <= 0.025 => 1.960,
a if a <= 0.05 => 1.645,
a if a <= 0.1 => 1.282,
_ => 1.960,
}
}
df if df >= 20 => match alpha {
a if a <= 0.005 => 2.845,
a if a <= 0.01 => 2.528,
a if a <= 0.025 => 2.093,
a if a <= 0.05 => 1.725,
a if a <= 0.1 => 1.325,
_ => 2.093,
},
df if df >= 10 => match alpha {
a if a <= 0.005 => 3.169,
a if a <= 0.01 => 2.764,
a if a <= 0.025 => 2.228,
a if a <= 0.05 => 1.812,
a if a <= 0.1 => 1.372,
_ => 2.228,
},
_ => {
match alpha {
a if a <= 0.005 => 4.032,
a if a <= 0.01 => 3.499,
a if a <= 0.025 => 2.776,
a if a <= 0.05 => 2.201,
a if a <= 0.1 => 1.533,
_ => 2.776,
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use voirs_sdk::AudioBuffer;
#[tokio::test]
async fn test_stoi_evaluator_creation() {
let evaluator = STOIEvaluator::new(16000).unwrap();
assert_eq!(evaluator.sample_rate, 16000);
assert!(!evaluator.octave_bands.is_empty());
}
#[tokio::test]
async fn test_stoi_calculation() {
let evaluator = STOIEvaluator::new(16000).unwrap();
let duration_samples = 3 * 16000;
let clean = AudioBuffer::new(vec![0.1; duration_samples], 16000, 1);
let processed = AudioBuffer::new(vec![0.08; duration_samples], 16000, 1);
let stoi_score = evaluator.calculate_stoi(&clean, &processed).await.unwrap();
assert!(stoi_score >= 0.0);
assert!(stoi_score <= 1.0);
}
#[tokio::test]
async fn test_estoi_calculation() {
let evaluator = STOIEvaluator::new(16000).unwrap();
let duration_samples = 3 * 16000;
let clean = AudioBuffer::new(vec![0.1; duration_samples], 16000, 1);
let processed = AudioBuffer::new(vec![0.08; duration_samples], 16000, 1);
let estoi_score = evaluator.calculate_estoi(&clean, &processed).await.unwrap();
assert!(estoi_score >= 0.0);
assert!(estoi_score <= 1.0);
}
#[test]
fn test_preemphasis_filter() {
let evaluator = STOIEvaluator::new(16000).unwrap();
let signal = vec![1.0, 0.5, 0.25, 0.125];
let filtered = evaluator.apply_preemphasis(&signal).unwrap();
assert_eq!(filtered[0], 1.0);
assert!((filtered[1] - (0.5 - 0.97 * 1.0)).abs() < 1e-6);
}
#[test]
fn test_correlation_coefficient() {
let evaluator = STOIEvaluator::new(16000).unwrap();
let x = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0]);
let y = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0]);
let correlation = evaluator.compute_correlation_coefficient(&x.view(), &y.view());
assert!((correlation - 1.0).abs() < 1e-6);
let z = Array1::from_vec(vec![4.0, 3.0, 2.0, 1.0]);
let neg_correlation = evaluator.compute_correlation_coefficient(&x.view(), &z.view());
assert!((neg_correlation + 1.0).abs() < 1e-6); }
#[test]
fn test_octave_bands_creation() {
let bands = STOIEvaluator::create_octave_bands(16000);
assert!(!bands.is_empty());
for &(f_low, f_high) in &bands {
assert!(f_low < 8000.0);
assert!(f_high < 8000.0);
assert!(f_low < f_high);
}
assert!(bands[0].0 >= 100.0);
assert!(bands[0].0 <= 200.0);
}
#[tokio::test]
async fn test_language_adapted_stoi() {
let evaluator = STOIEvaluator::new(16000).unwrap();
let duration_samples = 3 * 16000;
let mut clean_samples = Vec::with_capacity(duration_samples);
let mut processed_samples = Vec::with_capacity(duration_samples);
for i in 0..duration_samples {
let t = i as f32 / 16000.0;
let clean_val = 0.1 * (2.0 * PI * 440.0 * t).sin()
+ 0.05 * (2.0 * PI * 880.0 * t).sin()
+ 0.03 * (2.0 * PI * 1320.0 * t).sin();
let processed_val = clean_val * 0.8 + 0.02 * (2.0 * PI * 60.0 * t).sin();
clean_samples.push(clean_val);
processed_samples.push(processed_val);
}
let clean = AudioBuffer::new(clean_samples, 16000, 1);
let processed = AudioBuffer::new(processed_samples, 16000, 1);
let en_score = evaluator
.calculate_language_adapted_stoi(&clean, &processed, Some(LanguageCode::EnUs))
.await
.unwrap();
let ja_score = evaluator
.calculate_language_adapted_stoi(&clean, &processed, Some(LanguageCode::JaJp))
.await
.unwrap();
let zh_score = evaluator
.calculate_language_adapted_stoi(&clean, &processed, Some(LanguageCode::ZhCn))
.await
.unwrap();
assert!((0.0..=1.0).contains(&en_score));
assert!((0.0..=1.0).contains(&ja_score));
assert!((0.0..=1.0).contains(&zh_score));
let tolerance = 0.01;
assert!(
(en_score - ja_score).abs() > tolerance,
"English score ({}) should differ from Japanese score ({}) by more than {}",
en_score,
ja_score,
tolerance
);
assert!(
(en_score - zh_score).abs() > tolerance,
"English score ({}) should differ from Chinese score ({}) by more than {}",
en_score,
zh_score,
tolerance
);
}
#[tokio::test]
async fn test_stoi_with_confidence_interval() {
let evaluator = STOIEvaluator::new(16000).unwrap();
let duration_samples = 4 * 16000; let clean = AudioBuffer::new(vec![0.1; duration_samples], 16000, 1);
let processed = AudioBuffer::new(vec![0.08; duration_samples], 16000, 1);
let confidence_result = evaluator
.calculate_stoi_with_confidence(&clean, &processed, 0.95, 20)
.await
.unwrap();
assert!(confidence_result.score >= 0.0 && confidence_result.score <= 1.0);
assert!(confidence_result.lower_bound >= 0.0);
assert!(confidence_result.upper_bound <= 1.0);
assert!(confidence_result.lower_bound <= confidence_result.score);
assert!(confidence_result.score <= confidence_result.upper_bound);
assert_eq!(confidence_result.confidence_level, 0.95);
assert!(confidence_result.standard_error >= 0.0);
}
#[tokio::test]
async fn test_language_adapted_stoi_with_confidence() {
let evaluator = STOIEvaluator::new(16000).unwrap();
let duration_samples = 4 * 16000; let clean = AudioBuffer::new(vec![0.1; duration_samples], 16000, 1);
let processed = AudioBuffer::new(vec![0.08; duration_samples], 16000, 1);
let confidence_result = evaluator
.calculate_language_adapted_stoi_with_confidence(
&clean,
&processed,
Some(LanguageCode::EnUs),
0.95,
15,
)
.await
.unwrap();
assert!(confidence_result.score >= 0.0 && confidence_result.score <= 1.0);
assert!(confidence_result.lower_bound >= 0.0);
assert!(confidence_result.upper_bound <= 1.0);
assert!(confidence_result.lower_bound <= confidence_result.score);
assert!(confidence_result.score <= confidence_result.upper_bound);
assert_eq!(confidence_result.confidence_level, 0.95);
assert!(confidence_result.standard_error >= 0.0);
}
#[test]
fn test_language_params_creation() {
let en_params = STOIEvaluator::create_language_params(LanguageCode::EnUs);
let ja_params = STOIEvaluator::create_language_params(LanguageCode::JaJp);
let zh_params = STOIEvaluator::create_language_params(LanguageCode::ZhCn);
assert_eq!(en_params.language, LanguageCode::EnUs);
assert_eq!(ja_params.language, LanguageCode::JaJp);
assert_eq!(zh_params.language, LanguageCode::ZhCn);
assert_ne!(
en_params.intelligibility_threshold,
ja_params.intelligibility_threshold
);
assert_ne!(
en_params.intelligibility_threshold,
zh_params.intelligibility_threshold
);
}
#[test]
fn test_t_value_calculation() {
let evaluator = STOIEvaluator::new(16000).unwrap();
let t_005_30 = evaluator.calculate_t_value(0.005, 30);
let t_025_30 = evaluator.calculate_t_value(0.025, 30);
let t_05_30 = evaluator.calculate_t_value(0.05, 30);
assert!(t_005_30 > t_025_30);
assert!(t_025_30 > t_05_30);
assert!(t_005_30 > 0.0);
assert!(t_025_30 > 0.0);
assert!(t_05_30 > 0.0);
}
}