//! Frequency response system tests. #![allow( dead_code, clippy::precedence, clippy::type_complexity, clippy::float_cmp, clippy::len_zero, clippy::double_neg, clippy::many_single_char_names, clippy::manual_range_contains )] use fundsp::fft::*; use fundsp::hacker32::*; use num_complex::Complex32; fn is_equal_response(x: Complex32, y: Complex32) -> bool { // This tolerance has been tuned to a minimum value that allows the tests to pass. let tolerance = 2.0e-4; let norm = (x - y).norm(); norm <= max(1.0, max(x.norm(), y.norm())) * tolerance } fn test_response(mut filter: X) where X: AudioUnit, { assert!(filter.inputs() == 1 && filter.outputs() == 1); filter.allocate(); let length = 0x8000; let sample_rate = DEFAULT_SR; filter.reset(); filter.set_sample_rate(sample_rate); let mut input = 1.0; let mut in_buffer = Vec::with_capacity(length); // Try to remove effect of DC by warming up the filter. for _i in 0..length / 2 { filter.filter_mono(0.0); } for _i in 0..length { let x = filter.filter_mono(input); in_buffer.push(x); input = 0.0; } let mut buffer = vec![Complex32::ZERO; length / 2 + 1]; real_fft(&in_buffer, &mut buffer); let mut f = 10.0; while f <= 22_000.0 { let i = round(f * length as f64 / sample_rate) as usize; let f_i = i as f64 / length as f64 * sample_rate; let reported = filter.response(0, f_i).unwrap(); let reported = Complex32::new(reported.re as f32, reported.im as f32); let response = buffer[i]; if !is_equal_response(reported, response) { eprintln!( "{} Hz reported ({}, {}) measured ({}, {})", f_i, reported.norm(), reported.arg(), response.norm(), response.arg(), ); panic!() } if f < 1000.0 { f += 10.0; } else { f += 100.0; } } } /// Test frequency response system. #[test] fn test_responses() { test_response(bell_hz(500.0, 1.0, 2.0) * 0.5); test_response(lowshelf_hz(2000.0, 10.0, 5.0)); test_response(highshelf_hz(2000.0, 10.0, 5.0)); test_response(peak_hz(5000.0, 1.0)); test_response(allpass_hz(500.0, 5.0)); test_response(notch_hz(1000.0, 1.0)); test_response(lowpass_hz(50.0, 1.0)); test_response(highpass_hz(5000.0, 1.0)); test_response(bandpass_hz(100.0, 1.0)); test_response(highpass_hz(500.0, 1.0) & bandpass_hz(500.0, 2.0)); test_response(pinkpass() * dc(2.0)); test_response(follow(0.0002)); test_response(follow(0.01)); test_response(delay(0.0)); test_response(delay(0.0001)); test_response(delay(0.0001) >> delay(0.0002)); test_response(dcblock()); test_response(dcblock_hz(100.0) & follow(0.001)); test_response(lowpole_hz(1000.0)); test_response(split() >> (lowpole_hz(100.0) + lowpole_hz(190.0))); test_response(lowpole_hz(10000.0)); test_response(resonator_hz(300.0, 20.0)); test_response(butterpass_hz(200.0)); test_response(butterpass_hz(1000.0)); test_response(butterpass_hz(500.0) & bell_hz(2000.0, 10.0, 5.0)); test_response(butterpass_hz(6000.0) >> lowpass_hz(500.0, 3.0)); test_response(pass() & tick()); test_response(pass() * 0.25 & tick() * 0.5 & tick() >> tick() * 0.25); test_response(tick() & lowshelf_hz(500.0, 2.0, 0.1)); test_response(allpole_delay(0.5) & allpole_delay(1.3) & allpole_delay(0.1)); test_response(highpole_hz(5000.0) & highpole_hz(500.0) & highpole_hz(2000.0)); test_response( (delay(0.001) ^ delay(0.002)) >> reverse() >> (delay(0.003) | delay(0.007)) >> join(), ); test_response( (butterpass_hz(15000.0) ^ allpass_hz(10000.0, 10.0)) >> lowpole_hz(500.0) + pass(), ); test_response( (resonator_hz(12000.0, 500.0) ^ lowpass_hz(3000.0, 0.5)) >> pass() + highshelf_hz(3000.0, 0.5, 4.0), ); test_response(split() >> multipass::() >> join()); test_response( split() >> stacki::(|i| { resonator_hz(1000.0 + 1000.0 * i as f32, 100.0 + 100.0 * i as f32) }) >> join(), ); test_response(branchf::(|t| resonator_hz(xerp(100.0, 20000.0, t), 10.0)) >> join()); test_response(pipei::(|i| { bell_hz( 1000.0 + 1000.0 * i as f32, (i + 1) as f32, db_amp((i + 6) as f32), ) })); test_response( split() >> stacki::(|i| lowpole_hz(1000.0 + 1000.0 + i as f32)) >> join(), ); test_response(busi::(|i| { lowpass_hz(1000.0 + 1000.0 * rnd1(i) as f32, 1.0 + 1.0 * rnd2(i) as f32) })); test_response( split::() >> multisplit::() >> sumf::(|f| highshelf_hz(f * 10.0 + 10.0, 1.0 + f, 2.0 + f)), ); test_response(1.0 - pan(0.5) >> join()); test_response(0.5 * pan(0.0) >> join()); test_response(pan(0.5) - 1.0 >> join()); test_response(pan(-1.0) * 0.5 >> multijoin::()); let tmp = shared(0.0); test_response(fir((0.5, 0.5)) | timer(&tmp)); test_response(fir((0.25, 0.5, 0.25)) >> monitor(&tmp, Meter::Sample)); test_response(fir((0.4, 0.3, 0.2, 0.1))); test_response(morph_hz(1000.0, 1.0, 0.5)); test_response(morph_hz(2000.0, 2.0, -0.5)); test_response((1.0 + pass() | dc((1000.0, 0.5, 0.5))) >> morph()); test_response((pass() | dc((500.0, 2.0, -1.0))) >> morph()); test_response(biquad(0.0, 0.17149, 0.29287, 0.58574, 0.29287)); test_response(biquad(0.033717, 0.171773, 1.059253, -0.035714, 0.181952)); test_response(pass() + 1.0 >> lowpass_hz(1000.0, 1.0)); test_response((pass() | dc(1.0)) >> rotate(0.5, 1.0) >> (pass() | sink())); test_response((dc(2.0) | pass()) >> rotate(-0.1, 0.5) >> (pass() | sink())); let mut net1 = Net::new(1, 1); net1.chain(Box::new(lowpole_hz(1500.0))); test_response(net1); let mut net2 = Net::new(1, 1); net2.chain(Box::new(lowpole_hz(500.0))); net2.chain(Box::new(lowpole_hz(2500.0))); test_response(net2); let mut net3 = Net::new(1, 1); net3.chain(Box::new(highpole_hz(1500.0))); let mut net4 = Net::new(1, 1); net4.chain(Box::new(highpole_hz(500.0))); test_response(net3 >> net4); let mut net5 = Net::new(1, 1); net5.chain(Box::new(highpole_hz(1500.0))); let mut net6 = Net::new(1, 1); net6.chain(Box::new(highpole_hz(500.0))); test_response(net5 & net6 & pass()); let mut net7 = Net::new(1, 1); let id7 = net7.push(Box::new(highpass_hz(1000.0, 1.0))); net7.connect_input(0, id7, 0); net7.connect_output(id7, 0, 0); test_response(net7); let mut net8 = Net::new(1, 1); net8.chain(Box::new(highpole_hz(1500.0))); test_response(Net::wrap(Box::new(zero())) + net8); let mut net9 = Net::new(1, 1); net9.chain(Box::new(highpole_hz(2000.0))); test_response(Net::wrap(Box::new(dc(1.0))) - net9); let mut neta = Net::new(1, 1); neta.chain(Box::new(notch_hz(2500.0, 2.0))); test_response(Net::wrap(Box::new(dc(2.0))) * neta); let mut netb = Net::new(1, 1); netb.chain(Box::new(notch_hz(2500.0, 1.0))); test_response(netb * 2.0 >> lowpass_hz(1500.0, 1.0)); let mut netc = Net::new(1, 1); netc.chain(Box::new(highpass_hz(5500.0, 1.0))); test_response(netc >> highpass_hz(2500.0, 1.0) + 1.0); let mut netd = Net::new(1, 1); netd.chain(Box::new(lowpass_hz(5000.0, 1.0))); test_response((netd ^ highpass_hz(3000.0, 1.0)) >> (pass() + pass())); let mut nete = Net::new(1, 1); nete.chain(Box::new(notch_hz(5000.0, 1.0))); test_response((nete.clone() ^ peak_hz(3000.0, 1.0)) >> (Net::wrap(Box::new(pass())) + pass())); let mut netf = Net::new(1, 1); netf.chain(Box::new(notch_hz(2000.0, 1.0))); test_response( (netf.clone() ^ pass() ^ peak_hz(1000.0, 1.0)) >> (Net::wrap(Box::new(pass())) + pass() + pass()), ); let mut netg = Net::new(1, 1); netg.chain(Box::new(notch_hz(2000.0, 1.0))); test_response( (netg ^ pass() ^ pass()) >> (Net::wrap(Box::new(pass())) | pass() | pinkpass()) >> (Net::wrap(Box::new(pinkpass())) + pass() + pass()), ); } // Test various allpass filters for the allpass property. #[test] fn test_allpass() { let length = 0x8000; let mut spectrum = vec![Complex32::ZERO; length / 2 + 1]; let allpasses: [Box; 12] = [ Box::new(pass()), Box::new(tick()), Box::new(allpole_delay(0.5)), Box::new(allpole_delay(0.8)), Box::new(delay(0.0001)), Box::new(delay(0.001)), Box::new(allpass_hz(1000.0, 1.0)), Box::new(allpass_hz(2000.0, 2.0)), Box::new(allnest_c(0.5, pass())), Box::new(allnest_c(0.6, tick())), Box::new(allnest_c(0.7, allpole_delay(0.5))), Box::new(allnest_c(-0.6, allpass_hz(3000.0, 3.0))), ]; let impulse = Wave::render(DEFAULT_SR, 1.0 / DEFAULT_SR, &mut (impulse::())); for mut node in allpasses { let response = impulse.filter(length as f64 / DEFAULT_SR, &mut *node); real_fft(response.channel(0), &mut spectrum); // This tolerance has been tuned to a minimum value that allows the tests to pass. let tolerance = 1.0e-5; for s in &spectrum[1..] { let norm = s.norm(); assert!(norm >= 1.0 - tolerance && norm <= 1.0 + tolerance); } } }