use weld_core::{Engine, Listen, Settings}; fn signal(n: usize, hz: f32) -> f32 { (std::f32::consts::TAU * hz * n as f32 / 48000.).sin() * 0.5 } #[test] fn layer_output_is_the_main_input_with_zero_latency() { let mut engine = Engine::new(48000., 2).unwrap(); engine.set_settings(Settings::default()).unwrap(); assert_eq!(engine.latency_samples(), 0); for n in 0..4800 { let x = [signal(n, 110.), -signal(n, 220.)]; let out = engine.process_sample(x, [signal(n, 330.), 0.25]); assert_eq!(out, x); } } #[test] fn sum_modes_add_the_sidechain() { for listen in [Listen::Sum, Listen::SumDry] { let mut engine = Engine::new(48000., 2).unwrap(); engine .set_settings(Settings { listen, ..Settings::default() }) .unwrap(); for n in 0..480 { let x = [signal(n, 110.), signal(n, 120.)]; let s = [signal(n, 330.), -signal(n, 90.)]; let out = engine.process_sample(x, s); for c in 0..2 { assert!((out[c] - (x[c] + s[c])).abs() < 1e-6); } } } } #[test] fn mono_bypass_and_output_gain() { let mut engine = Engine::new(44100., 1).unwrap(); engine .set_settings(Settings { output: -6., ..Settings::default() }) .unwrap(); let out = engine.process_sample([0.5, 0.], [0., 0.]); assert!((out[0] - 0.5 * 10f32.powf(-6. / 20.)).abs() < 1e-6); assert_eq!(out[1], 0.); engine .set_settings(Settings { bypass: true, ..Settings::default() }) .unwrap(); assert_eq!(engine.process_sample([0.3, 0.], [0.9, 0.]), [0.3, 0.]); } #[test] fn analyzer_reports_sidechain_presence_and_level() { let mut engine = Engine::new(48000., 2).unwrap(); engine.set_settings(Settings::default()).unwrap(); for n in 0..4800 { engine.process_sample([signal(n, 100.); 2], [0.; 2]); } let frame = engine.take_analyzer(); assert!(!frame.side_seen); assert!(frame.side_db < -100.); assert!((frame.main_db - (-9.03)).abs() < 0.1, "{}", frame.main_db); for n in 0..4800 { engine.process_sample([0.; 2], [signal(n, 100.); 2]); } let frame = engine.take_analyzer(); assert!(frame.side_seen); assert!((frame.side_db - (-9.03)).abs() < 0.1); assert!((frame.side_peak_db - (-6.02)).abs() < 0.1); } #[test] fn invalid_settings_and_rates_are_rejected() { assert!(Engine::new(0., 2).is_err()); assert!(Engine::new(48000., 3).is_err()); let mut engine = Engine::new(48000., 2).unwrap(); assert!(engine .set_settings(Settings { output: f32::NAN, ..Settings::default() }) .is_err()); let json = serde_json::to_string(&Settings::default()).unwrap(); assert_eq!( serde_json::from_str::(&json).unwrap(), Settings::default() ); } use weld_core::{Analyzer, Chain, Fit, Mode, Section, Status}; fn random_fit(seed: u64) -> Fit { let mut s = seed; let mut rand = move || { s = s .wrapping_mul(6364136223846793005) .wrapping_add(1442695040888963407); (s >> 11) as f32 / (1u64 << 53) as f32 }; let mut fit = Fit { flip: rand() > 0.5, count: 1 + (rand() * 3.99) as usize, ..Fit::NONE }; for section in &mut fit.sections { *section = Section { fc: 30. * 600f32.powf(rand()), q: 0.2 * 20f32.powf(rand()), }; } fit } #[test] fn every_chain_is_flat_in_magnitude() { for seed in 1..40 { let fit = random_fit(seed); for i in 0..200 { let f = 20. * 1000f32.powf(i as f32 / 199.); let (re, im) = fit.response(f, 48000.); let db = 10. * (re * re + im * im).log10(); assert!(db.abs() < 0.01, "{fit:?} at {f} Hz: {db}"); } } } #[test] fn the_running_chain_matches_its_response() { let fit = random_fit(3); for f in [50f32, 333., 2100., 9000.] { let mut chain = Chain::new(fit, 48000.); let (mut i_sum, mut q_sum, mut energy) = (0f64, 0f64, 0f64); let n = 96000; for k in 0..n { let t = k as f64 / 48000.; let w = std::f64::consts::TAU * f as f64 * t; let y = chain.process(0, w.cos() as f32) as f64; if k > n / 2 { i_sum += y * w.cos(); q_sum += -y * w.sin(); energy += y * y; } } let m = (n / 2 - 1) as f64; let (re, im) = fit.response(f, 48000.); let measured = (2. * i_sum / m, 2. * q_sum / m); assert!( (measured.0 - re as f64).abs() < 0.01, "{f}: {measured:?} vs {re},{im}" ); assert!( (measured.1 - im as f64).abs() < 0.01, "{f}: {measured:?} vs {re},{im}" ); assert!((2. * energy / m - 1.).abs() < 0.01); } } #[test] fn the_impulse_response_starts_at_sample_zero() { let mut chain = Chain::new(random_fit(5), 48000.); let first = chain.process(0, 1.); assert!(first.abs() > 0.01, "{first}"); } #[test] fn a_new_fit_crossfades_without_clicks() { let mut engine = Engine::new(48000., 2).unwrap(); engine.set_settings(Settings::default()).unwrap(); let link = engine.link(); let x = |n: usize| signal(n, 97.) + 0.3 * signal(n, 1210.); let max_input_step = (1..48000) .map(|n| (x(n) - x(n - 1)).abs()) .fold(0f32, f32::max); let mut previous = [0f32; 2]; let mut largest = 0f32; for n in 0..48000 { if n == 4800 { link.push_fit(Fit { flip: true, count: 2, sections: [ Section { fc: 300., q: 0.7 }, Section { fc: 2000., q: 2. }, Section::default(), Section::default(), ], }); } let out = engine.process_sample([x(n), x(n)], [0.; 2]); if n > 0 { largest = largest.max((out[0] - previous[0]).abs()); } previous = out; } assert!( largest <= 1.5 * max_input_step, "{largest} vs {max_input_step}" ); assert!(engine.fit().flip); assert_eq!(engine.fit().count, 2); } #[test] fn sum_uses_the_aligned_layer_and_sum_dry_the_unprocessed_one() { for listen in [Listen::Sum, Listen::SumDry] { let mut engine = Engine::new(48000., 1).unwrap(); engine .set_settings(Settings { listen, ..Settings::default() }) .unwrap(); engine.set_fit(Fit { flip: true, ..Fit::NONE }); let out = engine.process_sample([0.5, 0.], [0.2, 0.]); let expected = if listen == Listen::Sum { -0.3 } else { 0.7 }; assert!((out[0] - expected).abs() < 1e-6, "{listen:?} {out:?}"); } } #[test] fn blocks_reach_the_worker_and_drops_are_counted() { let mut engine = Engine::new(48000., 2).unwrap(); engine.set_settings(Settings::default()).unwrap(); let link = engine.link(); assert!(link.pop_settings().is_some()); for n in 0..256 * 70 { engine.process_sample([signal(n, 100.); 2], [signal(n, 100.); 2]); } assert_eq!(link.dropped(), 6); let block = link.pop_block().unwrap(); assert!((block.a[10] - signal(10, 100.)).abs() < 1e-6); assert_eq!(block.a, block.b); } #[test] fn silence_holds_the_fit_and_produces_no_nan() { let flipped = Fit { flip: true, ..Fit::NONE }; let mut analyzer = Analyzer::new(48000., flipped); analyzer.set_settings(Settings::default()); let zeros = [0f32; 256]; let mut last = None; for _ in 0..400 { analyzer.push(&zeros, &zeros); if let Some(update) = analyzer.poll() { assert!(update.fit.is_none()); last = Some(update.report); } } let report = last.unwrap(); assert_eq!(report.status, Status::NoSidechain); assert_eq!(report.fit, flipped); assert!(report.cost_db.is_finite() && report.recovered_db.is_finite()); assert!(report .bands .iter() .all(|b| b.before.is_finite() && b.after.is_finite())); } #[test] fn follow_mode_finds_the_flip_and_then_holds_still() { let mut analyzer = Analyzer::new(48000., Fit::NONE); analyzer.set_settings(Settings { mode: Mode::Follow, ..Settings::default() }); let a: Vec = (0..48000 * 6) .map(|n| signal(n, 87.3) + 0.3 * signal(n, 261.9) + 0.1 * signal(n, 611.)) .collect(); let b: Vec = a.iter().map(|v| -0.7 * v).collect(); let mut accepted = Vec::new(); let mut last = None; for (x, y) in a.chunks(256).zip(b.chunks(256)) { analyzer.push(x, y); if let Some(update) = analyzer.poll() { if let Some(fit) = update.fit { accepted.push(fit); } last = Some(update.report); } } assert_eq!(accepted.len(), 1, "{accepted:?}"); assert!(accepted[0].flip); let report = last.unwrap(); assert_eq!(report.status, Status::Aligned); assert!(report.recovered_db > 10., "{}", report.recovered_db); assert!((report.recovered_db - report.cost_db).abs() < 0.2); } #[test] fn learn_measures_and_applies_on_exit() { let mut analyzer = Analyzer::new(48000., Fit::NONE); let learn = Settings { mode: Mode::Learn, ..Settings::default() }; analyzer.set_settings(learn); let a: Vec = (0..48000 * 2) .map(|n| signal(n, 87.3) + 0.3 * signal(n, 261.9)) .collect(); let b: Vec = a.iter().map(|v| -v).collect(); for (x, y) in a.chunks(256).zip(b.chunks(256)) { analyzer.push(x, y); let _ = analyzer.poll(); } analyzer.set_settings(Settings { mode: Mode::Hold, ..learn }); let update = analyzer.poll().expect("leaving Learn reports at once"); assert!(analyzer.current().flip); // Learn may already have applied the flip while measuring. assert!(update.fit.is_none_or(|f| f.flip)); // Hold never refits, even when the layers change. let c: Vec = a.clone(); for (x, y) in a.chunks(256).zip(c.chunks(256)) { analyzer.push(x, y); if let Some(update) = analyzer.poll() { assert!(update.fit.is_none()); } } assert!(analyzer.current().flip); }