use common_dsp::meter::HarmonicMeter; use common_dsp::Saw; use lathe_core::{Engine, Settings, Source}; const RATE: f32 = 48000.; fn engine(s: Settings) -> (Engine, usize) { let mut e = Engine::new(RATE, 2).unwrap(); e.set_settings(s).unwrap(); let l = e.latency_samples() as usize; (e, l) } fn saw(hz: f32, seconds: f32, amp: f32) -> Vec { let mut s = Saw::new(RATE, hz); (0..(seconds * RATE) as usize) .map(|_| s.sample() * amp) .collect() } fn render(e: &mut Engine, x: &[f32]) -> Vec { x.iter().map(|&v| e.process_sample([v, v])[0]).collect() } /// Harmonic levels of the last complete window of `x`. fn harmonics(x: &[f32], hz: f32) -> [f32; 32] { let mut m = HarmonicMeter::new(RATE, 85.); for &v in x { m.push(v, hz); } *m.levels() } #[test] fn a_cut_on_harmonic_three_stays_on_harmonic_three_on_every_note() { let mut s = Settings::default(); s.gains[2] = -12.; for hz in [43.65, 65.41, 130.81] { let x = saw(hz, 1., 0.5); let (mut e, l) = engine(s); let y = render(&mut e, &x); let a = harmonics(&x[..x.len() - l], hz); let b = harmonics(&y[l..], hz); let d: Vec = (0..6).map(|k| b[k] - a[k]).collect(); assert!((d[2] + 12.).abs() < 0.7, "{hz}: {d:?}"); for k in [0, 1, 3, 4] { assert!(d[k].abs() < 1., "{hz} harmonic {}: {d:?}", k + 1); } } } #[test] fn a_flat_map_passes_the_delayed_input_exactly() { let x = saw(55., 0.5, 0.5); let (mut e, l) = engine(Settings::default()); let y = render(&mut e, &x); for i in l..x.len() { assert_eq!(y[i], x[i - l]); } } #[test] fn odd_even_balances_the_harmonic_series() { let s = Settings { odd_even: 6., ..Settings::default() }; let hz = 55.; let x = saw(hz, 1., 0.5); let (mut e, l) = engine(s); let y = render(&mut e, &x); let a = harmonics(&x[..x.len() - l], hz); let b = harmonics(&y[l..], hz); for k in 1..8 { let want = if k % 2 == 1 { 3. } else { -3. }; let got = b[k - 1] - a[k - 1]; assert!((got - want).abs() < 0.8, "harmonic {k}: {got}"); } } #[test] fn note_changes_do_not_click() { let mut s = Settings::default(); s.gains[1] = 12.; s.gains[3] = -24.; s.tilt = -3.; let mut x = saw(43.65, 0.5, 0.5); x.extend(saw(58.27, 0.5, 0.5)); x.extend(saw(87.31, 0.5, 0.5)); let (mut e, _) = engine(s); let y = render(&mut e, &x); let step = y .windows(2) .map(|w| (w[1] - w[0]).abs()) .fold(0f32, f32::max); let in_step = x .windows(2) .map(|w| (w[1] - w[0]).abs()) .fold(0f32, f32::max); assert!(y.iter().all(|v| v.is_finite())); // A saw has its own jumps; the filtered output must not jump much further than that. assert!(step < 2. * in_step, "{step} vs {in_step}"); } #[test] fn midi_source_tunes_the_bands_at_the_delayed_note_time() { let mut s = Settings { source: Source::Midi, ..Settings::default() }; s.gains[0] = -24.; let (mut e, l) = engine(s); // 43.65 Hz is MIDI 29. Notes arrive at sample 0; the bands must be tuned by sample l. e.note_on(29); let x = saw(43.65, 1., 0.5); let y = render(&mut e, &x); let a = harmonics(&x[..x.len() - l], 43.65); let b = harmonics(&y[l..], 43.65); assert!(b[0] - a[0] < -20., "{}", b[0] - a[0]); let f = e.take_analyzer(); assert_eq!(f.note, Some(29)); assert!((f.hz - 43.65).abs() < 0.05); } #[test] fn settings_are_validated_and_serialise() { let s = Settings::default(); let json = serde_json::to_string(&s).unwrap(); assert_eq!(serde_json::from_str::(&json).unwrap(), s); let mut bad = s; bad.gains[5] = 40.; assert!(bad.validate().is_err()); bad = s; bad.harmonics = 4; assert!(bad.validate().is_err()); bad = s; bad.width = f32::NAN; assert!(bad.validate().is_err()); let mut e = Engine::new(RATE, 2).unwrap(); assert!(e.set_settings(bad).is_err()); assert_eq!(e.settings(), s); }