use common_dsp::meter::HarmonicMeter; use common_dsp::oversample::Oversampler; use common_dsp::svf::{Svf, SvfCoef}; use common_dsp::tracker::{NoteEvent, Tracker, TrackerConfig}; use common_dsp::{hz_to_midi, Saw}; use std::f32::consts::TAU; const RATE: f32 = 48000.; /// Steady-state gain in dB of a sine at `hz` through `f`. fn sine_gain(mut f: impl FnMut(f32) -> f32, hz: f32) -> f32 { let n = (RATE * 0.5) as usize; let (mut pin, mut pout) = (0., 0.); for i in 0..n { let x = (TAU * hz * i as f32 / RATE).sin(); let y = f(x); if i > n / 2 { pin += x * x; pout += y * y; } } 10. * (pout / pin).log10() } #[test] fn svf_bell_hits_its_gain_and_leaves_the_rest() { for (hz, db) in [(130., -12.), (400., 6.), (2000., -24.)] { let c = SvfCoef::bell(RATE, hz, 4., db); let mut s = Svf::default(); let g = sine_gain(|x| s.process(&c, x), hz); assert!((g - db).abs() < 0.1, "{hz}: {g}"); let mut s = Svf::default(); let g = sine_gain(|x| s.process(&c, x), hz * 4.); assert!(g.abs() < 0.3, "{hz} away: {g}"); } } #[test] fn svf_bell_at_zero_db_is_an_exact_identity() { let c = SvfCoef::bell(RATE, 100., 2., 0.); let mut s = Svf::default(); let mut saw = Saw::new(RATE, 55.); for _ in 0..4800 { let x = saw.sample(); assert_eq!(s.process(&c, x), x); } } #[test] fn svf_allpass_matches_a_linkwitz_riley_split() { let q = std::f32::consts::FRAC_1_SQRT_2; let (low, high, all) = ( SvfCoef::lowpass(RATE, 300., q), SvfCoef::highpass(RATE, 300., q), SvfCoef::allpass(RATE, 300., q), ); let mut s = [Svf::default(); 5]; let mut saw = Saw::new(RATE, 55.); for _ in 0..9600 { let x = saw.sample(); let l = s[0].process(&low, x); let l = s[1].process(&low, l); let h = s[2].process(&high, x); let h = s[3].process(&high, h); let a = s[4].process(&all, x); assert!((l + h - a).abs() < 1e-4, "{} vs {a}", l + h); } let mut s = Svf::default(); for hz in [30., 300., 3000.] { assert!(sine_gain(|x| s.process(&all, x), hz).abs() < 0.01); } } #[test] fn svf_survives_retuning_every_sample() { let mut s = Svf::default(); let mut saw = Saw::new(RATE, 55.); let mut peak: f32 = 0.; for i in 0..48000 { let hz = 40. + 4000. * (0.5 + 0.5 * (i as f32 * 0.003).sin()); let c = SvfCoef::bell(RATE, hz, 8., 12.); let y = s.process(&c, saw.sample()); assert!(y.is_finite()); peak = peak.max(y.abs()); } assert!(peak < 8., "{peak}"); let c = SvfCoef::lowpass(RATE, 200., 0.707); let mut s = Svf::default(); assert!(sine_gain(|x| s.process(&c, x), 2000.) < -35.); let c = SvfCoef::highpass(RATE, 200., 0.707); let mut s = Svf::default(); assert!(sine_gain(|x| s.process(&c, x), 20.) < -35.); } #[test] fn oversampler_round_trip_is_a_whole_sample_delay() { let mut o = Oversampler::new(); let l = Oversampler::latency(); let mut saw = Saw::new(RATE, 97.); let x: Vec = (0..9600).map(|_| saw.sample() * 0.5).collect(); let y: Vec = x .iter() .map(|&v| { let u = o.up(v); o.down(u) }) .collect(); // Band-limited content passes; compare after the delay, away from the start. let mut err: f32 = 0.; let mut sig: f32 = 0.; for i in 2000..9600 { let d = y[i] - x[i - l]; err += d * d; sig += x[i - l] * x[i - l]; } let null = 10. * (err / sig).log10(); // The saw has energy above the passband edge, so the null is limited by that, not by timing. assert!(null < -40., "{null}"); } #[test] fn oversampler_passband_is_flat_and_images_are_removed() { for hz in [50., 1000., 10000., 18000.] { let mut o = Oversampler::new(); let g = sine_gain( |x| { let u = o.up(x); o.down(u) }, hz, ); assert!(g.abs() < 0.1, "{hz}: {g}"); } // An image of a 1 kHz tone at 4× would sit at 47 kHz; check the 4× signal there. let mut o = Oversampler::new(); let n = 48000; let (mut a, mut b) = (0f64, 0f64); let w = TAU as f64 * 47000. / (4. * RATE as f64); for i in 0..n { let x = (TAU * 1000. * i as f32 / RATE).sin(); for (k, v) in o.up(x).into_iter().enumerate() { let t = (4 * i + k) as f64; a += v as f64 * (w * t).cos(); b += v as f64 * (w * t).sin(); } } let image = 20. * ((a * a + b * b).sqrt() / (2. * n as f64)) .max(1e-12) .log10(); assert!(image < -85., "{image}"); } #[test] fn harmonic_meter_reads_saw_harmonics() { let hz = 55.; let mut m = HarmonicMeter::new(RATE, 85.); let mut saw = Saw::new(RATE, hz); let mut done = 0; for _ in 0..48000 { if m.push(saw.sample(), hz) { done += 1; } } assert!(done >= 5); // A saw's k-th harmonic has peak 2/(πk). for k in 1..=16 { let want = 20. * (2. / (std::f32::consts::PI * k as f32)).log10(); let got = m.levels()[k - 1]; assert!((got - want).abs() < 0.5, "harmonic {k}: {got} vs {want}"); } } #[test] fn tracker_reports_notes_at_output_time() { let mut t = Tracker::new(RATE, TrackerConfig::BASS); t.set(false, -42., false); let l = t.latency(); assert!(l < (0.08 * RATE) as usize, "{l}"); let onset = 4800; let mut events = vec![]; let mut saw_a = Saw::new(RATE, 87.31); let mut saw_b = Saw::new(RATE, 220.); for i in 0..onset + 48000 { let x = if i < onset { 0. } else if i < onset + 24000 { 0.5 * saw_a.sample() } else { 0.5 * saw_b.sample() }; t.process(x, |e| events.push((i, e))); } let starts: Vec<_> = events .iter() .filter_map(|(i, e)| match e { NoteEvent::Start { pitch, .. } => Some((*i, *pitch)), _ => None, }) .collect(); assert_eq!(starts.len(), 2, "{starts:?}"); assert!( (starts[0].0 as i64 - (onset + l) as i64).abs() <= 24, "{starts:?}" ); assert!((starts[0].1 - hz_to_midi(87.31)).abs() < 0.2); assert!((starts[1].1 - hz_to_midi(220.)).abs() < 0.2); } #[test] fn tracker_delays_midi_by_the_latency() { let mut t = Tracker::new(RATE, TrackerConfig::BASS); assert!(t.set(true, -42., true)); let l = t.latency(); let mut events = vec![]; for i in 0..l + 2000 { if i == 100 { t.note_on(41); } if i == 600 { t.note_on(43); } if i == 900 { // Releasing the lower held note first leaves the top note; then the top ends it. t.note_off(41); t.note_off(43); } t.process(0., |e| events.push((i, e))); } assert_eq!( events, vec![ ( 100 + l, NoteEvent::Start { pitch: 41., glide: false } ), ( 600 + l, NoteEvent::Start { pitch: 43., glide: true } ), (900 + l, NoteEvent::Stop), ] ); }