use crate::fixtures::graph_of;
use sva_engine::{
Ask, Output, Render, RenderConfig, Representation, Source, Tier, answer, periodicity, render,
};
const CHORD: &str = "sin(2*pi*256*t) + sin(2*pi*512*t)\n";
fn rendered(name: &str, body: &str, asks: Vec<&str>) -> Render {
rendered_of(name, &[("node", body)], asks)
}
fn rendered_of(name: &str, files: &[(&str, &str)], asks: Vec<&str>) -> Render {
let g = graph_of(name, files);
let asks = asks
.into_iter()
.map(|a| Ask {
node: "node".to_string(),
representation: Representation::from_name(a).expect("a known reading"),
})
.collect();
let config = RenderConfig::seconds(8_192, 1.0).asking(asks);
render(&g, "node", config, &Tier::default()).unwrap_or_else(|e| panic!("{name}: {e}"))
}
#[test]
fn every_representation_reports_source_and_profile() {
let law = rendered("law-answers", CHORD, vec!["lines"]);
let id = law.id("node").expect("the root");
for name in ["lines", "atoms", "derivative", "envelope"] {
let representation = Representation::from_name(name).expect("a known reading");
let found = answer(&law, id, representation).unwrap_or_else(|e| panic!("{name}: {e}"));
assert_eq!(found.source, Source::Exact, "{name}");
assert_eq!(found.profile, "psychoacoustic-v1", "{name}");
assert_eq!(found.rate, None, "{name}: a law answers without a rate");
}
let sampled = rendered(
"sampled-answers",
"sample(sin(2*pi*256*t))\n",
vec!["samples"],
);
let id = sampled.id("node").expect("the root");
for name in [
"samples", "spectrum", "envelope", "bands", "crest", "loudness", "onsets",
] {
let representation = Representation::from_name(name).expect("a known reading");
let found = answer(&sampled, id, representation).unwrap_or_else(|e| panic!("{name}: {e}"));
assert_eq!(found.source, Source::Measured, "{name}");
assert_eq!(found.profile, "psychoacoustic-v1", "{name}");
assert_eq!(found.rate, Some(8_192), "{name}");
}
}
#[test]
fn a_line_list_stops_at_the_profiles_ceiling() {
let g = graph_of("ceiling", &[("node", "saw(1000)\n")]);
let config = RenderConfig {
profile: sva_engine::Profile {
ceiling_hz: 10_000.0,
..sva_engine::PSYCHOACOUSTIC_V1
},
..RenderConfig::seconds(8_192, 1.0)
};
let law = render(&g, "node", config, &Tier::default()).expect("a saw");
let id = law.id("node").expect("the root");
let found = answer(&law, id, Representation::Lines).expect("an exact line list");
let Output::Lines(lines) = &found.value else {
panic!("expected lines");
};
let highest = lines.iter().map(|l| l.hz.abs()).fold(0.0f64, f64::max);
assert_eq!(
highest.round(),
9_000.0,
"the harmonics under a 10 kHz ceiling"
);
}
#[test]
fn envelope_of_samples_labels_measured() {
let law = rendered("envelope-law", CHORD, vec!["lines"]);
let id = law.id("node").expect("the root");
let symbolic = answer(&law, id, Representation::Envelope { frame_secs: None }).expect("a law");
assert_eq!(symbolic.source, Source::Exact);
assert!(matches!(symbolic.value, Output::Symbolic(_)));
let sampled = rendered(
"envelope-samples",
"sample(sin(2*pi*256*t))\n",
vec!["envelope"],
);
let id = sampled.id("node").expect("the root");
let measured =
answer(&sampled, id, Representation::Envelope { frame_secs: None }).expect("samples");
assert_eq!(measured.source, Source::Measured);
let Output::Envelope(frames) = measured.value else {
panic!("a block envelope follower answers in frames");
};
assert!(!frames.is_empty());
}
#[test]
fn a_reading_off_a_buffer_no_render_held_refuses() {
let law = rendered("unheld", CHORD, vec!["lines"]);
let id = law.id("node").expect("the root");
let refused = answer(&law, id, Representation::Samples).expect_err("nothing was collapsed");
assert_eq!(refused.code(), "engine.not_materialized");
}
#[test]
fn bindings_and_the_ledger_read_off_the_render_that_ran() {
let g = graph_of(
"attributed",
&[
("src", "sin(2*pi*256*t)\n"),
("gain", "k = 1\nsample(x*k)\n"),
("node", "@gain(t, x=@src, k=0.5)\n"),
],
);
let ask = |representation| Ask {
node: "node".to_string(),
representation,
};
let config = RenderConfig::seconds(8_192, 0.25).asking(vec![
ask(Representation::Samples),
ask(Representation::Ledger { depth: 3 }),
]);
let held = render(&g, "node", config, &Tier::default()).expect("a sampled node");
let id = held.id("node").expect("the root");
let unasked = answer(&held, id, Representation::Ledger { depth: 2 })
.expect_err("a ledger the render never tallied");
assert_eq!(unasked.code(), "engine.not_materialized");
let instance = held
.tys
.paths()
.find(|(path, _)| path.starts_with("gain("))
.map(|(_, id)| id)
.expect("the gain instance");
let Output::Bindings(bound) = answer(&held, instance, Representation::Bindings)
.expect("bindings")
.value
else {
panic!("expected the resolved parameters");
};
let k = bound.iter().find(|b| b.name == "k").expect("k");
assert_eq!(k.source, "0.5", "the call site's override, not the default");
let Output::Ledger(entries) = answer(&held, id, Representation::Ledger { depth: 3 })
.expect("a ledger")
.value
else {
panic!("expected one entry per node");
};
assert!(!entries.is_empty(), "the target is attributed at least");
assert!(entries.iter().any(|e| e.rms > 0.0), "{entries:?}");
}
#[test]
fn alias_scores_a_collapse_and_refuses_where_no_law_is_behind_it() {
let collapsed = rendered(
"aliased",
"sample(tanh(sin(2*pi*3000*t)*4))\n",
vec!["alias"],
);
let id = collapsed.id("node").expect("the root");
let found = answer(&collapsed, id, Representation::Alias { oversample: 4 }).expect("a score");
assert_eq!(found.source, Source::Measured);
let Output::Alias(score) = found.value else {
panic!("expected an alias score");
};
assert!(score.asr_db.is_finite(), "{score:?}");
let solved = rendered("no-law", "chaigne_askenfelt(261.63)\n", vec!["alias"]);
let id = solved.id("node").expect("the root");
let refused = answer(&solved, id, Representation::Alias { oversample: 4 })
.expect_err("a solver has no law behind it");
assert_eq!(refused.code(), "engine.alias_needs_a_closed_form");
}
#[test]
fn an_alias_score_asked_again_reads_its_reference_from_memory() {
let g = graph_of(
"alias-again",
&[("node", "sample(tanh(sin(2*pi*3000*t)*4))\n")],
);
let tier = Tier::default();
let config = RenderConfig::seconds(8_192, 1.0);
let held = render(&g, "node", config, &tier).expect("a render");
let id = held.id("node").expect("the root");
let asked = Representation::Alias { oversample: 4 };
let first = answer(&held, id, asked).expect("a score");
let before = tier.counters().hits;
let again = answer(&held, id, asked).expect("a score");
assert!(tier.counters().hits > before, "{:?}", tier.counters());
assert_eq!(first, again);
}
#[test]
fn an_alias_score_reads_its_reference_the_way_the_node_renders() {
let vibrato = "crop(sum(k, 1, 40, sin(2*pi*110*k*t + 0.5*k*sin(2*pi*5*t))/k), 0s, 0.5s)\n";
let held = rendered("vibrato-alias", vibrato, vec!["alias"]);
let id = held.id("node").expect("the root");
let found = answer(&held, id, Representation::Alias { oversample: 4 }).expect("a score");
let Output::Alias(score) = found.value else {
panic!("an alias reading answers a score");
};
assert!(score.asr_db.is_finite(), "{score:?}");
}
#[test]
fn an_alias_score_answers_the_component_that_aliases_worst() {
const CLEAN: &str = "0.2*sin(2*pi*100*t)";
const ALIASING: &str = "tanh(6*sin(2*pi*3000*t))";
let asked = Representation::Alias { oversample: 4 };
let scored = |name: &str, body: String| {
let held = rendered(name, &format!("{body}\n"), vec!["alias"]);
let id = held.id("node").expect("the root");
let Output::Alias(score) = answer(&held, id, asked).expect("a score").value else {
panic!("an alias reading answers a score");
};
*score
};
let alone = scored("alias-loud", ALIASING.to_string());
let quiet = scored("alias-quiet", CLEAN.to_string());
assert!(
alone.nmr_peak_db > quiet.nmr_peak_db,
"the fixture only says something if one component aliases more: {alone:?} {quiet:?}"
);
let quiet_first = scored("alias-quiet-first", format!("join({CLEAN}, {ALIASING})"));
let loud_first = scored("alias-loud-first", format!("join({ALIASING}, {CLEAN})"));
assert_eq!(
quiet_first.asr_db, loud_first.asr_db,
"the score does not turn on which component was written first"
);
assert_eq!(
quiet_first.asr_db, alone.asr_db,
"a joined pair scores as its worst half, not as the half written first"
);
assert_eq!(quiet_first.audible, alone.audible);
}
#[test]
fn a_laws_spectrum_ignores_the_peak_budget_and_refuses_a_frame_length() {
let law = rendered(
"budget",
"sin(2*pi*100*t) + sin(2*pi*200*t) + sin(2*pi*300*t)\n",
vec!["lines"],
);
let id = law.id("node").expect("the root");
let found = answer(
&law,
id,
Representation::Spectrum {
max_peaks: 2,
frame_secs: None,
},
)
.expect("an exact line list");
assert_eq!(found.source, Source::Exact);
let Output::Lines(lines) = found.value else {
panic!("expected lines");
};
assert_eq!(
lines.len(),
6,
"three lines, each a conjugate pair, budget or not"
);
let refused = answer(
&law,
id,
Representation::Spectrum {
max_peaks: 2,
frame_secs: Some(0.05),
},
)
.expect_err("a law has no frames");
assert_eq!(refused.code(), "engine.observation_needs_samples");
}
#[test]
fn a_laws_envelope_refuses_a_frame_length() {
let law = rendered("framed-envelope", "sin(2*pi*100*t)\n", vec!["lines"]);
let id = law.id("node").expect("the root");
let exact =
answer(&law, id, Representation::Envelope { frame_secs: None }).expect("an exact envelope");
assert_eq!(exact.source, Source::Exact);
let refused = answer(
&law,
id,
Representation::Envelope {
frame_secs: Some(0.01),
},
)
.expect_err("a law has no frames");
assert_eq!(refused.code(), "engine.observation_needs_samples");
assert!(refused.to_string().contains("envelope"), "{refused}");
}
#[test]
fn a_finite_sum_lists_every_line() {
let law = rendered(
"finite-sum",
"sum(k, 2, 6, sin(2*pi*100*k*t))\n",
vec!["lines"],
);
let id = law.id("node").expect("the root");
let found = answer(&law, id, Representation::Lines).expect("an exact line list");
assert_eq!(found.source, Source::Exact);
let Output::Lines(lines) = found.value else {
panic!("expected lines");
};
let mut hz: Vec<i64> = lines
.iter()
.filter(|l| l.hz > 0.0)
.map(|l| l.hz.round() as i64)
.collect();
hz.sort_unstable();
assert_eq!(hz, vec![200, 300, 400, 500, 600]);
let found = answer(&law, id, Representation::Atoms).expect("an exact atom list");
let Output::Atoms(atoms) = found.value else {
panic!("expected atoms");
};
assert_eq!(atoms.len(), 10, "one atom per line, both signs");
}
#[test]
fn a_long_comb_lists_its_kept_lines_and_names_the_dropped() {
let law = rendered(
"long-comb",
"(sin(2*pi*200*t) + sin(2*pi*400*t) + sin(2*pi*600*t)) + 0.9*self(t - 0.01s)\n",
vec!["lines"],
);
let id = law.id("node").expect("the root");
let found = answer(&law, id, Representation::Lines).expect("an exact line list");
assert_eq!(found.source, Source::Exact);
let Output::Lines(lines) = &found.value else {
panic!("expected lines");
};
let echoes = lines.len() / 6;
assert!(
echoes > 1 && lines.len() == echoes * 6,
"each echo places three partials as six conjugate lines, got {}",
lines.len()
);
let loudest = |set: &[sva_engine::Line]| set.iter().map(|l| l.amp.abs()).fold(0.0f64, f64::max);
assert!(
loudest(lines) > 0.0,
"a truncated series still answers the lines it kept"
);
assert!(
!found.dropped.is_empty(),
"the terms the floor cut are named, not left to stand in the kept list"
);
assert!(
loudest(&found.dropped) < loudest(lines),
"what was dropped sits under what was kept"
);
assert!(
found.tail_db.is_some_and(|db| db < 0.0),
"the tail is stated in dB against the loudest line taken"
);
}
#[test]
fn a_cropped_series_lists_its_terms_under_its_window() {
let bare = rendered(
"bare-comb",
"sin(2*pi*200*t) + 0.9*self(t - 0.01s)\n",
vec!["atoms"],
);
let id = bare.id("node").expect("the root");
let Output::Atoms(loose) = answer(&bare, id, Representation::Atoms)
.expect("an atom list")
.value
else {
panic!("expected atoms");
};
let cropped = rendered_of(
"cropped-comb",
&[
("comb", "sin(2*pi*200*t) + 0.9*self(t - 0.01s)\n"),
("node", "crop(@comb(t), 0s, 0.5s)\n"),
],
vec!["atoms", "lines"],
);
let id = cropped.id("node").expect("the root");
let Output::Atoms(held) = answer(&cropped, id, Representation::Atoms)
.expect("an atom list")
.value
else {
panic!("expected atoms");
};
assert_eq!(
held.len(),
loose.len(),
"a window drops no term it still contains"
);
assert!(
held.iter().all(|a| a.sketch.contains("indicator")),
"every term carries the window: {held:?}"
);
let refused = answer(&cropped, id, Representation::Lines)
.expect_err("a cropped pair places no line, so it lists none");
assert_eq!(refused.code(), "read.lines_need_unwindowed_lines");
}
#[test]
fn a_line_an_envelope_widened_is_refused_by_the_factor_that_widened_it() {
for (name, body, factor) in [
(
"gaussian-pulse",
"exp(-40*t*t)*cos(2*pi*261.6255653005986*t)\n",
"Gaussian",
),
("decayed-tone", "exp(-3*t)*sin(2*pi*440*t)\n", "decaying"),
("swept-line", "t*sin(2*pi*440*t)\n", "polynomial"),
] {
let held = rendered(name, body, vec!["atoms"]);
let id = held.id("node").expect("the root");
for representation in [
Representation::Lines,
Representation::Spectrum {
max_peaks: 8,
frame_secs: None,
},
] {
let reading = representation.name();
let refused = match answer(&held, id, representation) {
Err(refused) => refused,
Ok(found) => panic!(
"{name}: `{reading}` answered {:?} where it has no line to list",
found.value
),
};
assert_eq!(
refused.code(),
"read.lines_need_unwindowed_lines",
"{name}/{reading}"
);
let said = refused.to_string();
assert!(
said.contains(factor),
"{name}/{reading} names what widened it: {said}"
);
}
}
}
#[test]
fn lines_under_a_window_are_listed_with_their_width_or_refused_by_name() {
let windowed = rendered(
"windowed-pair",
"crop(sin(2*pi*440*t), 0s, 0.25s)\n",
vec!["atoms"],
);
let id = windowed.id("node").expect("the root");
for representation in [
Representation::Lines,
Representation::Spectrum {
max_peaks: 8,
frame_secs: None,
},
Representation::Pitch {
max_notes: 4,
frame_secs: 0.05,
},
] {
let name = representation.name();
let refused = match answer(&windowed, id, representation) {
Err(refused) => refused,
Ok(held) => panic!(
"`{name}` answered {:?} where it has no line to list",
held.value
),
};
assert_eq!(refused.code(), "read.lines_need_unwindowed_lines", "{name}");
let said = refused.to_string();
assert!(said.contains("0.25"), "`{name}` names the window: {said}");
}
let mixed = rendered_of(
"windowed-beside-bare",
&[
("bare", "sin(2*pi*220*t)\n"),
("node", "@bare(t) + crop(sin(2*pi*440*t), 0s, 0.25s)\n"),
],
vec!["atoms"],
);
let id = mixed.id("node").expect("the root");
let refused = answer(&mixed, id, Representation::Lines)
.expect_err("half of a line list reads as the whole of one");
assert_eq!(refused.code(), "read.lines_need_unwindowed_lines");
let bare = rendered("bare-pair", "sin(2*pi*440*t)\n", vec!["lines"]);
let id = bare.id("node").expect("the root");
let found = answer(&bare, id, Representation::Lines).expect("an exact line list");
assert_eq!(found.source, Source::Exact);
let Output::Lines(lines) = found.value else {
panic!("expected lines");
};
assert_eq!(
lines.len(),
2,
"the same term unwindowed is one conjugate pair"
);
}
#[test]
fn each_atom_states_the_window_it_sounds_in() {
let held = rendered_of(
"atom-windows",
&[
(
"voice",
"f0 = C4\ncrop(exp(-t/0.25s), 0s, 0.5s)*sin(2*pi*f0*t)\n",
),
(
"node",
"0.1*sin(2*pi*50*t) + crop(sin(2*pi*440*t), 0.25s, 0.75s) + \
@voice(t - 1s, f0=330hz)\n",
),
],
vec!["atoms"],
);
let id = held.id("node").expect("the root");
let Output::Atoms(atoms) = answer(&held, id, Representation::Atoms)
.expect("an atom list")
.value
else {
panic!("expected atoms");
};
let at = |hz: f64| {
let found: Vec<_> = atoms
.iter()
.filter(|a| (a.turns.abs() - hz).abs() <= hz * f64::EPSILON)
.collect();
assert_eq!(found.len(), 2, "{hz} Hz is one conjugate pair: {atoms:?}");
found
};
for a in at(50.0) {
assert_eq!((a.start, a.end), (None, None), "nothing crops it: {a:?}");
assert_eq!((a.decay, a.degree), (0.0, 0), "a held line: {a:?}");
}
for a in at(440.0) {
assert_eq!((a.start, a.end), (Some(0.25), Some(0.75)), "{a:?}");
}
for a in at(330.0) {
assert_eq!((a.start, a.end), (Some(1.0), Some(1.5)), "{a:?}");
assert_eq!((a.decay, a.decay_from), (4.0, 1.0), "{a:?}");
assert!(a.sketch.contains("indicator"), "{a:?}");
}
}
#[test]
fn a_cropped_delta_train_keeps_only_the_instants_its_window_holds() {
let counted = |body: &str| {
let held = rendered("cropped-train", body, vec!["atoms"]);
let id = held.id("node").expect("the root");
let Output::Atoms(atoms) = answer(&held, id, Representation::Atoms)
.expect("an atom list")
.value
else {
panic!("expected atoms");
};
atoms.len()
};
let loose = counted("sum(k, 0, 9, delta(t - k*0.1s))\n");
assert_eq!(loose, 10, "ten strikes, ten deltas");
assert_eq!(
counted("crop(sum(k, 0, 9, delta(t - k*0.1s)), 0s, 0.25s)\n"),
3,
"the half-open window holds the strikes at 0, 0.1 and 0.2 alone"
);
}
#[test]
fn one_table_says_what_every_named_reading_consumes() {
use sva_engine::Representation;
use sva_samples::Consumes;
let always_closed = ["lines", "atoms", "bindings", "arguments"];
let turns_on_the_type = [
"spectrum",
"envelope",
"derivative",
"pitch",
"dissonance",
"voice_leading",
"tonal_distance",
"inharmonicity",
];
let names = [
"lines",
"atoms",
"spectrum",
"envelope",
"derivative",
"samples",
"ledger",
"pitch",
"formants",
"stereo",
"bands",
"crest",
"loudness",
"onsets",
"alias",
"dissonance",
"voice_leading",
"tonal_distance",
"inharmonicity",
"periodicity",
"modulation",
"self_similarity",
"bindings",
"arguments",
];
for name in names {
let held =
Representation::from_name(name).unwrap_or_else(|| panic!("`{name}` is a reading"));
assert_eq!(held.name(), name, "one name, one reading");
let closed = match always_closed.contains(&name) || turns_on_the_type.contains(&name) {
true => Consumes::ClosedForm,
false => Consumes::Buffer,
};
assert_eq!(held.consumes(true), closed, "{name} off a closed form");
let sampled = match always_closed.contains(&name) {
true => Consumes::ClosedForm,
false => Consumes::Buffer,
};
assert_eq!(held.consumes(false), sampled, "{name} off a sampled node");
}
let listed: Vec<&str> = sva_engine::READINGS.iter().map(|row| row.name).collect();
assert_eq!(
listed, names,
"the table lists exactly these, in this order"
);
}
#[test]
fn every_row_builds_its_own_reading_at_the_shared_defaults() {
use sva_engine::{DEFAULT_FRAME_SECS, DEFAULT_MAX_PEAKS, Given, READINGS};
for row in READINGS {
let built = row.representation(&Given::default());
assert!(
std::ptr::eq(built.reading(), row),
"{} builds {}, another row's variant",
row.name,
built.name()
);
}
assert_eq!(
Representation::from_name("pitch"),
Some(Representation::Pitch {
max_notes: DEFAULT_MAX_PEAKS,
frame_secs: DEFAULT_FRAME_SECS,
})
);
assert_eq!(
Representation::from_name("formants"),
Some(Representation::Formants {
max_formants: DEFAULT_MAX_PEAKS,
frame_secs: DEFAULT_FRAME_SECS,
})
);
}
#[test]
fn samples_alone_answer_what_reads_neither_a_graph_nor_the_written_math() {
use sva_engine::Reads;
for reads in [
Reads::ClosedForm,
Reads::Either,
Reads::Samples,
Reads::Graph,
Reads::Structure,
] {
let off_a_file = matches!(reads, Reads::Samples | Reads::Either);
assert_eq!(reads.off_samples(true), off_a_file, "{reads:?} on samples");
assert_eq!(
reads.off_samples(false),
reads == Reads::Samples,
"{reads:?} on a closed form"
);
}
}
#[test]
fn a_refusal_off_a_twins_sum_names_the_twin_asked() {
let g = graph_of(
"twins-located",
&[
("a", "exp(-3*t)*sin(2*pi*440*t)\n"),
("b", "exp(-3 * t) * sin(2 * pi * 440 * t)\n"),
("node", "@a(t) + @b(t)\n"),
],
);
let held = render(
&g,
"node",
RenderConfig::seconds(8_192, 1.0),
&Tier::default(),
)
.expect("a render");
for twin in ["a", "b"] {
let id = held.id(twin).expect("a twin");
let refused = answer(&held, id, Representation::Lines).expect_err("a decayed line");
let sva_engine::EngineError::Refused(d) = &refused else {
panic!("{twin}: a located refusal, not {refused:?}");
};
assert_eq!(d.location.node, twin, "{twin}: {refused}");
}
}
fn periodicity_of(name: &str, secs: f64) -> (Render, Representation) {
let asked = Representation::Periodicity {
frame_secs: 0.5,
window_secs: 4.0,
curve: false,
};
let g = graph_of(
name,
&[("node", "sin(2*pi*1000*t)*(0.5 + 0.5*cos(2*pi*2*t))\n")],
);
let config = RenderConfig::seconds(8_192, secs).asking(vec![Ask {
node: "node".to_string(),
representation: asked,
}]);
let law = render(&g, "node", config, &Tier::default()).unwrap_or_else(|e| panic!("{e}"));
(law, asked)
}
#[test]
fn a_closed_forms_periodicity_is_measured_off_its_samples() {
let (law, asked) = periodicity_of("periodic-swell", 8.0);
let found = answer(&law, law.id("node").expect("the root"), asked).expect("8 s holds 4 s");
assert_eq!((found.source, found.rate), (Source::Measured, Some(8_192)));
let Output::Periodicity(p) = found.value else {
panic!("periodicity answers its windows, not {:?}", found.value)
};
for track in p.tracks.iter().filter(|t| t.group.key() != "low") {
let whole = |w: &&periodicity::Window| {
w.from_secs >= 0.5 && w.to_secs - w.from_secs == p.window_secs
};
for w in track.windows.iter().filter(whole) {
let (lo, hi) = w.peaks[0].bpm_bounds.expect("a whole window");
assert!(lo <= 120.0 && 120.0 <= hi, "{}: {w:?}", track.group.key());
}
}
}
#[test]
fn periodicity_of_a_target_under_its_window_names_the_remedies_that_read() {
let (law, asked) = periodicity_of("periodic-short", 0.1);
let refused = answer(&law, law.id("node").expect("the root"), asked).expect_err("0.1 s < 4 s");
assert_eq!(refused.code(), "read.target_under_window");
let said = format!("{refused}");
assert!(said.contains("([0, 4s])"), "{said}");
assert!(!said.contains("periodicity(window="), "{said}");
assert!(said.contains("no window under 0.2s holds"), "{said}");
let (law, asked) = periodicity_of("periodic-one", 1.0);
let refused = answer(&law, law.id("node").expect("the root"), asked).expect_err("1 s < 4 s");
let said = format!("{refused}");
assert!(said.contains("([0, 4s])"), "{said}");
assert!(said.contains("periodicity(window=1s)"), "{said}");
}
fn modulation_of(name: &str, window_secs: f64) -> (Render, Representation) {
let asked = Representation::Modulation {
frame_secs: 0.5,
window_secs,
bpm: Some(120.0),
};
let g = graph_of(
name,
&[("node", "sin(2*pi*1000*t)*(1 + 0.5*sin(2*pi*4*t))\n")],
);
let config = RenderConfig::seconds(8_192, 8.0).asking(vec![Ask {
node: "node".to_string(),
representation: asked,
}]);
let law = render(&g, "node", config, &Tier::default()).unwrap_or_else(|e| panic!("{e}"));
(law, asked)
}
#[test]
fn a_closed_forms_modulation_is_measured_off_its_samples() {
let (law, asked) = modulation_of("tremolo", 4.0);
let found = answer(&law, law.id("node").expect("the root"), asked).expect("8 s holds 4 s");
assert_eq!((found.source, found.rate), (Source::Measured, Some(8_192)));
let Output::Modulation(m) = found.value else {
panic!("modulation answers its windows, not {:?}", found.value)
};
let mid = m
.tracks
.iter()
.find(|t| t.group.key() == "mid")
.expect("a mid group");
for w in mid.windows.iter().filter(|w| w.from_secs >= 0.5) {
let lead = w.peaks[0];
assert!((lead.hz - 4.0).abs() < 1e-3, "{w:?}");
assert!((lead.depth - 0.5).abs() < 1e-3, "{w:?}");
assert!(
(lead.period_beats.expect("a bpm") - 0.5).abs() < 1e-3,
"{w:?}"
);
}
}
#[test]
fn modulation_under_two_periods_names_the_least_window() {
let (law, asked) = modulation_of("tremolo-narrow", 0.05);
let refused =
answer(&law, law.id("node").expect("the root"), asked).expect_err("0.05 s < 0.1 s");
assert_eq!(refused.code(), "read.window_under_two_periods");
let said = format!("{refused}");
assert!(said.contains("modulation(window=0.1s)"), "{said}");
}
fn self_similarity_of(name: &str, frame_secs: f64, cell_secs: f64) -> (Render, Representation) {
let asked = Representation::SelfSimilarity {
frame_secs,
cell_secs,
smooth_secs: 2.0,
window_secs: 4.0,
floor_db: -40.0,
listed: 16,
curve: false,
};
let g = graph_of(
name,
&[(
"node",
"sin(2*pi*262*t)*(0.5 + 0.5*cos(2*pi*0.25*t)) + \
sin(2*pi*392*t)*(0.5 - 0.5*cos(2*pi*0.25*t))\n",
)],
);
let config = RenderConfig::seconds(8_192, 16.0).asking(vec![Ask {
node: "node".to_string(),
representation: asked,
}]);
let law = render(&g, "node", config, &Tier::default()).unwrap_or_else(|e| panic!("{e}"));
(law, asked)
}
#[test]
fn a_closed_forms_self_similarity_is_measured_off_its_samples() {
let (law, asked) = self_similarity_of("crossfade", 0.05, 0.5);
let found = answer(&law, law.id("node").expect("the root"), asked).expect("32 cells");
assert_eq!((found.source, found.rate), (Source::Measured, Some(8_192)));
let Output::SelfSimilarity(s) = found.value else {
panic!("self_similarity answers its views, not {:?}", found.value)
};
let lead = s.views[0].stripes[0];
assert_eq!(lead.shift_semitones, 0, "{lead:?}");
assert!((lead.lag_secs - 4.0).abs() <= s.hop_secs, "{lead:?}");
assert!(lead.similarity_mean > 0.99, "{lead:?}");
}
#[test]
fn self_similarity_past_the_cap_names_both_remedies() {
let (law, asked) = self_similarity_of("crossfade-fine", 0.01, 0.01);
let refused =
answer(&law, law.id("node").expect("the root"), asked).expect_err("1600 > 1024 cells");
assert_eq!(refused.code(), "read.too_many_cells");
let said = format!("{refused}");
assert!(said.contains("self_similarity(cell=0.02"), "{said}");
assert!(said.contains("@x([0, 10.25s])"), "{said}");
}
#[test]
fn each_unreadable_ask_refuses_under_its_own_code_naming_what_reads() {
let self_similarity =
|frame_secs: f64, cell_secs: f64, window_secs: f64| Representation::SelfSimilarity {
frame_secs,
cell_secs,
smooth_secs: 2.0,
window_secs,
floor_db: -40.0,
listed: 16,
curve: false,
};
let tone = "sin(2*pi*100*t)\n";
let slow = "sin(2*pi*3*t)\n";
for (body, rate, secs, asked, code, says) in [
(
"sample(tanh(sin(2*pi*3000*t)*4))\n",
8_192,
0.1,
Representation::Alias { oversample: 4 },
"read.target_under_frame",
"@x([0, 0.125s])",
),
(
tone,
2_000,
1.0,
Representation::Loudness,
"read.rate_under_weighting",
"over 3363.94 Hz",
),
(
slow,
30,
10.0,
self_similarity(0.5, 0.5, 4.0),
"read.rate_under_bands",
"over 57.77 Hz",
),
(
slow,
30,
10.0,
Representation::Periodicity {
frame_secs: 0.5,
window_secs: 4.0,
curve: false,
},
"read.rate_under_bands",
"over 57.77 Hz",
),
(
slow,
30,
10.0,
Representation::Modulation {
frame_secs: 0.5,
window_secs: 4.0,
bpm: None,
},
"read.rate_under_bands",
"over 57.77 Hz",
),
(
tone,
8_192,
1.0,
self_similarity(0.05, 0.5, 1e10),
"read.kernel_past_cap",
"self_similarity(window=1025s)",
),
(
tone,
16_000,
0.05,
self_similarity(2.0 / 16_000.0, 2.0 / 16_000.0, 0.01),
"read.cell_under_band_step",
"self_similarity(cell=0.0005s)",
),
(
tone,
16_000,
0.05,
Representation::Formants {
max_formants: 4,
frame_secs: 0.0005,
},
"read.frame_under_order",
"formants(frame=0.0012s)",
),
(
tone,
16_000,
0.001,
Representation::Formants {
max_formants: 4,
frame_secs: 0.05,
},
"read.target_under_order",
"@x([0, 0.0012s])",
),
(
"join(sin(t), sin(2*t), sin(3*t))\n",
8_192,
0.1,
Representation::Stereo { frame_secs: 0.05 },
"type.width_mismatch",
"needs exactly 2 components and the node holds 3",
),
] {
let g = graph_of("unreadable", &[("node", body)]);
let config = RenderConfig::seconds(rate, secs);
let held = render(&g, "node", config, &Tier::default()).unwrap_or_else(|e| panic!("{e}"));
let refused = answer(&held, held.id("node").expect("the root"), asked)
.expect_err("an ask no reading answers");
let said = format!("{refused}");
assert_eq!(refused.code(), code, "{said}");
assert!(said.contains(says), "{says}: {said}");
}
}