use std::cell::RefCell;
use crate::fixtures::{Now, added, graph_of};
use sva_ast::Graph;
use sva_engine::{
Change, Changed, EngineError, Placed, Range, RenderConfig, Stream, StreamConfig, Tier, change,
render,
};
const RATE: u32 = 8_000;
const BLOCK: usize = 256;
fn composition() -> Graph {
graph_of(
"skip",
&[
("held", "0.3*sin(2*pi*220*t)\n"),
("slow", "sin(2*pi*330*t)*exp(0 - 0.5*t)\n"),
(
"sawed",
"lowpass(sample(0.3*saw(220*t)), cutoff=900, q=0.8)\n",
),
("blip", "crop(sin(2*pi*440*t)*exp(0 - 8*t), 0s, 0.25s)\n"),
],
)
}
fn expr(text: &str) -> sva_ast::Expr {
sva_ast::parse_expr(text).unwrap_or_else(|e| panic!("`{text}`: {}", e.message))
}
fn opened(g: &Graph, target: &str, live: bool) -> RefCell<Stream> {
let config = StreamConfig {
block: BLOCK,
channels: None,
render: RenderConfig {
range: Range {
start: Some(0),
end: Some(4 * i64::from(RATE)),
},
..RenderConfig::at(RATE)
},
};
let mut stream = Stream::open(g, &expr(target), config, &Tier::default())
.now()
.unwrap_or_else(|e| panic!("{e}"));
if live {
stream.go_live();
}
RefCell::new(stream)
}
fn endless(g: &Graph, target: &str) -> RefCell<Stream> {
let config = StreamConfig {
block: BLOCK,
channels: None,
render: RenderConfig::at(RATE),
};
let mut stream = Stream::open(g, &expr(target), config, &Tier::default())
.now()
.unwrap_or_else(|e| panic!("{e}"));
stream.go_live();
RefCell::new(stream)
}
fn landing(stream: &RefCell<Stream>, g: &Graph, term: &str) -> sva_engine::Handle {
let build =
|_: &Stream| Ok::<_, EngineError>(Change::Add(g.clone(), expr(term), Placed::Landing));
let Ok(Changed::Added(note)) = change(stream, build, &Tier::default()).now() else {
panic!("an add answers its handle");
};
note
}
fn add(stream: &RefCell<Stream>, g: &Graph, term: &str) {
added(stream, g, &expr(term), &Tier::default())
.now()
.unwrap_or_else(|e| panic!("`{term}`: {e}"));
}
fn read(stream: &RefCell<Stream>, at: usize) -> Vec<f64> {
let block = stream.borrow_mut().read(at as i64, BLOCK);
let block = block
.unwrap_or_else(|e| panic!("{e}"))
.expect("before the end");
assert_eq!(block.start, at as i64, "the block starts where it was read");
block.plane(0).to_vec()
}
fn whole(g: &Graph, text: &str, samples: usize) -> Vec<f64> {
let mut g = g.clone();
assert!(g.define("whole", expr(text)));
let secs = samples as f64 / f64::from(RATE);
let held = render(
&g,
"whole",
RenderConfig::seconds(RATE, secs),
&Tier::default(),
)
.unwrap_or_else(|e| panic!("`{text}`: {e}"));
let id = held.id("whole").expect("the root");
held.output(id).expect("a buffer").plane(0).to_vec()
}
#[test]
fn a_live_skip_reads_a_formula_on_exactly_and_computes_nothing_between() {
let g = composition();
let stream = opened(&g, "@notes", true);
add(&stream, &g, "@held");
add(&stream, &g, "@slow");
read(&stream, 0);
read(&stream, BLOCK);
let at = 40 * BLOCK + 17;
let heard = read(&stream, at);
let want = whole(&g, "@held + @slow", at + BLOCK);
assert_eq!(heard, want[at..], "the whole render there");
assert_eq!(stream.borrow().position(), (at + BLOCK) as i64);
assert_eq!(stream.borrow().work().samples, 3 * BLOCK as u64);
assert!(stream.borrow().dropped().is_empty());
}
#[test]
fn a_live_skip_starts_a_filter_silent_there_and_names_it() {
let g = composition();
let stream = opened(&g, "@notes", true);
add(&stream, &g, "@sawed");
read(&stream, 0);
read(&stream, BLOCK);
let at = 40 * BLOCK + 17;
let heard = read(&stream, at);
let fresh = "lowpass(sample(crop(0.3*saw(220*t), {at}sp, inf)), cutoff=900, q=0.8)";
let want = whole(&g, &fresh.replace("{at}", &at.to_string()), at + BLOCK);
assert_eq!(heard, want[at..], "silent at the skip, stepping from there");
assert_eq!(stream.borrow().counts().dropped, 1);
assert_eq!(stream.borrow().dropped(), ["sawed"]);
let gap = (2 * BLOCK) as i64..at as i64;
for segment in stream.borrow().evaluated("sawed") {
assert!(
segment.end <= gap.start || segment.start >= gap.end,
"{segment:?} is inside the skipped span"
);
}
}
#[test]
fn an_exact_skip_computes_through_and_plays_the_whole_render() {
let g = composition();
let stream = opened(&g, "@sawed", false);
read(&stream, 0);
let at = 40 * BLOCK + 17;
let heard = read(&stream, at);
assert_eq!(heard, whole(&g, "@sawed", at + BLOCK)[at..]);
assert!(stream.borrow().dropped().is_empty());
}
#[test]
fn a_read_behind_the_stream_or_of_nothing_is_refused() {
let g = composition();
let stream = opened(&g, "@held", true);
read(&stream, 0);
let behind = stream.borrow_mut().read(BLOCK as i64 - 1, BLOCK).err();
assert_eq!(
behind.map(|e| e.code().to_string()).as_deref(),
Some("engine.stream_behind")
);
let empty = stream.borrow_mut().read(BLOCK as i64, 0).err();
assert_eq!(
empty.map(|e| e.code().to_string()).as_deref(),
Some("engine.empty_read")
);
assert_eq!(read(&stream, BLOCK), whole(&g, "@held", 2 * BLOCK)[BLOCK..]);
}
#[test]
fn a_term_placed_at_its_landing_keeps_its_time_across_a_skip() {
let g = composition();
let stream = opened(&g, "@notes", true);
read(&stream, 0);
let build =
|_: &Stream| Ok::<_, EngineError>(Change::Add(g.clone(), expr("@slow"), Placed::Landing));
let Ok(Changed::Added(note)) = change(&stream, build, &Tier::default()).now() else {
panic!("an add answers its handle");
};
assert_eq!(stream.borrow().landed(note), Some(BLOCK as i64));
let at = 40 * BLOCK + 17;
let heard = read(&stream, at);
let want = whole(&g, &format!("@slow(t - {BLOCK}sp)"), at + BLOCK);
assert_eq!(heard, want[at..]);
assert_eq!(stream.borrow().landed(note), Some(BLOCK as i64));
}
#[test]
fn a_silent_live_stream_reads_full_frames_of_zeros() {
let g = composition();
let stream = endless(&g, "@notes");
assert_eq!(read(&stream, 0), vec![0.0; BLOCK]);
let far = 30 * RATE as usize + 17;
assert_eq!(read(&stream, far), vec![0.0; BLOCK]);
assert_eq!(stream.borrow().position(), (far + BLOCK) as i64);
assert_eq!(stream.borrow().end(), None);
}
#[test]
fn a_note_added_after_a_silent_skip_lands_there_with_its_attack() {
let g = composition();
let stream = endless(&g, "@notes");
landing(&stream, &g, "@blip");
for block in 0..9 {
read(&stream, block * BLOCK);
}
assert_eq!(stream.borrow().counts().terms, 0, "the note ended");
let far = 30 * RATE as usize + 17;
assert_eq!(read(&stream, far), vec![0.0; BLOCK]);
let note = landing(&stream, &g, "@blip");
let at = far + BLOCK;
assert_eq!(stream.borrow().landed(note), Some(at as i64));
let attack = whole(&g, "@blip", BLOCK);
assert!(attack.iter().any(|v| *v != 0.0));
assert_eq!(read(&stream, at), attack, "the attack is whole");
}