#![allow(clippy::unwrap_used)]
#![allow(clippy::expect_used)]
#![allow(clippy::print_stdout)]
#![allow(clippy::print_stderr)]
use std::time::Instant;
use speech_prep::error::Result;
use speech_prep::preprocessing::{
DcHighPassFilter, NoiseReducer, NoiseReductionConfig, Normalizer, PreprocessingConfig,
QualityAssessor, VadContext,
};
const SAMPLE_RATE: u32 = 16000;
const ONE_SECOND_SAMPLES: usize = 16000;
#[derive(Debug)]
struct PreprocessedAudio {
samples: Vec<f32>,
snr_db: f32,
energy: f32,
quality_score: f32,
}
fn preprocess_audio(input: &[f32]) -> Result<PreprocessedAudio> {
let dc_config = PreprocessingConfig::default();
let mut dc_filter = DcHighPassFilter::new(dc_config)?;
let filtered = dc_filter.process(input, None)?;
let noise_config = NoiseReductionConfig::default();
let mut noise_reducer = NoiseReducer::new(noise_config)?;
let vad_ctx = VadContext { is_silence: false };
let denoised = noise_reducer.reduce(&filtered, Some(vad_ctx))?;
let normalizer = Normalizer::new(0.5, 10.0)?;
let normalized_audio = normalizer.normalize(&denoised)?;
let assessor = QualityAssessor::new(SAMPLE_RATE);
let metrics = assessor.assess(&normalized_audio)?;
Ok(PreprocessedAudio {
samples: normalized_audio,
snr_db: metrics.snr_db,
energy: metrics.energy,
quality_score: metrics.quality_score,
})
}
#[test]
fn test_pipeline_integration_basic() {
let input = vec![0.3f32; ONE_SECOND_SAMPLES];
let result = preprocess_audio(&input).expect("Pipeline should execute without errors");
assert_eq!(result.samples.len(), ONE_SECOND_SAMPLES);
assert!((0.0..=60.0).contains(&result.snr_db));
assert!((0.0..=1.0).contains(&result.energy));
assert!((0.0..=1.0).contains(&result.quality_score));
}
#[test]
fn test_pipeline_stages_executed() {
let mut input = vec![0.0f32; ONE_SECOND_SAMPLES];
for (i, sample) in input.iter_mut().enumerate() {
let sine_wave = (2.0 * std::f32::consts::PI * 440.0 * i as f32 / SAMPLE_RATE as f32).sin();
*sample = 0.1f32.mul_add(sine_wave, 0.5);
}
let result = preprocess_audio(&input).expect("Pipeline should process audio with DC offset");
assert_ne!(result.samples, input);
assert!(result.snr_db > 0.0 || result.quality_score > 0.0);
}
#[test]
fn test_dc_offset_removal_integration() {
let dc_offset = 0.3f32;
let mut input = vec![0.0f32; ONE_SECOND_SAMPLES];
for (i, sample) in input.iter_mut().enumerate() {
let sine_component =
0.1 * (2.0 * std::f32::consts::PI * 440.0 * i as f32 / SAMPLE_RATE as f32).sin();
*sample = sine_component.mul_add(1.0, dc_offset);
}
let result = preprocess_audio(&input).expect("Pipeline should remove DC offset");
let mean: f32 = result.samples.iter().sum::<f32>() / result.samples.len() as f32;
assert!(
mean.abs() < 0.1,
"Expected DC offset removal, mean = {mean:.3}"
);
}
#[test]
fn test_normalization_integration() {
let mut input = vec![0.0f32; ONE_SECOND_SAMPLES];
for (i, sample) in input.iter_mut().enumerate() {
*sample = 0.05 * (2.0 * std::f32::consts::PI * 440.0 * i as f32 / SAMPLE_RATE as f32).sin();
}
let result = preprocess_audio(&input).expect("Pipeline should normalize quiet audio");
assert!(
result.energy > 0.001,
"Expected measurable energy after normalization, got {:.6}",
result.energy
);
assert!((0.0..=1.0).contains(&result.quality_score));
}
#[test]
fn test_quality_assessment_integration() {
let mut input = vec![0.0f32; ONE_SECOND_SAMPLES];
let start = ONE_SECOND_SAMPLES / 4;
let end = 3 * ONE_SECOND_SAMPLES / 4;
for (offset, sample) in input.iter_mut().skip(start).take(end - start).enumerate() {
let i = start + offset;
*sample = (2.0 * std::f32::consts::PI * 440.0 * i as f32 / SAMPLE_RATE as f32).sin() * 0.5;
}
let result = preprocess_audio(&input).expect("Pipeline should process high-quality audio");
assert!(
result.snr_db > 10.0,
"Expected SNR > 10 dB for clean audio, got {:.1} dB",
result.snr_db
);
assert!(
result.quality_score > 0.3,
"Expected quality > 0.3 for clean audio, got {:.2}",
result.quality_score
);
}
#[test]
fn test_noise_reduction_integration() {
let mut input = vec![0.0f32; ONE_SECOND_SAMPLES];
for (i, sample) in input.iter_mut().enumerate() {
let signal =
0.3 * (2.0 * std::f32::consts::PI * 440.0 * i as f32 / SAMPLE_RATE as f32).sin();
let noise =
0.05 * (2.0 * std::f32::consts::PI * 1200.0 * i as f32 / SAMPLE_RATE as f32).cos();
*sample = signal + noise;
}
let input_assessor = QualityAssessor::new(SAMPLE_RATE);
let input_metrics = input_assessor
.assess(&input)
.expect("Should assess input quality");
let result = preprocess_audio(&input).expect("Pipeline should reduce noise");
assert!(
result.quality_score >= input_metrics.quality_score * 0.8,
"Quality degraded significantly: {:.2} -> {:.2}",
input_metrics.quality_score,
result.quality_score
);
}
#[test]
fn test_performance_contract_total_latency() {
let input = vec![0.2f32; ONE_SECOND_SAMPLES];
let start = Instant::now();
let _ = preprocess_audio(&input).expect("Pipeline should execute for performance test");
let elapsed = start.elapsed();
if cfg!(debug_assertions) {
println!(
"Pipeline latency (debug build): {} ms — informational only",
elapsed.as_millis()
);
} else {
assert!(
elapsed.as_millis() < 30,
"Pipeline latency {} ms exceeds 30 ms target",
elapsed.as_millis()
);
}
}
#[test]
fn test_performance_per_stage() {
let input = vec![0.2f32; ONE_SECOND_SAMPLES];
let dc_config = PreprocessingConfig::default();
let mut dc_filter = DcHighPassFilter::new(dc_config).expect("Should create DC filter");
let start = Instant::now();
let filtered = dc_filter
.process(&input, None)
.expect("Should process DC/high-pass");
let dc_elapsed = start.elapsed();
let noise_config = NoiseReductionConfig::default();
let mut noise_reducer = NoiseReducer::new(noise_config).expect("Should create noise reducer");
let vad_ctx = VadContext { is_silence: false };
let start = Instant::now();
let denoised = noise_reducer
.reduce(&filtered, Some(vad_ctx))
.expect("Should reduce noise");
let noise_elapsed = start.elapsed();
let normalizer = Normalizer::new(0.5, 10.0).expect("Should create normalizer");
let start = Instant::now();
let normalized_audio = normalizer
.normalize(&denoised)
.expect("Should normalize audio");
let norm_elapsed = start.elapsed();
let assessor = QualityAssessor::new(SAMPLE_RATE);
let start = Instant::now();
let _ = assessor
.assess(&normalized_audio)
.expect("Should assess quality");
let qual_elapsed = start.elapsed();
println!("Stage latencies (1 second of audio @ 16 kHz):");
println!(
" DC/High-pass: {:>4} ms (target: <5 ms)",
dc_elapsed.as_millis()
);
println!(
" Noise reduction: {:>4} ms (target: <15 ms)",
noise_elapsed.as_millis()
);
println!(
" Normalization: {:>4} ms (target: <5 ms)",
norm_elapsed.as_millis()
);
println!(
" Quality assess: {:>4} ms (target: <10 ms)",
qual_elapsed.as_millis()
);
if cfg!(debug_assertions) {
println!("Per-stage latency checks skipped for debug builds");
return;
}
assert!(
dc_elapsed.as_millis() < 5,
"DC/High-pass {} ms exceeds 5 ms target",
dc_elapsed.as_millis()
);
assert!(
noise_elapsed.as_millis() < 15,
"Noise reduction {} ms exceeds 15 ms target",
noise_elapsed.as_millis()
);
assert!(
norm_elapsed.as_millis() < 5,
"Normalization {} ms exceeds 5 ms target",
norm_elapsed.as_millis()
);
assert!(
qual_elapsed.as_millis() < 10,
"Quality assessment {} ms exceeds 10 ms target",
qual_elapsed.as_millis()
);
}
#[test]
fn test_error_propagation() {
let empty_input: Vec<f32> = vec![];
let result = preprocess_audio(&empty_input);
assert!(result.is_err(), "Pipeline should reject empty input");
}
#[test]
fn test_silence_handling() {
let input = vec![0.0f32; ONE_SECOND_SAMPLES];
let result = preprocess_audio(&input).expect("Pipeline should handle silence");
assert!(
result.energy < 0.01,
"Expected near-zero energy for silence, got {:.3}",
result.energy
);
assert!(
result.quality_score < 0.2,
"Expected low quality for silence, got {:.2}",
result.quality_score
);
}
#[test]
fn test_extreme_amplitude_handling() {
let input = vec![0.95f32; ONE_SECOND_SAMPLES];
let result = preprocess_audio(&input).expect("Pipeline should handle extreme amplitudes");
let max_sample = result
.samples
.iter()
.copied()
.fold(f32::NEG_INFINITY, f32::max);
assert!(
max_sample <= 1.0,
"Output amplitude {max_sample:.2} exceeds 1.0 (clipping)"
);
}
#[test]
fn test_output_bounds_validation() {
let input = vec![0.5f32; ONE_SECOND_SAMPLES];
let result = preprocess_audio(&input).expect("Pipeline should validate output bounds");
for (i, &sample) in result.samples.iter().enumerate() {
assert!(
(-1.0..=1.0).contains(&sample),
"Sample {i} = {sample:.3} out of bounds [-1.0, 1.0]"
);
}
}