use naad::delay::{AllpassDelay, CombFilter, DelayLine};
use naad::effects::{Chorus, Distortion, DistortionType, Flanger, Phaser};
use naad::envelope::{Adsr, EnvelopeSegment, MultiStageEnvelope};
use naad::filter::{BiquadFilter, FilterType, StateVariableFilter};
use naad::modulation::{FmModulator, Lfo, LfoShape, RingModulator};
use naad::noise::{NoiseGenerator, NoiseType};
use naad::oscillator::{Oscillator, Waveform};
use naad::tuning;
use naad::wavetable::{MorphWavetable, Wavetable, WavetableOscillator};
#[test]
fn sine_440_correct_period() {
let sample_rate = 44100.0;
let frequency = 440.0;
let mut osc = Oscillator::new(Waveform::Sine, frequency, sample_rate).unwrap();
let num_samples = 4410; let mut samples = Vec::with_capacity(num_samples);
for _ in 0..num_samples {
samples.push(osc.next_sample());
}
let mut crossings = Vec::new();
for i in 1..samples.len() {
if samples[i - 1] <= 0.0 && samples[i] > 0.0 {
let frac = -samples[i - 1] / (samples[i] - samples[i - 1]);
crossings.push(i as f32 - 1.0 + frac);
}
}
let expected_period = sample_rate / frequency;
assert!(
crossings.len() >= 2,
"need at least 2 zero crossings, got {}",
crossings.len()
);
for i in 1..crossings.len() {
let period = crossings[i] - crossings[i - 1];
assert!(
(period - expected_period).abs() < 0.5,
"period should be ~{expected_period}, got {period}"
);
}
}
#[test]
fn polyblep_saw_anti_aliased() {
let sample_rate = 44100.0;
let frequency = 5000.0; let mut osc = Oscillator::new(Waveform::Saw, frequency, sample_rate).unwrap();
let num_samples = 4096;
let mut samples = vec![0.0f32; num_samples];
osc.fill_buffer(&mut samples);
for &s in &samples {
assert!(
s.abs() <= 1.5,
"PolyBLEP saw should have controlled amplitude, got {s}"
);
}
let mut diff_energy = 0.0f32;
for i in 1..num_samples {
let diff = samples[i] - samples[i - 1];
diff_energy += diff * diff;
}
let avg_diff_energy = diff_energy / num_samples as f32;
assert!(
avg_diff_energy < 0.5,
"PolyBLEP saw should have reduced HF energy, avg diff^2 = {avg_diff_energy}"
);
}
#[test]
fn adsr_sustain_holds() {
let mut env = Adsr::new(0.001, 0.001, 0.6, 0.1).unwrap();
env.gate_on();
for _ in 0..500 {
let _ = env.next_value();
}
let mut sustain_values = Vec::new();
for _ in 0..1000 {
sustain_values.push(env.next_value());
}
for &v in &sustain_values {
assert!(
(v - 0.6).abs() < 0.01,
"sustain should hold at 0.6, got {v}"
);
}
}
#[test]
fn biquad_lp_minus_3db_at_cutoff() {
let sample_rate = 44100.0;
let cutoff = 1000.0;
let mut filter = BiquadFilter::new(FilterType::LowPass, sample_rate, cutoff, 0.707).unwrap();
let mut osc = Oscillator::new(Waveform::Sine, cutoff, sample_rate).unwrap();
for _ in 0..10000 {
let input = osc.next_sample();
let _ = filter.process_sample(input);
}
let mut max_output = 0.0f32;
for _ in 0..1000 {
let input = osc.next_sample();
let output = filter.process_sample(input);
max_output = max_output.max(output.abs());
}
let db = 20.0 * max_output.log10();
assert!(
(db - (-3.0)).abs() < 1.5,
"LP at cutoff should be ~-3 dB, got {db:.2} dB (amplitude {max_output:.4})"
);
}
#[test]
fn equal_temperament_a4_c4() {
let a4 = tuning::midi_to_freq(69);
assert!((a4 - 440.0).abs() < 0.01, "A4 should be 440 Hz, got {a4}");
let c4 = tuning::midi_to_freq(60);
assert!(
(c4 - 261.63).abs() < 0.1,
"C4 should be ~261.63 Hz, got {c4}"
);
let a3 = tuning::midi_to_freq(57);
assert!((a3 - 220.0).abs() < 0.1, "A3 should be 220 Hz, got {a3}");
}
#[test]
fn fm_synthesis_produces_sidebands() {
let sample_rate = 44100.0;
let mut fm = FmModulator::new(440.0, 220.0, 2.0, sample_rate).unwrap();
let num_samples = 4096;
let mut samples = vec![0.0f32; num_samples];
fm.fill_buffer(&mut samples);
let mut zero_crossings = 0;
for i in 1..num_samples {
if (samples[i - 1] >= 0.0 && samples[i] < 0.0)
|| (samples[i - 1] < 0.0 && samples[i] >= 0.0)
{
zero_crossings += 1;
}
}
assert!(
zero_crossings > 10,
"FM should produce significant output, got {zero_crossings} zero crossings"
);
let mut crossing_positions = Vec::new();
for i in 1..num_samples {
if samples[i - 1] <= 0.0 && samples[i] > 0.0 {
crossing_positions.push(i);
}
}
if crossing_positions.len() >= 3 {
let mut periods: Vec<usize> = Vec::new();
for i in 1..crossing_positions.len() {
periods.push(crossing_positions[i] - crossing_positions[i - 1]);
}
let min_period = periods.iter().copied().min().unwrap_or(0);
let max_period = periods.iter().copied().max().unwrap_or(0);
assert!(
max_period >= min_period,
"FM should produce varying periods"
);
}
}
#[test]
fn serde_roundtrip_oscillator() {
let osc = Oscillator::new(Waveform::Saw, 440.0, 44100.0).unwrap();
let json = serde_json::to_string(&osc).unwrap();
let back: Oscillator = serde_json::from_str(&json).unwrap();
assert_eq!(osc.waveform(), back.waveform());
assert!((osc.frequency() - back.frequency()).abs() < f32::EPSILON);
assert!((osc.sample_rate() - back.sample_rate()).abs() < f32::EPSILON);
}
#[test]
fn serde_roundtrip_adsr() {
let env = Adsr::new(0.01, 0.1, 0.5, 0.2).unwrap();
let json = serde_json::to_string(&env).unwrap();
let back: Adsr = serde_json::from_str(&json).unwrap();
assert!((env.attack_time - back.attack_time).abs() < f32::EPSILON);
assert!((env.sustain_level - back.sustain_level).abs() < f32::EPSILON);
}
#[test]
fn serde_roundtrip_filter_type() {
let ft = naad::filter::FilterType::BandPass;
let json = serde_json::to_string(&ft).unwrap();
let back: naad::filter::FilterType = serde_json::from_str(&json).unwrap();
assert_eq!(ft, back);
}
#[test]
fn pink_noise_spectral_slope() {
let mut ngen = NoiseGenerator::new(NoiseType::Pink, 42);
let num_samples = 65536;
let sample_rate = 44100.0;
let mut samples = vec![0.0f32; num_samples];
ngen.fill_buffer(&mut samples);
let low_period = (sample_rate / 150.0) as usize;
let mut low_energy = 0.0f32;
let mut low_count = 0;
for chunk in samples.chunks(low_period) {
let avg: f32 = chunk.iter().sum::<f32>() / chunk.len() as f32;
low_energy += avg * avg;
low_count += 1;
}
low_energy /= low_count as f32;
let high_period = (sample_rate / 1500.0) as usize;
let mut high_energy = 0.0f32;
let mut high_count = 0;
for chunk in samples.chunks(high_period.max(1)) {
if chunk.len() >= 2 {
let diff: f32 = chunk.windows(2).map(|w| (w[1] - w[0]).powi(2)).sum::<f32>()
/ (chunk.len() - 1) as f32;
high_energy += diff;
high_count += 1;
}
}
if high_count > 0 {
high_energy /= high_count as f32;
}
assert!(
low_energy > 0.0 || high_energy > 0.0,
"noise should have energy"
);
}
#[test]
fn wavetable_harmonics() {
let wt = Wavetable::from_harmonics(1, &[1.0], 1024).unwrap();
assert_eq!(wt.len(), 1024);
let max = wt.samples().iter().map(|s| s.abs()).fold(0.0f32, f32::max);
assert!(
(max - 1.0).abs() < 0.01,
"normalized wavetable max should be 1.0, got {max}"
);
let quarter = wt.samples()[256]; assert!(
quarter > 0.9,
"sine wavetable at 1/4 period should be near 1.0, got {quarter}"
);
}
#[test]
fn serde_roundtrip_tuning_table() {
let table = tuning::TuningTable::from_system(tuning::TuningSystem::JustIntonation, 442.0);
let json = serde_json::to_string(&table).unwrap();
let back: tuning::TuningTable = serde_json::from_str(&json).unwrap();
assert_eq!(table.name, back.name);
assert!((table.a4_hz - back.a4_hz).abs() < f32::EPSILON);
}
#[test]
fn serde_roundtrip_multi_stage_envelope() {
let segments = vec![
EnvelopeSegment {
target: 1.0,
duration: 0.01,
},
EnvelopeSegment {
target: 0.0,
duration: 0.05,
},
];
let env = MultiStageEnvelope::new(segments).unwrap();
let json = serde_json::to_string(&env).unwrap();
let back: MultiStageEnvelope = serde_json::from_str(&json).unwrap();
assert_eq!(env.segments.len(), back.segments.len());
}
#[test]
fn serde_roundtrip_svf() {
let svf = StateVariableFilter::new(1000.0, 0.707, 44100.0).unwrap();
let json = serde_json::to_string(&svf).unwrap();
let back: StateVariableFilter = serde_json::from_str(&json).unwrap();
assert!((svf.frequency() - back.frequency()).abs() < f32::EPSILON);
assert!((svf.q() - back.q()).abs() < f32::EPSILON);
}
#[test]
fn serde_roundtrip_lfo() {
let lfo = Lfo::new(LfoShape::Sine, 5.0, 44100.0).unwrap();
let json = serde_json::to_string(&lfo).unwrap();
let back: Lfo = serde_json::from_str(&json).unwrap();
assert!((lfo.depth - back.depth).abs() < f32::EPSILON);
}
#[test]
fn serde_roundtrip_ring_modulator() {
let rm = RingModulator::new(Waveform::Sine, 300.0, 44100.0).unwrap();
let json = serde_json::to_string(&rm).unwrap();
let _back: RingModulator = serde_json::from_str(&json).unwrap();
}
#[test]
fn serde_roundtrip_chorus() {
let chorus = Chorus::new(3, 0.5, 10.0, 2.0, 0.5, 44100.0).unwrap();
let json = serde_json::to_string(&chorus).unwrap();
let back: Chorus = serde_json::from_str(&json).unwrap();
assert!((chorus.mix - back.mix).abs() < f32::EPSILON);
}
#[test]
fn serde_roundtrip_flanger() {
let flanger = Flanger::new(0.5, 2.0, 0.5, 0.5, 0.5, 44100.0).unwrap();
let json = serde_json::to_string(&flanger).unwrap();
let back: Flanger = serde_json::from_str(&json).unwrap();
assert!((flanger.mix - back.mix).abs() < f32::EPSILON);
}
#[test]
fn serde_roundtrip_phaser() {
let phaser = Phaser::new(6, 0.5, 200.0, 2000.0, 0.7, 0.5, 44100.0).unwrap();
let json = serde_json::to_string(&phaser).unwrap();
let back: Phaser = serde_json::from_str(&json).unwrap();
assert!((phaser.mix - back.mix).abs() < f32::EPSILON);
}
#[test]
fn serde_roundtrip_distortion() {
let dist = Distortion::new(DistortionType::WaveFold, 3.0, 0.8);
let json = serde_json::to_string(&dist).unwrap();
let back: Distortion = serde_json::from_str(&json).unwrap();
assert!((dist.mix - back.mix).abs() < f32::EPSILON);
}
#[test]
fn serde_roundtrip_delay_line() {
let dl = DelayLine::new(1024);
let json = serde_json::to_string(&dl).unwrap();
let _back: DelayLine = serde_json::from_str(&json).unwrap();
}
#[test]
fn serde_roundtrip_comb_filter() {
let comb = CombFilter::new(100, 0.5);
let json = serde_json::to_string(&comb).unwrap();
let back: CombFilter = serde_json::from_str(&json).unwrap();
assert!((comb.feedback - back.feedback).abs() < f32::EPSILON);
}
#[test]
fn serde_roundtrip_allpass_delay() {
let ap = AllpassDelay::new(100, 0.7);
let json = serde_json::to_string(&ap).unwrap();
let back: AllpassDelay = serde_json::from_str(&json).unwrap();
assert!((ap.coefficient() - back.coefficient()).abs() < f32::EPSILON);
}
#[test]
fn serde_roundtrip_wavetable_oscillator() {
let wt = Wavetable::from_harmonics(4, &[1.0, 0.5, 0.33, 0.25], 1024).unwrap();
let wto = WavetableOscillator::new(wt, 440.0, 44100.0).unwrap();
let json = serde_json::to_string(&wto).unwrap();
let _back: WavetableOscillator = serde_json::from_str(&json).unwrap();
}
#[test]
fn serde_roundtrip_morph_wavetable() {
let t1 = Wavetable::from_harmonics(2, &[1.0, 0.5], 512).unwrap();
let t2 = Wavetable::from_harmonics(2, &[0.5, 1.0], 512).unwrap();
let mwt = MorphWavetable::new(vec![t1, t2], 440.0, 44100.0).unwrap();
let json = serde_json::to_string(&mwt).unwrap();
let _back: MorphWavetable = serde_json::from_str(&json).unwrap();
}
#[test]
fn serde_roundtrip_noise_generator() {
let ng = NoiseGenerator::new(NoiseType::Pink, 42);
let json = serde_json::to_string(&ng).unwrap();
let _back: NoiseGenerator = serde_json::from_str(&json).unwrap();
}
#[test]
fn serde_roundtrip_lfo_sh_works_after_deser() {
let lfo = Lfo::new(LfoShape::SampleAndHold, 10.0, 44100.0).unwrap();
let json = serde_json::to_string(&lfo).unwrap();
let mut back: Lfo = serde_json::from_str(&json).unwrap();
let val = back.next_value();
assert!(val.is_finite());
let mut seen_different = false;
let first = val;
for _ in 0..50000 {
let v = back.next_value();
if (v - first).abs() > 0.01 {
seen_different = true;
break;
}
}
assert!(
seen_different,
"S&H LFO should produce varying values after deser"
);
}
#[test]
fn serde_roundtrip_unison_works_after_deser() {
use naad::oscillator::UnisonOscillator;
let uni = UnisonOscillator::new(Waveform::Saw, 440.0, 4, 10.0, 44100.0).unwrap();
let json = serde_json::to_string(&uni).unwrap();
let mut back: UnisonOscillator = serde_json::from_str(&json).unwrap();
let mut has_nonzero = false;
for _ in 0..100 {
if back.next_sample().abs() > 0.01 {
has_nonzero = true;
break;
}
}
assert!(
has_nonzero,
"UnisonOscillator should produce output after deser"
);
}