mod fixtures;
use fixtures::graph_of;
use sva_engine::{RenderConfig, render};
#[test]
fn an_fd_node_renders_at_the_observation_rate() {
let g = graph_of("fd", &[("body", "chaigne_askenfelt(261.63)\n")]);
for rate in [22_050u32, 44_100] {
let held = render(&g, "body", RenderConfig::seconds(rate, 0.02), None)
.unwrap_or_else(|e| panic!("{rate}: {e}"));
let root = held.id("body").expect("the root");
let buffer = held.buffer(root).expect("a rendered solver");
assert_eq!(buffer.rate, rate);
assert_eq!(buffer.len(), (rate as f64 * 0.02).round() as usize);
assert!(
buffer.plane(0).iter().any(|s| s.abs() > 0.0),
"{rate}: the string sounded"
);
}
}
#[test]
fn a_one_step_accumulator_renders() {
let g = graph_of(
"accumulator",
&[("acc", "sample(0.25 + 0*t) + self(t - 1sp)\n")],
);
let held = render(&g, "acc", RenderConfig::seconds(1_000, 0.01), None).expect("a recurrence");
let root = held.id("acc").expect("the root");
let buffer = held.buffer(root).expect("a rendered loop");
assert_eq!(buffer.len(), 10);
for (i, held) in buffer.plane(0).iter().enumerate() {
let want = 0.25 * (i + 1) as f64;
assert!(
(held - want).abs() < 1e-12,
"sample {i}: {held} against {want}"
);
}
}
#[test]
fn a_filter_over_samples_runs_on_the_grid() {
let g = graph_of(
"biquad",
&[("voice", "lowpass(sample(sin(2*pi*4000*t)), 200, 0.7)\n")],
);
let held = render(&g, "voice", RenderConfig::seconds(44_100, 0.05), None).expect("a biquad");
let root = held.id("voice").expect("the root");
let buffer = held.buffer(root).expect("a rendered filter");
let tail: f64 = buffer.plane(0)[2_000..]
.iter()
.map(|s| s * s)
.sum::<f64>()
.sqrt();
assert!(tail < 0.2, "4 kHz under a 200 Hz lowpass is gone: {tail}");
}
#[test]
fn a_one_pole_smoother_written_with_sp_renders_at_two_rates() {
let g = graph_of(
"smoother",
&[(
"smooth",
"1 - exp(0 - 1sp/0.01) + self(t - 1sp)*exp(0 - 1sp/0.01)\n",
)],
);
let level = |rate: u32| -> f64 {
let held = render(&g, "smooth", RenderConfig::seconds(rate, 0.02), None)
.unwrap_or_else(|e| panic!("{rate}: {e}"));
let root = held.id("smooth").expect("the root");
held.buffer(root)
.expect("a rendered loop")
.at(0, rate as usize / 100)
};
let settled = 1.0 - (-1.0f64).exp();
let (low, high) = (level(44_100), level(96_000));
for (rate, held) in [(44_100, low), (96_000, high)] {
let apart = 20.0 * (held / settled).log10();
assert!(
apart.abs() < 0.5,
"{rate}: one time constant reads {held}, not {settled}"
);
}
let apart = 20.0 * (low / high).log10();
assert!(
apart.abs() < 0.5,
"the two rates state one time constant: {low} against {high}"
);
}
#[test]
fn a_swept_cutoff_on_samples_renders() {
let g = graph_of(
"swept",
&[(
"filtered",
"lowpass(sample(sin(2*pi*1000*t)), cutoff=150 + 7800*t, q=0.707)\n",
)],
);
let held =
render(&g, "filtered", RenderConfig::seconds(44_100, 0.5), None).expect("a swept filter");
let root = held.id("filtered").expect("the root");
let buffer = held.buffer(root).expect("a rendered recurrence");
let peak = |from: f64, to: f64| {
let span = (from * 44_100.0) as usize..(to * 44_100.0) as usize;
span.map(|i| buffer.at(0, i).abs()).fold(0.0f64, f64::max)
};
let (stopped, passed) = (peak(0.006, 0.03), peak(0.45, 0.5));
assert!(
20.0 * (stopped / passed).log10() < -12.0,
"the corner below the tone stops it: {stopped} against {passed}"
);
assert!(
(20.0 * passed.log10()).abs() < 0.5,
"the corner above the tone passes it: {passed}"
);
}
#[test]
fn a_self_read_is_founded_sample_by_sample_without_a_block() {
let g = graph_of(
"three-step",
&[("acc", "sample(1 + 0*t) + 0.5*self(t - 3sp)\n")],
);
let held = render(&g, "acc", RenderConfig::seconds(1_000, 0.01), None).expect("a recurrence");
let root = held.id("acc").expect("the root");
let buffer = held.buffer(root).expect("a rendered loop");
assert_eq!(buffer.len(), 10);
let mut want = [0.0f64; 10];
for i in 0..10 {
want[i] = 1.0 + 0.5 * if i >= 3 { want[i - 3] } else { 0.0 };
}
for (i, held) in buffer.plane(0).iter().enumerate() {
assert!(
(held - want[i]).abs() < 1e-12,
"sample {i}: {held} against {}",
want[i]
);
}
assert_eq!(buffer.at(0, 2), 1.0, "before the first founded read");
assert_eq!(buffer.at(0, 3), 1.5, "the first founded read");
}