mod fixtures;
use fixtures::graph_of;
use sva_engine::{EngineError, Held, RenderConfig, Value, Var, render, types};
use sva_formula::Body;
fn form(name: &str, body: &str) -> (Held, bool) {
let g = graph_of(name, &[("body", body)]);
let typing = types(&g, "body").expect("a composition that types");
let id = typing.id("body").expect("the root");
(typing.ty(id).held, typing.ty(id).dual)
}
fn peak(name: &str, body: &str) -> f64 {
let g = graph_of(name, &[("body", body)]);
let held = render(&g, "body", RenderConfig::seconds(44_100, 0.05), None).expect("a render");
let root = held.id("body").expect("the root");
held.buffer(root)
.expect("a rendered law")
.plane(0)
.iter()
.fold(0f64, |so_far, s| so_far.max(s.abs()))
}
fn drawn(name: &str, body: &str) -> f64 {
let g = graph_of(name, &[("body", body)]);
let typing = types(&g, "body").expect("a keyed hash types");
let id = typing.id("body").expect("the root");
let Value::ClosedForm(form) = typing.value(id) else {
panic!("a keyed hash of a constant key is a law");
};
match &form.body {
Body::Const(c) => c.re,
other => panic!("a constant seed draws one number, not {other:?}"),
}
}
#[test]
fn a_modulo_inside_a_constant_argument_folds() {
let written = "bandpass(sin(2*pi*300*t), cutoff=1450 + 130*(5 % 4), q=1.15)\n";
assert_eq!(
form("modulo-cutoff", written),
(Held::Form(Var::T), true),
"every argument is constant, so the filter keeps its dual"
);
let folded = peak("modulo-peak", written);
let spelled = peak(
"literal-peak",
"bandpass(sin(2*pi*300*t), cutoff=1580, q=1.15)\n",
);
assert!(
(folded - spelled).abs() < 1e-12,
"1450 + 130*(5 % 4) is 1580: {folded} against {spelled}"
);
}
#[test]
fn a_modulo_inside_a_seed_is_not_dropped() {
let written = drawn("modulo-seed", "rand(seed=311 % 256)\n");
assert_eq!(
written,
drawn("literal-seed", "rand(seed=55)\n"),
"311 % 256 is 55"
);
assert_ne!(
written,
drawn("fallback-seed", "rand(seed=0)\n"),
"the seed a `%` spells is not the seed a missing argument falls back to"
);
}
#[test]
fn a_called_operator_inside_a_constant_argument_folds() {
let folded = peak(
"called-peak",
"bandpass(sin(2*pi*300*t), cutoff=pow(2, 3)*max(190, 12) + abs(-60), q=min(1.15, 4))\n",
);
let spelled = peak(
"called-literal",
"bandpass(sin(2*pi*300*t), cutoff=1580, q=1.15)\n",
);
assert!(
(folded - spelled).abs() < 1e-12,
"pow(2, 3)*max(190, 12) + abs(-60) is 1580: {folded} against {spelled}"
);
}
#[test]
fn a_constant_that_names_no_number_refuses() {
for written in [
"sin(2*pi*300*t) * (5 % 0)\n",
"sin(2*pi*300*t) * (1/0)\n",
"sin(2*pi*300*t) * log(0)\n",
"sin(2*pi*300*t) * exp(710)\n",
] {
let g = graph_of("no-number", &[("body", written)]);
let Err(refused) = render(&g, "body", RenderConfig::seconds(44_100, 0.01), None) else {
panic!("{written} names no number and renders nothing");
};
let EngineError::Refused(d) = &refused else {
panic!("{written} is refused in writing, not {refused:?}");
};
assert!(
d.message.contains("no number") || d.message.contains("no value"),
"{written}: {}",
d.message
);
}
}
#[test]
fn an_argument_that_names_no_number_refuses() {
let g = graph_of(
"no-number-argument",
&[(
"body",
"bandpass(sin(2*pi*300*t), cutoff=1450 + log(0), q=1.15)\n",
)],
);
types(&g, "body").expect_err("a cutoff that names no number is no cutoff");
}
#[test]
fn a_crop_bound_may_be_a_constant_expression() {
let g = graph_of(
"crop-bound",
&[
("variables/bar", "0.5\n"),
("tone", "sin(2*pi*440*t)\n"),
("written", "crop(@tone(t), 0s, 1.6s - 0.1s)\n"),
("read", "crop(@tone(t), 0s, @variables/bar*3)\n"),
],
);
let ends = |node: &str| {
let held = render(&g, node, RenderConfig::seconds(8_192, 2.0), None)
.unwrap_or_else(|e| panic!("{node}: {e}"));
let id = held.id(node).unwrap_or_else(|| panic!("{node} typed"));
let plane = held.buffer(id).expect("a rendered law").plane(0).to_vec();
let last = plane.iter().rposition(|s| *s != 0.0).expect("some sound");
(last + 1) as f64 / 8_192.0
};
assert!(
(ends("written") - 1.5).abs() < 1e-3,
"1.6s - 0.1s is 1.5s, got {}",
ends("written")
);
assert!(
(ends("read") - 1.5).abs() < 1e-3,
"a bar of 0.5s three times over is 1.5s, got {}",
ends("read")
);
}
#[test]
fn a_filter_cutoff_may_be_a_scalar_variable() {
let g = graph_of(
"filter-cutoff-ref",
&[
("variables/key", "440\n"),
("tone", "sin(2*pi*220*t)\n"),
("written", "bandpass(@tone(t), cutoff=440, q=1)\n"),
("read", "bandpass(@tone(t), cutoff=@variables/key, q=1)\n"),
],
);
let plane = |node: &str| {
let held = render(&g, node, RenderConfig::seconds(8_192, 0.05), None)
.unwrap_or_else(|e| panic!("{node}: {e}"));
let id = held.id(node).unwrap_or_else(|| panic!("{node} typed"));
held.buffer(id).expect("a rendered law").plane(0).to_vec()
};
assert_eq!(
plane("read"),
plane("written"),
"a scalar ref in a filter argument is the number it names"
);
}