use crate::fixtures::graph_of;
use sva_ast::Graph;
use sva_engine::{
Ask, Extent, Output, Range, Render, RenderConfig, Representation, Tier, answer, flops, render,
};
const RATE: u32 = 8_000;
fn notes() -> Graph {
graph_of(
"pruning",
&[
("note", "crop(sin(2*pi*440*t)*exp(-t/0.2), 0s, 0.5s)\n"),
("song", "@note(t) + @note(t - 2s) + @note(t - 4s)\n"),
],
)
}
fn sampled_notes() -> Graph {
graph_of(
"pruning-sampled",
&[
(
"note",
"crop(lowpass(sample(0.5*sin(2*pi*440*t)), cutoff=2000, q=0.7), 0s, 0.5s)\n",
),
("song", "@note(t) + @note(t - 2s) + @note(t - 4s)\n"),
],
)
}
fn over(g: &Graph, target: &str, secs: f64, cache: &Tier) -> Render {
render(g, target, config(secs), cache).unwrap_or_else(|e| panic!("{target}: {e}"))
}
fn config(secs: f64) -> RenderConfig {
RenderConfig {
range: Range {
start: Some(0),
end: Some((secs * f64::from(RATE)) as i64),
},
..RenderConfig::at(RATE)
}
}
#[test]
fn a_note_read_three_times_is_computed_once_and_added() {
let g = notes();
let cache = Tier::default();
let held = over(&g, "song", 6.0, &cache);
let note = held.id("note").expect("the note");
assert_eq!(
held.evaluated(note),
vec![Extent::new(0, i64::from(RATE / 2))]
);
let alone = over(&g, "note", 6.0, &Tier::default());
let own = alone
.output(alone.root)
.expect("the note")
.plane(0)
.to_vec();
let at = |n: i64| {
usize::try_from(n)
.ok()
.and_then(|n| own.get(n))
.copied()
.unwrap_or(0.0)
};
let samples = held.output(held.root).expect("the song").plane(0).to_vec();
let gap = 2 * i64::from(RATE);
for (n, sample) in samples.iter().enumerate() {
let n = n as i64;
let added = 0.0 + at(n) + at(n - gap) + at(n - 2 * gap);
assert_eq!(sample.to_bits(), added.to_bits(), "sample {n}");
}
let further = over(&g, "song", 8.0, &cache);
let stats = further.cache_stats.as_ref().expect("stats");
assert_eq!(stats.hits(), stats.lookups.len(), "{stats:?}");
let reread = further.output(further.root).expect("the song").plane(0)[..samples.len()].to_vec();
assert_eq!(reread, samples);
}
#[test]
fn a_sampled_sum_reads_each_operand_only_where_it_is_nonzero() {
let g = sampled_notes();
let held = over(&g, "song", 4.5, &Tier::default());
let song = held.output(held.root).expect("the song").plane(0).to_vec();
let alone = over(&g, "note", 4.5, &Tier::default());
let note = alone
.output(alone.root)
.expect("the note")
.plane(0)
.to_vec();
let at = |n: i64| {
usize::try_from(n)
.ok()
.and_then(|n| note.get(n))
.copied()
.unwrap_or(0.0)
};
let gap = i64::from(RATE) * 2;
for (n, sample) in song.iter().enumerate() {
let n = n as i64;
let added = 0.0 + at(n) + at(n - gap) + at(n - 2 * gap);
assert_eq!(sample.to_bits(), added.to_bits(), "sample {n}");
}
let note = u128::from(RATE / 2);
let silent = song.len() as u128 - 3 * note;
assert_eq!(flops::tree(&held).rows[0].own, 3 * 2 * note + silent);
}
#[test]
fn a_long_sum_of_index_reads_shares_one_value_and_pays_each_term_only_where_it_sounds() {
let terms = 12;
let song: Vec<String> = (0..terms)
.map(|k| format!("@note[idx(t - {}s)]", 2 * k))
.collect();
let g = graph_of(
"pruning-indexed",
&[
("note", "crop(sin(2*pi*440*t)*exp(-t/0.2), 0s, 0.5s)\n"),
("song", &format!("{}\n", song.join(" + "))),
],
);
let held = over(&g, "song", 2.0 * (terms - 1) as f64 + 0.5, &Tier::default());
let note = held.id("note").expect("the note");
assert_eq!(
held.evaluated(note),
vec![Extent::new(0, i64::from(RATE / 2))]
);
let stats = held.cache_stats.as_ref().expect("stats");
let reads = stats
.lookups
.iter()
.filter(|l| l.node == "note" && l.store.is_none());
let reads = reads.count();
assert_eq!(
reads, terms,
"each read looks up the note's one value: {stats:?}"
);
assert_eq!(stats.hits(), terms - 1, "{stats:?}");
let len = held.output(held.root).expect("the song").plane(0).len() as u128;
let note = u128::from(RATE / 2);
let sounding = terms as u128 * note;
assert_eq!(
flops::tree(&held).rows[0].own,
2 * sounding + (len - sounding)
);
}
#[test]
fn a_scaled_sampled_term_is_read_only_where_it_is_nonzero() {
let g = sampled_notes();
let scaled = graph_of(
"pruning-scaled",
&[
(
"note",
"crop(lowpass(sample(0.5*sin(2*pi*440*t)), cutoff=2000, q=0.7), 0s, 0.5s)\n",
),
(
"song",
"@note(t)*0.5 + @note(t - 2s)*-0.75 + @note(t - 4s)*0.25\n",
),
],
);
let held = over(&scaled, "song", 4.5, &Tier::default());
let song = held.output(held.root).expect("the song").plane(0).to_vec();
let alone = over(&g, "note", 4.5, &Tier::default());
let note = alone
.output(alone.root)
.expect("the note")
.plane(0)
.to_vec();
let at = |n: i64| {
usize::try_from(n)
.ok()
.and_then(|n| note.get(n))
.copied()
.unwrap_or(0.0)
};
let gap = i64::from(RATE) * 2;
for (n, sample) in song.iter().enumerate() {
let n = n as i64;
let added = 0.0 + at(n) * 0.5 + at(n - gap) * -0.75 + at(n - 2 * gap) * 0.25;
assert_eq!(sample.to_bits(), added.to_bits(), "sample {n}");
}
assert_eq!(
flops::tree(&held).rows[0].own,
flops::tree(&over(&g, "song", 4.5, &Tier::default())).rows[0].own,
"a note scaled by constants is priced only while it sounds, as an unscaled one is"
);
}
#[test]
fn a_count_prices_the_render_it_names() {
let g = sampled_notes();
let held = over(&g, "song", 4.5, &Tier::default());
let asked = config(4.5).asking(vec![Ask {
node: "song".to_string(),
representation: Representation::Flops,
}]);
let counted = render(&g, "song", asked, &Tier::default()).expect("a count");
let Output::Flops(tree) = answer(&counted, counted.root, Representation::Flops)
.expect("a count")
.value
else {
panic!("a count");
};
assert_eq!(tree.total, held.work().priced_flops);
}
#[test]
fn a_ramp_and_a_decay_end_where_they_are_exactly_zero() {
let g = graph_of(
"pruning-decay",
&[
("decay", "sin(2*pi*440*t)*exp(-t/0.05)*exp(-t/0.1)\n"),
("ramp", "sample(max(0, 1 - t/2))\n"),
],
);
let exact = RenderConfig {
profile: sva_engine::Profile {
prune_db: f64::NEG_INFINITY,
..sva_engine::PSYCHOACOUSTIC_V1
},
..RenderConfig::at(RATE)
};
let open = |target: &str| {
render(&g, target, exact.clone(), &Tier::default()).unwrap_or_else(|e| panic!("{e}"))
};
let ramp = open("ramp");
assert_eq!(ramp.range.expect("a range").end, 2 * i64::from(RATE));
let decay = open("decay");
let end = decay.range.expect("a range").end;
let sum = &sva_engine::spectral_sum_of(&decay.tys, decay.root, sva_engine::Var::T)
.expect("a decay's sum");
let rate = f64::from(RATE);
let at = |n: i64| sva_samples::eval_spectral_sum_at(sum, 0, n as f64 / rate).expect("a value");
let last = (0..end).rev().find(|n| !at(*n).is_zero()).expect("a sound");
assert!(end - last < i64::from(RATE), "{last} {end}");
assert!((end..end + i64::from(RATE)).all(|n| at(n).is_zero()));
}
fn cut(r: &Render, node: &str) -> Option<i64> {
let pruned = r.labels[&r.root].pruned.clone().expect("a stated level");
pruned
.cuts
.iter()
.find(|(n, _)| n == node)
.map(|(_, at)| *at)
}
#[test]
fn a_chain_under_a_window_computes_each_level_only_where_the_window_and_its_cuts_ask() {
let depth = 20;
let mut files = vec![(
"c0".to_string(),
"sample(crop(sin(2*pi*440*t)*exp(-t/0.1), 0s, 10s))\n".to_string(),
)];
for k in 1..=depth {
files.push((format!("c{k}"), format!("0.9*@c{}(t - 10ms)\n", k - 1)));
}
let held: Vec<(&str, &str)> = files
.iter()
.map(|(n, b)| (n.as_str(), b.as_str()))
.collect();
let g = graph_of("pruning-window", &held);
let (start, end) = (i64::from(RATE / 2), i64::from(RATE) * 2);
let config = RenderConfig {
range: Range {
start: Some(start),
end: Some(end),
},
..RenderConfig::at(RATE)
};
let top = format!("c{depth}");
let r = render(&g, &top, config, &Tier::default()).unwrap_or_else(|e| panic!("{e}"));
let late = i64::from(RATE / 100);
let back = |k: usize| (depth - k) as i64 * late;
let mut to = end;
for k in (0..=depth).rev() {
let name = format!("c{k}");
let cut = cut(&r, &name).expect("each level decays under the level");
to = to.min(cut + back(k));
let asked = Extent::new((start - back(k)).max(k as i64 * late), to - back(k));
let computed = r.evaluated(r.id(&name).expect("a level"));
match k {
0 => assert!(
computed.iter().all(|e| e.intersect(asked) == *e),
"{name} computed {computed:?} outside {asked:?}"
),
_ => assert_eq!(computed, vec![asked], "{name}"),
}
}
}