//! Parameter sweep tool for jitter buffer configurations. //! //! Tests different (target_depth, max_depth) combinations in a local //! encoder-to-decoder pipeline (no network) and reports frame loss, //! estimated latency, underruns, and overruns for each configuration. use crate::call::{CallConfig, CallDecoder, CallEncoder}; use wzp_proto::QualityProfile; const FRAME_SAMPLES: usize = 960; // 20ms @ 48kHz const SAMPLE_RATE: u32 = 48_000; const FRAME_DURATION_MS: u32 = 20; /// Configuration for a parameter sweep. pub struct SweepConfig { /// Target jitter buffer depths to test (in packets). pub target_depths: Vec, /// Maximum jitter buffer depths to test (in packets). pub max_depths: Vec, /// Duration in seconds to run each configuration. pub test_duration_secs: u32, /// Frequency of the test tone in Hz. pub tone_freq_hz: f32, } impl Default for SweepConfig { fn default() -> Self { Self { target_depths: vec![10, 25, 50, 100, 200], max_depths: vec![50, 100, 250, 500], test_duration_secs: 2, tone_freq_hz: 440.0, } } } /// Result from one (target_depth, max_depth) configuration. #[derive(Debug, Clone)] pub struct SweepResult { /// Jitter buffer target depth used. pub target_depth: usize, /// Jitter buffer max depth used. pub max_depth: usize, /// Total frames sent into the encoder. pub frames_sent: u64, /// Total frames successfully decoded. pub frames_received: u64, /// Frame loss percentage. pub loss_pct: f64, /// Estimated latency in ms (target_depth * frame_duration). pub avg_latency_ms: f64, /// Number of jitter buffer underruns. pub underruns: u64, /// Number of jitter buffer overruns (packets dropped due to full buffer). pub overruns: u64, } /// Generate a sine wave frame at the given frequency and frame offset. fn sine_frame(freq_hz: f32, frame_offset: u64) -> Vec { let start = frame_offset * FRAME_SAMPLES as u64; (0..FRAME_SAMPLES) .map(|i| { let t = (start + i as u64) as f32 / SAMPLE_RATE as f32; (f32::sin(2.0 * std::f32::consts::PI * freq_hz * t) * 16000.0) as i16 }) .collect() } /// Run a local parameter sweep (no network). /// /// For each (target_depth, max_depth) combination, creates an encoder and /// decoder, pushes frames through the pipeline, and collects statistics. /// Combinations where `target_depth > max_depth` are skipped. pub fn run_local_sweep(config: &SweepConfig) -> Vec { let frames_per_config = (config.test_duration_secs as u64) * (1000 / FRAME_DURATION_MS as u64); let mut results = Vec::new(); for &target in &config.target_depths { for &max in &config.max_depths { // Skip invalid combinations where target exceeds max. if target > max { continue; } let call_cfg = CallConfig { profile: QualityProfile::GOOD, jitter_target: target, jitter_max: max, jitter_min: target.min(3).max(1), ..Default::default() }; let mut encoder = CallEncoder::new(&call_cfg); let mut decoder = CallDecoder::new(&call_cfg); let mut pcm_out = vec![0i16; FRAME_SAMPLES]; let mut frames_decoded = 0u64; for frame_idx in 0..frames_per_config { // Encode a tone frame. let pcm_in = sine_frame(config.tone_freq_hz, frame_idx); let packets = match encoder.encode_frame(&pcm_in) { Ok(p) => p, Err(_) => continue, }; // Feed all packets (source + repair) into the decoder. for pkt in packets { decoder.ingest(pkt); } // Attempt to decode one frame. if decoder.decode_next(&mut pcm_out).is_some() { frames_decoded += 1; } } // Drain: keep decoding until the jitter buffer is empty. for _ in 0..max { if decoder.decode_next(&mut pcm_out).is_some() { frames_decoded += 1; } else { break; } } let stats = decoder.stats().clone(); let loss_pct = if frames_per_config > 0 { (1.0 - frames_decoded as f64 / frames_per_config as f64) * 100.0 } else { 0.0 }; results.push(SweepResult { target_depth: target, max_depth: max, frames_sent: frames_per_config, frames_received: frames_decoded, loss_pct: loss_pct.max(0.0), avg_latency_ms: target as f64 * FRAME_DURATION_MS as f64, underruns: stats.underruns, overruns: stats.overruns, }); } } results } /// Print a formatted ASCII table of sweep results. pub fn print_sweep_table(results: &[SweepResult]) { println!(); println!("=== Jitter Buffer Parameter Sweep ==="); println!(); println!( " {:>6} | {:>4} | {:>6} | {:>6} | {:>6} | {:>10} | {:>9} | {:>8}", "target", "max", "sent", "recv", "loss%", "latency_ms", "underruns", "overruns" ); println!( " {:-<6}-+-{:-<4}-+-{:-<6}-+-{:-<6}-+-{:-<6}-+-{:-<10}-+-{:-<9}-+-{:-<8}", "", "", "", "", "", "", "", "" ); for r in results { println!( " {:>6} | {:>4} | {:>6} | {:>6} | {:>5.1}% | {:>10.0} | {:>9} | {:>8}", r.target_depth, r.max_depth, r.frames_sent, r.frames_received, r.loss_pct, r.avg_latency_ms, r.underruns, r.overruns, ); } println!(); } /// Run a default sweep and print the results. /// /// This is the entry point for the `--sweep` CLI flag. pub fn run_and_print_default_sweep() { let config = SweepConfig::default(); let results = run_local_sweep(&config); print_sweep_table(&results); } #[cfg(test)] mod tests { use super::*; #[test] fn sweep_config_default() { let cfg = SweepConfig::default(); assert_eq!(cfg.target_depths.len(), 5); assert_eq!(cfg.max_depths.len(), 4); assert!(cfg.test_duration_secs > 0); assert!(cfg.tone_freq_hz > 0.0); // All default targets should be positive. assert!(cfg.target_depths.iter().all(|&d| d > 0)); assert!(cfg.max_depths.iter().all(|&d| d > 0)); } #[test] fn local_sweep_runs() { let cfg = SweepConfig { target_depths: vec![3, 10], max_depths: vec![50, 100], test_duration_secs: 1, tone_freq_hz: 440.0, }; let results = run_local_sweep(&cfg); // 2 targets x 2 maxes = 4 configs (all valid since targets < maxes). assert_eq!(results.len(), 4); for r in &results { assert!(r.frames_sent > 0, "frames_sent should be > 0"); assert!(r.frames_received > 0, "frames_received should be > 0"); assert!(r.avg_latency_ms > 0.0, "latency should be > 0"); } } #[test] fn sweep_table_formats() { // Verify print_sweep_table doesn't panic with various inputs. print_sweep_table(&[]); let results = vec![ SweepResult { target_depth: 10, max_depth: 50, frames_sent: 100, frames_received: 98, loss_pct: 2.0, avg_latency_ms: 200.0, underruns: 2, overruns: 0, }, SweepResult { target_depth: 25, max_depth: 100, frames_sent: 100, frames_received: 100, loss_pct: 0.0, avg_latency_ms: 500.0, underruns: 0, overruns: 0, }, ]; print_sweep_table(&results); } }