use std::collections::HashSet; use std::f32::consts::TAU; use soundcraft_dsp::{Plugin, Unit, create, db_to_gain, gain_to_db, is_generator, plugin_info, plugins}; const SR: f32 = 48_000.0; const BLOCK: usize = 512; fn run(p: &mut dyn Plugin, input: &[Vec]) -> Vec> { let len = input[0].len(); let ch = input.len(); let mut out = vec![Vec::with_capacity(len); ch]; let mut pos = 0; let mut buf = vec![vec![0.0f32; BLOCK]; ch]; while pos < len { let n = BLOCK.min(len - pos); for c in 0..ch { buf[c][..n].copy_from_slice(&input[c][pos..pos + n]); } p.process(&mut buf, n); for c in 0..ch { out[c].extend_from_slice(&buf[c][..n]); } pos += n; } out } fn sine(freq: f32, amp: f32, len: usize) -> Vec { (0..len).map(|i| amp * (TAU * freq * i as f32 / SR).sin()).collect() } fn peak(x: &[f32]) -> f32 { x.iter().fold(0.0f32, |m, v| m.max(v.abs())) } fn rms(x: &[f32]) -> f32 { (x.iter().map(|v| v * v).sum::() / x.len().max(1) as f32).sqrt() } fn prepared(id: &str) -> Box { let mut p = create(id).unwrap(); p.prepare(SR, BLOCK, 2); p } fn is_source(id: &str) -> bool { plugin_info(id).unwrap().is_instrument || is_generator(id) } #[test] fn registry_ids_unique_and_creatable() { let mut ids = HashSet::new(); for info in plugins() { assert!(ids.insert(info.id), "duplicate id {}", info.id); assert!(info.short_name.chars().count() <= 8, "{} short name too long", info.id); let p = create(info.id).unwrap_or_else(|| panic!("cannot create {}", info.id)); assert_eq!(p.info().id, info.id); assert_eq!(plugin_info(info.id).unwrap().id, info.id); } assert!(create("nope").is_none()); assert!(plugins().len() >= 25); } #[test] fn params_well_formed() { for info in plugins() { let mut ids = HashSet::new(); for p in info.params { assert!(ids.insert(p.id), "{}: duplicate param {}", info.id, p.id); assert!(p.min <= p.default && p.default <= p.max, "{}.{} default out of range", info.id, p.id); assert!(p.min.is_finite() && p.max.is_finite()); if p.unit == Unit::Choice { assert_eq!(p.choices.len() as f32, p.max + 1.0, "{}.{}", info.id, p.id); } let n = p.to_normalized(p.default); assert!((p.from_normalized(n) - p.default).abs() <= 1e-3 * (1.0 + p.default.abs()), "{}.{}", info.id, p.id); } let mut plug = create(info.id).unwrap(); for p in info.params { assert_eq!(plug.param(p.id), Some(p.default), "{}.{}", info.id, p.id); assert!(plug.set_param(p.id, p.max + 1000.0)); assert_eq!(plug.param(p.id), Some(p.max)); } assert!(!plug.set_param("definitely_not_a_param", 1.0)); assert_eq!(plug.param("definitely_not_a_param"), None); } } #[test] fn silence_in_silence_out() { for info in plugins() { if is_source(info.id) { continue; } let mut p = prepared(info.id); let out = run(p.as_mut(), &vec![vec![0.0; 48_000]; 2]); for c in &out { assert!(peak(c) < 1e-6, "{} made noise from silence: {}", info.id, peak(c)); } } } #[test] fn impulse_response_finite_and_bounded() { for info in plugins() { let mut p = prepared(info.id); let mut x = vec![vec![0.0; 96_000]; 2]; x[0][100] = 1.0; x[1][100] = 1.0; p.note_on(0, 36, 100); let out = run(p.as_mut(), &x); for c in &out { assert!(c.iter().all(|v| v.is_finite()), "{} produced non-finite output", info.id); assert!(peak(c) < 8.0, "{} impulse peak {}", info.id, peak(c)); } } } #[test] fn hostile_params_never_break_processing() { let hostile = [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, -1e30, 1e30, 0.0, -0.0]; for info in plugins() { for &v in &hostile { let mut p = prepared(info.id); for param in info.params { assert!(p.set_param(param.id, v)); let got = p.param(param.id).unwrap(); assert!(got.is_finite() && got >= param.min && got <= param.max, "{}.{} = {got}", info.id, param.id); } p.note_on(3, 60, 127); let x = vec![sine(440.0, 0.5, 9600), sine(660.0, 0.5, 9600)]; let out = run(p.as_mut(), &x); assert!(out.iter().flatten().all(|s| s.is_finite()), "{} non-finite with param value {v}", info.id); let _ = p.latency(); let _ = p.tail_samples(); assert!(p.gain_reduction_db().is_finite()); } } } #[test] fn odd_host_calls_do_not_panic() { for info in plugins() { let mut p = create(info.id).unwrap(); // Unprepared-ish, weird rates, block bigger than prepared, mismatched channels. for (sr, block, ch) in [(0.0, 0, 0), (f32::NAN, 16, 1), (-44_100.0, 64, 2), (1e9, 64, 8), (44_100.0, 32, 2)] { p.prepare(sr, block, ch); let mut io = vec![vec![0.25f32; 300]; 3]; p.process(&mut io, 4096); p.process(&mut io, 0); p.process(&mut [], 128); let mut short = vec![vec![0.1f32; 10], vec![0.1f32; 3]]; p.process(&mut short, 100); p.note_on(10_000, 200, 255); p.note_off(0, 200); p.all_notes_off(); p.process(&mut io, 300); p.reset(); } } } #[test] fn param_changes_are_smooth() { // Jumping gain by 24 dB mid-stream must not produce a step discontinuity. let mut p = prepared("gain"); let x = vec![vec![0.5f32; 4096]; 1]; let _ = run(p.as_mut(), &x); p.set_param("gain", 24.0); let out = run(p.as_mut(), &x); let max_step = out[0].windows(2).map(|w| (w[1] - w[0]).abs()).fold(0.0f32, f32::max); assert!(max_step < 0.05, "zipper step {max_step}"); let out = run(p.as_mut(), &x); assert!((out[0][4095] - 0.5 * db_to_gain(24.0)).abs() < 0.01); } #[test] fn eq_magnitude_at_center_matches_gain() { let sr = SR; let resp = soundcraft_dsp::eq7_response(&[("mid_freq", 1000.0), ("mid_gain", 9.0), ("mid_q", 2.0)], &[1000.0, 30.0, 15_000.0], sr); assert!((resp[0] - 9.0).abs() < 0.05, "{resp:?}"); assert!(resp[1].abs() < 0.2 && resp[2].abs() < 0.2, "{resp:?}"); let r1 = soundcraft_dsp::eq1_response(&[("freq", 2500.0), ("gain", -12.0)], &[2500.0], sr); assert!((r1[0] + 12.0).abs() < 0.05); // And the real-time EQ agrees with the curve. let mut p = prepared("eq_7band"); p.set_param("mid_freq", 1000.0); p.set_param("mid_gain", 9.0); p.set_param("mid_q", 2.0); p.reset(); let out = run(p.as_mut(), &[sine(1000.0, 0.1, 48_000)]); let g = gain_to_db(rms(&out[0][24_000..]) / rms(&sine(1000.0, 0.1, 48_000)[24_000..])); assert!((g - 9.0).abs() < 0.2, "measured {g}"); // HPF attenuates lows. let r = soundcraft_dsp::eq7_response(&[("hpf_on", 1.0), ("hpf_freq", 200.0), ("hpf_slope", 3.0)], &[50.0, 200.0, 2000.0], sr); assert!(r[0] < -40.0 && (r[1] + 3.0).abs() < 0.2 && r[2].abs() < 0.1, "{r:?}"); } #[test] fn eq_switches_and_choices_apply_to_a_running_eq() { // (plugin, settings, the one switch or choice changed while running, test tone) let cases: [(&str, &[(&str, f32)], (&str, f32), f32); 7] = [ ("eq_7band", &[("hpf_freq", 2000.0), ("hpf_slope", 3.0)], ("hpf_on", 1.0), 1000.0), ("eq_7band", &[("hpf_freq", 2000.0), ("hpf_slope", 3.0), ("hpf_on", 1.0)], ("hpf_on", 0.0), 1000.0), ("eq_7band", &[("hpf_freq", 2000.0), ("hpf_slope", 0.0), ("hpf_on", 1.0)], ("hpf_slope", 3.0), 1000.0), ("eq_7band", &[("lpf_freq", 2000.0), ("lpf_slope", 3.0)], ("lpf_on", 1.0), 4000.0), ("eq_7band", &[("lpf_freq", 2000.0), ("lpf_slope", 0.0), ("lpf_on", 1.0)], ("lpf_slope", 3.0), 4000.0), ("eq_1band", &[("freq", 1000.0)], ("type", 4.0), 10_000.0), ("channel_strip", &[("hpf_freq", 2000.0)], ("hpf_on", 1.0), 1000.0), ]; let mut stale = Vec::new(); for (id, setup, (key, value), tone) in cases { let x = [sine(tone, 0.5, 24_000)]; let level = |p: &mut dyn Plugin| gain_to_db(rms(&run(p, &x)[0][12_000..])); let build = |settings: &[(&str, f32)]| { let mut p = create(id).unwrap(); p.prepare(SR, BLOCK, 1); for &(k, v) in settings { assert!(p.set_param(k, v), "{id} {k}"); } p.reset(); p }; let mut live = build(setup); let before = level(live.as_mut()); assert!(live.set_param(key, value)); let after = level(live.as_mut()); let want = level(build(&[setup, &[(key, value)]].concat()).as_mut()); assert!((before - want).abs() > 3.0, "{id} {key}={value}: the tone must tell the settings apart"); if (after - want).abs() >= 0.1 { stale.push(format!("{id} {key}={value}: live {after:.2} dB, fresh {want:.2} dB, before {before:.2} dB")); } } assert!(stale.is_empty(), "{stale:#?}"); } #[test] fn compressor_steady_state_gain_reduction() { let mut p = prepared("compressor"); p.set_param("threshold", -20.0); p.set_param("ratio", 4.0); p.set_param("knee", 0.0); p.set_param("attack", 1.0); p.set_param("release", 50.0); // Constant level at -10 dBFS: 10 dB over → 7.5 dB of reduction. let level = db_to_gain(-10.0); let out = run(p.as_mut(), &vec![vec![level; 48_000]; 2]); let got = gain_to_db(out[0][47_999]) - -10.0; assert!((got + 7.5).abs() < 0.1, "gain change {got}"); assert!((p.gain_reduction_db() - 7.5).abs() < 0.1); // A sine settles near the same reduction (peak detection). p.reset(); let x = sine(1000.0, level, 48_000); let out = run(p.as_mut(), &[x.clone(), x]); let got = gain_to_db(peak(&out[0][40_000..])) + 10.0; assert!((got + 7.5).abs() < 1.0, "sine gain change {got}"); // Limiter mode with lookahead reports latency. p.set_param("limiter", 1.0); p.set_param("lookahead", 5.0); assert_eq!(p.latency(), 240); } #[test] fn gate_closes_below_threshold() { let mut p = prepared("expander_gate"); p.set_param("threshold", -40.0); p.set_param("range", 80.0); p.set_param("hold", 10.0); p.set_param("release", 20.0); let quiet = sine(300.0, db_to_gain(-60.0), 48_000); let out = run(p.as_mut(), std::slice::from_ref(&quiet)); let atten = gain_to_db(peak(&out[0][24_000..]) / peak(&quiet[24_000..])); assert!(atten < -70.0, "gate attenuation only {atten}"); let loud = sine(300.0, db_to_gain(-10.0), 48_000); let out = run(p.as_mut(), std::slice::from_ref(&loud)); let g = gain_to_db(peak(&out[0][24_000..]) / peak(&loud[24_000..])); assert!(g.abs() < 0.5, "open gate changed level {g}"); } #[test] fn maximizer_respects_ceiling_and_reports_latency() { let mut p = prepared("maximizer"); p.set_param("threshold", -12.0); p.set_param("ceiling", -1.0); let x = sine(220.0, 0.9, 48_000); let out = run(p.as_mut(), &[x.clone(), x]); assert!(peak(&out[0]) <= db_to_gain(-1.0) + 1e-5, "{}", peak(&out[0])); assert!(p.latency() > 0 && p.latency() < 200); assert!(p.gain_reduction_db() > 5.0); } #[test] fn de_esser_reduces_sibilance_only() { let mut p = prepared("de_esser"); p.set_param("threshold", -40.0); p.set_param("range", 20.0); let hi = sine(8000.0, 0.5, 48_000); let out = run(p.as_mut(), std::slice::from_ref(&hi)); assert!(gain_to_db(rms(&out[0][24_000..]) / rms(&hi[24_000..])) < -6.0); p.reset(); let lo = sine(200.0, 0.005, 48_000); let out = run(p.as_mut(), std::slice::from_ref(&lo)); assert!(gain_to_db(rms(&out[0][24_000..]) / rms(&lo[24_000..])).abs() < 0.5); } #[test] fn latency_reporting_aligns_pitch_shifter() { let mut p = prepared("pitch_shifter"); let lat = p.latency(); assert!(lat > 0); let mut x = vec![0.0f32; 8000]; x[1000] = 1.0; let out = run(p.as_mut(), &[x]); let pos = out[0].iter().enumerate().max_by(|a, b| a.1.abs().total_cmp(&b.1.abs())).unwrap().0; assert_eq!(pos, 1000 + lat); } #[test] fn pitch_shifter_moves_pitch() { let mut p = prepared("pitch_shifter"); p.set_param("semitones", 12.0); p.reset(); let out = run(p.as_mut(), &[sine(440.0, 0.5, 48_000)]); let spec = soundcraft_dsp::spectrum::magnitude_db(&out[0][16_384..], 16_384); let bin = spec.iter().enumerate().max_by(|a, b| a.1.total_cmp(b.1)).unwrap().0; let hz = soundcraft_dsp::spectrum::bin_hz(bin, 16_384, SR); assert!((hz - 880.0).abs() < 15.0, "peak at {hz}"); } #[test] fn reverb_has_tail_and_decays() { for id in ["room_reverb", "plate_reverb"] { let mut p = prepared(id); p.set_param("mix", 100.0); p.set_param("decay", 1.0); let mut x = vec![vec![0.0; 48_000 * 4]; 2]; x[0][0] = 1.0; x[1][0] = 1.0; let out = run(p.as_mut(), &x); let early = rms(&out[0][2400..24_000]); let late = rms(&out[0][48_000 * 3..]); assert!(early > 1e-3, "{id} no reverb"); assert!(late < early * 1e-2, "{id} not decaying {early} {late}"); assert!(p.tail_samples() > 48_000); // Stereo decorrelation. assert!(out[0][2400..4800] != out[1][2400..4800]); } } #[test] fn delay_repeats_at_time() { let mut p = prepared("mod_delay"); p.set_param("time", 100.0); p.set_param("mix", 100.0); p.set_param("feedback", 0.0); p.set_param("lpf", 20_000.0); p.reset(); let mut x = vec![0.0f32; 20_000]; x[0] = 1.0; let out = run(p.as_mut(), &[x]); let pos = out[0].iter().enumerate().max_by(|a, b| a.1.abs().total_cmp(&b.1.abs())).unwrap().0; assert!((pos as i64 - 4800).abs() <= 2, "echo at {pos}"); } #[test] fn modulation_tail_covers_feedback_echoes() { for (id, delay, feedback) in [("chorus", 30.0, 90.0), ("flanger", 10.0, 95.0), ("flanger", 10.0, -95.0)] { let mut p = prepared(id); for (param, v) in [("delay", delay), ("feedback", feedback), ("depth", 0.0), ("mix", 100.0)] { assert!(p.set_param(param, v)); } p.reset(); let mut x = vec![0.0f32; 48_000 * 5]; x[0] = 0.5; let out = run(p.as_mut(), &[x]); let floor = peak(&out[0]) * 1e-3; let last = out[0].iter().rposition(|v| v.abs() > floor).unwrap(); assert!(last < p.tail_samples(), "{id} feedback {feedback}: echo at {last}, tail {}", p.tail_samples()); } } #[test] fn trim_and_invert() { let mut p = prepared("invert"); let out = run(p.as_mut(), &[vec![0.5; 16]]); assert!(out[0].iter().all(|&v| v == -0.5)); let mut t = prepared("trim"); t.set_param("gain", -96.0); t.reset(); let out = run(t.as_mut(), &[vec![0.5; 16]]); assert!(out[0].iter().all(|&v| v == 0.0)); t.set_param("gain", 0.0); t.set_param("invert", 1.0); t.reset(); let out = run(t.as_mut(), &[vec![0.5; 16]]); assert!(out[0].iter().all(|&v| (v + 0.5).abs() < 1e-6)); } #[test] fn time_shift_delays() { let mut p = prepared("time_shift"); p.set_param("delay_samples", 123.0); p.reset(); let mut x = vec![0.0f32; 1000]; x[10] = 1.0; let out = run(p.as_mut(), &[x]); assert_eq!(out[0][133], 1.0); } #[test] fn dc_removal_removes_dc() { let mut p = prepared("dc_offset_removal"); let out = run(p.as_mut(), &[vec![0.3; 96_000]]); assert!(out[0][95_999].abs() < 1e-3); } #[test] fn signal_generator_level() { let mut p = prepared("signal_generator"); p.set_param("level", -6.0); p.reset(); let out = run(p.as_mut(), &[vec![0.0; 48_000]]); assert!((gain_to_db(peak(&out[0])) + 6.0).abs() < 0.05); } #[test] fn synth_note_on_sounds_and_note_off_decays() { let mut p = prepared("subtractive_synth"); p.note_on(0, 60, 100); let out = run(p.as_mut(), &vec![vec![0.0; 24_000]; 2]); assert!(rms(&out[0][4800..]) > 0.01, "synth silent"); assert_eq!(out[0], out[1]); p.note_off(0, 60); let out = run(p.as_mut(), &vec![vec![0.0; 96_000]; 2]); assert!(peak(&out[0][72_000..]) < 1e-6, "synth did not decay"); // Polyphony: a chord is louder than one note and stays finite. for n in [60, 64, 67, 71, 74, 77, 81, 84, 60, 62, 65, 69, 72, 76, 79, 83, 86, 88] { p.note_on(0, n, 127); } let out = run(p.as_mut(), &vec![vec![0.0; 9600]; 2]); assert!(out[0].iter().all(|v| v.is_finite()) && peak(&out[0]) > 0.05); p.all_notes_off(); let out = run(p.as_mut(), &vec![vec![0.0; 96_000]; 2]); assert!(peak(&out[0][72_000..]) < 1e-6); } #[test] fn synth_follows_the_sustain_pedal_and_pitch_bend() { let mut p = prepared("subtractive_synth"); p.midi(0, [0xB0, 64, 127]); p.note_on(0, 60, 100); run(p.as_mut(), &vec![vec![0.0; 4800]; 2]); p.note_off(0, 60); let out = run(p.as_mut(), &vec![vec![0.0; 96_000]; 2]); assert!(rms(&out[0][72_000..]) > 0.01, "the pedal holds the note"); p.midi(0, [0xB0, 64, 0]); let out = run(p.as_mut(), &vec![vec![0.0; 96_000]; 2]); assert!(peak(&out[0][72_000..]) < 1e-6, "lifting the pedal releases it"); let rises = |x: &[f32]| x.windows(2).filter(|w| w[0] < 0.0 && w[1] >= 0.0).count() as f64; let mut p = prepared("subtractive_synth"); p.set_param("osc1_wave", 2.0); p.set_param("osc_mix", 0.0); p.note_on(0, 69, 100); let plain = run(p.as_mut(), &[vec![0.0; 48_000]]); p.midi(0, [0xE0, 0x7F, 0x7F]); let bent = run(p.as_mut(), &[vec![0.0; 48_000]]); let ratio = rises(&bent[0][4800..]) / rises(&plain[0][4800..]); assert!((ratio - 2f64.powf(2.0 / 12.0)).abs() < 0.01, "{ratio}"); p.midi(0, [0xB0, 121, 0]); let reset = run(p.as_mut(), &[vec![0.0; 48_000]]); assert!((rises(&reset[0][4800..]) / rises(&plain[0][4800..]) - 1.0).abs() < 0.01); } #[test] fn synth_event_offsets_are_sample_accurate() { let mut p = prepared("subtractive_synth"); p.set_param("amp_attack", 0.5); p.note_on(300, 69, 127); let out = run(p.as_mut(), &[vec![0.0; 512]]); assert!(out[0][..300].iter().all(|&v| v == 0.0)); assert!(out[0][300..].iter().any(|&v| v != 0.0)); } #[test] fn drum_kit_notes_sound_and_end() { for note in [36, 38, 39, 42, 46, 45, 47, 50, 49, 51] { let mut p = prepared("drum_synth"); p.note_on(0, note, 120); let out = run(p.as_mut(), &[vec![0.0; 48_000 * 7]]); assert!(peak(&out[0][..4800]) > 0.01, "drum note {note} silent"); assert!(peak(&out[0][48_000 * 6..]) < 1e-6, "drum note {note} rings forever"); } let mut p = prepared("drum_synth"); p.note_on(0, 99, 120); let out = run(p.as_mut(), &[vec![0.0; 4800]]); assert!(peak(&out[0]) == 0.0, "unmapped note should be silent"); } #[test] fn saturator_adds_harmonics_and_stays_bounded() { let mut p = prepared("saturator"); p.set_param("drive", 30.0); let out = run(p.as_mut(), &[sine(1000.0, 1.0, 48_000)]); assert!(peak(&out[0]) <= 1.01); let spec = soundcraft_dsp::spectrum::magnitude_db(&out[0][8192..], 8192); let third = (3000.0 / (SR / 8192.0)) as usize; assert!(spec[third - 1..=third + 1].iter().cloned().fold(f32::MIN, f32::max) > -30.0); } #[test] fn lofi_quantizes() { let mut p = prepared("lofi"); p.set_param("bits", 2.0); p.set_param("sample_rate", 48_000.0); let out = run(p.as_mut(), &[sine(100.0, 0.9, 4800)]); let mut levels: Vec = out[0].iter().map(|v| (v * 1000.0).round() as i32).collect(); levels.sort(); levels.dedup(); assert!(levels.len() <= 5, "{levels:?}"); }