use crate::EvaluationError;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
use voirs_sdk::AudioBuffer;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RealTimeQualityConfig {
pub analysis_window_size: usize,
pub window_overlap: f32,
pub quality_threshold: f32,
pub max_processing_latency_ms: u64,
pub adaptive_thresholds: bool,
pub history_buffer_size: usize,
pub detailed_analysis: bool,
pub target_sample_rate: u32,
}
impl Default for RealTimeQualityConfig {
fn default() -> Self {
Self {
analysis_window_size: 1024,
window_overlap: 0.5,
quality_threshold: 0.7,
max_processing_latency_ms: 10,
adaptive_thresholds: true,
history_buffer_size: 100,
detailed_analysis: false,
target_sample_rate: 22050,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RealTimeQualityMetrics {
pub overall_quality: f32,
pub snr_db: f32,
pub spectral_distortion: f32,
pub temporal_consistency: f32,
pub perceptual_quality: f32,
pub quality_trend: QualityTrend,
pub processing_latency_us: u64,
pub timestamp: DateTime<Utc>,
pub detailed_metrics: Option<DetailedQualityMetrics>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum QualityTrend {
Improving,
Stable,
Degrading,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DetailedQualityMetrics {
pub frequency_metrics: FrequencyDomainMetrics,
pub time_metrics: TimeDomainMetrics,
pub perceptual_metrics: PerceptualMetrics,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FrequencyDomainMetrics {
pub spectral_centroid: f32,
pub spectral_rolloff: f32,
pub harmonic_distortion: f32,
pub frequency_flatness: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeDomainMetrics {
pub zero_crossing_rate: f32,
pub rms_energy: f32,
pub envelope_consistency: f32,
pub click_pop_score: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerceptualMetrics {
pub naturalness: f32,
pub intelligibility: f32,
pub pleasantness: f32,
pub robotic_score: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum QualityAlert {
QualityBelowThreshold {
current_quality: f32,
threshold: f32,
severity: AlertSeverity,
},
LatencyExceeded {
current_latency_ms: u64,
max_latency_ms: u64,
},
QualityDegrading {
trend_duration: Duration,
degradation_rate: f32,
},
SpectralAnomaly {
anomaly_type: String,
confidence: f32,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum AlertSeverity {
Low,
Medium,
High,
Critical,
}
pub struct RealTimeQualityMonitor {
config: RealTimeQualityConfig,
quality_history: Arc<Mutex<VecDeque<RealTimeQualityMetrics>>>,
adaptive_thresholds: Arc<RwLock<AdaptiveThresholds>>,
alert_callbacks: Arc<Mutex<Vec<Box<dyn Fn(QualityAlert) + Send + Sync>>>>,
processing_stats: Arc<Mutex<ProcessingStats>>,
}
#[derive(Debug)]
struct AdaptiveThresholds {
quality_threshold: f32,
snr_threshold: f32,
last_update: Instant,
update_interval: Duration,
}
#[derive(Debug, Default)]
pub struct ProcessingStats {
pub total_samples_processed: u64,
pub total_processing_time: Duration,
pub peak_latency: Duration,
pub average_latency: Duration,
pub quality_violations: u32,
}
impl RealTimeQualityMonitor {
pub fn new(config: RealTimeQualityConfig) -> Self {
let adaptive_thresholds = AdaptiveThresholds {
quality_threshold: config.quality_threshold,
snr_threshold: 15.0, last_update: Instant::now(),
update_interval: Duration::from_secs(30),
};
Self {
config,
quality_history: Arc::new(Mutex::new(VecDeque::new())),
adaptive_thresholds: Arc::new(RwLock::new(adaptive_thresholds)),
alert_callbacks: Arc::new(Mutex::new(Vec::new())),
processing_stats: Arc::new(Mutex::new(ProcessingStats::default())),
}
}
pub fn add_alert_callback<F>(&self, callback: F)
where
F: Fn(QualityAlert) + Send + Sync + 'static,
{
self.alert_callbacks
.lock()
.expect("value should be present")
.push(Box::new(callback));
}
pub async fn process_chunk(
&self,
audio_chunk: &AudioBuffer,
) -> Result<RealTimeQualityMetrics, EvaluationError> {
let start_time = Instant::now();
if audio_chunk.samples().is_empty() {
return Err(EvaluationError::InvalidInput {
message: "Empty audio chunk".to_string(),
});
}
if audio_chunk.sample_rate() != self.config.target_sample_rate {
return Err(EvaluationError::InvalidInput {
message: format!(
"Sample rate mismatch: expected {}, got {}",
self.config.target_sample_rate,
audio_chunk.sample_rate()
),
});
}
let metrics = self.calculate_quality_metrics(audio_chunk).await?;
self.update_processing_stats(audio_chunk.samples().len(), start_time.elapsed())
.await;
self.check_quality_alerts(&metrics).await;
self.store_quality_metrics(metrics.clone()).await;
if self.config.adaptive_thresholds {
self.update_adaptive_thresholds().await;
}
Ok(metrics)
}
async fn calculate_quality_metrics(
&self,
audio_chunk: &AudioBuffer,
) -> Result<RealTimeQualityMetrics, EvaluationError> {
let samples = audio_chunk.samples();
let snr_db = self.calculate_snr(samples);
let spectral_distortion = self.calculate_spectral_distortion(samples);
let temporal_consistency = self.calculate_temporal_consistency(samples);
let perceptual_quality = self.calculate_perceptual_quality(samples);
let overall_quality = 0.3 * (snr_db / 30.0).clamp(0.0, 1.0)
+ 0.25 * (1.0 - spectral_distortion.clamp(0.0, 1.0))
+ 0.25 * temporal_consistency.clamp(0.0, 1.0)
+ 0.2 * perceptual_quality.clamp(0.0, 1.0);
let quality_trend = self.calculate_quality_trend(overall_quality).await;
let detailed_metrics = if self.config.detailed_analysis {
Some(self.calculate_detailed_metrics(samples))
} else {
None
};
Ok(RealTimeQualityMetrics {
overall_quality,
snr_db,
spectral_distortion,
temporal_consistency,
perceptual_quality,
quality_trend,
processing_latency_us: 0, timestamp: Utc::now(),
detailed_metrics,
})
}
fn calculate_snr(&self, samples: &[f32]) -> f32 {
if samples.is_empty() {
return 0.0;
}
let signal_rms =
(samples.iter().map(|&x| x * x).sum::<f32>() / samples.len() as f32).sqrt();
let noise_estimate = self.estimate_noise_level(samples);
if noise_estimate > 0.0 {
20.0 * (signal_rms / noise_estimate).log10()
} else {
60.0 }
}
fn estimate_noise_level(&self, samples: &[f32]) -> f32 {
if samples.len() < 2 {
return 0.001; }
let mut noise_energy = 0.0;
for i in 1..samples.len() {
let diff = samples[i] - samples[i - 1];
noise_energy += diff * diff;
}
(noise_energy / (samples.len() - 1) as f32)
.sqrt()
.max(0.001)
}
fn calculate_spectral_distortion(&self, samples: &[f32]) -> f32 {
if samples.is_empty() {
return 1.0; }
let mut spectral_peaks = 0;
let window_size = (samples.len() / 10).max(1);
for window_start in (0..samples.len()).step_by(window_size) {
let window_end = (window_start + window_size).min(samples.len());
let window = &samples[window_start..window_end];
let max_val = window.iter().fold(0.0f32, |acc, &x| acc.max(x.abs()));
let avg_val = window.iter().map(|&x| x.abs()).sum::<f32>() / window.len() as f32;
if avg_val > 0.0 && max_val / avg_val > 3.0 {
spectral_peaks += 1;
}
}
(spectral_peaks as f32 / 10.0).clamp(0.0, 1.0)
}
fn calculate_temporal_consistency(&self, samples: &[f32]) -> f32 {
if samples.len() < 2 {
return 1.0; }
let frame_size = (samples.len() / 20).max(1);
let mut frame_energies = Vec::new();
for frame_start in (0..samples.len()).step_by(frame_size) {
let frame_end = (frame_start + frame_size).min(samples.len());
let frame = &samples[frame_start..frame_end];
let energy = frame.iter().map(|&x| x * x).sum::<f32>() / frame.len() as f32;
frame_energies.push(energy);
}
if frame_energies.len() < 2 {
return 1.0;
}
let mean_energy = frame_energies.iter().sum::<f32>() / frame_energies.len() as f32;
let variance = frame_energies
.iter()
.map(|&e| (e - mean_energy).powi(2))
.sum::<f32>()
/ frame_energies.len() as f32;
if mean_energy > 0.0 {
1.0 / (1.0 + variance / mean_energy)
} else {
1.0
}
}
fn calculate_perceptual_quality(&self, samples: &[f32]) -> f32 {
if samples.is_empty() {
return 0.0;
}
let dynamic_range = self.calculate_dynamic_range(samples);
let spectral_richness = self.calculate_spectral_richness(samples);
let temporal_smoothness = self.calculate_temporal_smoothness(samples);
0.4 * dynamic_range + 0.3 * spectral_richness + 0.3 * temporal_smoothness
}
fn calculate_dynamic_range(&self, samples: &[f32]) -> f32 {
if samples.is_empty() {
return 0.0;
}
let max_val = samples.iter().fold(0.0f32, |acc, &x| acc.max(x.abs()));
let rms = (samples.iter().map(|&x| x * x).sum::<f32>() / samples.len() as f32).sqrt();
if rms > 0.0 {
(max_val / rms / 10.0).clamp(0.0, 1.0)
} else {
0.0
}
}
fn calculate_spectral_richness(&self, samples: &[f32]) -> f32 {
if samples.len() < 4 {
return 0.5;
}
let mut zero_crossings = 0;
for i in 1..samples.len() {
if (samples[i] >= 0.0) != (samples[i - 1] >= 0.0) {
zero_crossings += 1;
}
}
let normalized_crossings = zero_crossings as f32 / samples.len() as f32;
(normalized_crossings * 10.0).clamp(0.0, 1.0)
}
fn calculate_temporal_smoothness(&self, samples: &[f32]) -> f32 {
if samples.len() < 2 {
return 1.0;
}
let mut total_variation = 0.0;
for i in 1..samples.len() {
total_variation += (samples[i] - samples[i - 1]).abs();
}
let average_variation = total_variation / (samples.len() - 1) as f32;
1.0 / (1.0 + average_variation * 10.0)
}
fn calculate_detailed_metrics(&self, samples: &[f32]) -> DetailedQualityMetrics {
DetailedQualityMetrics {
frequency_metrics: FrequencyDomainMetrics {
spectral_centroid: self.calculate_spectral_centroid(samples),
spectral_rolloff: self.calculate_spectral_rolloff(samples),
harmonic_distortion: self.calculate_harmonic_distortion(samples),
frequency_flatness: self.calculate_frequency_flatness(samples),
},
time_metrics: TimeDomainMetrics {
zero_crossing_rate: self.calculate_zero_crossing_rate(samples),
rms_energy: self.calculate_rms_energy(samples),
envelope_consistency: self.calculate_envelope_consistency(samples),
click_pop_score: self.calculate_click_pop_score(samples),
},
perceptual_metrics: PerceptualMetrics {
naturalness: self.calculate_naturalness(samples),
intelligibility: self.calculate_intelligibility(samples),
pleasantness: self.calculate_pleasantness(samples),
robotic_score: self.calculate_robotic_score(samples),
},
}
}
fn calculate_spectral_centroid(&self, samples: &[f32]) -> f32 {
if samples.is_empty() {
return 0.0;
}
let zcr = self.calculate_zero_crossing_rate(samples);
zcr * 1000.0 }
fn calculate_spectral_rolloff(&self, samples: &[f32]) -> f32 {
if samples.is_empty() {
return 0.0;
}
let high_freq_energy = self.calculate_high_frequency_energy(samples);
high_freq_energy * 8000.0 }
fn calculate_harmonic_distortion(&self, samples: &[f32]) -> f32 {
self.calculate_spectral_distortion(samples)
}
fn calculate_frequency_flatness(&self, samples: &[f32]) -> f32 {
1.0 - self.calculate_spectral_distortion(samples)
}
fn calculate_zero_crossing_rate(&self, samples: &[f32]) -> f32 {
if samples.len() < 2 {
return 0.0;
}
let mut zero_crossings = 0;
for i in 1..samples.len() {
if (samples[i] >= 0.0) != (samples[i - 1] >= 0.0) {
zero_crossings += 1;
}
}
zero_crossings as f32 / (samples.len() - 1) as f32
}
fn calculate_rms_energy(&self, samples: &[f32]) -> f32 {
if samples.is_empty() {
return 0.0;
}
(samples.iter().map(|&x| x * x).sum::<f32>() / samples.len() as f32).sqrt()
}
fn calculate_envelope_consistency(&self, samples: &[f32]) -> f32 {
self.calculate_temporal_consistency(samples)
}
fn calculate_click_pop_score(&self, samples: &[f32]) -> f32 {
if samples.len() < 3 {
return 0.0;
}
let mut click_count = 0;
let threshold = 0.1;
for i in 1..samples.len() - 1 {
let prev_diff = (samples[i] - samples[i - 1]).abs();
let next_diff = (samples[i + 1] - samples[i]).abs();
if prev_diff > threshold && next_diff > threshold {
click_count += 1;
}
}
(click_count as f32 / samples.len() as f32 * 100.0).clamp(0.0, 1.0)
}
fn calculate_naturalness(&self, samples: &[f32]) -> f32 {
let spectral_richness = self.calculate_spectral_richness(samples);
let temporal_smoothness = self.calculate_temporal_smoothness(samples);
let dynamic_range = self.calculate_dynamic_range(samples);
(spectral_richness + temporal_smoothness + dynamic_range) / 3.0
}
fn calculate_intelligibility(&self, samples: &[f32]) -> f32 {
let snr = self.calculate_snr(samples);
let spectral_clarity = 1.0 - self.calculate_spectral_distortion(samples);
((snr / 30.0).clamp(0.0, 1.0) + spectral_clarity) / 2.0
}
fn calculate_pleasantness(&self, samples: &[f32]) -> f32 {
let smoothness = self.calculate_temporal_smoothness(samples);
let low_distortion = 1.0 - self.calculate_spectral_distortion(samples);
let good_dynamics = self.calculate_dynamic_range(samples);
(smoothness + low_distortion + good_dynamics) / 3.0
}
fn calculate_robotic_score(&self, samples: &[f32]) -> f32 {
let low_variation = 1.0 - self.calculate_temporal_consistency(samples);
let spectral_artifacts = self.calculate_spectral_distortion(samples);
(low_variation + spectral_artifacts) / 2.0
}
fn calculate_high_frequency_energy(&self, samples: &[f32]) -> f32 {
if samples.len() < 2 {
return 0.0;
}
let mut high_freq_energy = 0.0;
for i in 1..samples.len() {
let derivative = samples[i] - samples[i - 1];
high_freq_energy += derivative * derivative;
}
(high_freq_energy / (samples.len() - 1) as f32).sqrt()
}
async fn calculate_quality_trend(&self, current_quality: f32) -> QualityTrend {
let history = self
.quality_history
.lock()
.expect("lock should not be poisoned");
if history.len() < 3 {
return QualityTrend::Unknown;
}
let recent_qualities: Vec<f32> = history
.iter()
.rev()
.take(5)
.map(|m| m.overall_quality)
.collect();
if recent_qualities.len() < 3 {
return QualityTrend::Unknown;
}
let n = recent_qualities.len() as f32;
let x_sum = (0..recent_qualities.len()).map(|i| i as f32).sum::<f32>();
let y_sum = recent_qualities.iter().sum::<f32>();
let xy_sum = recent_qualities
.iter()
.enumerate()
.map(|(i, &y)| i as f32 * y)
.sum::<f32>();
let x2_sum = (0..recent_qualities.len())
.map(|i| (i as f32).powi(2))
.sum::<f32>();
let slope = (n * xy_sum - x_sum * y_sum) / (n * x2_sum - x_sum * x_sum);
match slope {
s if s > 0.01 => QualityTrend::Improving,
s if s < -0.01 => QualityTrend::Degrading,
_ => QualityTrend::Stable,
}
}
async fn store_quality_metrics(&self, metrics: RealTimeQualityMetrics) {
let mut history = self
.quality_history
.lock()
.expect("lock should not be poisoned");
history.push_back(metrics);
while history.len() > self.config.history_buffer_size {
history.pop_front();
}
}
async fn check_quality_alerts(&self, metrics: &RealTimeQualityMetrics) {
let thresholds = self.adaptive_thresholds.read().await;
let mut alerts = Vec::new();
if metrics.overall_quality < thresholds.quality_threshold {
let severity = match metrics.overall_quality {
q if q < 0.3 => AlertSeverity::Critical,
q if q < 0.5 => AlertSeverity::High,
q if q < 0.6 => AlertSeverity::Medium,
_ => AlertSeverity::Low,
};
alerts.push(QualityAlert::QualityBelowThreshold {
current_quality: metrics.overall_quality,
threshold: thresholds.quality_threshold,
severity,
});
}
if metrics.snr_db < thresholds.snr_threshold {
alerts.push(QualityAlert::SpectralAnomaly {
anomaly_type: "Low SNR".to_string(),
confidence: 1.0 - (metrics.snr_db / thresholds.snr_threshold).clamp(0.0, 1.0),
});
}
if matches!(metrics.quality_trend, QualityTrend::Degrading) {
alerts.push(QualityAlert::QualityDegrading {
trend_duration: Duration::from_secs(30), degradation_rate: 0.1, });
}
if !alerts.is_empty() {
let callbacks = self
.alert_callbacks
.lock()
.expect("lock should not be poisoned");
for alert in alerts {
for callback in callbacks.iter() {
callback(alert.clone());
}
}
}
}
async fn update_adaptive_thresholds(&self) {
let mut thresholds = self.adaptive_thresholds.write().await;
if thresholds.last_update.elapsed() < thresholds.update_interval {
return;
}
let history = self
.quality_history
.lock()
.expect("lock should not be poisoned");
if history.len() < 10 {
return; }
let recent_qualities: Vec<f32> = history
.iter()
.rev()
.take(20)
.map(|m| m.overall_quality)
.collect();
if !recent_qualities.is_empty() {
let mean_quality = recent_qualities.iter().sum::<f32>() / recent_qualities.len() as f32;
let std_dev = {
let variance = recent_qualities
.iter()
.map(|&q| (q - mean_quality).powi(2))
.sum::<f32>()
/ recent_qualities.len() as f32;
variance.sqrt()
};
thresholds.quality_threshold = (mean_quality - std_dev).clamp(0.1, 0.9);
}
thresholds.last_update = Instant::now();
}
async fn update_processing_stats(&self, samples_processed: usize, processing_time: Duration) {
let mut stats = self
.processing_stats
.lock()
.expect("lock should not be poisoned");
stats.total_samples_processed += samples_processed as u64;
stats.total_processing_time += processing_time;
if processing_time > stats.peak_latency {
stats.peak_latency = processing_time;
}
let total_chunks = (stats.total_samples_processed / 1024).max(1); stats.average_latency = stats.total_processing_time / total_chunks as u32;
}
pub async fn get_processing_stats(&self) -> ProcessingStats {
let stats = self
.processing_stats
.lock()
.expect("lock should not be poisoned");
ProcessingStats {
total_samples_processed: stats.total_samples_processed,
total_processing_time: stats.total_processing_time,
peak_latency: stats.peak_latency,
average_latency: stats.average_latency,
quality_violations: stats.quality_violations,
}
}
pub async fn get_quality_history(&self) -> Vec<RealTimeQualityMetrics> {
let history = self
.quality_history
.lock()
.expect("lock should not be poisoned");
history.iter().cloned().collect()
}
pub async fn get_adaptive_thresholds(&self) -> (f32, f32) {
let thresholds = self.adaptive_thresholds.read().await;
(thresholds.quality_threshold, thresholds.snr_threshold)
}
pub async fn reset(&self) {
self.quality_history
.lock()
.expect("lock should not be poisoned")
.clear();
*self
.processing_stats
.lock()
.expect("lock should not be poisoned") = ProcessingStats::default();
let mut thresholds = self.adaptive_thresholds.write().await;
thresholds.quality_threshold = self.config.quality_threshold;
thresholds.snr_threshold = 15.0;
thresholds.last_update = Instant::now();
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio;
#[tokio::test]
async fn test_real_time_quality_monitor_creation() {
let config = RealTimeQualityConfig::default();
let monitor = RealTimeQualityMonitor::new(config);
let stats = monitor.get_processing_stats().await;
assert_eq!(stats.total_samples_processed, 0);
}
#[tokio::test]
async fn test_process_chunk() {
let config = RealTimeQualityConfig::default();
let monitor = RealTimeQualityMonitor::new(config);
let test_samples = vec![0.1, 0.2, -0.1, -0.2, 0.15, -0.15]; let audio_buffer = AudioBuffer::new(test_samples, 22050, 1);
let result = monitor.process_chunk(&audio_buffer).await;
assert!(result.is_ok());
let metrics = result.unwrap();
assert!(metrics.overall_quality >= 0.0 && metrics.overall_quality <= 1.0);
assert!(metrics.snr_db >= -40.0); }
#[tokio::test]
async fn test_quality_metrics_calculation() {
let config = RealTimeQualityConfig::default();
let monitor = RealTimeQualityMonitor::new(config);
let test_cases = vec![
vec![0.0; 1024], (0..1024)
.map(|i| (i as f32 * 0.01).sin())
.collect::<Vec<f32>>(), (0..1024)
.map(|i| (i as f32 * 0.037).sin() * 0.05)
.collect::<Vec<f32>>(), ];
for samples in test_cases {
let audio_buffer = AudioBuffer::new(samples, 22050, 1);
let result = monitor.process_chunk(&audio_buffer).await;
assert!(result.is_ok());
let metrics = result.unwrap();
assert!(metrics.overall_quality >= 0.0 && metrics.overall_quality <= 1.0);
assert!(metrics.temporal_consistency >= 0.0 && metrics.temporal_consistency <= 1.0);
assert!(metrics.spectral_distortion >= 0.0 && metrics.spectral_distortion <= 1.0);
}
}
#[tokio::test]
async fn test_quality_trend_detection() {
let config = RealTimeQualityConfig::default();
let monitor = RealTimeQualityMonitor::new(config);
let quality_values = vec![0.9, 0.85, 0.8, 0.75, 0.7];
for quality in quality_values {
let samples = vec![quality; 1024]; let audio_buffer = AudioBuffer::new(samples, 22050, 1);
let _ = monitor.process_chunk(&audio_buffer).await;
}
let history = monitor.get_quality_history().await;
assert!(!history.is_empty());
}
#[tokio::test]
async fn test_alert_callback() {
let config = RealTimeQualityConfig {
quality_threshold: 0.8, ..Default::default()
};
let monitor = RealTimeQualityMonitor::new(config);
let alert_received = Arc::new(Mutex::new(false));
let alert_received_clone = alert_received.clone();
monitor.add_alert_callback(move |_alert| {
*alert_received_clone.lock().unwrap() = true;
});
let low_quality_samples = vec![0.01; 1024]; let audio_buffer = AudioBuffer::new(low_quality_samples, 22050, 1);
let _ = monitor.process_chunk(&audio_buffer).await;
tokio::time::sleep(Duration::from_millis(10)).await;
}
#[tokio::test]
async fn test_adaptive_thresholds() {
let config = RealTimeQualityConfig {
adaptive_thresholds: true,
..Default::default()
};
let monitor = RealTimeQualityMonitor::new(config);
let initial_thresholds = monitor.get_adaptive_thresholds().await;
for i in 0..15 {
let quality = 0.7 + (i as f32 * 0.01); let samples = vec![quality; 1024];
let audio_buffer = AudioBuffer::new(samples, 22050, 1);
let _ = monitor.process_chunk(&audio_buffer).await;
}
let final_thresholds = monitor.get_adaptive_thresholds().await;
assert!(final_thresholds.0 > 0.0);
assert!(final_thresholds.1 > 0.0);
}
#[tokio::test]
async fn test_detailed_metrics() {
let config = RealTimeQualityConfig {
detailed_analysis: true,
..Default::default()
};
let monitor = RealTimeQualityMonitor::new(config);
let test_samples = (0..1024)
.map(|i| (i as f32 * 0.01).sin())
.collect::<Vec<f32>>();
let audio_buffer = AudioBuffer::new(test_samples, 22050, 1);
let result = monitor.process_chunk(&audio_buffer).await;
assert!(result.is_ok());
let metrics = result.unwrap();
assert!(metrics.detailed_metrics.is_some());
let detailed = metrics.detailed_metrics.unwrap();
assert!(detailed.frequency_metrics.spectral_centroid >= 0.0);
assert!(detailed.time_metrics.zero_crossing_rate >= 0.0);
assert!(detailed.perceptual_metrics.naturalness >= 0.0);
}
#[tokio::test]
async fn test_processing_stats() {
let config = RealTimeQualityConfig::default();
let monitor = RealTimeQualityMonitor::new(config);
for _ in 0..3 {
let samples = vec![0.1; 1024];
let audio_buffer = AudioBuffer::new(samples, 22050, 1);
let _ = monitor.process_chunk(&audio_buffer).await;
}
let stats = monitor.get_processing_stats().await;
assert!(stats.total_samples_processed > 0);
assert!(stats.total_processing_time > Duration::from_nanos(0));
}
#[tokio::test]
async fn test_reset_functionality() {
let config = RealTimeQualityConfig::default();
let monitor = RealTimeQualityMonitor::new(config);
let samples = vec![0.1; 1024];
let audio_buffer = AudioBuffer::new(samples, 22050, 1);
let _ = monitor.process_chunk(&audio_buffer).await;
let history_before = monitor.get_quality_history().await;
assert!(!history_before.is_empty());
monitor.reset().await;
let history_after = monitor.get_quality_history().await;
assert!(history_after.is_empty());
let stats_after = monitor.get_processing_stats().await;
assert_eq!(stats_after.total_samples_processed, 0);
}
}