use patina_core::*; use std::f32::consts::TAU; fn noise(i: usize) -> f32 { let x = (i as u32).wrapping_mul(747796405).wrapping_add(2891336453); let word = ((x >> ((x >> 28) + 4)) ^ x).wrapping_mul(277803737); (((word >> 22) ^ word) as f32 / u32::MAX as f32 - 0.5) * 0.1 } #[test] fn delayed_identity_and_all_mix_positions_at_all_rates() { for rate in [44100., 48000., 88200., 96000., 176400., 192000.] { for mix in [0., 37., 100.] { let mut e = Engine::new(rate, 2).unwrap(); e.set_settings(Settings { depth: 0., mix, ..Default::default() }) .unwrap(); let n = e.latency_samples() as usize; let mut error = 0f64; let mut power = 0f64; for i in 0..n * 4 { let x = if i < n * 2 { noise(i) } else { 0. }; let y = e.process_sample([x, -x], Monitor::default()); let expected = if i >= n && i < n * 3 { noise(i - n) } else { 0. }; error += (y[0] as f64 - expected as f64).powi(2); power += (expected as f64).powi(2); assert!((y[0] + y[1]).abs() < 1e-6); } assert!( 10. * (error / power).max(1e-30).log10() < -100., "rate {rate}, mix {mix}: {error}" ); } } } #[test] fn impulse_latency_delta_and_reset() { let mut e = Engine::new(48000., 1).unwrap(); e.set_settings(Settings { depth: 0., ..Default::default() }) .unwrap(); let latency = e.latency_samples() as usize; for i in 0..latency * 2 { let y = e.process_sample([if i == 0 { 1. } else { 0. }, 0.], Monitor::default())[0]; assert!((y - if i == latency { 1. } else { 0. }).abs() < 1e-6); } e.reset(); for _ in 0..latency * 2 { assert_eq!(e.process_sample([0., 0.], Monitor { delta: true }), [0.; 2]); } } /// Band-limited sawtooth: every harmonic up to 18 kHz, amplitude 1/k. fn saw(f0: f32, i: usize, rate: f32) -> f32 { let t = i as f32 / rate; (1..) .map(|k| k as f32 * f0) .take_while(|f| *f < 18000.) .enumerate() .map(|(k, f)| (TAU * f * t).sin() / (k + 1) as f32) .sum::() * 0.2 } /// RBJ peaking biquad, used to put a known resonance on a test signal. struct Peak { b: [f32; 3], a: [f32; 2], z: [f32; 2], } impl Peak { fn new(f: f32, q: f32, gain: f32, rate: f32) -> Self { let a = 10f32.powf(gain / 40.); let w = TAU * f / rate; let alpha = w.sin() / (2. * q); let a0 = 1. + alpha / a; Self { b: [ (1. + alpha * a) / a0, -2. * w.cos() / a0, (1. - alpha * a) / a0, ], a: [-2. * w.cos() / a0, (1. - alpha / a) / a0], z: [0.; 2], } } fn tick(&mut self, x: f32) -> f32 { let y = self.b[0] * x + self.z[0]; self.z[0] = self.b[1] * x - self.a[0] * y + self.z[1]; self.z[1] = self.b[2] * x - self.a[1] * y; y } } fn cell(f: f32) -> usize { (0..GRID) .min_by(|a, b| { (frequency(*a) - f) .abs() .total_cmp(&(frequency(*b) - f).abs()) }) .unwrap() } /// Run one second of a signal and return the last analyzer frame. fn settle(settings: Settings, mut signal: impl FnMut(usize) -> f32) -> AnalyzerFrame { let mut e = Engine::new(48000., 1).unwrap(); e.set_settings(settings).unwrap(); let mut frame = AnalyzerFrame::default(); for i in 0..48000 { e.process_sample([signal(i), 0.], Monitor::default()); if let Some(f) = e.take_analyzer() { frame = f; } } frame } fn peak_reduction(frame: &AnalyzerFrame, low: f32, high: f32) -> f32 { (0..GRID) .filter(|i| (low..=high).contains(&frequency(*i))) .map(|i| frame.reduction[i]) .fold(0., f32::max) } #[test] fn plain_harmonic_tone_is_left_alone() { for f0 in [110., 220., 440.] { let frame = settle(Settings::default(), |i| saw(f0, i, 48000.)); let peak = peak_reduction(&frame, 20., 20000.); assert!(peak < 1., "{f0} Hz sawtooth cut by {peak} dB"); } } #[test] fn resonant_bump_is_cut_where_it_is_and_nowhere_else() { for f0 in [110., 220.] { let mut bump = Peak::new(3000., 4., 12., 48000.); let frame = settle(Settings::default(), |i| bump.tick(saw(f0, i, 48000.))); let near = peak_reduction(&frame, 3000. / 1.26, 3000. * 1.26); assert!(near >= 4., "{f0} Hz: bump cut only {near} dB"); for f in [1000., 8000.] { let away = frame.reduction[cell(f)]; assert!(away < 1., "{f0} Hz: {f} Hz cut by {away} dB"); } assert!(peak_reduction(&frame, 20., 20000.) <= Settings::default().depth); } } #[test] fn depth_threshold_and_focus_each_do_one_thing() { let run = |s: Settings| { let mut bump = Peak::new(3000., 4., 12., 48000.); let frame = settle(s, |i| bump.tick(saw(220., i, 48000.))); frame.reduction[cell(3000.)] }; let base = run(Settings::default()); let shallow = run(Settings { depth: 2., ..Default::default() }); let picky = run(Settings { threshold: 5.5, ..Default::default() }); let mut protect = Settings::default(); protect.focus[0] = Focus { enabled: true, frequency: 3000., width: 0.5, sensitivity: -12., }; let protected = run(protect); assert!(shallow <= 2.001 && shallow < base, "{shallow} vs {base}"); assert!(picky < base - 1., "{picky} vs {base}"); assert!(protected < base - 2., "{protected} vs {base}"); } #[test] fn detection_is_level_independent() { let s = Settings { transient_guard: 0., ..Default::default() }; let run = |gain: f32| { let mut bump = Peak::new(3000., 4., 12., 48000.); settle(s, |i| bump.tick(saw(220., i, 48000.)) * gain) }; let loud = run(1.); let quiet = run(0.03); for i in 0..GRID { assert!( (loud.reduction[i] - quiet.reduction[i]).abs() < 0.5, "{} Hz: {} vs {}", frequency(i), loud.reduction[i], quiet.reduction[i] ); } } #[test] fn noise_does_not_cause_flickering_cuts() { let mut e = Engine::new(48000., 1).unwrap(); e.set_settings(Settings::default()).unwrap(); let mut frames = Vec::new(); for i in 0..96000 { e.process_sample([noise(i) * 3., 0.], Monitor::default()); if let Some(f) = e.take_analyzer() { if i > 24000 { frames.push(f); } } } let mean = frames .iter() .map(|f| f.reduction.iter().sum::() / GRID as f32) .sum::() / frames.len() as f32; let jitter = frames .windows(2) .map(|w| { (0..GRID) .map(|i| (w[1].reduction[i] - w[0].reduction[i]).abs()) .sum::() / GRID as f32 }) .sum::() / (frames.len() - 1) as f32; assert!(mean < 0.5, "white noise mean cut {mean} dB"); assert!(jitter < 0.1, "frame-to-frame change {jitter} dB"); } #[test] fn processing_range_is_respected_and_cuts_recover_in_silence() { let s = Settings { low: 500., high: 9000., transient_guard: 0., release: 20., ..Default::default() }; let mut e = Engine::new(48000., 2).unwrap(); e.set_settings(s).unwrap(); let mut bump = Peak::new(3000., 4., 12., 48000.); let mut frame = AnalyzerFrame::default(); for i in 0..48000 { let x = bump.tick(saw(220., i, 48000.)); e.process_sample([x, -x], Monitor::default()); if let Some(f) = e.take_analyzer() { frame = f; } } assert!(frame.reduction[cell(3000.)] > 3.); for i in 0..GRID { if frequency(i) < 500. || frequency(i) > 9000. { assert_eq!(frame.reduction[i], 0.); } } for _ in 0..48000 { e.process_sample([0.; 2], Monitor::default()); } let f = e.take_analyzer().unwrap(); assert!(f.reduction.iter().all(|d| *d < 0.001)); } #[test] fn invalid_settings_do_not_mutate_and_numerical_inputs_stay_finite() { let mut e = Engine::new(48000., 1).unwrap(); let before = e.settings(); assert!(e .set_settings(Settings { attack: f32::NAN, ..before }) .is_err()); assert_eq!(e.settings(), before); assert!(Engine::new(8000., 2).is_err()); for _ in 0..10000 { let y = e.process_sample([f32::NAN, f32::INFINITY], Monitor::default()); assert!(y.iter().all(|v| v.is_finite())); } } #[test] fn soft_knee_and_presets_are_valid() { for i in 0..Settings::PRESETS.len() { Settings::preset(i).validate().unwrap(); } assert_eq!(knee(-2., 3.), 0.); assert_eq!(knee(5., 3.), 5.); assert_eq!(knee(-1., 0.), 0.); let p = Focus { enabled: true, frequency: 2000., width: 1., sensitivity: -12., }; assert_eq!(focus_offset(2000., &[p; 4]), -48.); } #[test] fn settled_delta_and_bypass_have_precise_taps() { let s = Settings { attack: 1., transient_guard: 0., ..Default::default() }; let mut wet = Engine::new(48000., 1).unwrap(); wet.set_settings(s).unwrap(); let mut delta = Engine::new(48000., 1).unwrap(); delta.set_settings(s).unwrap(); let mut bypass = Engine::new(48000., 1).unwrap(); bypass.set_settings(Settings { bypass: true, ..s }).unwrap(); let latency = wet.latency_samples() as usize; for i in 0..24000 { let x = noise(i); let a = wet.process_sample([x, 0.], Monitor::default())[0]; let b = delta.process_sample([x, 0.], Monitor { delta: true })[0]; let c = bypass.process_sample([x, 0.], Monitor::default())[0]; if i > 12000 { let expected = noise(i - latency); assert!((a + b - expected).abs() < 1e-5); assert!((c - expected).abs() < 1e-6); } } } #[test] fn narrow_ring_on_noise_is_cut_and_sharpness_narrows_the_cut() { let width = |sharpness: f32| { let s = Settings { sharpness, ..Default::default() }; let frame = settle(s, |i| { 0.05 * (TAU * 5000. * i as f32 / 48000.).sin() + noise(i) * 3. }); let peak = frame.reduction[cell(5000.)]; let wide = (0..GRID) .filter(|i| frame.reduction[*i] > peak * 0.5) .count(); (peak, wide) }; let (soft, soft_width) = width(0.); let (sharp, sharp_width) = width(100.); assert!(soft > 3. && sharp > 3., "{soft} / {sharp}"); assert!(sharp_width < soft_width, "{sharp_width} vs {soft_width}"); } #[test] fn high_sample_rates_detect_the_same_bump() { for rate in [96000., 192000.] { let mut e = Engine::new(rate, 1).unwrap(); e.set_settings(Settings::default()).unwrap(); let mut bump = Peak::new(3000., 4., 12., rate); let mut frame = AnalyzerFrame::default(); for i in 0..rate as usize { e.process_sample([bump.tick(saw(220., i, rate)), 0.], Monitor::default()); if let Some(f) = e.take_analyzer() { frame = f; } } let near = peak_reduction(&frame, 3000. / 1.26, 3000. * 1.26); let away = peak_reduction(&frame, 20., 1500.); assert!(near >= 4. && away < 1., "{rate}: {near} / {away}"); } } /// Notes of a riddim-style line, in Hz (F1, G#1, C2, D#2, F2). const LINE: [f32; 5] = [43.65, 51.91, 65.41, 77.78, 87.31]; /// Additive bass at `f0`: harmonics 1/k up to 16 kHz, each scaled by `shape(k, Hz)`. fn bass(f0: f32, seconds: f32, shape: &dyn Fn(usize, f32) -> f32) -> Vec { let rate = 48000.; let n = (rate * seconds) as usize; let mut x = vec![0f32; n]; for k in 1..=(16000. / f0) as usize { let f = k as f32 * f0; let a = 0.08 * shape(k, f) / k as f32; let w = TAU * f / rate; for (i, v) in x.iter_mut().enumerate() { *v += a * (w * i as f32 + k as f32).sin(); } } x } /// Plays `notes` in turn (0.4 s each, `rounds` times) and returns the mean cut near `hz` /// (largest within ±3 grid cells) over the second half of the last note. fn tone_cut( guard: f32, notes: &[f32], rounds: usize, shape: &dyn Fn(usize, f32) -> f32, hz: &dyn Fn(f32) -> f32, ) -> f32 { let mut e = Engine::new(48000., 1).unwrap(); e.set_settings(Settings { tone_guard: guard, low: 150., ..Default::default() }) .unwrap(); let last = *notes.last().unwrap(); let at = patina_core::cell(hz(last)).round() as usize; let (mut sum, mut count) = (0., 0); for round in 0..rounds { for &f0 in notes { let x = bass(f0, 0.4, shape); let measure = round + 1 == rounds && f0 == last; for (i, v) in x.iter().enumerate() { e.process_sample([*v, 0.], Monitor::default()); if let Some(frame) = e.take_analyzer() { if measure && i > x.len() / 2 { sum += frame.reduction[at - 3..=at + 3] .iter() .fold(0f32, |m, v| m.max(*v)); count += 1; } } } } } sum / count as f32 } fn harmonic_six(k: usize, _: f32) -> f32 { if k == 6 { 4. } else { 1. } } fn formant(_: usize, f: f32) -> f32 { let z = (f / 1500.).log2() / 0.15; 1. + 3. * (-z * z).exp() } #[test] fn tone_guard_holds_back_cuts_on_bumps_that_follow_the_note() { let h6 = |f0: f32| 6. * f0; let plain = tone_cut(0., &LINE, 3, &harmonic_six, &h6); let guarded = tone_cut(100., &LINE, 3, &harmonic_six, &h6); assert!( plain > 4., "without guard the boosted harmonic is cut: {plain}" ); assert!( guarded < 0.25 * plain, "guard keeps the harmonic: {guarded} vs {plain}" ); } #[test] fn tone_guard_still_cuts_resonances_fixed_in_hz() { let at = |_: f32| 1500.; let plain = tone_cut(0., &LINE, 3, &formant, &at); let guarded = tone_cut(100., &LINE, 3, &formant, &at); assert!(plain > 4., "the formant is cut: {plain}"); assert!( guarded > 0.85 * plain, "guard still cuts it: {guarded} vs {plain}" ); } #[test] fn tone_guard_needs_different_notes_to_tell_bumps_apart() { let h6 = |f0: f32| 6. * f0; let one = [LINE[2]]; let plain = tone_cut(0., &one, 10, &harmonic_six, &h6); let guarded = tone_cut(100., &one, 10, &harmonic_six, &h6); assert!( (guarded - plain).abs() < 0.5, "one held note gives no evidence: {guarded} vs {plain}" ); } #[test] fn old_settings_without_tone_guard_still_load() { let mut json = serde_json::to_value(Settings::default()).unwrap(); json.as_object_mut().unwrap().remove("tone_guard"); let s: Settings = serde_json::from_value(json).unwrap(); assert_eq!(s.tone_guard, 0.); } /// Cut at the bump for a resonance of `gain` dB (Q 4 at 3 kHz) on a 220 Hz saw. fn bump_cut(s: Settings, gain: f32) -> f32 { let mut bump = Peak::new(3000., 4., gain, 48000.); let frame = settle( Settings { transient_guard: 0., ..s }, |i| bump.tick(saw(220., i, 48000.)), ); peak_reduction(&frame, 3000. / 1.26, 3000. * 1.26) } #[test] fn ratio_spares_small_peaks_and_still_cuts_big_ones() { let at = |ratio: f32, gain: f32| { bump_cut( Settings { depth: 20., ratio, ..Default::default() }, gain, ) }; let (small_hard, big_hard) = (at(RATIO_INFINITE, 6.), at(RATIO_INFINITE, 18.)); let (small, big) = (at(2., 6.), at(2., 18.)); // At 2:1 each cut is about half the ∞:1 cut. for (half, full) in [(small, small_hard), (big, big_hard)] { assert!((half / full - 0.5).abs() < 0.15, "{half} vs {full}"); } assert!(small < 1., "small bump cut by {small} dB at 2:1"); assert!(big > 4., "big bump cut only {big} dB at 2:1"); // Ratio between the two ends gives a cut between them. let mid = at(4., 18.); assert!(big < mid && mid < big_hard, "{big} < {mid} < {big_hard}"); } #[test] fn cut_grows_with_the_bump_until_depth() { let s = Settings { depth: 20., ratio: 3., ..Default::default() }; let cuts: Vec = [4., 8., 12., 18., 24.] .iter() .map(|g| bump_cut(s, *g)) .collect(); for w in cuts.windows(2) { assert!(w[1] > w[0] + 0.5, "{cuts:?}"); } let capped = bump_cut(Settings { depth: 4., ..s }, 24.); assert!(capped <= 4.001 && capped > 3., "{capped}"); } #[test] fn threshold_moves_where_cuts_start() { let low = bump_cut(Settings::default(), 9.); let high = bump_cut( Settings { threshold: 9., ..Default::default() }, 9., ); assert!(low > 2. && high < 0.5, "{low} / {high}"); } #[test] fn hard_knee_starts_cutting_closer_to_the_threshold() { // A bump just under the threshold: the soft knee already cuts a little, the hard knee not. let s = Settings { threshold: 4., ..Default::default() }; let soft = bump_cut(s, 6.); let hard = bump_cut( Settings { knee: Knee::Hard, ..s }, 6., ); assert!(soft > hard + 0.2, "soft {soft} vs hard {hard}"); assert_eq!(knee(0.25, 1.), 0.28125); } #[test] fn settings_from_before_threshold_still_load() { let mut json = serde_json::to_value(Settings::default()).unwrap(); let map = json.as_object_mut().unwrap(); for key in ["threshold", "ratio", "knee", "tone_guard"] { map.remove(key); } map.insert("selectivity".into(), 100.into()); let s: Settings = serde_json::from_value(json).unwrap(); assert_eq!(s.threshold, 5.5); assert_eq!(s.ratio, RATIO_INFINITE); assert_eq!(s.knee, Knee::Soft); assert!(serde_json::from_str::(r#"{"oops":1}"#).is_err()); let back: Settings = serde_json::from_str(&serde_json::to_string(&s).unwrap()).unwrap(); assert_eq!(back, s); }