use common_dsp::{hz_to_midi, midi_to_hz, note_name, wav, Crossover, Saw, Yin}; fn cents(a: f32, b: f32) -> f32 { 1200. * (a / b).log2() } #[test] fn yin_tracks_bass_fundamentals_within_two_cents() { let rate = 48000.; let mut yin = Yin::new(2400); for hz in [43.65, 55., 110., 220.] { let mut saw = Saw::new(rate, hz); let x: Vec = (0..6000).map(|_| saw.sample()).collect(); let (got, clarity) = yin.estimate(&x, rate, 25., 1000.).unwrap(); assert!(cents(got, hz).abs() < 2., "{hz}: {got}"); assert!(clarity > 0.8); } } #[test] fn yin_rejects_silence_and_noise() { let mut yin = Yin::new(2400); assert!(yin.estimate(&[0.; 6000], 48000., 25., 1000.).is_none()); let mut seed = 1u32; let noise: Vec = (0..6000) .map(|_| { seed = seed.wrapping_mul(1664525).wrapping_add(1013904223); (seed >> 8) as f32 / (1u32 << 24) as f32 - 0.5 }) .collect(); assert!(yin.estimate(&noise, 48000., 25., 1000.).is_none()); } #[test] fn crossover_bands_sum_to_flat_magnitude() { let rate = 48000.; for hz in [20., 45., 90., 180., 1000., 8000., 18000.] { let mut x = Crossover::new(rate, 90.); let (mut a, mut b) = (0f64, 0f64); let n = 96000; for i in 0..n { let s = (std::f32::consts::TAU * hz * i as f32 / rate).sin(); let (l, h) = x.process(s); if i > n / 2 { a += (s as f64).powi(2); b += ((l + h) as f64).powi(2); } } let db = 10. * (b / a).log10(); assert!(db.abs() < 0.1, "{hz} Hz: {db} dB"); } } #[test] fn notes_round_trip() { assert!((midi_to_hz(29.) - 43.65).abs() < 0.01); assert!((hz_to_midi(440.) - 69.).abs() < 1e-4); assert_eq!(note_name(29), "F1"); assert_eq!(note_name(61), "C#4"); } #[test] fn wave_round_trip_keeps_root_and_wavetable_chunks() { let w = wav::Wave { channels: 2, rate: 44100, samples: (0..999) .map(|i| (i as f32 * 0.01).sin()) .collect::>()[..998] .to_vec(), root: Some(29), frame: Some(2048), }; let bytes = wav::encode(&w); assert_eq!(&bytes[..4], b"RIFF"); assert_eq!( u32::from_le_bytes(bytes[4..8].try_into().unwrap()) as usize, bytes.len() - 8 ); assert_eq!(wav::decode(&bytes).unwrap(), w); assert!(wav::decode(&bytes[..40]).is_err()); } #[test] fn k_weighting_reads_a_full_scale_997_hz_sine_at_minus_3_lufs() { use common_dsp::loudness::{lufs, KWeighting}; let level = |rate: f32, hz: f64| { let mut k = KWeighting::new(rate); let n = rate as usize; let mut sum = 0f64; for i in 0..2 * n { let x = (std::f64::consts::TAU * hz * i as f64 / rate as f64).sin() as f32; let y = k.process(x) as f64; if i >= n { sum += y * y; } } lufs(sum / n as f64) }; // BS.1770-4: a 0 dBFS 997 Hz sine in one channel reads −3.01 LKFS, at every rate. for rate in [44100., 48000., 96000., 192000.] { let got = level(rate, 997.); // BS.1770 allows meters 0.1 LU; the prototypes land within 0.03 up to 192 kHz. assert!((got + 3.01).abs() < 0.05, "{rate}: {got}"); } // The RLB high-pass counts a 30 Hz sub much less than the mids. assert!(level(48000., 30.) < level(48000., 997.) - 5.); }